N?? and `show PKG` now print the installed record in the package
manager's own key/value layout, after one line explaining the format:
# lines starting with > come from the upstream candidate: a differing
value, or a field the installed record lacks
After each field whose candidate value differs comes the candidate's
line prefixed with "> "; fields only the candidate has are appended the
same way; installed-only fields stay; identical fields print once.
Multi-line values (pacman Optional Deps, dnf Description, RFC-822
continuation lines) are compared as blocks and a differing candidate
block gets "> " on every line. A package that is not installed shows the
candidate alone ("# not installed: this is the upstream candidate").
Merged where the backend gives comparable records for both sides:
- pacman: -Qi vs -Si REPO/PKG, REPO = the first repository -Sl lists
- apt: dpkg-query -s (Status, Conffiles, the long description; richer
than apt-cache show PKG=<installed>, which gives the archive record
when that version is still listed) vs apt-cache show --no-all-versions;
Description-xx pairs with Description and a one-line candidate
Description equal to the installed first line counts as identical
(the archives ship the long text in Translation files)
- dnf: info --installed vs info --upgrades (--available lists other
architectures first and nothing for an up-to-date package); not
installed: --available; dnf5's differently padded keys are re-padded
zypper keeps `zypper info --requires`: rpm -qi and zypper info share
almost no keys and zypper cannot print the installed record itself.
No candidate, or not exactly one record per side, keeps the old output.
Formats verified in containers and recorded in discovery/*.md. Unit
tests replay recorded pacman, apt, dnf4 and dnf5 records; the E2E suite
checks 2?? on su-dummy-b on all 12 distros.
34 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):
- The fresh image has no sync databases:
pacman -Si/-Sl/-Quare useless (and-S batssays "not found") untilpacman -Syhas run. pacman -Quhas 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.pacman -Quexits 1 when nothing is upgradable (empty output);-Su/-Syuexit 0 withthere is nothing to do.- 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 withpacman -Sup. -Qi/-Siwrap long values at$COLUMNS(even when piped) ifCOLUMNSis exported. UseCOLUMNS=0and a continuation-aware parser (lib.sh does both).pactree -l -uprints dependency strings (libncursesw.so=6-64,linux-api-headers>=4.10), not package names, andpacman-contribis not installed.- The image's
/etc/pacman.confends with a second[options]block (NoExtract lines). AppendingInclude = /etc/pacman.d/ignorepkgat EOF works by accident here; on a stock Arch host the last section is[extra]and pacman just warnsdirective 'IgnorePkg' in section 'extra' not recognized.and ignores nothing. makepkgrefuses root (exit 10);runuser -u <user> --works,sudois 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 withpacman -Su --noconfirm --ignore ...(note-Su, not-Syu: it applies exactly the reviewed DB snapshot;-Syuwould re-sync and could add newer, unreviewed versions). If the user aborts after the refresh, tell them the DB is refreshed (do not runpacman -S xwithout-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 untouchedWith the default
mktemp -dmode 0700 it fails:error: could not open file /tmp/checkdb.X/sync/download-XXXX/core.db.part: Permission denied(the sandboxed download useralpmcannot traverse it);chmod 755or--disable-sandboxfixes it. When applying, use-Syu(re-sync) since the real DB was not refreshed.lib.sh: pm_refresh_tmpdbsetsPM_OPTS=(--dbpath ...). -
checkupdates(pacman-contrib) is NOT in the base image (archlinux:latestand:base-develboth lack it andpactree), and it hard-requiresfakeroot(Cannot find the fakeroot binary; also absent fromlatest, present inbase-devel) even as root. Afterpacman -S pacman-contrib fakerootit works: prints the same-Qulines (drops[ignored]rows, per its sourcegrep -v '\[.*\]'), exit 0 = updates (verified), 2 = none (verified), 1 = error (from the source:Cannot fetch updates). It is just B withfakerootinstead ofchmod 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).-SihasRepository,Download Size,Build Date, newDepends On; it has noRequired By/Install Reason(those only exist for-Qi). -
Description only (simple, single target, piped, no
COLUMNSexported):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 byCOLUMNS=20(verified: rawCOLUMNS=20 pacman -Qi bashdoes wrap). -
Full dump = the raw command output, never wrapped when
COLUMNS=0or unset and stdout is not a tty (wrapping is driven by theCOLUMNSenv var, not by a real tty:stty cols 100+ pipe did not wrap,COLUMNS=80+ pipe did).Optional Depsis 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 foundon 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 xis invalid (error: invalid option '--ignore', exit 1):[ignored]marks onlyIgnorePkg/IgnoreGroupfrom 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/ignorepkgmust be inside[options]. Inserted right after the first header withsed -i '0,/^\[options\]/s||[options]\nInclude = /etc/pacman.d/ignorepkg|' /etc/pacman.conf(that is whatfixture.shdoes). 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 withpacman-conf IgnorePkg. -
Dependency trap (
FIXTURE_STRICT_DEP=1:su-dummy-a2.0 needssu-dummy-b>=2.0),--ignore su-dummy-b(orIgnorePkg = 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=1With
--noconfirmthe default answer is N: the whole run aborts and nothing (not evensu-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 ..., stderrerror: 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-bexits 0, upgrades onlysu-dummy-cand silently leavessu-dummy-abehind. Probably not what a skeptical user wants; at most offer it as an explicit choice after showing thepm_planfailure.
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):
- Builds
su-dummy-{a,b,c}at 1.0-1 (to install) and 2.0-1 (to offer).su-dummy-ahasdepend = 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)"). - Default builder =
tar: hand-made.pkg.tar.zstcontaining only.PKGINFO+ payload (bsdtar --zstd -cf ... .PKGINFO usr). Verified thatpacman -U,repo-add,-Qi,-Qkand-Syuaccept it without.MTREE/.BUILDINFO. No network, no fakeroot, no build user.FIXTURE_BUILDER=makepkguses real makepkg as usersu-builderviarunuser(installs fakeroot withpacman -Sy fakerootif missing, so network; setsPKGDEST,SOURCE_DATE_EPOCH,options=('!debug' '!strip')). Both verified onlatestandbase-devel; both produce identical-Quresults. repo-addcreates/srv/su-repo/su-dummy.db.tar.gz(+.dbsymlink) containing the 2.0 packages only; appends to/etc/pacman.conf:[su-dummy] SigLevel = Optional TrustAll Server = file:///srv/su-repo- Installs the 1.0 packages with
pacman -U, thenpacman -D --asdeps su-dummy-b(a and c explicit, b "Installed as a dependency for another package", sopacman -Rns su-dummy-aremoves b too). - Creates an empty
/etc/pacman.d/ignorepkgand insertsInclude = /etc/pacman.d/ignorepkgafter the first[options]header (pman layout). - 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 fooafterpman ignore barfooprintsfoo is already ignored, skipping...(the check isgrep -e "$pkg$", an unanchored suffix match) andfoois never added.pman unignore su-dummydeletes the lineIgnorePkg = su-dummy-c(sed "/IgnorePkg = su-dummy/d"is an unanchored prefix/substring match); regex characters in names are not escaped either.ignored/ignore/unignoreassume one package per line inIgnorePkg = <pkg>form and never check thatpacman.confactually includes the file in[options](silently ineffective, section 6).- The tool should read the effective list with
pacman-conf IgnorePkgand 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/--asdepsflip the line at once. Works asnobodyand withdb.lckpresent (-Qi takes no lock). One line, no wrapping withCOLUMNS=0.pacman -Qi shanswers for the provider (bash), like the description andRequired Byalready 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-bis installed--asdeps,a/cexplicitly;e2e_resetre-applies both after itspacman -U. No change needed.
14. Full info (N?? / show): -Qi merged with -Si REPO/PKG
Verified 2026-10-10 on archlinux:latest (pacman 7.1.0), COLUMNS=0 LC_ALL=C: the fixture (FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1) and a real upgradable package after pacman -Sy (glib2 2.88.3-1 installed, core 2.90.1-1).
$ pacman -Qi su-dummy-b $ pacman -Si su-dummy/su-dummy-b
Name : su-dummy-b Repository : su-dummy
Version : 1.0-1 Name : su-dummy-b
Description : Skeptical-updater dummy ... Version : 2.0-1
Architecture : any Description : Skeptical-updater dummy ...
URL : https://example.invalid/... Architecture : any
Licenses : custom URL / Licenses / Groups / Provides / Depends On /
Groups / Provides / Depends On / Optional Deps Optional Deps / Conflicts With / Replaces
Required By : su-dummy-a Download Size : 388.00 B
Optional For : None Installed Size : 4.00 B
Conflicts With / Replaces Packager : su-fixture <...>
Installed Size : 4.00 B Build Date : Tue Nov 14 22:13:20 2023
Packager : su-fixture <...> Validated By : SHA-256 Sum
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Sat Oct 10 11:55:39 2026
Install Reason : Installed as a dependency for another package
Install Script : No
Validated By : None
- Both are one record per target (blank line after it), every key padded to the same column (
:at column 17 in both, C locale), so the keys pair one to one.-Qionly:Required By,Optional For,Install Date,Install Reason,Install Script.-Sionly:Repository,Download Size. Both, usually different:Version,Installed Size,Build Date,Validated By(None/Signatureinstalled,SHA-256 Sum Signaturein the repo),Depends On/Provides/Optional Depswhen they changed. - Multi-line: only
Optional Deps(continuation lines indented to the value column, one entry each - glib2:dconf: .../glib2-devel: .../gvfs: ...); withCOLUMNS=0nothing else wraps (Providesof glib2, 6 entries, stays one line). - The candidate is
-Si REPO/PKGwith REPO = the first repository-Sllists PKG in (pacman's choice, the joinpacman_upgradableuses); plain-Si PKGprints one record per repository that has PKG. - Missing sides:
-Qiof a package only in a sync database fails (not installed: the candidate alone); a local-only package has no-Sirecord (the plain-Qistays).