#!/bin/sh # Install lvcs on Linux or macOS from a GitHub release # (docs/production-readiness.md, R3; decision 252): # # curl -fsSL https://sh.lvcs.sh | sh # # It downloads lvcs--.tar.gz and SHA256SUMS, refuses on a # checksum mismatch, verifies SHA256SUMS's Sigstore bundle when cosign is on # PATH (saying so when it is not), and installs without sudo. # # Environment: # LVCS_VERSION the release to install, as 0.1.0 or v0.1.0 (default: # the latest release) # LVCS_INSTALL_DIR where lvcs goes (default: ~/.local/bin) # LVCS_BASE_URL a URL serving one release's files in place of GitHub (a # mirror, or the release workflow's install test); needs # LVCS_VERSION # LVCS_NO_SERVICE when 1, skip the last step, `lvcs install`, which runs # the per-user daemon as a service (decision 272) # # Everything runs from main, called on the last line, so a truncated download # runs nothing. set -eu repo=gabewillen/lvcs signer="^https://github\\.com/$repo/\\.github/workflows/release\\.yml@refs/tags/v" issuer=https://token.actions.githubusercontent.com say() { printf 'lvcs-install: %s\n' "$*" >&2; } die() { say "$*"; exit 1; } fetch() { if [ -n "${LVCS_BASE_URL:-}" ]; then curl -fsSL -o "$2" "$1" else curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1" fi } detect_target() { arch=$(uname -m) case $arch in x86_64 | amd64) arch=x86_64 ;; aarch64 | arm64) arch=aarch64 ;; *) die "no release for architecture $arch: releases cover x86_64 and aarch64" ;; esac case $(uname -s) in Linux) echo "$arch-unknown-linux-musl" ;; Darwin) # Under Rosetta uname reports x86_64; install the native build. if [ "$arch" = x86_64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=aarch64 fi echo "$arch-apple-darwin" ;; MINGW* | MSYS* | CYGWIN*) die "on Windows, run in PowerShell: irm https://ps.lvcs.sh | iex" ;; *) die "no release for $(uname -s): releases cover Linux, macOS, and Windows" ;; esac } latest_version() { url=$(curl -fsSI --proto '=https' --tlsv1.2 -o /dev/null -w '%{redirect_url}' "https://github.com/$repo/releases/latest") \ || die "could not reach https://github.com/$repo/releases/latest; set LVCS_VERSION to pick a release" case $url in */releases/tag/v*) echo "${url##*/releases/tag/v}" ;; *) die "found no latest release at https://github.com/$repo/releases; set LVCS_VERSION to pick one" ;; esac } sha256() { 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 else die "found neither sha256sum nor shasum to check the download with" fi } main() { command -v curl >/dev/null 2>&1 || die "curl is required" command -v tar >/dev/null 2>&1 || die "tar is required" target=$(detect_target) version=${LVCS_VERSION:-} version=${version#v} if [ -n "${LVCS_BASE_URL:-}" ]; then [ -n "$version" ] || die "LVCS_BASE_URL needs LVCS_VERSION" base=${LVCS_BASE_URL%/} else [ -n "$version" ] || version=$(latest_version) base="https://github.com/$repo/releases/download/v$version" fi dir=${LVCS_INSTALL_DIR:-${HOME:?HOME is unset; set LVCS_INSTALL_DIR}/.local/bin} archive="lvcs-$version-$target.tar.gz" tmp=$(mktemp -d 2>/dev/null || mktemp -d -t lvcs-install) trap 'rm -rf "$tmp"' EXIT trap 'exit 1' HUP INT TERM say "downloading lvcs $version for $target" fetch "$base/$archive" "$tmp/$archive" || die "could not download $base/$archive" fetch "$base/SHA256SUMS" "$tmp/SHA256SUMS" || die "could not download $base/SHA256SUMS" if command -v cosign >/dev/null 2>&1; then fetch "$base/SHA256SUMS.sigstore.json" "$tmp/SHA256SUMS.sigstore.json" \ || die "could not download $base/SHA256SUMS.sigstore.json" cosign verify-blob --bundle "$tmp/SHA256SUMS.sigstore.json" \ --certificate-identity-regexp "$signer" --certificate-oidc-issuer "$issuer" \ "$tmp/SHA256SUMS" >/dev/null \ || die "refusing to install: the Sigstore signature on SHA256SUMS did not verify" say "verified the Sigstore signature on SHA256SUMS" else say "cosign is not on PATH, so the Sigstore signature on SHA256SUMS is not checked (the checksum still is)" fi want=$(awk -v f="$archive" '$2 == f || $2 == "*" f { print $1; exit }' "$tmp/SHA256SUMS") [ -n "$want" ] || die "SHA256SUMS lists no $archive" got=$(sha256 "$tmp/$archive") [ "$got" = "$want" ] || die "refusing to install: $archive has SHA-256 $got, but SHA256SUMS says $want" mkdir -p "$tmp/x" tar -xzf "$tmp/$archive" -C "$tmp/x" [ -f "$tmp/x/lvcs" ] || die "$archive holds no lvcs binary" mkdir -p "$dir" || die "could not create $dir; set LVCS_INSTALL_DIR to a writable directory" # Copy beside the target, then rename over it, so a running lvcs keeps # its old binary and a failed copy leaves the old one in place. cp "$tmp/x/lvcs" "$dir/.lvcs.install.$$" chmod 0755 "$dir/.lvcs.install.$$" mv -f "$dir/.lvcs.install.$$" "$dir/lvcs" # The marker lvcs update needs before it replaces a binary: this one is # the install script's (decision 293). : > "$dir/lvcs.install" say "installed $("$dir/lvcs" --version) to $dir/lvcs" case ":${PATH:-}:" in *":$dir:"*) ;; *) say "$dir is not on PATH; add it, for example: export PATH=\"$dir:\$PATH\"" ;; esac # The per-user daemon, as a service of this user's (decision 272). if [ "${LVCS_NO_SERVICE:-}" = 1 ]; then say "LVCS_NO_SERVICE=1: not installing the daemon's service; run lvcs install, or lvcs daemon in the foreground" else "$dir/lvcs" install || die "lvcs install failed; run it again, or set LVCS_NO_SERVICE=1 and run lvcs daemon yourself" fi } main "$@"