diff --git a/omp-docs/tools/packages.md b/omp-docs/tools/packages.md index 41d80b5..f685f13 100644 --- a/omp-docs/tools/packages.md +++ b/omp-docs/tools/packages.md @@ -8,7 +8,7 @@ tags: - provisioning - fleet - blankpad -verified: 2026-10-20 +verified: 2026-10-21 timestamp: 2026-10-20 --- @@ -48,7 +48,7 @@ undeclared additions (declare these): N ← exit 1 when N > 0 aur (needs a helper): → belongs in packages/aur.pkgs installer-provided, undeclared: N + names → a reinstall supplies these; do not declare declared, not installed: → install.sh installs these -orphans ignored: → pacman -Qtdq +orphans ignored: → pacman -Qtdq, never drift (see the law below) flatpak: → reported only, install.sh installs no flatpaks ``` @@ -78,15 +78,23 @@ how a manifest becomes a pile of source builds. Classify with `pacman -Sl` (world-readable). **`pacman -Sqq` requires root** — a tool that only reads the machine must not need sudo to answer a question about it. -**Known gap:** `aur.pkgs` is read for *every* host, so a desktop-only AUR package (a niri module, a -GUI client) cannot be declared there without asking the headless servers to build it. Such a name is -still reported as an AUR addition; leave it undeclared until `install.sh` grows a per-host AUR +**Known gap:** `aur.pkgs` is read for *every* host, so a desktop-only AUR package — the niri +module `nirimod-git`, say, or a GUI client — cannot be declared there without asking the +headless servers to build it. Such a name is still reported as an AUR addition and stays +undeclared (see the note in `packages/blankpad.pkgs`) until `install.sh` grows a per-host AUR manifest. ## Law: orphans never reach a manifest -`pacman -Qtdq` names are packages nothing depends on; emitting them would teach a fresh machine to -install the same dead weight. They are printed as `orphans ignored` and excluded from `--emit`. +Drift is `pacman -Qqe` — *explicitly* installed, therefore yours. `pacman -Qtdq` names packages +installed **as a dependency** that nothing requires any more, and a package carries exactly one +install reason, so the two sets cannot overlap (blankpad 2026-10-21: 206 explicit, 3 orphans, none +in common). That is why nothing in `pkg-inventory.sh` subtracts the orphan list from the additions: +the query is already the guard. Do not add such a subtraction — it cannot fire, and it would bury +the invariant that matters, *an implicit install is never drift no matter when it arrived*, which +`tests/pkg-inventory.bats` pins with a dependency installed after the baseline. What the +`orphans ignored` line is for is the other half: dead weight on the machine that a manifest must +never teach a fresh box to install. ## Two traps that produced this tool diff --git a/scripts/pkg-inventory.sh b/scripts/pkg-inventory.sh index 97f5386..f6e4ae9 100755 --- a/scripts/pkg-inventory.sh +++ b/scripts/pkg-inventory.sh @@ -19,11 +19,19 @@ # aur (needs a helper): → belongs in packages/aur.pkgs (yay) # installer-provided, undeclared: N a reinstall supplies these; do not declare # declared, not installed: N install.sh installs these -# orphans ignored: pacman -Qtdq — never emitted +# orphans ignored: pacman -Qtdq — never drift, never emitted # flatpak: reported only, install.sh installs no flatpaks # # --emit prints the additions as manifest lines, ready to fold into packages/. Names are bare # tokens so install.sh's `sed 's/#.*//'` + first-field parser reads them unchanged. + +# Drift is `pacman -Qqe`: explicitly installed, therefore something you asked for and a rebuild +# must declare. `pacman -Qtdq` orphans are packages installed AS A DEPENDENCY and no longer +# required, and a package carries exactly one install reason, so the two sets are disjoint by +# construction (blankpad, 2026-10-21: 206 explicit, 3 orphans, none in common). Nothing here +# subtracts the orphan list from the additions and nothing should — the query is the guard. Do not +# add such a subtraction: it cannot fire, and it would hide the real invariant (implicit installs +# are never drift, whenever they arrived) behind a line that looks like protection. # # Exit status: 0 clean · 1 drift (undeclared additions exist) · 2 usage · 3 cannot classify # (pacman log unreadable or sync databases unavailable — worse to guess than to say so, because @@ -42,23 +50,22 @@ MODE="report" while [[ $# -gt 0 ]]; do case "$1" in --emit) MODE="emit"; shift ;; - -h | --help) sed -n '2,40p' "$0"; exit 0 ;; + -h | --help) awk '/^set /{exit} NR>1' "$0"; exit 0 ;; *) echo "usage: pkg-inventory.sh [--emit]" >&2; exit 2 ;; esac done +HERE="$(dirname "$(realpath "$0")")" REPO="${PKGINV_REPO:-}" if [[ -z "$REPO" ]]; then - REPO="$(git -C "$(dirname "$(realpath "$0")")" rev-parse --show-toplevel 2>/dev/null || true)" + REPO="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null || true)" fi [[ -n "$REPO" && -d "$REPO/packages" ]] || { echo "pkg-inventory: no packages/ under ${REPO:-}" >&2; exit 2; } HOST="${PKGINV_HOST:-$(hostname)}" TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT -read_manifest() { # same parser install.sh uses, so a name means the same thing to both - sed 's/#.*//' "$1" | tr -d '\r' | awk '{if ($1) print $1}' -} +source "$HERE/lib/manifest.sh" # the same parser install.sh installs with # ---- installed state --------------------------------------------------------- # Four queries are the tool's whole interface to pacman. -Sl is the unprivileged sync listing: # -Sqq asks for root, and a read of the machine must not need sudo. Field 2 of -Sl is the package diff --git a/tests/pkg-inventory.bats b/tests/pkg-inventory.bats index 7446e94..9c22d95 100755 --- a/tests/pkg-inventory.bats +++ b/tests/pkg-inventory.bats @@ -39,15 +39,18 @@ EOF [2026-03-04T09:00:00-0800] [ALPM] installed bun (1.2-1) [2026-03-04T09:30:00-0800] [ALPM] upgraded filesystem (2026.01.01-1 -> 2026.03.01-1) [2026-03-05T09:00:00-0800] [ALPM] installed nirimod-git (r7-1) +[2026-03-07T09:00:00-0800] [ALPM] installed libstray (1.0-1) [2026-03-06T09:00:00-0800] [ALPM] removed base (3-1) EOF # --- installed state -------------------------------------------------------- # ghostty/zoxide/neovim/typora declared. libfoo: explicit, arrived with the installer. - # bun/nirimod-git: installed by hand afterwards, undeclared. python-hatchling: orphan. - printf '%s\n' base bun filesystem ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-all" + # bun/nirimod-git: installed by hand afterwards, undeclared. python-hatchling and libstray: + # installed as dependencies and now unrequired (pacman -Qtdq). libstray arrived AFTER the + # baseline, so the date filter alone cannot keep it out of drift — only the explicit query can. + printf '%s\n' base bun filesystem ghostty libfoo libstray neovim nirimod-git typora zoxide > "$TEST_TMP/local-all" printf '%s\n' bun ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-explicit" - printf '%s\n' python-hatchling > "$TEST_TMP/orphans" + printf '%s\n' python-hatchling libstray > "$TEST_TMP/orphans" # nirimod-git is deliberately ABSENT here: no sync db can serve it, which is the fact that # routes it to aur.pkgs in the test below. printf '%s\n' bun ghostty neovim typora zoxide libfoo vlc python-hatchling base filesystem > "$TEST_TMP/sync-all" @@ -131,11 +134,34 @@ ADDITIONS() { section "^undeclared additions" "^installer-provided"; } [[ "$(section "^declared," "^orphans")" == *vlc* ]] } -@test "orphans are reported as ignored and never emitted" { +@test "orphans are named on their own line, not mixed into the drift answer" { run "$SCRIPT" - [[ "$output" == *orphans*python-hatchling* ]] + [[ "$(section "^orphans" "^flatpak")" == *python-hatchling* ]] + [[ "$(ADDITIONS)" != *python-hatchling* ]] +} + +@test "a dependency installed after the baseline is never drift, orphan or not" { + # Drift means `pacman -Qqe` — you asked for this one. libstray arrived as a dependency + # after install day and is now unrequired, so the baseline filter does not exclude it: + # only the explicit query does. Swap -Qqe for -Qq and every dependency on the machine, + # dead ones included, becomes a manifest line install.sh will reinstall on a rebuild. + run "$SCRIPT" + [[ "$(ADDITIONS)" != *libstray* ]] + [[ "$output" == *"undeclared additions (declare these): 2"* ]] + [[ "$(section "^orphans" "^flatpak")" == *libstray* ]] run "$SCRIPT" --emit - [[ "$output" != *python-hatchling* ]] + [[ "$output" != *libstray* ]] +} + +@test "--help prints the header and stops before the shell" { + # The header IS the contract (report shape, exit status, seams). A hardcoded line range + # that runs past the comment block hands the reader `set -euo pipefail` as if it were + # documentation. + run "$SCRIPT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Exit status"* ]] + [[ "$output" != *"pipefail"* ]] + [[ "$output" != *"LC_ALL"* ]] } @test "--baseline moves packages installed on that date into the installer set" {