#!/bin/sh : ${TINY_BOTS_RELEASE_URL:=https://aicrew.in/releases} export TINY_BOTS_RELEASE_URL #!/bin/sh # Download and verify a release before atomically replacing tb. The final # compound command must parse in full before a piped copy can start installing. usage() { printf '%s\n' \ 'Usage: install.sh [--version VERSION] [--dir DIRECTORY] [-h|--help]' \ '' \ 'Installs tb for macOS or Linux on amd64 or arm64.' \ '--version VERSION Release tag to install (default: latest).' \ '--dir DIRECTORY Install directory (default: ~/.local/bin).' \ 'TINY_BOTS_RELEASE_URL overrides the trusted GitHub releases base URL.' \ 'HTTPS is required, except for a loopback HTTP test server.' \ '' \ 'Exit codes: 0 installed, 1 failed without replacing tb, 2 usage, 3 missing tool.' } fail() { printf 'install.sh: %s\n' "$1" >&2; exit 1; } main() { set -eu version=latest dir=${HOME:+$HOME/.local/bin} while [ "$#" -gt 0 ]; do case "$1" in -h | --help) usage; exit 0 ;; --version | --dir) if [ "$#" -lt 2 ] || [ -z "$2" ]; then printf 'install.sh: %s needs a value\n' "$1" >&2 exit 2 fi case "$1" in --version) version=$2 ;; --dir) dir=$2 ;; esac shift 2 ;; *) printf 'install.sh: unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; esac done case "$version" in "" | [!0-9A-Za-z]* | *[!0-9A-Za-z._+-]*) printf 'install.sh: invalid release tag: %s\n' "$version" >&2; exit 2 ;; esac [ -n "$dir" ] || { printf 'install.sh: set HOME or pass --dir\n' >&2; exit 2; } case "$dir" in /*) ;; *) dir=$PWD/$dir ;; esac for tool in curl uname mktemp awk chmod mv mkdir rm tr; do command -v "$tool" >/dev/null 2>&1 || { printf 'install.sh: install %s and try again\n' "$tool" >&2; exit 3; } done if command -v sha256sum >/dev/null 2>&1; then sum=sha256sum elif command -v shasum >/dev/null 2>&1; then sum='shasum -a 256' else printf 'install.sh: install sha256sum or shasum and try again\n' >&2 exit 3 fi os=$(uname -s) case "$os" in Darwin) os=darwin ;; Linux) os=linux ;; *) fail "unsupported operating system: $os; use macOS or Linux" ;; esac arch=$(uname -m) case "$arch" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) fail "unsupported architecture: $arch; use amd64 or arm64" ;; esac asset=tb-$os-$arch base=${TINY_BOTS_RELEASE_URL:-https://github.com/yashness/tiny-bots/releases} base=${base%/} case "$base" in https://*) protocols='=https' ;; http://127.0.0.1:* | http://localhost:*) protocols='=http,https' ;; *) fail 'the release URL must use HTTPS (loopback HTTP is only for tests)' ;; esac if [ "$version" = latest ]; then resolved=$(curl --disable --fail --silent --show-error --location --head \ --proto "$protocols" --proto-redir "$protocols" --connect-timeout 15 --max-time 60 \ --output /dev/null --write-out '%{url_effective}' "$base/latest") || fail 'cannot resolve the latest release; retry or pass --version' case "$resolved" in "$base"/tag/*) version=${resolved##*/} ;; *) fail 'latest did not resolve to a release tag; pass --version' ;; esac case "$version" in "" | latest | [!0-9A-Za-z]* | *[!0-9A-Za-z._+-]*) fail 'latest resolved to an invalid release tag; pass --version' ;; esac fi mkdir -p "$dir" || fail "cannot create $dir; choose a writable --dir" [ ! -d "$dir/tb" ] || fail "$dir/tb is a directory; choose a different --dir" if [ -e "$dir/tb" ] || [ -L "$dir/tb" ]; then if installed=$("$dir/tb" version 2>/dev/null); then printf 'Installed: %s (%s/tb)\n' "$installed" "$dir" else printf 'Installed: %s/tb (version could not be read)\n' "$dir" fi else printf 'Installed: none at %s/tb\n' "$dir" fi printf 'Arriving: tb %s (%s/%s)\n' "$version" "$os" "$arch" # A temporary directory beside tb keeps the final rename on one filesystem. stage=$(mktemp -d "$dir/.tb-install.XXXXXXXX") || fail "cannot stage in $dir; choose a writable --dir" trap 'rm -rf "$stage"' 0 trap 'exit 1' HUP INT TERM url=$base/download/$version for file in SHA256SUMS "$asset"; do curl --disable --fail --silent --show-error --location --proto "$protocols" --proto-redir "$protocols" \ --connect-timeout 15 --max-time 300 --output "$stage/$file" "$url/$file" || fail "download failed for $file; any existing tb is unchanged. Check the release and retry" done expected=$(awk -v asset="$asset" 'NF == 2 && ($2 == asset || $2 == "*" asset) { digest = $1; count++ } END { if (count != 1) exit 1; print digest }' "$stage/SHA256SUMS") || fail "SHA256SUMS must contain exactly one checksum for $asset; any existing tb is unchanged" case "$expected" in *[!0-9A-Fa-f]* | "") fail "invalid SHA-256 for $asset; any existing tb is unchanged" ;; esac [ "${#expected}" -eq 64 ] || fail "invalid SHA-256 for $asset; any existing tb is unchanged" expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f') # shellcheck disable=SC2086 # $sum includes shasum's algorithm arguments. actual=$($sum "$stage/$asset") || fail 'cannot checksum the download; any existing tb is unchanged' actual=${actual%% *} if [ "$actual" != "$expected" ]; then printf 'install.sh: SHA-256 MISMATCH for %s\nExpected: %s\nArrived: %s\n' "$asset" "$expected" "$actual" >&2 fail 'refusing to install; any existing tb is unchanged. Download again from a trusted release' fi printf 'Verified SHA-256: %s\n' "$actual" chmod 755 "$stage/$asset" || fail 'cannot make the verified binary executable; any existing tb is unchanged' "$stage/$asset" version || fail 'the verified binary cannot run on this machine; any existing tb is unchanged' mv -f "$stage/$asset" "$dir/tb" || fail 'cannot replace tb; check directory permissions and retry' printf 'Installed tb %s at %s/tb\nRun tb doctor after setup. Restart any running daemon to use this version.\n' "$version" "$dir" case ":${PATH-}:" in *":$dir:"*) ;; *) printf 'Add this directory to PATH: %s\n' "$dir" ;; esac } { main "$@"; }