#!/bin/sh # ───────────────────────────────────────────────────────────────────────────── # Birdify CLI installer · macOS · Linux · Windows (Git Bash, MSYS2, Cygwin) # # curl -fsSL https://cli.birdify.dev/install.sh | sh # # In PowerShell on Windows: irm https://cli.birdify.dev/install.ps1 | iex # # Detects your OS and CPU, downloads the matching binary from # github.com/birdifyofficial/birdify-release, verifies its SHA-256 against the # release's checksums, and installs it in a folder you own — which is what # lets birdify keep itself up to date without sudo. # # Environment overrides: # BIRDIFY_VERSION release to install, e.g. v1.2.0 (default: latest) # BIRDIFY_INSTALL_DIR install location (default: the folder of an existing # install, else /usr/local/bin if you can write to it, # else ~/.local/bin; ~/.birdify/bin on Windows) # BIRDIFY_ARCH force amd64 or arm64 (default: detected) # BIRDIFY_INSTALL_BASE_URL download from here instead of GitHub (testing) # ───────────────────────────────────────────────────────────────────────────── # # Everything is inside functions and main runs on the last line, so a download # cut off partway through being piped into sh runs nothing at all. set -eu REPO="birdifyofficial/birdify-release" if [ -t 2 ]; then B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); BLUE=$(printf '\033[38;5;33m') GREEN=$(printf '\033[32m'); YELLOW=$(printf '\033[33m'); RED=$(printf '\033[31m'); R=$(printf '\033[0m') else B=''; DIM=''; BLUE=''; GREEN=''; YELLOW=''; RED=''; R='' fi say() { printf '%s\n' "$*" >&2; } step() { printf ' %s➜%s %s\n' "$BLUE" "$R" "$*" >&2; } ok() { printf ' %s✓%s %s\n' "$GREEN" "$R" "$*" >&2; } warn() { printf ' %s!%s %s\n' "$YELLOW" "$R" "$*" >&2; } die() { printf ' %s✗ %s%s\n' "$RED" "$*" "$R" >&2; exit 1; } # fetch fetch() { if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-delay 1 --connect-timeout 20 "$1" -o "$2" elif command -v wget >/dev/null 2>&1; then wget -q -T 60 -O "$2" "$1" else die "birdify needs curl or wget to download itself" fi } # sha256_file sha256_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else openssl dgst -sha256 "$1" | awk '{print $NF}' fi } detect_platform() { kernel=$(uname -s 2>/dev/null || echo unknown) case "$kernel" in Darwin) os=darwin ;; Linux) os=linux ;; MINGW* | MSYS* | CYGWIN*) os=windows ;; *) die "unsupported operating system: $kernel — birdify runs on macOS, Linux and Windows" ;; esac if [ -n "${BIRDIFY_ARCH:-}" ]; then arch=$BIRDIFY_ARCH elif [ "$os" = windows ]; then cpu=${PROCESSOR_ARCHITEW6432:-${PROCESSOR_ARCHITECTURE:-unknown}} case "$cpu" in ARM64 | arm64) arch=arm64 ;; AMD64 | amd64 | EM64T | x86_64) arch=amd64 ;; *) die "unsupported CPU: $cpu — birdify ships 64-bit x86 and ARM builds" ;; esac else cpu=$(uname -m 2>/dev/null || echo unknown) case "$cpu" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) die "unsupported CPU: $cpu — birdify ships 64-bit x86 and ARM builds" ;; esac # A shell running under Rosetta reports x86_64 on Apple silicon. Install # the native build, which is also the one birdify's own updates fetch. if [ "$os" = darwin ] && [ "$arch" = amd64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then arch=arm64 fi fi case "$arch" in amd64 | arm64) ;; *) die "BIRDIFY_ARCH must be amd64 or arm64 (got: $arch)" ;; esac if [ "$os" = windows ]; then exe=birdify.exe asset="birdify_windows_$arch.exe" else exe=birdify asset="birdify_${os}_$arch" fi } choose_dir() { if [ -n "${BIRDIFY_INSTALL_DIR:-}" ]; then dir=${BIRDIFY_INSTALL_DIR%/} return 0 fi [ -n "${HOME:-}" ] || die "HOME is not set — set BIRDIFY_INSTALL_DIR to choose where birdify goes" # Reinstalling: replace the birdify already on PATH rather than adding a # second copy that one of the two would shadow — as long as it is a plain # file in a folder we can write to, not a package manager's symlink. current=$(command -v "$exe" 2>/dev/null || true) if [ -n "$current" ] && [ -f "$current" ] && [ ! -L "$current" ] && [ -w "$(dirname "$current")" ]; then dir=$(dirname "$current") return 0 fi if [ "$os" = windows ]; then dir="$HOME/.birdify/bin" elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then dir=/usr/local/bin else dir="$HOME/.local/bin" fi } install_binary() { sudo='' mkdir -p "$dir" 2>/dev/null || true if [ ! -d "$dir" ] || [ ! -w "$dir" ]; then [ -n "${BIRDIFY_INSTALL_DIR:-}" ] || die "cannot write to $dir — set BIRDIFY_INSTALL_DIR to a folder you own" command -v sudo >/dev/null 2>&1 || die "cannot write to $dir and sudo is not available — choose a folder you own with BIRDIFY_INSTALL_DIR" step "Installing to ${B}$dir${R} ${DIM}(needs sudo)${R}" warn "birdify will need sudo to update itself there; a folder you own avoids that" sudo=sudo $sudo mkdir -p "$dir" || die "could not create $dir" fi # Stage inside the destination and rename into place: one filesystem, so the # rename is atomic and the binary gets a new inode. Copying over the old file # instead can leave a half-written binary if interrupted, and on macOS gets # the next run killed for a stale code signature. staged="$dir/.birdify-install.$$" $sudo cp "$tmp/$exe" "$staged" || die "could not write to $dir" $sudo chmod 755 "$staged" || die "could not make $staged executable" if [ "$os" = windows ] && [ -f "$dir/$exe" ]; then # A running birdify.exe cannot be replaced, only renamed. $sudo mv -f "$dir/$exe" "$dir/$exe.old" 2>/dev/null || true fi $sudo mv -f "$staged" "$dir/$exe" || die "could not install $dir/$exe" staged='' } path_hint() { shell_name=${SHELL:-sh} shell_name=${shell_name##*/} say "" step "Add ${B}$dir${R} to your PATH, then open a new terminal:" # shellcheck disable=SC2088 # the ~ is for the user to paste, not to expand here case "$shell_name" in zsh) say " ${DIM}echo 'export PATH=\"$dir:\$PATH\"' >> ~/.zshrc${R}" ;; bash) if [ "$os" = darwin ]; then rc='~/.bash_profile'; else rc='~/.bashrc'; fi say " ${DIM}echo 'export PATH=\"$dir:\$PATH\"' >> $rc${R}" ;; fish) say " ${DIM}fish_add_path $dir${R}" ;; *) say " ${DIM}export PATH=\"$dir:\$PATH\"${R}" ;; esac } main() { tmp='' staged='' trap 'rm -rf "$tmp"; if [ -n "$staged" ]; then rm -f "$staged" 2>/dev/null || true; fi' EXIT version=${BIRDIFY_VERSION:-latest} case "$version" in latest | v*) ;; *) version="v$version" ;; esac detect_platform command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || command -v openssl >/dev/null 2>&1 || die "birdify needs sha256sum, shasum or openssl to verify the download" if [ -n "${BIRDIFY_INSTALL_BASE_URL:-}" ]; then base=${BIRDIFY_INSTALL_BASE_URL%/} elif [ "$version" = latest ]; then base="https://github.com/$REPO/releases/latest/download" else base="https://github.com/$REPO/releases/download/$version" fi say "" say " ${B}${BLUE}🐦 Birdify${R} ${DIM}· over-the-air code push${R}" say "" tmp=$(mktemp -d 2>/dev/null || mktemp -d -t birdify) step "Downloading ${B}$asset${R} ${DIM}($os/$arch, $version)${R}" fetch "$base/$asset" "$tmp/$exe" || die "download failed: $base/$asset" fetch "$base/checksums.txt" "$tmp/checksums.txt" || die "could not download checksums.txt from $base" expected=$(awk -v a="$asset" '{ f = $2; sub(/^\*/, "", f) } f == a { print tolower($1); exit }' "$tmp/checksums.txt") [ -n "$expected" ] || die "this release has no $os/$arch build (no $asset in checksums.txt)" actual=$(sha256_file "$tmp/$exe" | tr 'A-F' 'a-f') [ "$expected" = "$actual" ] || die "checksum mismatch for $asset (expected $expected, got $actual) — the download is corrupt or has been tampered with" ok "Checksum verified ${DIM}(sha256)${R}" choose_dir install_binary installed=$("$dir/$exe" version 2>&1) || die "birdify was installed to $dir/$exe but will not run on this machine: $installed" ok "Installed ${B}$installed${R} ${DIM}→ $dir/$exe${R}" case ":$PATH:" in *":$dir:"*) first=$(command -v "$exe" 2>/dev/null || true) if [ -n "$first" ] && [ "$first" != "$dir/$exe" ]; then warn "${B}$first${R} comes earlier on your PATH and will run instead of this install" fi ;; *) path_hint ;; esac if [ "$(id -u 2>/dev/null || echo 1)" = 0 ] && [ -n "${SUDO_USER:-}" ]; then warn "Installed with sudo, so updates will need sudo too — run the installer without sudo to install for $SUDO_USER" fi say "" ok "birdify keeps itself up to date automatically ${DIM}(opt out: BIRDIFY_NO_AUTO_UPDATE=1)${R}" ok "Get started: ${B}birdify login${R}, then ${B}birdify flutter create${R} or ${B}birdify rn create${R}" say "" } main "$@"