# Discovery: pacman (Arch Linux) and `pman` Everything below was run in throwaway containers (`docker run --rm -i`, `LC_ALL=C`) on 2026-10-09 against `archlinux:latest` (pacman 7.1.0 / libalpm 16.0.1, bash 5.3.20). Companion files (all in `discovery/pacman/`): | file | purpose | |--------------|---------| | `fixture.sh` | E2E fixture: dummy packages `su-dummy-{a,b,c}` 1.0 installed, 2.0 offered by a local repo | | `lib.sh` | proven bash helpers (`pm_upgradable`, `pm_field`, `pm_closure`, `pm_plan`, ...) | | `checks.sh` | executable proof of this document (`ALL CHECKS PASSED` on `archlinux:latest` and `archlinux:base-devel`) | Re-run the proof: ``` docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/fixture.sh docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/checks.sh ``` ## 0. Summary / chosen commands | need | command | |------|---------| | refresh (root) | `pacman -Sy` (then apply with `pacman -Su`, see section 1) | | upgradable list | `pacman -Qu` -> `name old -> new [ignored]`; repo from `pacman -Sl` (first repo with that name wins) | | exact transaction (incl. new deps) | `pacman -Sup --noconfirm [--ignore a,b] --print-format '%r/%n %v'` | | description | `pacman -Qi -- name` / `pacman -Si -- repo/name`, field `Description` | | installed reverse deps | `pacman -Qi` fields `Required By` / `Optional For` | | dependency closure | one `pacman -Qi` dump + awk BFS (`pm_closure`); `pactree` is not in the base image | | per-run exclusion | `pacman -Su --noconfirm --ignore a,b` (or repeated `--ignore`); globs work | | upgrade | `pacman -Su --noconfirm` (`-Syu` if you did not refresh before) | | purge | `pacman -Rns --noconfirm -- pkg...` | Top gotchas (details below): 1. The fresh image has **no sync databases**: `pacman -Si/-Sl/-Qu` are useless (and `-S bats` says "not found") until `pacman -Sy` has run. 2. `pacman -Qu` has **no `--ignore`** (`error: invalid option '--ignore'`, exit 1). Per-run exclusions must be filtered by the tool itself; `[ignored]` is only shown for IgnorePkg/IgnoreGroup from the config. 3. `pacman -Qu` exits **1** when nothing is upgradable (empty output); `-Su`/`-Syu` exit **0** with ` there is nothing to do`. 4. Excluding a package that another upgraded package needs at a higher version makes the **whole transaction fail** under `--noconfirm` (exit 1, nothing upgraded). Pre-check with `pacman -Sup`. 5. `-Qi`/`-Si` **wrap** long values at `$COLUMNS` (even when piped) if `COLUMNS` is exported. Use `COLUMNS=0` and a continuation-aware parser (lib.sh does both). 6. `pactree -l -u` prints **dependency strings** (`libncursesw.so=6-64`, `linux-api-headers>=4.10`), not package names, and `pacman-contrib` is not installed. 7. The image's `/etc/pacman.conf` ends with a second `[options]` block (NoExtract lines). Appending `Include = /etc/pacman.d/ignorepkg` at EOF works *by accident* here; on a stock Arch host the last section is `[extra]` and pacman just warns `directive 'IgnorePkg' in section 'extra' not recognized.` and ignores nothing. 8. `makepkg` refuses root (exit 10); `runuser -u --` works, `sudo` is absent. ## 1. Refreshing metadata ``` $ pacman -Sy # root; image has only /var/lib/pacman/{local,sync(empty)} before this :: Synchronizing package databases... core downloading... extra downloading... exit=0 ``` Timing: cold `-Sy` 0.21-0.25 s, warm 0.12 s (fast CDN; core.db 130 KB, extra.db 8.8 MB). A second `-Sy` prints the same "downloading..." lines (NoProgressBar), so the output cannot tell "up to date" from "refreshed". On a fresh image (no `-Sy`): ``` $ pacman -Qu ; echo $? warning: database file for 'core' does not exist (use '-Sy' to download) warning: database file for 'extra' does not exist (use '-Sy' to download) 1 $ pacman -Si bats error: package 'bats' was not found ``` How many upgrades does a fresh image have? Today only 6 (image built 5 days earlier): ``` $ pacman -Sy && pacman -Qu ca-certificates-mozilla 3.130-1 -> 3.131-1 device-mapper 2.03.42-1 -> 2.03.43-2 expat 2.8.5-1 -> 2.9.0-1 glib2 2.88.3-1 -> 2.90.1-1 hwdata 0.411-1 -> 0.412-1 (+ libsysprof-capture) ``` The count is time-dependent; tests must not assert on it. Use `FIXTURE_ISOLATE=1` (section 10) for an exact set. ### Partial-upgrade concern `pacman -Sy` alone moves the sync DB ahead of the installed packages. That is only dangerous if you then install something (`pacman -S foo`) without a full upgrade; a `-Sy` immediately followed by `-Su` is the same as `-Syu`. Two safe designs: * **A (recommended, simple):** `pacman -Sy`, list with `-Qu`, let the user exclude, then apply with `pacman -Su --noconfirm --ignore ...` (note `-Su`, not `-Syu`: it applies exactly the reviewed DB snapshot; `-Syu` would re-sync and could add newer, unreviewed versions). If the user aborts after the refresh, tell them the DB is refreshed (do not run `pacman -S x` without `-u`). * **B (no side effects, root, no extra packages):** temporary sync DB. Verified: ``` tmp=$(mktemp -d /tmp/checkdb.XXXXXX); chmod 755 "$tmp" # 755 is REQUIRED (see below) ln -s /var/lib/pacman/local "$tmp/local" pacman -Sy --dbpath "$tmp" --logfile /dev/null # exit 0 pacman -Qu --dbpath "$tmp" # same list as design A pacman -Sup --dbpath "$tmp" --print-format '%r/%n %v' # same transaction view ls /var/lib/pacman/sync # still empty: system DB untouched ``` With the default `mktemp -d` mode 0700 it fails: `error: could not open file /tmp/checkdb.X/sync/download-XXXX/core.db.part: Permission denied` (the sandboxed download user `alpm` cannot traverse it); `chmod 755` or `--disable-sandbox` fixes it. When applying, use `-Syu` (re-sync) since the real DB was not refreshed. `lib.sh: pm_refresh_tmpdb` sets `PM_OPTS=(--dbpath ...)`. * **`checkupdates` (pacman-contrib) is NOT in the base image** (`archlinux:latest` and `:base-devel` both lack it and `pactree`), and it hard-requires `fakeroot` (`Cannot find the fakeroot binary`; also absent from `latest`, present in `base-devel`) even as root. After `pacman -S pacman-contrib fakeroot` it works: prints the same `-Qu` lines (drops `[ignored]` rows, per its source `grep -v '\[.*\]'`), exit **0** = updates (verified), **2** = none (verified), 1 = error (from the source: `Cannot fetch updates`). It is just B with `fakeroot` instead of `chmod 755`; prefer B. `pacman -Qu` and `pacman -Sup` need no root (verified as a normal user, exit 0), only the refresh and the real upgrade do. ## 2. Machine-parsable upgradable list `pacman -Qu` (piped: no colour even with `Color` in pacman.conf; add `--color never` to be safe): ``` $ pacman -Qu su-dummy-a 1.0-1 -> 2.0-1 su-dummy-b 1.0-1 -> 2.0-1 su-dummy-c 1.0-1 -> 2.0-1 $ pacman -Qu -q # names only ``` With `IgnorePkg` (from `Include = /etc/pacman.d/ignorepkg` inside `[options]`, one `IgnorePkg = x` per line, several names per line allowed, globs allowed): ``` $ pacman -Qu ca-certificates-mozilla 3.130-1 -> 3.131-1 expat 2.8.5-1 -> 2.9.0-1 [ignored] glib2 2.88.3-1 -> 2.90.1-1 [ignored] # after "IgnorePkg = glib2 hwdata" ``` Format: `name SP oldver SP "->" SP newver [SP "[ignored]"]`; versions contain no spaces (epochs look like `1:1.37.2-3`). Exit 0 if >=1 line, exit 1 and empty output otherwise (`[ignored]` rows count as lines, so exit stays 0 when only ignored rows remain). Repo of the candidate: `-Qu` does not print it. Use `pacman -Sl` (`repo name version [installed[: oldver]]`): ``` $ pacman -Sl | grep -E ' (expat|glib2) ' core expat 2.9.0-1 [installed: 2.8.5-1] core glib2 2.90.1-1 [installed: 2.88.3-1] ``` A name can be in several repos. Verified with `su-dummy-b` 2.0-1 in `[su-dummy]` and 3.0-1 in a later `[su-dummy2]`: `-Qu` reports `1.0-1 -> 2.0-1` (the **first repo in pacman.conf order** wins), `-Sl` lists both rows in config order, and `-Si su-dummy-b` prints *both* blocks, so take the first `-Sl` row per name and use `repo/name` for `-Si`. Exact parse (this is `lib.sh: pm_upgradable`, output `nameoldnewrepoignored(0|1)`): ```bash awk ' FNR == NR { if (!($2 in repo)) repo[$2] = $1; next } # pacman -Sl { printf "%s\t%s\t%s\t%s\t%d\n", $1, $2, $4, repo[$1], ($5 == "[ignored]") } # pacman -Qu ' <(LC_ALL=C pacman -Sl) <(LC_ALL=C pacman -Qu) ``` ``` su-dummy-a 1.0-1 2.0-1 su-dummy 0 ca-certificates-mozilla 3.130-1 3.131-1 core 0 ``` Alternative that also yields the repo directly and is authoritative for what `-Su` will do: `pacman -Sup --print-format '%r|%n|%v|%s|%l'` (`%s` download size, `%l` URL; works as non-root; dependency order; **includes new dependencies and skips ignored packages**, which `-Qu` does not): ``` $ pacman -Sup --print-format '%r|%n|%v|%s' core|ca-certificates-mozilla|3.131-1|391549 ... ``` Verified with `su-dummy-a` 3.0 depending on a brand-new `su-dummy-new`: `-Qu` listed 3 packages, `-Sup` listed 4 (`su-dummy-new 1.0-1` appears before `su-dummy-a`), and the new package is installed "as a dependency". So a skeptical UI should show `-Qu` rows plus "also installs: ..." from `-Sup` minus `-Qu`. Not covered by `-Qu`: packages pulled in via `replaces`. ## 3. Description and full info ``` $ pacman -Qi bash | head -5 Name : bash Version : 5.3.20-1 Description : The GNU Bourne Again shell Architecture : x86_64 URL : https://www.gnu.org/software/bash/bash.html $ pacman -Si su-dummy/su-dummy-a | head -4 # the candidate (new version) Repository : su-dummy Name : su-dummy-a Version : 2.0-1 Description : Skeptical-updater dummy package A ``` * Installed: `pacman -Qi -- name`; candidate: `pacman -Si -- repo/name` (always qualify with the repo, see the two-repo note above). `-Si` has `Repository`, `Download Size`, `Build Date`, new `Depends On`; it has **no** `Required By`/`Install Reason` (those only exist for `-Qi`). * Description only (simple, single target, piped, no `COLUMNS` exported): `pacman -Qi -- bash | sed -n 's/^Description *: //p'`. * Robust version (any number of targets, wrapped lines joined, field split at the first colon) is `lib.sh: pm_field -Qi|-Si Field target...` -> `namevalue`: ```bash pm_field() { local op=$1 field=$2; shift 2 LC_ALL=C COLUMNS=0 pacman "$op" -- "$@" 2>/dev/null | awk -v f="$field" ' function flush() { if (name != "") print name "\t" val; name = ""; val = ""; infield = 0 } /^[ \t]*$/ { flush(); next } /^[^ \t]/ { infield = 0; i = index($0, ":"); k = substr($0, 1, i-1); sub(/[ \t]+$/, "", k) v = substr($0, i+1); sub(/^[ \t]+/, "", v) if (k == "Name") name = v; else if (k == f) { val = v; infield = 1 }; next } infield { sub(/^[ \t]+/, ""); val = val " " $0 } END { flush() }'; } ``` `pm_field -Qi Description bash` -> `bashThe GNU Bourne Again shell`; also unaffected by `COLUMNS=20` (verified: raw `COLUMNS=20 pacman -Qi bash` does wrap). * Full dump = the raw command output, never wrapped when `COLUMNS=0` or unset and stdout is not a tty (wrapping is driven by the `COLUMNS` env var, not by a real tty: `stty cols 100` + pipe did not wrap, `COLUMNS=80` + pipe did). `Optional Deps` is the exception: one entry per line (continuation lines are *new entries*, not wraps): ``` Optional Deps : diffutils: for pacdiff fakeroot: for checkupdates ``` * Missing target: `error: package 'x' was not found` on stderr, exit 1, other targets still printed. ## 4. Installed reverse dependencies `pacman -Qi` fields (names only, no versions, two-space separated, `None` when empty; computed over installed packages and already resolved through provides, e.g. `bash` is listed as a dependent of `readline` although it depends on `libreadline.so=8-64`): ``` $ pacman -Qi bash | grep -E '^(Required By|Optional For)' Required By : audit base bzip2 ca-certificates-utils device-mapper e2fsprogs gawk ... Optional For : ncurses pcre2 tzdata ``` ```bash pm_required_by() { pm_field -Qi 'Required By' "$1" | cut -f2- | tr -s ' ' '\n' | grep -vxE 'None|' | sort -u; } pm_optional_for() { pm_field -Qi 'Optional For' "$1" | cut -f2- | tr -s ' ' '\n' | grep -vxE 'None|' | sort -u; } ``` Proven: `pm_required_by su-dummy-b` -> `su-dummy-a`; `pm_required_by su-dummy-a` -> empty; `pm_optional_for bash` -> `ncurses pcre2 tzdata`; `pm_required_by glibc | wc -l` -> 111 (a 1021-char line). Note `-Si` does not have these fields; for the candidate, reverse deps = the installed ones. ## 5. Recursive dependency closure restricted to installed packages Premise check: **`pacman -Qq`/`-Qi`/`-Q` DO resolve provides and version-constrained dependency strings in pacman 7.1** (they use the satisfier lookup), so no special handling is needed on this version: ``` $ pacman -Qq sh -> bash (exit 0) $ pacman -Qq 'glibc>=2.38' -> glibc (exit 0) $ pacman -Qq libreadline.so=8-64 -> readline (exit 0) $ pacman -Qq 'glibc>=99' -> error: package 'glibc>=99' was not found (exit 1) $ pacman -T sh ; echo $? -> (nothing) 0 ; pacman -T nonexist -> "nonexist", exit 127 $ pacman -Qqs '^sh$' -> bash (-Qs also searches provides, but it is a regex search; do not use) ``` (Unquoted `>=` is a shell redirect, always quote dependency strings. I did not verify older pacman releases, so `pm_closure` below does not rely on this.) `pactree` (pacman-contrib, **absent** from `latest` and `base-devel`): after installing it, `pactree -l -u bash` prints dependency strings, not names: ``` $ pactree -l -u bash bash readline glibc linux-api-headers>=4.10 tzdata filesystem iana-etc libncursesw.so=6-64 <- provided name, the package "ncurses" itself never appears libgcc libstdc++ $ pactree -l -u pacman | wc -l -> 108 (contains libcrypt.so=2-64, libp11-kit=0.26.5-1, ca-certificates-utils>=20181109-3, ...) ``` So to use it you must resolve every line: `pactree -l -u P | xargs -r pacman -Qq -- | sort -u` (works on 7.1). `pactree -r -l -u P` is the reverse closure. `pactree -l -u nonexist` -> `error: package 'nonexist' not found`, exit 1. **Pure-pacman implementation** (`lib.sh: pm_closure`): a single `pacman -Qi` dump (all installed packages, ~10 ms) parsed by one awk process that builds `name`, `Provides` -> provider map, `Depends On`, `Required By`, `Optional For`, then BFS. A dependency string `d` is resolved as: strip the constraint (`sub(/[<>=].*/,"")`); if it is a real installed package use it; otherwise use every installed package whose `Provides` has that base name. Usage: ``` pm_closure bash # forward closure, roots excluded, sorted, one real name per line pm_closure -r glibc # reverse closure via "Required By" (add -o to follow "Optional For" too) pm_closure sh # roots may be provided names -> same as bash ``` Sample: `pm_closure bash` -> `filesystem glibc iana-etc libgcc libstdc++ linux-api-headers ncurses readline tzdata` (9 packages; pactree printed 10 strings, including `libncursesw.so=6-64` for `ncurses`). Proof (`checks.sh`, all PASS): for `bash` (9), `pacman` (107) and `su-dummy-a` (1), the output equals `pactree -l -u P | xargs -r pacman -Qq -- | sort -u` minus the root; reverse closures of `glibc` and `bash` equal `pactree -r -l -u`. If several installed packages provide the same name the closure is a superset of pactree's single pick (does not happen in the base image). Optional deps are excluded (as in pactree's default). ## 6. Per-run exclusion Both forms work (verified, `--noconfirm`, fixture): ``` pacman -Su --noconfirm --ignore su-dummy-a,su-dummy-c # comma separated pacman -Su --noconfirm --ignore su-dummy-a --ignore su-dummy-c # repeated pacman -Su --noconfirm --ignore 'su-dummy-[ab]' # globs work, like IgnorePkg ``` Result: `exit=0`, only `su-dummy-b` upgraded; the warning goes to **stderr**: ``` warning: su-dummy-a: ignoring package upgrade (1.0-1 => 2.0-1) warning: su-dummy-c: ignoring package upgrade (1.0-1 => 2.0-1) ``` Ignoring everything: stdout `:: Starting full system upgrade...` + ` there is nothing to do`, warnings on stderr, exit 0. * `pacman -Qu --ignore x` is **invalid** (`error: invalid option '--ignore'`, exit 1): `[ignored]` marks only `IgnorePkg`/`IgnoreGroup` from the configuration. Effective list wherever it was configured: `pacman-conf IgnorePkg` (one pattern per line; `IgnorePkg = glib2 hwdata` -> two lines). * Include placement: `Include = /etc/pacman.d/ignorepkg` must be inside `[options]`. Inserted right after the first header with `sed -i '0,/^\[options\]/s||[options]\nInclude = /etc/pacman.d/ignorepkg|' /etc/pacman.conf` (that is what `fixture.sh` does). Appended at EOF of a stock config (last section `[extra]`): `warning: config file /etc/pacman.d/ignorepkg, line 1: directive 'IgnorePkg' in section 'extra' not recognized.` and the package is still upgraded. After writing, verify with `pacman-conf IgnorePkg`. * **Dependency trap** (`FIXTURE_STRICT_DEP=1`: `su-dummy-a` 2.0 needs `su-dummy-b>=2.0`), `--ignore su-dummy-b` (or `IgnorePkg = su-dummy-b`): ``` warning: su-dummy-b: ignoring package upgrade (1.0-1 => 2.0-1) warning: cannot resolve "su-dummy-b>=2.0", a dependency of "su-dummy-a" :: The following package cannot be upgraded due to unresolvable dependencies: su-dummy-a :: Do you want to skip the above package for this upgrade? [y/N] :: unable to satisfy dependency 'su-dummy-b>=2.0' required by su-dummy-a error: failed to prepare transaction (could not satisfy dependencies) exit=1 ``` With `--noconfirm` the default answer is N: the whole run aborts and **nothing** (not even `su-dummy-c`) is upgraded. Same for excluding a *new dependency* (`--ignore su-dummy-new`). Pre-check without root/side effects: `pacman -Sup --noconfirm --ignore su-dummy-b --print-format '%r/%n %v' exit 1, stdout `:: unable to satisfy dependency ...`, stderr `error: failed to prepare transaction ...` (`lib.sh: pm_plan [skip...]`; exit 0 and an empty list when everything is ignored). `--ask 16` (verified) answers that prompt "yes": `pacman -Su --noconfirm --ask 16 --ignore su-dummy-b` exits 0, upgrades only `su-dummy-c` and silently leaves `su-dummy-a` behind. Probably not what a skeptical user wants; at most offer it as an explicit choice after showing the `pm_plan` failure. ## 7. Non-interactive upgrade, exit codes ``` $ pacman -Syu --noconfirm; echo $? # upgrades; prints the ":: Proceed with installation? [Y/n]" line but auto-answers 0 $ pacman -Syu --noconfirm # nothing left :: Synchronizing package databases... su-dummy downloading... :: Starting full system upgrade... there is nothing to do <- leading space; exit 0 $ pacman -Qu; echo $? # nothing upgradable: no output 1 ``` | situation | exit | message | |-----------|------|---------| | upgrade ok / nothing to do | 0 | ` there is nothing to do` (stdout) | | `pacman -Qu` nothing upgradable | **1** | (empty) | | not root (`-Sy`, `-Su`, `-Rns`) | 1 | `error: you cannot perform this operation unless you are root.` | | db locked (`/var/lib/pacman/db.lck`) | 1 | `error: failed to synchronize all databases (unable to lock database)` (`-Qu`/`-Qi` still work, exit 0) | | excluded dep unsatisfiable | 1 | see section 6 | | `checkupdates` | 0 updates / 2 none / 1 error | | `--noconfirm` answers every question with its default: install prompts are Y, but "skip unresolvable package?", conflict removal and HoldPkg prompts are N (they abort with exit 1). Messages are split over stdout and stderr (`warning:`/`error:` -> stderr, `:: unable to satisfy ...` -> stdout), so capture both. ## 8. Purge ``` $ pacman -Rns --noconfirm su-dummy-a ; echo $? # a explicit, b installed --asdeps and only needed by a removing su-dummy-a... removing su-dummy-b... # -s also removed the now-unneeded dependency 0 $ pacman -Rns --noconfirm su-dummy-b ; echo $? # required by a checking dependencies... (stdout) :: removing su-dummy-b breaks dependency 'su-dummy-b' required by su-dummy-a error: failed to prepare transaction (could not satisfy dependencies) (stderr) 1 # nothing removed $ pacman -Rns --noconfirm nope ; echo $? -> error: target not found: nope 1 $ pacman -Rns --noconfirm su-dummy-c nope -> same error, exit 1, su-dummy-c NOT removed (all or nothing) $ pacman -Rns --noconfirm su-dummy-b su-dummy-a -> 0 (order irrelevant, both removed) $ pacman -Rns --noconfirm pacman -> breaks dependency 'pacman' required by base, exit 1 $ pacman -Rdd --noconfirm pacman -> ":: HoldPkg was found in target list. Do you want to continue? [y/N]" exit 1 ``` `-R` without `-s` leaves the dependency as an orphan (`pacman -Qdtq` -> `su-dummy-b`). ## 9. Base image facts | item | `archlinux:latest` (= `archlinux:base`, same manifest digest) | `archlinux:base-devel` | |------|-----|-----| | exists | yes; also `archlinux:multilib-devel` | yes (pulled for this test, 950 MB vs 399 MB) | | bash | 5.3.20 | 5.3.20 | | pacman | 7.1.0 (libalpm 16.0.1) | 7.1.0 | | installed packages | 137 | 170 | | sudo | **no** | yes (`/etc/sudoers` has only `root ALL=(ALL:ALL) ALL`) | | python3 | **no** (core/python 3.14.7-1, 13.5 MB download) | yes | | fakeroot | **no** (core/fakeroot 1:1.37.2-3, 77 KB) | yes | | makepkg | yes (`/usr/sbin/makepkg`) but unusable without fakeroot | yes, works | | gcc/make/debugedit | no | yes | | pacman-contrib (`checkupdates`, `pactree`) | no (extra/pacman-contrib 1.13.1-1) | no | | bats / git | no / no | no / no | | su, runuser, bsdtar, repo-add, pacman-conf, vercmp, curl, awk(gawk) | yes | yes | | default `$LANG` | `C.UTF-8` (set `LC_ALL=C` anyway) | same | | sync DBs | **absent** until `pacman -Sy` | absent | `which` is not installed; use `command -v`. All binaries live in `/usr/bin` (`/usr/sbin` is a symlink). **bats**: `extra/bats 1.14.0-1` (installs `parallel`, `perl`, `db5.3` as deps). After `pacman -Sy`: `pacman -S --noconfirm bats` took 6.3 s, `bats --version` -> `Bats 1.14.0`, a smoke test printed `1..1` / `ok 1 hello`. Without a prior `-Sy`: `pacman -Si bats` -> `error: package 'bats' was not found`. (Installing straight after `-Sy` works today; the proper form on a stale image is `pacman -Syu` first.) **makepkg as root** (exit 10): ``` ==> ERROR: Running makepkg as root is not allowed as it can cause permanent, catastrophic damage to your system. ``` (`--asroot` no longer exists: `makepkg: invalid option '--asroot'`.) Recipe, verified in both images (on `latest` first `pacman -Sy --noconfirm fakeroot`; the base-devel image already has it): ``` useradd -m builder runuser -u builder -- bash -c 'cd /home/builder/pkg && makepkg --force --nodeps --noconfirm' # exit 0 su -s /bin/bash builder -c 'cd /home/builder/pkg && makepkg --force --nodeps' # also works sudo -u builder ... # works in base-devel (verified); sudo does not exist in archlinux:latest ``` Use `PKGDEST=/dir` (writable by the build user!) to choose where the `.pkg.tar.zst` goes and `options=('!debug' '!strip')` in the PKGBUILD for `arch=('any')` payloads: without it makepkg 7 aborts on `latest` with `==> ERROR: Cannot find the debugedit binary required for including source files in debug packages.` (debugedit is only in base-devel). `--nodeps` skips the dependency check (no sudo needed). ## 10. E2E fixture (`discovery/pacman/fixture.sh`) ``` docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/fixture.sh fixture: builder=tar isolate=0 repo=/srv/su-repo fixture: installed: su-dummy-a 1.0-1;su-dummy-b 1.0-1;su-dummy-c 1.0-1; fixture: repo list: core extra su-dummy exit 0 ``` What it does (root, inside the container): 1. Builds `su-dummy-{a,b,c}` at 1.0-1 (to install) and 2.0-1 (to offer). `su-dummy-a` has `depend = su-dummy-b`. Each ships `/usr/share/su-dummy//VERSION` (contains the version) so tests can check an upgrade really happened. Descriptions: `Skeptical-updater dummy package A` (B has "(dependency of A)", C "(independent)"). 2. **Default builder = `tar`**: hand-made `.pkg.tar.zst` containing only `.PKGINFO` + payload (`bsdtar --zstd -cf ... .PKGINFO usr`). Verified that `pacman -U`, `repo-add`, `-Qi`, `-Qk` and `-Syu` accept it without `.MTREE`/`.BUILDINFO`. No network, no fakeroot, no build user. `FIXTURE_BUILDER=makepkg` uses real makepkg as user `su-builder` via `runuser` (installs fakeroot with `pacman -Sy fakeroot` if missing, so network; sets `PKGDEST`, `SOURCE_DATE_EPOCH`, `options=('!debug' '!strip')`). Both verified on `latest` and `base-devel`; both produce identical `-Qu` results. 3. `repo-add` creates `/srv/su-repo/su-dummy.db.tar.gz` (+`.db` symlink) containing the 2.0 packages only; appends to `/etc/pacman.conf`: ``` [su-dummy] SigLevel = Optional TrustAll Server = file:///srv/su-repo ``` 4. Installs the 1.0 packages with `pacman -U`, then `pacman -D --asdeps su-dummy-b` (a and c explicit, b "Installed as a dependency for another package", so `pacman -Rns su-dummy-a` removes b too). 5. Creates an empty `/etc/pacman.d/ignorepkg` and inserts `Include = /etc/pacman.d/ignorepkg` after the first `[options]` header (pman layout). 6. Does **not** sync (unless `FIXTURE_SYNC=1`), so the tool under test performs the refresh itself. Result after `pacman -Sy`: ``` $ pacman -Qu | tail -3 su-dummy-a 1.0-1 -> 2.0-1 su-dummy-b 1.0-1 -> 2.0-1 su-dummy-c 1.0-1 -> 2.0-1 $ pacman -Sl | grep dummy su-dummy su-dummy-a 2.0-1 [installed: 1.0-1] ``` Knobs: `FIXTURE_ISOLATE=1` comments out `[core]`/`[extra]` so `-Qu` lists **exactly** the 3 dummies, works offline and is deterministic (default mode additionally lists whatever real upgrades the image has, 6 today; filter with `grep '^su-dummy-'`); `FIXTURE_STRICT_DEP=1` makes a-2.0 depend on `su-dummy-b>=2.0` (the section 6 trap); `FIXTURE_REPO`, `FIXTURE_SYNC`. The file can be `source`d: it defines `fx_build_tar [dep...]` and does not enable `set -e` in the caller (used to publish extra packages, e.g. the `su-dummy-new` and two-repo experiments above). Typical flow in a test: `FIXTURE_ISOLATE=1 bash fixture.sh; pacman -Sy; ; pacman -Q su-dummy-a` -> `su-dummy-a 2.0-1`, `cat /usr/share/su-dummy/su-dummy-a/VERSION` -> `2.0`. Re-running the fixture in the same container is safe (verified): `[su-dummy]` and the `Include` are not duplicated, the repo is rebuilt, and if the packages were already upgraded to 2.0 they are *downgraded* back to 1.0 by `pacman -U` (so it also works as a reset between test cases). ## 11. Timings (this host, fast CDN, three runs agree within 0.05 s) | step | time | |------|------| | container start (`docker run --rm archlinux:latest true`) | 0.30 s | | `pacman -Sy` cold / warm | 0.21-0.25 s / 0.12 s | | fixture (tar builder, includes 3 installs + repo-add) | 0.23 s | | fixture (makepkg builder, includes `pacman -Sy fakeroot`, 6 makepkg runs) | 7.3-7.7 s (one cold run: 12.9 s) | | `pm_upgradable` (`-Sl` + `-Qu` + awk) | 0.15 s | | `pm_closure pacman` (107 packages) | 0.02 s | | `pacman -S --noconfirm bats` | 6.3 s | | `pacman -S pacman-contrib` | 0.4 s | | whole `checks.sh` incl. container start, contrib install and a full real `-Su` | ~5-8 s | | image pull `archlinux:latest` | ~10 s (399 MB); `base-devel` 950 MB | ## 12. pman mapping and bugs found pman (`/home/bryanpedini/projects/sources/pman/pman.sh`) is a bash `case` over verbs; it prepends `sudo` when not root and the verb needs it (`config`, `install`, `update`, `upgrade`, `remove`, `purge`, `clean`, `provides`, `providefiles`, `reflector`, `ignore`, `unignore` when `$UID != 0`; it exits 1 if sudo is absent, as in the base image when not root: verified `pman upgrade` as an unprivileged user in `archlinux:latest` -> exit 1; in that container the `sudo command needed but not found` message itself was swallowed by `echo ... > /dev/stderr` failing with `Permission denied`, a side effect of `runuser` + docker's root-owned stderr pipe). | pman verb | runs | skeptical-updater uses | |-----------|------|------------------------| | `update` | `pacman -Sy` then prints `N packages are available for update` (`pacman -Qu \| wc -l`, **counts `[ignored]` rows**) | refresh | | `upgradable` | `pacman -Qu` | list | | `localinfo` / `info` | `pacman -Qi` / `-Si` | info (`info` shows both repos if duplicated) | | `upgrade` | `pacman -Syu` + extra args (`pman upgrade --noconfirm --ignore a,b` works, verified) | apply (prefer `-Su` after a refresh) | | `purge` / `remove` | `pacman -Rns` / `-Rs` | purge | | `ignored` | `echo "$(sed 's/IgnorePkg = //' /etc/pacman.d/ignorepkg \| sort)"` | read | | `ignore p...` | appends `IgnorePkg = p` to `/etc/pacman.d/ignorepkg` | write | | `unignore p...` | `sed -i "/IgnorePkg = p/d"` | write | Verified end to end with the fixture as root: `pman update` -> `3 packages are available for update`; `pman ignore su-dummy-c` + `pman upgradable` -> `su-dummy-c 1.0-1 -> 2.0-1 [ignored]`; `pman upgrade --noconfirm` printed `warning: su-dummy-c: ignoring package upgrade (1.0-1 => 2.0-1)` and upgraded a and b (exit 0); `pman purge --noconfirm su-dummy-a` removed a and b. pman bugs (verified; relevant if the tool reuses `/etc/pacman.d/ignorepkg`): * `pman ignore foo` after `pman ignore barfoo` prints `foo is already ignored, skipping...` (the check is `grep -e "$pkg$"`, an unanchored suffix match) and `foo` is never added. * `pman unignore su-dummy` deletes the line `IgnorePkg = su-dummy-c` (`sed "/IgnorePkg = su-dummy/d"` is an unanchored prefix/substring match); regex characters in names are not escaped either. * `ignored`/`ignore`/`unignore` assume one package per line in `IgnorePkg = ` form and never check that `pacman.conf` actually includes the file in `[options]` (silently ineffective, section 6). * The tool should read the effective list with `pacman-conf IgnorePkg` and write with anchored matches (`grep -qxF "IgnorePkg = $pkg"`, `sed -i "/^IgnorePkg = ${pkg}\$/d"` after escaping).