Files
skeptical-updater/discovery/pacman.md
T
bryanpedini 71d7f87aa3 feat(info): show the install reason and drop the indentation
N? and `info PKG` now print three lines, none indented:

    PKG: <description>
    reason: explicitly installed | installed as a dependency | unknown | not installed
    required by: <installed dependents, comma separated> | nothing

The reason comes from a new <be>_reason backend function: apt-mark
showauto / showmanual (dpkg decides for a package in neither list), the
Install Reason of pacman -Qi, dnf repoquery --installed %{reason} (dnf4
and dnf5 words) and the Installed line of zypper info (rpm plus libzypp's
AutoInstalled when zypper is locked). A row whose old version is "-" is
not installed and the backend is not asked. The "(installed)" qualifier
moves to --help, README and DESIGN.

The rpm E2E fixture and e2e_reset now also mark su-dummy-a and su-dummy-c
as user-installed on dnf, which records plain rpm installs as "unknown",
so 1? and 2? show both reasons on all 12 distros. The verified commands
are recorded in discovery/{apt,pacman,rpm}.md.
2026-10-10 13:32:42 +02:00

31 KiB

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 <user> -- 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 name<TAB>old<TAB>new<TAB>repo<TAB>ignored(0|1)):

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... -> name<TAB>value:

    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 -> bash<TAB>The 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
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' </dev/null -> 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/<name>/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 sourced: it defines fx_build_tar <outdir> <name> <ver> <desc> [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; <tool>; 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 = <pkg> 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).

13. Install reason (N? / info): Install Reason of pacman -Qi

Verified 2026-10-10 on archlinux:latest (pacman 7.1.0), fixture FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1, COLUMNS=0 LC_ALL=C pacman -Qi -- PKG:

su-dummy-a       Install Reason  : Explicitly installed                              rc 0
su-dummy-b       Install Reason  : Installed as a dependency for another package     rc 0   (-D --asdeps)
su-dummy-c       Install Reason  : Explicitly installed                              rc 0
su-dummy-orphan  Install Reason  : Installed as a dependency for another package     rc 0   (-U --asdeps)
no-such-pkg      error: package 'no-such-pkg' was not found                          rc 1   (also for a package only in a sync db)
  • pacman -D --asexplicit su-dummy-b / --asdeps flip the line at once. Works as nobody and with db.lck present (-Qi takes no lock). One line, no wrapping with COLUMNS=0.
  • pacman -Qi sh answers for the provider (bash), like the description and Required By already do.
  • Mapping: Explicitly installed -> explicit, Installed as a dependency for another package -> dependency, -Qi failing -> not installed, anything else (or no such line) -> unknown.
  • Fixture: su-dummy-b is installed --asdeps, a/c explicitly; e2e_reset re-applies both after its pacman -U. No change needed.