The lists are derived rather than typed. beastix has 512 explicit packages - years of experiments among them - so taking that as "what this machine needs" would be meaningless. curate.sh combines four usage signals, each of which alone has a blind spot: shell history sees no GUI application, KDE's activity database sees no chat client, autostart sees only what starts by itself, and flatpak entries never appear in pacman -Qqe at all. That reduces 512 to 62. The split follows the data: what beastix and shron both have becomes the base, the rest of beastix becomes desktop, shron's own becomes server. base-devel is one entry in the server list rather than its 26 members. It stopped being a package group in 2022 - "pacman -Sg base-devel" now fails outright - and is a meta package today. base, grub and linux-lts are dropped entirely; they arrive with the operating system. packages.sh never removes anything. Extras are reported and left alone, for the same reason the BookStack playbook refuses to: on a server, individual packages carry mail and web services. It avoids process substitution because /bin/sh is dash on Debian and a POSIX-mode bash on macOS, and this has to run on both. Verified on beastix: 62 wanted, 0 missing, 450 extra reported and untouched; install is a no-op. The server list is a snapshot from 2026-08-06 and wants re-reading when shron is reachable again - its login session has since expired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.1 KiB
Bash
Executable File
89 lines
3.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Compare the installed packages against the lists for this machine's role.
|
|
#
|
|
# packages.sh what is missing and what is extra
|
|
# packages.sh install install what is missing
|
|
#
|
|
# It never removes anything. Extra packages are reported and left alone: on a
|
|
# server, individual packages carry mail and web services, and a tool that
|
|
# tidies up on its own is a tool that eventually takes one of them down. The
|
|
# same caution is why the BookStack upgrade playbook refuses to remove
|
|
# anything either.
|
|
#
|
|
# The lists are data, not code - plain names, one per line. That is deliberate:
|
|
# they map onto home-manager and environment.systemPackages almost unchanged if
|
|
# this ever moves to Nix, whereas an install script would not.
|
|
set -eu
|
|
|
|
ROLE=$(cat "$HOME/.config/dotconfs/role" 2>/dev/null || echo desktop)
|
|
DIR="$(cd "$(dirname "$0")/../packages" 2>/dev/null && pwd)" || {
|
|
echo "packages/ not found next to $0" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Comments and blank lines are for the reader, not the package manager.
|
|
read_list() {
|
|
[ -f "$1" ] || return 0
|
|
sed -E 's/#.*//' "$1" | tr -d '\r' | awk 'NF'
|
|
}
|
|
|
|
case "$(uname -s)" in
|
|
Linux)
|
|
command -v pacman >/dev/null 2>&1 || { echo "not an Arch machine" >&2; exit 1; }
|
|
WANT=$(read_list "$DIR/arch-base.txt"; read_list "$DIR/arch-$ROLE.txt" || true)
|
|
HAVE=$(pacman -Qqe)
|
|
INSTALL="yay -S --needed --noconfirm"
|
|
command -v yay >/dev/null 2>&1 || INSTALL="sudo pacman -S --needed"
|
|
;;
|
|
Darwin)
|
|
command -v brew >/dev/null 2>&1 || { echo "Homebrew not in PATH - is this a login shell?" >&2; exit 1; }
|
|
WANT=$(read_list "$DIR/brew-$ROLE.txt")
|
|
HAVE=$(brew leaves)
|
|
INSTALL="brew install"
|
|
;;
|
|
*)
|
|
echo "unsupported platform: $(uname -s)" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Temporary files rather than process substitution: <(...) is not POSIX, and
|
|
# /bin/sh is dash on Debian and a POSIX-mode bash on macOS. This script has to
|
|
# run on both.
|
|
TMP=$(mktemp -d)
|
|
trap 'rm -rf "$TMP"' EXIT INT TERM
|
|
printf '%s\n' "$WANT" | awk 'NF' | sort -u > "$TMP/want"
|
|
printf '%s\n' "$HAVE" | awk 'NF' | sort -u > "$TMP/have"
|
|
MISSING=$(comm -23 "$TMP/want" "$TMP/have")
|
|
EXTRA=$(comm -13 "$TMP/want" "$TMP/have")
|
|
WANT=$(cat "$TMP/want")
|
|
HAVE=$(cat "$TMP/have")
|
|
|
|
count() { printf '%s\n' "$1" | awk 'NF' | wc -l | tr -d ' '; }
|
|
|
|
case "${1:-check}" in
|
|
check)
|
|
echo "role: $ROLE wanted: $(count "$WANT") installed: $(count "$HAVE")"
|
|
echo
|
|
echo "missing ($(count "$MISSING")):"
|
|
printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /'
|
|
echo
|
|
echo "extra, not touched ($(count "$EXTRA")):"
|
|
printf '%s\n' "$EXTRA" | awk 'NF' | sed 's/^/ /'
|
|
;;
|
|
install)
|
|
if [ -z "$(printf '%s\n' "$MISSING" | awk 'NF')" ]; then
|
|
echo "nothing to install"
|
|
exit 0
|
|
fi
|
|
echo "installing:"
|
|
printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /'
|
|
# shellcheck disable=SC2086
|
|
$INSTALL $(printf '%s ' $MISSING)
|
|
;;
|
|
*)
|
|
echo "usage: $0 [check|install]" >&2
|
|
exit 1
|
|
;;
|
|
esac
|