#!/bin/sh # instabox CLI installer. instabox-server serves this file at /install.sh with its # public URL filled in below (for the "next steps" text only): # # curl -fsSL https:///install.sh | sh # # The CLI ships inside the `instabox` npm package, which CI publishes on every # merge (npm/README.md). This script asks the npm registry for the package's # latest version, downloads that version's tarball, verifies it against the # sha512 the registry publishes for it (dist.integrity), extracts # package/cli/instabox-- and installs it as "instabox" into # $INSTABOX_INSTALL_DIR, else /usr/local/bin when writable, else ~/.local/bin. # Nothing is installed when the integrity check fails. # # INSTABOX_VERSION a package version instead of latest (0.8.7) # INSTABOX_NPM_REGISTRY a registry mirror (default: the server's npmRegistry, # else https://registry.npmjs.org) set -eu INSTABOX_URL='https://api.instabox.sh' REGISTRY="${INSTABOX_NPM_REGISTRY:-https://registry.npmjs.org}" REGISTRY="${REGISTRY%/}" PACKAGE=instabox WANT_VERSION="${INSTABOX_VERSION:-latest}" err() { printf 'instabox install: %s\n' "$*" >&2 exit 1 } # download URL FILE fetches URL into FILE. A registry or CDN hiccup (a # refused connection, a 502) is retried for about a minute before the # install fails: curl with --retry (10 retries 5 s apart; --retry treats # 408, 429, 500, 502, 503 and 504 as transient, --retry-connrefused adds a # refused connection), wget with a loop of 10 attempts 5 s apart, since # busybox wget (Alpine) has no retry options at all. download() { # url file if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 10 --retry-delay 5 --retry-connrefused -o "$2" "$1" || err "download failed: $1" elif command -v wget >/dev/null 2>&1; then # fetch, not attempt: sh functions share main's variables, and # main's attempt counts integrity mismatches. fetch=1 while :; do wget -q -O "$2" "$1" && return 0 [ "$fetch" -lt 10 ] || break fetch=$((fetch + 1)) sleep 5 done err "download failed: $1" else err "need curl or wget" fi } sha512_of() { if command -v sha512sum >/dev/null 2>&1; then sha512sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 512 "$1" | awk '{print $1}' else err "need sha512sum or shasum to verify the download" fi } sha256_of() { 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 echo unknown fi } # The registry's integrity is "sha512-" + base64; sha512sum prints hex. base64_to_hex() { { printf '%s' "$1" | base64 -d 2>/dev/null || printf '%s' "$1" | base64 -D 2>/dev/null; } | od -An -v -tx1 | tr -d ' \n' } # The string value of key in a JSON document, compact or pretty-printed, in # any key order: empty unless the key appears exactly once. A package # version's metadata names version once (at the top) and tarball and # integrity once (in dist), so nesting and order never matter. json_field() { # file key tr -d '\n\r' <"$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | sed 's/^.*:[[:space:]]*"\([^"]*\)"$/\1/' | awk '{ v = $0; n++ } END { if (n == 1) print v }' } main() { case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; MINGW* | MSYS* | CYGWIN*) err "this installer is for macOS and Linux; on Windows, run: npx instabox@latest" ;; *) err "unsupported OS $(uname -s) (macOS and Linux only)" ;; esac case "$(uname -m)" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) err "unsupported architecture $(uname -m) (amd64 and arm64 only)" ;; esac # An x86_64 shell under Rosetta on Apple silicon: install the native build. if [ "$os" = darwin ] && [ "$arch" = amd64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then arch=arm64 fi name="instabox-$os-$arch" command -v tar >/dev/null 2>&1 || err "need tar to unpack the download" tmp="$(mktemp -d 2>/dev/null || mktemp -d -t instabox-install)" trap 'rm -rf "$tmp"' EXIT trap 'exit 1' INT TERM meta="$REGISTRY/$PACKAGE/$WANT_VERSION" download "$meta" "$tmp/meta.json" version="$(json_field "$tmp/meta.json" version)" tarball="$(json_field "$tmp/meta.json" tarball)" integrity="$(json_field "$tmp/meta.json" integrity)" case "$version" in "" | *[!0-9A-Za-z.+-]*) err "$meta did not name a version; nothing installed" ;; esac case "$tarball" in https://* | http://*) ;; *) err "$meta has no tarball URL for $PACKAGE $version; nothing installed" ;; esac case "$integrity" in sha512-*) want="$(base64_to_hex "${integrity#sha512-}")" ;; *) want="" ;; esac [ "${#want}" -eq 128 ] || err "$meta has no sha512 integrity for $PACKAGE $version; nothing installed" printf 'Downloading %s %s from %s\n' "$PACKAGE" "$version" "$tarball" attempt=1 while :; do download "$tarball" "$tmp/package.tgz" got="$(sha512_of "$tmp/package.tgz")" [ "$got" = "$want" ] && break [ "$attempt" -lt 3 ] || err "integrity mismatch for $tarball after $attempt downloads (sha512 $got, want $want from $meta); nothing installed" printf 'Integrity mismatch for %s (sha512 %s, want %s from %s); downloading again\n' "$tarball" "$got" "$want" "$meta" >&2 attempt=$((attempt + 1)) sleep 1 done tar -xzf "$tmp/package.tgz" -C "$tmp" "package/cli/$name" 2>/dev/null || err "$PACKAGE $version has no $name (package/cli/$name is not in $tarball); nothing installed" # A regular file, as the Go updater requires (cmd/instabox/update.go). [ ! -L "$tmp/package/cli/$name" ] && [ -f "$tmp/package/cli/$name" ] || err "$PACKAGE $version has no $name; nothing installed" if [ -n "${INSTABOX_INSTALL_DIR:-}" ]; then dir="$INSTABOX_INSTALL_DIR" elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then dir=/usr/local/bin else [ -n "${HOME:-}" ] || err "HOME is not set; set INSTABOX_INSTALL_DIR" dir="$HOME/.local/bin" fi mkdir -p "$dir" || err "cannot create $dir; set INSTABOX_INSTALL_DIR" [ -w "$dir" ] || err "$dir is not writable; set INSTABOX_INSTALL_DIR to a directory you own" # Copy next to the target, set the mode explicitly (cp follows the # caller's umask), check that it runs, then rename: atomic, and safe # while an old instabox is running. new="$dir/.instabox.install.$$" cp "$tmp/package/cli/$name" "$new" || err "cannot write to $dir" chmod 0755 "$new" || { rm -f "$new" err "cannot make $new executable" } if ! installed="$("$new" --version 2>&1)"; then rm -f "$new" err "the downloaded instabox does not run (instabox --version failed: $installed); nothing installed" fi mv -f "$new" "$dir/instabox" || { rm -f "$new" err "cannot install $dir/instabox" } [ -x "$dir/instabox" ] || err "$dir/instabox is not executable" printf 'Installed %s (%s, sha256 %s)\n' "$dir/instabox" "$installed" "$(sha256_of "$dir/instabox")" case ":${PATH:-}:" in *":$dir:"*) ;; *) printf '\n%s is not on your PATH. Add it (to ~/.zshrc or ~/.bashrc as well):\n' "$dir" # shellcheck disable=SC2016 # $PATH is for the user's shell printf ' export PATH="%s:$PATH"\n' "$dir" ;; esac cat <