#!/bin/sh # # Installs the Sandy CLI. # # curl -fsSL https://sandy.computer/install.sh | sh # # One static Go binary, gzipped, fetched for this machine's platform and put in # /usr/local/bin. Set SANDY_BIN_DIR to install somewhere else. # # This file is a template: prerender.ts fills in the BASE_URL below with the # site's own origin when it renders dist/install.sh, the same way every other # URL on this site is baked in at build time. A script piped from a host always # fetches its binary from that same host, so the dev cluster never sends anyone # to production for a binary. # # POSIX sh throughout, and nothing here reads stdin — stdin is the script. set -eu BASE_URL="https://sandy.computer" BIN_DIR="${SANDY_BIN_DIR:-/usr/local/bin}" die() { printf 'sandy: %s\n' "$*" >&2 exit 1 } command -v curl >/dev/null 2>&1 || die "curl is required" command -v gzip >/dev/null 2>&1 || die "gzip is required" os=$(uname -s) case "$os" in Darwin) os=darwin ;; Linux) os=linux ;; *) die "unsupported system: $os — Sandy builds for macOS and Linux" ;; esac arch=$(uname -m) case "$arch" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) die "unsupported architecture: $arch — Sandy builds for amd64 and arm64" ;; esac asset="sandy-$os-$arch.gz" url="$BASE_URL/dl/$asset" tmp=$(mktemp -d) || die "could not create a temporary directory" trap 'rm -rf "$tmp"' EXIT INT TERM printf 'downloading %s\n' "$url" curl -fsSL "$url" -o "$tmp/$asset" || die "download failed: $url" curl -fsSL "$url.sha256" -o "$tmp/sum" || die "download failed: $url.sha256" # The checksum comes from the same origin as the binary, so it is not a defence # against that origin: TLS is what makes the download trustworthy. What it does # catch is a truncated or corrupted transfer, which is the failure that would # otherwise surface much later as a binary that will not exec. want=$(cut -d' ' -f1 <"$tmp/sum") if command -v sha256sum >/dev/null 2>&1; then got=$(sha256sum "$tmp/$asset" | cut -d' ' -f1) elif command -v shasum >/dev/null 2>&1; then got=$(shasum -a 256 "$tmp/$asset" | cut -d' ' -f1) else die "no sha256sum or shasum — cannot verify the download" fi [ "$want" = "$got" ] || die "checksum mismatch — the download is corrupt, try again" gzip -dc "$tmp/$asset" >"$tmp/sandy" || die "could not decompress $asset" chmod 755 "$tmp/sandy" # sudo only when the destination actually needs it, so the common cases — a # container running as root, an agent's sandbox — never prompt for anything. sudo="" if [ -d "$BIN_DIR" ]; then # `mkdir -p` on a directory that exists succeeds whatever its permissions, # so the existing case has to be tested for writability directly. [ -w "$BIN_DIR" ] || sudo="sudo" else mkdir -p "$BIN_DIR" 2>/dev/null || sudo="sudo" fi if [ -n "$sudo" ]; then command -v sudo >/dev/null 2>&1 || die "$BIN_DIR is not writable and sudo is not available — set SANDY_BIN_DIR to a directory you own" printf '%s needs root, using sudo\n' "$BIN_DIR" $sudo mkdir -p "$BIN_DIR" || die "could not create $BIN_DIR" fi # Staged inside the destination directory and renamed into place: the rename is # atomic, and it never writes through a `sandy` that is currently executing. dest="$BIN_DIR/sandy" stage="$BIN_DIR/.sandy.$$" $sudo cp "$tmp/sandy" "$stage" || die "could not write to $BIN_DIR" $sudo mv -f "$stage" "$dest" || { $sudo rm -f "$stage" die "could not install $dest" } printf '%s installed to %s\n' "$("$dest" version)" "$dest" case ":$PATH:" in *":$BIN_DIR:"*) ;; *) printf '\n%s is not on your PATH — add it, or run %s directly\n' "$BIN_DIR" "$dest" ;; esac