fix(pkg-inventory): state the orphan law, test the boundary it rests on, stop --help mid-code
Three things from the review (#60), one of them a finding of my own that did not survive measurement: - The reported defect — additions never subtract `pacman -Qtdq`, so an orphan can be emitted as a manifest line — is wrong. A package carries one install reason: `-Qtdq` selects installed-as-dependency, additions come from `-Qqe`, so the sets are disjoint (blankpad: 206 explicit, 3 orphans, none in common). A subtraction would be dead code that looks like a guard, so the header now says why not to add one. The packages page law was rewritten to the same effect. - What the old test should have pinned: an implicit dependency installed AFTER the baseline. Only `-Qqe` keeps it out of drift — mutate that query to `-Qq` and the new test goes red (verified), where the old assertion passed no matter what. - `--help` printed `set -euo pipefail` and the LC_ALL line as documentation: the hardcoded sed range ran past the comment block. It now stops at the first line of code, so the header can keep growing. Refs #60
This commit is contained in:
@@ -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): <names> → belongs in packages/aur.pkgs
|
||||
installer-provided, undeclared: N + names → a reinstall supplies these; do not declare
|
||||
declared, not installed: <names> → install.sh installs these
|
||||
orphans ignored: <names> → pacman -Qtdq
|
||||
orphans ignored: <names> → pacman -Qtdq, never drift (see the law below)
|
||||
flatpak: <app ids> → 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
|
||||
|
||||
|
||||
@@ -19,11 +19,19 @@
|
||||
# aur (needs a helper): <names> → 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: <names> pacman -Qtdq — never emitted
|
||||
# orphans ignored: <names> pacman -Qtdq — never drift, never emitted
|
||||
# flatpak: <app ids> 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:-<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
|
||||
|
||||
@@ -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" {
|
||||
|
||||
Reference in New Issue
Block a user