--- type: reference title: package desired state description: packages/*.pkgs plus the two tools that keep a machine and its manifest agreeing — install.sh installs desired-minus-installed, pkg-inventory.sh reports installed-minus-declared. tags: - packages - pacman - provisioning - fleet - blankpad verified: 2026-10-21 timestamp: 2026-10-20 --- # Package desired state Two tools, one invariant: **the manifests must describe the machine.** A rebuild is only faithful if both directions are checked. |tool|direction|writes| |---|---|---| |`scripts/install.sh`|desired → installed|installs `desired − installed` with `--apply`; never removes| |`scripts/pkg-inventory.sh`|installed → declared|nothing — read-only report| ## Manifests `packages/common.pkgs` (every host) + `packages/.pkgs` (this host) + `packages/aur.pkgs` (AUR-only, installed through `yay`). **One parser reads them for both tools**: `scripts/lib/manifest.sh` — `sed 's/#.*//'` then the first field, so `#` comments trail freely and one token per line is the whole grammar; an unreadable manifest is a loud failure rather than an empty desired list. `tests/manifest-parser.bats` owns that contract. The two scripts carried verbatim copies of the body until 2026-10-21, which is exactly how a manifest line can come to mean two things to the two halves of the same check. An unknown `--host` gets the common manifest only: a wrong-host run is a no-op, not a hazard. ## Rebuild a workstation 1. Install the OS (CachyOS, niri/noctalia profile). The installer's own package set is *not* in the manifests and must not be — see the baseline law below. 2. Clone this repo to `~/.omp`, then `scripts/install.sh --apply` — packages first, because the enablement step needs `sshd` and `firewalld` present ([dotfiles-deploy](dotfiles-deploy.md)). 3. `scripts/deploy.sh sync` for the config tier ([dotfiles-deploy](dotfiles-deploy.md)). 4. `scripts/pkg-inventory.sh --emit` on a *known-good* machine and fold the output into `packages/.pkgs` / `packages/aur.pkgs` before step 2 — that is how a hand install stops being invisible to the next rebuild. ## pkg-inventory report ``` undeclared additions (declare these): N ← exit 1 when N > 0 repo (sync dbs): → belongs in packages/.pkgs 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, never drift (see the law below) flatpak: → reported only, install.sh installs no flatpaks ``` Exit codes: `0` clean · `1` drift · `2` usage · `3` classification impossible. Overrides `PKGINV_REPO` / `PKGINV_HOST` / `PKGINV_LOG` / `PKGINV_BASELINE` exist for tests. ## Law: the installer's own day is the baseline Whether a package is *yours* is decided by the pacman log, not by opinion: the baseline date is the oldest `[ALPM] installed` entry — Calamares' pacstrap — and everything installed **on or before** it is installer furniture. Without that cut, a base install reports several hundred "additions" and the list nobody can act on is worse than no list. - **Read the rotated logs too** (`pacman.log.N`, `.N.gz`). On a machine older than the rotation window the install day exists only there; skip them and the whole base system is reported as hand-installed. - Calamares keeps installing the desktop for minutes after the base transaction, which is why the date cut is inclusive. ## Law: refuse rather than guess the AUR split `aur.pkgs` vs `.pkgs` is decided by whether any sync database can serve the name — Chaotic-AUR counts as a sync database, so only true AUR belongs in `aur.pkgs`. If the sync databases are unavailable the tool exits `3` instead of assuming: guessing sends every package to `yay`, which is 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 — 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 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 - **`set -o pipefail` plus `head -1`**: `head` closes the pipe, `sort` dies of `SIGPIPE`, the substitution returns `141`, and `set -e` aborts the script. It survives a small test fixture and fails on a real 1000-line install-day log — take a minimum in one pass (`awk 'END'`-style), never sort-then-first. The same applies to `awk 'NR==1{exit}'`, which closes the pipe exactly as `head` does. - **`tr '\n' ' '` swallows the final newline** and `fold` faithfully preserves that absence, so the next report label lands on the same line as the last name. `tr '\n' ' '; echo` instead. ## Related - [tools/index](index.md) · [dotfiles-deploy](dotfiles-deploy.md) (the config tier that follows) · [live-sync](live-sync.md) (the heartbeat that commits machine state) - Tests: `tests/pkg-inventory.bats` (fixtures + stubbed `pacman`, no real package database touched) and `tests/manifest-parser.bats` (the grammar above, observed as names in / names out). - Refs #51.