#!/usr/bin/env sh # lib/fetch.sh — helpers for installing userland binaries into ~/.local/bin. # # Everything lands under $HOME, so installed tools survive Kasm redeploys # (which wipe the root filesystem but keep $HOME) with no reinstall needed. # No sudo, no system packages. Prefer statically linked release binaries so # they keep working even after the root fs — and its shared libraries — is # reset. # # Source this (callers already run `set -eu`), then use fetch_arch / # fetch_arch_deb, have_version + mark_version, install_tarball_bin, # install_tarball_tree, and install_raw_bin. BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" VERSION_DIR="$BIN_DIR/.versions" fsay() { printf '%s\n' "$*" >&2; } # Normalize `uname -m` to the tokens rust-style release names use. fetch_arch() { case "$(uname -m)" in x86_64|amd64) printf 'x86_64\n' ;; aarch64|arm64) printf 'aarch64\n' ;; *) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;; esac } # Same, but in the Go/Debian token style most devops tools use. fetch_arch_deb() { case "$(uname -m)" in x86_64|amd64) printf 'amd64\n' ;; aarch64|arm64) printf 'arm64\n' ;; *) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;; esac } # Download to using whatever downloader is available. _fetch_dl() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1" else fsay "fetch: need curl or wget"; return 1 fi } # have_version — true if is installed and was # stamped at , so bootstrap can skip a re-download. Versions are # recorded in ~/.local/bin/.versions rather than parsed from each tool's # `--version` output, which varies wildly (kubectl/helm/k9s have no uniform # flag). have_version() { [ -x "$BIN_DIR/$1" ] && [ -f "$VERSION_DIR/$1" ] && [ "$(cat "$VERSION_DIR/$1")" = "$2" ] } # shadowed — true (printing the winner's path) when another sits # EARLIER on PATH than $BIN_DIR, so a copy installed there would never run. # # WHY: on a desktop that already manages these tools (Omarchy: pacman for VS # Code and jq, mise for kubectl/helm/node/...), ~/.local/bin is at the END of # PATH. Installing there anyway left a second, older copy of every tool that # nothing ran but `type -a` and the version stamps still reported. On the VMs # this repo targets, zshrc puts ~/.local/bin FIRST, so nothing shadows it and # this is always false -- installs there behave exactly as before. If $BIN_DIR # is not on PATH at all (a fresh box, before zshrc), it is false too. # # A mise shim with no version configured is not a real provider (it errors when # run), so shims only count when `mise which` resolves them. shadowed() { _sh_name="$1" _sh_ifs="$IFS"; IFS=: # shellcheck disable=SC2086 # split PATH on ':' deliberately set -- $PATH IFS="$_sh_ifs" case ":$PATH:" in *":$BIN_DIR:"*) ;; *) return 1 ;; esac for _sh_dir; do [ "$_sh_dir" = "$BIN_DIR" ] && return 1 [ -n "$_sh_dir" ] && [ -x "$_sh_dir/$_sh_name" ] && [ ! -d "$_sh_dir/$_sh_name" ] || continue case "$_sh_dir" in */mise/shims) mise which "$_sh_name" >/dev/null 2>&1 || continue ;; esac printf '%s\n' "$_sh_dir/$_sh_name" return 0 done return 1 } # mark_version — record the installed version. mark_version() { mkdir -p "$VERSION_DIR" printf '%s\n' "$2" >"$VERSION_DIR/$1" } # install_tarball_bin — download a # .tar.gz, extract, and install one binary from it into ~/.local/bin. install_tarball_bin() { mkdir -p "$BIN_DIR" _tmp="$(mktemp -d)" _fetch_dl "$1" "$_tmp/archive.tar.gz" tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp" install -m 0755 "$_tmp/$2" "$BIN_DIR/$3" rm -rf "$_tmp" fsay "installed $BIN_DIR/$3" } # install_zip_bin — like # install_tarball_bin but for .zip assets (e.g. 1Password CLI). Uses unzip # when present, else python3's zipfile module, so no hard dependency on # either being installed. install_zip_bin() { mkdir -p "$BIN_DIR" _tmp="$(mktemp -d)" _fetch_dl "$1" "$_tmp/archive.zip" if command -v unzip >/dev/null 2>&1; then unzip -q "$_tmp/archive.zip" -d "$_tmp" elif command -v python3 >/dev/null 2>&1; then python3 -m zipfile -e "$_tmp/archive.zip" "$_tmp" else fsay "fetch: need unzip or python3 to extract zip"; rm -rf "$_tmp"; return 1 fi install -m 0755 "$_tmp/$2" "$BIN_DIR/$3" rm -rf "$_tmp" fsay "installed $BIN_DIR/$3" } # install_tarball_tree — download a # .tar.gz that holds a whole application directory rather than one binary, and # swap it into . install_tarball_bin cannot be reused here: VS Code # is an Electron bundle of thousands of files that only runs from an intact # tree, not a single binary that can be picked out of the archive. # # The new tree is staged beside the target and moved into place only once the # download and extraction have both succeeded, so an interrupted run leaves the # previously installed version intact instead of a half-extracted directory. install_tarball_tree() { _dest="$3" _tmp="$(mktemp -d)" _fetch_dl "$1" "$_tmp/archive.tar.gz" tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp" if [ ! -d "$_tmp/$2" ]; then fsay "fetch: '$2' not found in archive"; rm -rf "$_tmp"; return 1 fi mkdir -p "$(dirname "$_dest")" rm -rf "$_dest.new" "$_dest.old" mv "$_tmp/$2" "$_dest.new" if [ -e "$_dest" ]; then mv "$_dest" "$_dest.old" fi mv "$_dest.new" "$_dest" rm -rf "$_dest.old" "$_tmp" fsay "installed $_dest" } # install_raw_bin — download a single raw binary. install_raw_bin() { mkdir -p "$BIN_DIR" _tmp="$(mktemp)" _fetch_dl "$1" "$_tmp" install -m 0755 "$_tmp" "$BIN_DIR/$2" rm -f "$_tmp" fsay "installed $BIN_DIR/$2" }