30 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).