Files
dotfiles/lib/fetch.sh
T
noconnorandClaude Opus 5.5 9cb18a795d Coexist with a desktop that already manages its tools (Omarchy)
On an Omarchy laptop bootstrap left two of almost everything: VS Code from
both pacman and the tarball (two menu entries, one shared ~/.config/Code
profile), and kubectl/helm/k9s/kubectx/yq/jq/node/ansible in ~/.local/bin
behind mise and pacman copies that always won on PATH. The zsh helpers
(op*, grmb, secrets.env) also never reached the bash login shell.

- lib/fetch.sh: add `shadowed`, true when another copy of a tool comes
  earlier on PATH than ~/.local/bin. Always false where zshrc puts
  ~/.local/bin first, so VM installs are unchanged.
- bin/, node/, ansible/: skip tools that are shadowed.
- vscode/: defer to any system `code`, and remove a tarball copy this
  module installed earlier.
- bash/: new module; bash port of the zshrc helpers, sourced from
  ~/.bashrc via a marked block. No-ops without ~/.bashrc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:44:18 -04:00

165 lines
5.8 KiB
Bash

#!/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 <url> to <dest-file> 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 <name> <version> — true if <name> is installed and was
# stamped at <version>, 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 <name> — true (printing the winner's path) when another <name> 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 <name> <version> — record the installed version.
mark_version() {
mkdir -p "$VERSION_DIR"
printf '%s\n' "$2" >"$VERSION_DIR/$1"
}
# install_tarball_bin <url> <path-within-archive> <dest-name> — 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 <url> <path-within-archive> <dest-name> — 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 <url> <root-within-archive> <dest-dir> — download a
# .tar.gz that holds a whole application directory rather than one binary, and
# swap it into <dest-dir>. 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 <url> <dest-name> — 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"
}