Portable install: the binary installs itself; static builds on five architectures; root via sudo, doas or uid 0; every package manager; older systemd; no lsblk

- hamlab install / uninstall (internal/setup): systemd or OpenRC, elevates through
  sudo or doas, runs as the invoking user, sets or generates the password, names only
  journal groups that exist, waits for /healthz, reports an active firewalld/ufw.
  --purge and the data chown never touch a directory that isn't Ham Lab's alone.
- install.sh: curl-able bootstrap (arch detection, SHA256SUMS, offline beside the
  binaries, noexec /tmp fallback). make release builds dist/ for amd64, arm64, arm
  (ARMv6), 386, riscv64. Every build is CGO_ENABLED=0; the native one used to link glibc.
- internal/priv replaces every literal `sudo -n`: uid 0, sudo or doas.
- Updates acts on dnf, zypper, pacman and apk as well as apt; dnf shows installed versions.
- Services falls back to the plain systemctl table before systemd 246 and says so on
  hosts without systemd. Disks falls back to /sys/block without lsblk.
- Listener binds with IP_FREEBIND, so a LAN address that isn't up at boot is no failure.
- Self-update: the new build must pass `check` against the live config, is put in
  place by atomic rename, and the daemon re-execs itself (same under any supervisor).
- State and config writes are fsynced; a corrupt state.json is set aside, not fatal.
- Terminal shutdown no longer empties the saved list, so names come back after a
  restart by design rather than by winning a race.
- Config default is ~/.config/hamlab; an existing ~/apps/hamlab is still found.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 23:00:20 -07:00
co-authored by Claude Fable 5.1
parent 91b7f4f009
commit 488d929733
26 changed files with 1617 additions and 178 deletions
Executable
+77
View File
@@ -0,0 +1,77 @@
#!/bin/sh
# Ham Lab installer: fetches the build for this machine and hands over to `hamlab install`,
# which sets up the binary, config, login password and a boot-time service.
#
# curl -fsSL "$HAMLAB_URL/install.sh" | sh
# curl -fsSL "$HAMLAB_URL/install.sh" | sh -s -- --listen 0.0.0.0:8100 --tls auto
# ./install.sh # beside the hamlab-linux-* files: installs offline
#
# Everything after `--` goes to `hamlab install` (see `hamlab install -h`).
#
# HAMLAB_URL where hamlab-linux-<arch> and SHA256SUMS are served from
# HAMLAB_TOKEN access token, when that place is a private Gitea/Forgejo repo
set -eu
HAMLAB_URL="${HAMLAB_URL:-https://git.bigham.dev/mitchell/hamlab/releases/download/@VERSION@}"
die() { echo "install.sh: $*" >&2; exit 1; }
[ "$(uname -s)" = Linux ] || die "Ham Lab runs on Linux; this is $(uname -s)"
case "$(uname -m)" in
x86_64|amd64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
armv6l|armv7l|armv8l|arm) arch=arm ;;
i386|i486|i586|i686) arch=386 ;;
riscv64) arch=riscv64 ;;
*) die "no build for $(uname -m) (have: amd64, arm64, arm, 386, riscv64)" ;;
esac
file="hamlab-linux-$arch"
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t hamlab)
trap 'rm -rf "$tmp"' EXIT INT TERM
fetch() { # url dest
if command -v curl >/dev/null 2>&1; then
curl -fsSL ${HAMLAB_TOKEN:+-H "Authorization: token $HAMLAB_TOKEN"} -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then
wget -q ${HAMLAB_TOKEN:+--header="Authorization: token $HAMLAB_TOKEN"} -O "$2" "$1"
else
die "need curl or wget"
fi
}
here=$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo .)
if [ -f "$here/$file" ]; then
cp "$here/$file" "$tmp/hamlab"
[ -f "$here/SHA256SUMS" ] && cp "$here/SHA256SUMS" "$tmp/SHA256SUMS"
else
echo "Downloading $file" >&2
fetch "$HAMLAB_URL/$file" "$tmp/hamlab" || die "download failed: $HAMLAB_URL/$file"
fetch "$HAMLAB_URL/SHA256SUMS" "$tmp/SHA256SUMS" 2>/dev/null || rm -f "$tmp/SHA256SUMS"
fi
if [ -f "$tmp/SHA256SUMS" ] && command -v sha256sum >/dev/null 2>&1; then
want=$(awk -v f="$file" '$2 == f || $2 == "*" f { print $1 }' "$tmp/SHA256SUMS")
got=$(sha256sum "$tmp/hamlab" | awk '{ print $1 }')
[ -n "$want" ] || die "SHA256SUMS has no entry for $file"
[ "$want" = "$got" ] || die "checksum mismatch for $file"
fi
chmod 0755 "$tmp/hamlab"
bin="$tmp/hamlab"
if ! "$bin" version >/dev/null 2>&1; then
# a noexec /tmp is the usual reason: try from the home directory
alt="${HOME:-/root}/.hamlab-install.$$"
trap 'rm -rf "$tmp" "$alt"' EXIT INT TERM
cp "$bin" "$alt" 2>/dev/null && chmod 0755 "$alt" || true
"$alt" version >/dev/null 2>&1 || die "the $arch build doesn't run on this machine"
bin="$alt"
fi
# Piped from curl, stdin is this script: take the password prompt from the terminal.
case " $* " in *password-stdin*) piped=0 ;; *) piped=1 ;; esac
if [ "$piped" = 1 ] && [ ! -t 0 ] && [ -r /dev/tty ] && (: </dev/tty) 2>/dev/null; then
"$bin" install "$@" </dev/tty
else
"$bin" install "$@"
fi