#!/bin/sh # Install the firmfact command-line client on Linux or macOS: # # curl -fsSL https://firmfact.com/install.sh | sh # # This file is the whole installer, so you can read it before you run it. It # downloads a release from https://github.com/firmfact/cli/releases and checks # it before it installs anything: # # 1. The release's checksums.txt must carry a valid signature by a firmfact # release key. The keys are below, and `firmfact update` trusts the same # ones. OpenSSL 3 checks the signature. Where there is no OpenSSL 3 (a # Mac has none out of the box), the GitHub CLI (gh) 2.49 or later, if it # is signed in, checks instead that GitHub attests the archive was built # by the firmfact/cli release workflow from the release's own tag. # 2. The archive must match its SHA-256 in checksums.txt. # 3. The new program must run here and report the version it was # downloaded as. # # If a check fails, or cannot be made, it stops and installs nothing. It puts # firmfact in ~/.local/bin and its tab completion and manual pages under # ~/.local/share, never uses sudo and changes no shell startup file. # # Settings, as environment variables (--help lists them too): # FIRMFACT_VERSION the version to install, such as 0.1.0; the latest # release by default # FIRMFACT_INSTALL_DIR the directory to install into; ~/.local/bin by # default, and required when running as root, when # it must be a directory only root can change # FIRMFACT_DOWNLOAD_BASE an HTTPS mirror of the release downloads, laid out # as GitHub's are; needs FIRMFACT_VERSION # # curl -fsSL https://firmfact.com/install.sh | FIRMFACT_VERSION=0.1.0 sh # # Documentation, and the same checks by hand: https://firmfact.com/cli set -eu REPOSITORY='firmfact/cli' RELEASE_WORKFLOW='https://github.com/firmfact/cli/.github/workflows/release.yml' RELEASES_URL='https://github.com/firmfact/cli/releases' LATEST_URL='https://github.com/firmfact/cli/releases/latest' DOWNLOAD_BASE='https://github.com/firmfact/cli/releases/download' CLI_PAGE='https://firmfact.com/cli' INSTALL_SH_URL='https://firmfact.com/install.sh' INSTALL_PS1='irm https://firmfact.com/install.ps1 | iex' HOMEBREW_INSTALL='brew install firmfact/tap/firmfact' SECURITY_EMAIL='security@firmfact.com' # write_release_keys DIR: the firmfact release keys, as PEM files OpenSSL # reads. Each body is a fixed header (MCowBQYDK2VwAyEA) followed by the # base64 of an Ed25519 public key. write_release_keys() { printf '%s\n' '-----BEGIN PUBLIC KEY-----' \ 'MCowBQYDK2VwAyEAgBd0tWG56OhnOnS2t5ixh1ZWdDUoHWQoUarNgP1Cc+o=' \ '-----END PUBLIC KEY-----' >"$1/release-key-1.pem" } usage() { cat <&2 } # fail MESSAGE [DETAIL...]: stop without installing anything. Each detail # goes on a line of its own. fail() { printf 'firmfact installer: %s\n' "$1" >&2 shift for detail in ${1+"$@"}; do printf ' %s\n' "$detail" >&2 done printf '%s\n' 'Nothing was installed.' >&2 exit 1 } cleanup() { if [ -n "${staged:-}" ]; then rm -f "$staged" fi if [ -n "${tmp:-}" ]; then rm -rf "$tmp" fi } detect_platform() { kernel=$(uname -s) case "$kernel" in Linux) os=linux ;; Darwin) os=darwin ;; MINGW* | MSYS* | CYGWIN* | Windows_NT) fail "this is Windows, where firmfact installs from PowerShell:" "$INSTALL_PS1" ;; *) fail "firmfact is built for Linux, macOS and Windows, not $kernel." ;; esac machine=$(uname -m) case "$machine" in x86_64 | amd64) arch=amd64 ;; aarch64 | arm64) arch=arm64 ;; *) fail "firmfact is built for amd64 (x86_64) and arm64 (aarch64) processors, not $machine." ;; esac # A shell running under Rosetta on Apple silicon says x86_64; the build # for the processor itself is the one to install. if [ "$os" = darwin ] && [ "$arch" = amd64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64 fi } # choose_install_dir: FIRMFACT_INSTALL_DIR, or ~/.local/bin for anyone but # root. Installing into root's home directory is almost never what was meant. choose_install_dir() { if [ -n "${FIRMFACT_INSTALL_DIR:-}" ]; then dir=$FIRMFACT_INSTALL_DIR case "$dir" in /*) ;; *) fail "FIRMFACT_INSTALL_DIR must be an absolute path, such as /opt/tools/bin, not $dir." ;; esac while [ "$dir" != / ] && [ "${dir%/}" != "$dir" ]; do dir=${dir%/} done default_dir= return 0 fi if [ "$(id -u)" = 0 ]; then fail "this is running as root, so it would install into root's home directory." \ "Run it as the user who will use firmfact. To install for every user, set" \ "FIRMFACT_INSTALL_DIR to a shared directory such as /usr/local/bin." fi if [ -z "${HOME:-}" ]; then fail "HOME is not set, so there is no ~/.local/bin to install into." \ "Set FIRMFACT_INSTALL_DIR to the directory to install firmfact into." fi dir=$HOME/.local/bin default_dir=1 } # choose_fetcher: curl, or else GNU Wget 1.x. BusyBox wget cannot be held to # HTTPS with TLS 1.2 or later, and Wget2 (the wget of Fedora 40 and later) # prints no Location header for a redirect it does not follow, which this # needs to follow redirects to HTTPS addresses only. choose_fetcher() { if command -v curl >/dev/null 2>&1; then fetcher=curl elif command -v wget >/dev/null 2>&1 && wget --version 2>/dev/null | head -n 1 | grep -q '^GNU Wget 1\.'; then fetcher=wget elif command -v wget >/dev/null 2>&1; then fail "this needs curl, or GNU Wget 1.x, to download firmfact, and the wget here is neither." \ "Install curl, then run this again." else fail "this needs curl, or GNU Wget, to download firmfact." "Install curl, then run this again." fi } # fetch URL FILE: download URL into FILE over HTTPS with TLS 1.2 or later, # following redirects only to HTTPS addresses, and fail on an HTTP error. fetch() { if [ "$fetcher" = curl ]; then curl --proto '=https' --proto-redir '=https' --tlsv1.2 --fail --silent --show-error \ --location --max-redirs 10 --retry 2 --connect-timeout 30 --output "$2" "$1" else wget_fetch "$1" "$2" fi } # redirect_of URL: one HEAD request, following no redirect; prints the # status and, for a redirect, where it points ("302 https://..."). redirect_of() { if [ "$fetcher" = curl ]; then curl --proto '=https' --tlsv1.2 --silent --show-error --head --connect-timeout 30 \ --output /dev/null --write-out '%{http_code} %{redirect_url}' "$1" else wget_once "$1" /dev/null fi } # wget_once URL FILE: one request with GNU Wget, following no redirect; # prints the status and the Location header, if any. wget's own exit code # goes to $tmp/wget.rc: a redirect it does not follow is not a success to # wget, and neither is a transfer cut short after a 200. wget_once() { wget_rc=0 LC_ALL=C wget --secure-protocol=TLSv1_2 --max-redirect=0 --timeout=30 --tries=2 \ --server-response --output-document="$2" "$1" >"$tmp/wget.log" 2>&1 || wget_rc=$? printf '%s\n' "$wget_rc" >"$tmp/wget.rc" wget_status=$(sed -n 's/^ *HTTP\/[0-9.]* \([0-9][0-9][0-9]\).*/\1/p' "$tmp/wget.log" | tail -n 1) wget_location=$(sed -n 's/^ *[Ll]ocation: *\([^ ]*\).*/\1/p' "$tmp/wget.log" | tail -n 1 | tr -d '\r') printf '%s %s' "${wget_status:-000}" "$wget_location" } # wget_fetch URL FILE: fetch for GNU Wget, which cannot be told to follow # HTTPS redirects only, so this follows them itself. wget_fetch() { wget_url=$1 wget_hops=0 while :; do case "$wget_url" in https://*) ;; *) warn "refusing to follow a redirect to $wget_url, which is not HTTPS." return 1 ;; esac wget_answer=$(wget_once "$wget_url" "$2") case "${wget_answer%% *}" in 200) if [ "$(cat "$tmp/wget.rc")" = 0 ]; then return 0 fi warn "the download of $wget_url was cut short: $(tail -n 1 "$tmp/wget.log")" return 1 ;; 301 | 302 | 303 | 307 | 308) ;; 000) warn "no answer from $wget_url: $(tail -n 1 "$tmp/wget.log")" return 1 ;; *) warn "$wget_url answered HTTP ${wget_answer%% *}." return 1 ;; esac wget_hops=$((wget_hops + 1)) if [ "$wget_hops" -gt 10 ]; then warn "stopped after 10 redirects from $1." return 1 fi wget_next=${wget_answer#* } case "$wget_next" in /*) wget_url=$(printf '%s\n' "$wget_url" | sed 's|^\(https://[^/]*\).*|\1|')$wget_next ;; *) wget_url=$wget_next ;; esac done } # is_version V: whether V looks like a release version (1.2.0, 1.3.0-rc.1). # The version becomes part of URLs and file names, so nothing else passes. is_version() { case "$1" in '' | *[!0-9A-Za-z.-]*) return 1 ;; esac printf '%s\n' "$1" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' } # resolve_version: FIRMFACT_VERSION, or the latest release, found the way # the CLI finds it: GitHub's latest-release page redirects to the newest # release's tag. That page is not GitHub's API, whose 60 requests an hour # per address an office behind one address soon uses up. The only redirect # followed is to another latest-release page, as a renamed repository sends. resolve_version() { if [ -n "${FIRMFACT_VERSION:-}" ]; then version=${FIRMFACT_VERSION#v} is_version "$version" || fail "FIRMFACT_VERSION is $FIRMFACT_VERSION, not a version such as 0.1.0." version_source="the version FIRMFACT_VERSION names" return 0 fi if [ -n "${FIRMFACT_DOWNLOAD_BASE:-}" ]; then fail "FIRMFACT_DOWNLOAD_BASE is set, so set FIRMFACT_VERSION too:" \ "a mirror has no latest-release page to ask." fi page=$LATEST_URL hops=0 while :; do answer=$(redirect_of "$page") || fail "could not look up the latest release at $page." \ "Check your connection, or set FIRMFACT_VERSION to the version to install." http_status=${answer%% *} location=${answer#* } case "$http_status" in 301 | 302 | 303 | 307 | 308) ;; *) fail "$page answered HTTP $http_status, not a redirect to the latest release." \ "Check your connection, or set FIRMFACT_VERSION to the version to install." ;; esac case "$location" in */releases/tag/*) tag=${location##*/releases/tag/} tag=${tag%%[?#]*} break ;; https://*/releases/latest) ;; *) fail "$page names no latest release." ;; esac hops=$((hops + 1)) if [ "$hops" -gt 5 ]; then fail "stopped after 5 redirects looking up the latest release." fi page=$location done is_version "${tag#v}" || fail "the latest release is tagged $tag, which is not a version." version=${tag#v} version_source="the latest release" } # openssl_can_verify OPENSSL: whether this openssl checks Ed25519 signatures # correctly. It must accept the signature from RFC 8032's test 2 and refuse # the same signature over another message. LibreSSL (macOS /usr/bin/openssl) # and OpenSSL 1.1 cannot, and neither can a build whose policy leaves Ed25519 # out. openssl_can_verify() { mkdir -p "$tmp/selftest" printf '%s\n' '-----BEGIN PUBLIC KEY-----' \ 'MCowBQYDK2VwAyEAPUAXw+hDiVqStwqnTRt+vJyYLM8uxJaMwM1V8Sr0Zgw=' \ '-----END PUBLIC KEY-----' >"$tmp/selftest/key.pem" printf 'r' >"$tmp/selftest/signed" printf 's' >"$tmp/selftest/other" printf '%s' 'kqAJqfDUyrhyDoILX2QlQKKye1QWUD+Ps3YiI+vbadoIWsHkPhWZbkWPNhPQ8R2MOHsurrQwKu6wDSkWErsMAA==' | "$1" base64 -d -A >"$tmp/selftest/sig" 2>/dev/null || return 1 "$1" pkeyutl -verify -pubin -inkey "$tmp/selftest/key.pem" -rawin \ -in "$tmp/selftest/signed" -sigfile "$tmp/selftest/sig" >/dev/null 2>&1 || return 1 ! "$1" pkeyutl -verify -pubin -inkey "$tmp/selftest/key.pem" -rawin \ -in "$tmp/selftest/other" -sigfile "$tmp/selftest/sig" >/dev/null 2>&1 } # find_openssl: the first openssl that can check the signature, whether on # the PATH or where Homebrew keeps OpenSSL 3 without linking it. Sets openssl # and openssl_name, or leaves openssl empty and names in openssl_seen the # first one that could not. find_openssl() { openssl= openssl_seen= for candidate in openssl openssl3 \ /opt/homebrew/opt/openssl@3/bin/openssl \ /usr/local/opt/openssl@3/bin/openssl \ /home/linuxbrew/.linuxbrew/opt/openssl@3/bin/openssl; do command -v "$candidate" >/dev/null 2>&1 || continue if openssl_can_verify "$candidate"; then openssl=$candidate openssl_name=$("$candidate" version 2>/dev/null | head -n 1 | cut -d ' ' -f 1-2) return 0 fi if [ -z "$openssl_seen" ]; then openssl_seen=$("$candidate" version 2>/dev/null | head -n 1 | cut -d ' ' -f 1-2) fi done return 1 } # gh_can_attest: whether a GitHub CLI is here that can check a build # attestation against an exact workflow identity (gh 2.49 or later), and is # signed in to github.com, which that check needs. gh_can_attest() { command -v gh >/dev/null 2>&1 || return 1 gh attestation verify --help 2>&1 | grep -q -- '--cert-identity' || { gh_too_old=1 return 1 } gh auth status --hostname github.com >/dev/null 2>&1 } # verify_signature: check that a firmfact release key signed checksums.txt, # or else that GitHub attests the archive; stop if neither can be shown. verify_signature() { sig_size=$(wc -c <"$tmp/checksums.txt.sig" | tr -d ' ') if [ "$sig_size" != 64 ]; then fail "checksums.txt.sig is $sig_size bytes, not the 64 of an Ed25519 signature." fi provenance_check= if find_openssl; then write_release_keys "$tmp" for key in "$tmp"/release-key-*.pem; do if "$openssl" pkeyutl -verify -pubin -inkey "$key" -rawin \ -in "$tmp/checksums.txt" -sigfile "$tmp/checksums.txt.sig" >/dev/null 2>&1; then signature_check="checksums.txt is signed with a firmfact release key (checked with $openssl_name)" return 0 fi done fail "the signature on checksums.txt does not match firmfact's release key." \ "The download may have been changed on its way here. Please tell $SECURITY_EMAIL." fi # Without OpenSSL 3, GitHub's build attestation for the archive, which # must name the release workflow run for this version's tag exactly: any # workflow on any branch of the repository can attest a file, so the # repository alone would not show that a release built it. gh_too_old= if gh_can_attest; then say "No OpenSSL 3 here to check the signature; asking GitHub how the archive was built." if gh attestation verify "$tmp/$archive" --repo "$REPOSITORY" \ --cert-identity "$RELEASE_WORKFLOW@refs/tags/v$version" \ --deny-self-hosted-runners >"$tmp/gh.log" 2>&1; then signature_check="not checked, as there is no OpenSSL 3 here; GitHub's attestation was checked instead" provenance_check="GitHub attests that the $REPOSITORY release workflow built this archive from tag v$version" return 0 fi fail "GitHub does not attest that the $REPOSITORY release workflow built $archive from tag v$version:" \ "$(gh_errors)" fi case "$openssl_seen" in '') why="there is no OpenSSL 3 here" ;; 'OpenSSL 3'*) why="the OpenSSL 3 here cannot check Ed25519 signatures (under a FIPS policy, for example)" ;; *) why="the openssl here is $openssl_seen, which cannot check it" ;; esac if [ -n "$gh_too_old" ]; then gh_why="the GitHub CLI (gh) here is older than 2.49, which cannot check GitHub's attestation instead" else gh_why="no GitHub CLI (gh) is signed in to check GitHub's attestation instead" fi if [ "$os" = darwin ]; then fail "cannot check the release signature: $why, and $gh_why." \ "Homebrew checks and installs firmfact for you: $HOMEBREW_INSTALL" \ "Or install OpenSSL 3 (brew install openssl@3), or the GitHub CLI 2.49 or later and" \ "sign it in (gh auth login), then run this again. To check a release by hand: $CLI_PAGE" fi case "$openssl_seen" in 'OpenSSL 3'*) fail "cannot check the release signature: $why, and $gh_why." \ "Install the GitHub CLI 2.49 or later and sign it in (gh auth login), then run this" \ "again. To check a release by hand: $CLI_PAGE" ;; esac fail "cannot check the release signature: $why, and $gh_why." \ "Install OpenSSL 3 (the openssl package on current distributions; openssl3 from EPEL on" \ "RHEL 8), or the GitHub CLI 2.49 or later and sign it in (gh auth login), then run this" \ "again. To check a release by hand: $CLI_PAGE" } # gh_errors: what gh said went wrong, or else the last lines it printed. gh_errors() { gh_lines=$(grep -i -E 'error|fail|unknown|denied|not found|no attestation|mismatch' "$tmp/gh.log" | head -n 3) if [ -z "$gh_lines" ]; then gh_lines=$(grep -v '^ *$' "$tmp/gh.log" | tail -n 3) fi printf '%s\n' "$gh_lines" | sed '2,$s/^/ /' } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum <"$1" | cut -d ' ' -f 1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 <"$1" | cut -d ' ' -f 1 elif [ -n "$openssl" ] || command -v openssl >/dev/null 2>&1; then "${openssl:-openssl}" dgst -sha256 <"$1" | awk '{ print $NF }' else return 1 fi } # verify_checksum: the archive must match its line in checksums.txt, by # exact file name. verify_checksum() { want=$(awk -v name="$archive" 'NF == 2 && $2 == name { print $1; exit }' "$tmp/checksums.txt") [ -n "$want" ] || fail "checksums.txt lists no SHA-256 for $archive." got=$(sha256_of "$tmp/$archive") || fail "found nothing to compute a SHA-256 with (sha256sum, shasum or openssl)." want=$(printf '%s' "$want" | tr 'A-F' 'a-f') got=$(printf '%s' "$got" | tr 'A-F' 'a-f') if [ "$got" != "$want" ]; then fail "$archive does not match its SHA-256 in checksums.txt." \ "The download may have been changed on its way here. Please tell $SECURITY_EMAIL." fi } # install_binary: put the new firmfact in place with a rename, once a trial # run shows it works here, reports the version it was downloaded as, and is # a copy that firmfact update may replace. The copy is made next to the # target, so a /tmp that does not allow programs to run does not matter. install_binary() { mkdir -p "$tmp/unpacked" tar -xzf "$tmp/$archive" -C "$tmp/unpacked" || fail "could not unpack $archive." if [ ! -f "$tmp/unpacked/firmfact" ] || [ -h "$tmp/unpacked/firmfact" ]; then fail "$archive holds no firmfact program." fi mkdir -p "$dir" 2>/dev/null || fail "could not create $dir." \ "Set FIRMFACT_INSTALL_DIR to a directory you can write to." check_root_dir if [ -d "$dir/firmfact" ]; then fail "$dir/firmfact is a directory." fi staged=$(mktemp "$dir/.firmfact-install.XXXXXX" 2>/dev/null) || fail "could not write to $dir." "Set FIRMFACT_INSTALL_DIR to a directory you can write to." if ! cp "$tmp/unpacked/firmfact" "$staged" || ! chmod 755 "$staged"; then fail "could not write to $dir." "Set FIRMFACT_INSTALL_DIR to a directory you can write to." fi trial=$(FIRMFACT_NO_UPDATE_CHECK=1 "$staged" --version 2>"$tmp/trial.log") || fail "the new firmfact does not run on this machine:" "$(head -n 3 "$tmp/trial.log")" reported=$(printf '%s\n' "$trial" | awk '{ if (NF) last = $NF } END { print last }') if [ "${reported#v}" != "$version" ]; then fail "the new firmfact reports version ${reported:-nothing}, not $version." fi # How firmfact judges its own install decides whether firmfact update # replaces it; a copy it takes for a package manager's, it leaves alone. method=$(FIRMFACT_NO_UPDATE_CHECK=1 "$staged" version --json 2>/dev/null | sed -n 's/.*"install_method": *"\([a-z]*\)".*/\1/p' | head -n 1) case "$method" in homebrew | scoop) fail "firmfact takes a copy in $dir for a $method install, so firmfact update would not update it." \ "Set FIRMFACT_INSTALL_DIR to another directory, or install with $method." ;; esac mv -f "$staged" "$dir/firmfact" || fail "could not put firmfact in place in $dir." staged= } # check_root_dir: as root, install only where no one else can change what is # there. The new firmfact runs from dir for its trial run, and after that as # root; in a directory another user can change, that user could put a program # of their own there in between, and root would run it. So dir, and every # directory above it, must belong to root and be writable by no one else, # unless it is sticky like /tmp, where others cannot move root's files. check_root_dir() { [ "$(id -u)" = 0 ] || return 0 real_dir=$(cd "$dir" && pwd -P) || fail "could not open $dir." check=$real_dir while :; do found=$(find "$check" -prune \( ! -user 0 -o \( -perm -0002 ! -perm -1000 \) \ -o \( -perm -0020 ! -group 0 ! -perm -1000 \) \) -print 2>/dev/null) || fail "could not check who can change $check." if [ -n "$found" ]; then fail "this is running as root, and users other than root can change $check," \ "so a program root runs from $dir could be swapped for another." \ "Set FIRMFACT_INSTALL_DIR to a directory only root can change, such as /usr/local/bin," \ "or run this as the user who will use firmfact." fi [ "$check" != / ] || break check=${check%/*} [ -n "$check" ] || check=/ done } # install_extras: the tab completion for bash and fish and the manual pages, # where those look in the user's own data directory. Only with the default # ~/.local/bin, whose ../share this is. Best effort: none of it is needed. install_extras() { extras= [ -n "$default_dir" ] || return 0 case "${XDG_DATA_HOME:-}" in /*) data=$XDG_DATA_HOME ;; *) data=$HOME/.local/share ;; esac if put_file "$tmp/unpacked/completions/firmfact.bash" "$data/bash-completion/completions/firmfact"; then extras="bash" fi if put_file "$tmp/unpacked/completions/firmfact.fish" "$data/fish/vendor_completions.d/firmfact.fish"; then extras="${extras:+$extras and }fish" fi if [ -n "$extras" ]; then extras="tab completion for $extras" fi pages=0 for page in "$tmp"/unpacked/manpages/*.1; do if put_file "$page" "$data/man/man1/${page##*/}"; then pages=$((pages + 1)) fi done if [ "$pages" -gt 0 ]; then extras="${extras:+$extras, and }the manual pages (man firmfact)" fi } # put_file SOURCE DEST: copy SOURCE to DEST, readable by all, if it exists. put_file() { [ -f "$1" ] || return 1 mkdir -p "${2%/*}" 2>/dev/null || return 1 cp "$1" "$2" 2>/dev/null && chmod 644 "$2" 2>/dev/null } # path_advice: if dir is not on the PATH, the line that puts it there for the # user's shell. Startup files are the user's own, so this only says what to # add. SHELL and HOME may be unset (docker run, cron, CI), and set -u is on. path_advice() { case ":${PATH:-}:" in *":$dir:"* | *":$dir/:"*) return 0 ;; esac shown=$dir if [ -n "${HOME:-}" ] && [ "$HOME" != / ]; then case "$dir" in "$HOME"/*) shown="\$HOME${dir#"$HOME"}" ;; esac fi login_shell=${SHELL:-sh} say "" case "${login_shell##*/}" in fish) say "$dir is not on your PATH. To add it, run this once in fish:" \ " fish_add_path $shown" return 0 ;; zsh) rc=.zshrc ;; bash) if [ "$os" = darwin ]; then rc=.bash_profile else rc=.bashrc fi ;; *) rc=.profile ;; esac printf 'Add %s to your PATH with this line in ~/%s, then open a new terminal:\n' "$dir" "$rc" printf ' export PATH="%s:%s"\n' "$shown" "\$PATH" } summary() { say "" "firmfact $version ($version_source) is installed at $dir/firmfact:" FIRMFACT_NO_UPDATE_CHECK=1 "$dir/firmfact" version 2>/dev/null | sed 's/^/ /' || true say "" "Checked before it was installed:" printf ' %-11s %s\n' signature "$signature_check" if [ -n "$provenance_check" ]; then printf ' %-11s %s\n' provenance "$provenance_check" fi printf ' %-11s %s\n' checksum "$archive matches its SHA-256 in checksums.txt" printf ' %-11s %s\n' "trial run" "the new program runs here and reports version $version" say "" if [ -n "$extras" ]; then say "Also installed under $data: $extras." \ "Tab completion for zsh and other shells: firmfact completion --help" else say "Tab completion: firmfact completion --help" fi path_advice found=$(command -v firmfact 2>/dev/null || true) if [ -n "$found" ] && [ "$found" != "$dir/firmfact" ]; then say "" "Another firmfact comes first on your PATH: $found" \ "Remove it, or put $dir before its directory, to use this one." fi say "" "Next:" \ " firmfact signup create an account from the terminal" \ " firmfact login sign in to an account you already have" \ " firmfact update update to a newer release when there is one" \ "Documentation: $CLI_PAGE" } main() { for arg in ${1+"$@"}; do case "$arg" in -h | --help) usage return 0 ;; *) fail "unknown option $arg. The settings are environment variables; see --help." ;; esac done if [ "${FIRMFACT_INSTALL_HELP:-0}" != 0 ]; then usage return 0 fi staged= tmp= trap cleanup EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM detect_platform choose_install_dir choose_fetcher tmp=$(mktemp -d 2>/dev/null || mktemp -d -t firmfact-install) || fail "could not create a temporary directory." resolve_version archive=firmfact_${version}_${os}_${arch}.tar.gz base=${FIRMFACT_DOWNLOAD_BASE:-$DOWNLOAD_BASE} base=${base%/} case "$base" in https://*) ;; *) fail "FIRMFACT_DOWNLOAD_BASE must be an https:// address, not $base." ;; esac say "Downloading firmfact $version for $os/$arch from $base/v$version/" for file in checksums.txt checksums.txt.sig "$archive"; do if fetch "$base/v$version/$file" "$tmp/$file"; then continue fi # checksums.txt came, so the release exists; without its signature # it cannot be checked. if [ "$file" = checksums.txt.sig ]; then fail "could not download the signature for firmfact $version, $base/v$version/$file." \ "A release cannot be checked without it." fi fail "could not download $base/v$version/$file." \ "Check that firmfact $version exists: $RELEASES_URL" done verify_signature verify_checksum install_binary install_extras summary } main ${1+"$@"}