N?? / show marks the upstream candidate with "*" instead of ">" (the
prompt, which read as a command in logs), and every line but the note
gets a two-character gutter: " " for the installed record, "* " for the
candidate, so keys and colons line up. The plain native fallback (zypper,
unmergeable records) and the not-installed candidate use the same gutter.
pacman records (-Qi with -Si or the AUR helper's -Sia) are now compared
the way they are meant:
- values unwrapped (continuation lines joined, runs of blanks as one):
yay wraps Depends On / Optional Deps at 80 columns even without a
terminal unless COLUMNS=0, which the helper now gets for -Qua and -Sia
- Depends On, Provides, Conflicts With, Make/Check Deps and the other
lists as sets of exact tokens in any order (a constraint is a change)
- Optional Deps by package names ("name: description [installed]" in
-Qi, bare names from the AUR)
- a differing value is one line like -Qi, Optional Deps one entry per
line; candidate-only fields sit next to their counterpart (Repository
after Version, AUR URL after URL, Maintainer after Packager, ...)
Verified on recorded claude-desktop records (paru 2.1.0, yay 13.0.1, the
installed 2.9939.4-1 from the AUR's own .SRCINFO); discovery/pacman.md
section 15.2.
45 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) |
aur.sh |
proof of section 15 (paru and yay output formats; needs network: Arch mirrors, the AUR, GitHub) |
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... |
| AUR (paru / yay, section 15) | list <helper> -Qua, record <helper> -Sia PKG, upgrade <helper> -Sua [--noconfirm] [--ignore a,b], never as root |
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).
15. AUR helpers: paru and yay (-Qua, -Sia, -Sua)
Verified 2026-10-10 on archlinux:latest (pacman 7.1.0 / libalpm 16.0.1) with paru 2.1.0 and yay 13.0.1,
both built from the AUR, run as the unprivileged user bob (wheel, NOPASSWD sudo) with stdin from /dev/null,
stdout piped, LC_ALL=C. paru-bin 2.1.0 does not start on this pacman (paru: error while loading shared libraries: libalpm.so.15: cannot open shared object file, exit 127: it links libalpm 15), so paru was built from
the paru package (cargo); yay is yay-bin. Outdated AUR package: pfetch 1.10.0-1 (built from an older commit
of its AUR git), 1.11.0-1 in the AUR; devel package: pfetch-git; foreign package that is not in the AUR:
su-local-only (hand-built). Proof: discovery/pacman/aur.sh (needs network):
docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/aur.sh
15.1 -Qua: the AUR upgrades
$ paru -Qua $ yay -Qua
pfetch 1.10.0-1 -> 1.11.0-1 pfetch 1.10.0-1 -> 1.11.0-1 [122d13h]
[exit 0] [exit 0]
-
Format
name SP old SP "->" SP new [SP extra], likepacman -Qu; no colour when piped (both). yay 13 appends the time since the package'sLast Modifiedin the AUR ([122d13h]: 9 Jun 2026 to 10 Oct 2026); paru nothing.paru -Qu/yay -Quprint the same line here (repository and AUR upgrades together). -
IgnorePkg = pfetch: paru printspfetch 1.10.0-1 -> 1.11.0-1 [ignored](exit 0); yay leaves the package out (no output, exit 1). -
Nothing to upgrade: exit 1, nothing on stdout or stderr (both, with and without
--devel). -
The AUR unreachable (
127.0.0.1 aur.archlinux.orgin/etc/hosts): exit 1 with the error on stderr:paru: error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect: tcp connect error: Connection refused (os error 111): ... Connection refused (os error 111) (one line) yay: -> 1 error occurred: * request failed: Get "https://aur.archlinux.org/rpc?arg%5B%5D=paru&...&type=info&v=5": dial tcp 127.0.0.1:443: connect: connection refused (then 2 blank lines)So exit 1 with an empty stderr = nothing to upgrade; anything else = failure.
-
A foreign package that is not in the AUR is not mentioned by
-Qua(only-Suasays:: packages not in the AUR: su-local-only/-> Packages not in AUR: su-local-only). Foreign = in no sync database: with the repositories of the E2E fixture commented out, every base package is foreign to the helper. -
Devel (VCS) packages, after
paru -S pfetch-git,paru --gendb,yay -Y --gendband replacing the commit ids recorded in~/.local/state/paru/devel.toml/~/.cache/yay/vcs.jsonby older ones: paru checks them by default (paru -Qua, with or without--devel;-Suaprints:: Looking for devel upgrades...), yay only with--devel(oryay -Y --devel --save). The new version is the literallatest-commit:$ paru -Qua $ yay -Qua --devel ($ yay -Qua: exit 1, nothing) pfetch-git r432.a906ff8-1 -> latest-commit pfetch-git r432.a906ff8-1 -> latest-commit [2299d6h] -
As root:
paru -Quaworks (same output);yay -Quaworks but prints^[[1m^[[33m -> ^[[0m^[[0mAvoid running yay as root/sudo.on stdout.paru -Suaas root:error: can't install AUR package as root(exit 1);yay -Suaas root fails as well (exit 1). The helpers must run as a normal user.
15.2 -Sia PKG: the AUR record
paru -Si pfetch, paru -Sia pfetch and paru -Si --aur pfetch print the same record; yay -Si pfetch asks the
repositories only (error: package 'pfetch' was not found / -> exit status 1, exit 1), yay -Sia pfetch gives
the AUR record. Both are pacman-style, one record followed by a blank line:
$ paru -Sia pfetch $ yay -Sia pfetch (keys padded to 30)
Repository : aur Repository : aur
Name : pfetch Name / Version / Description / URL / Licenses /
Version : 1.11.0-1 Groups / Provides / Depends On / Optional Deps /
Description : A pretty system information tool ... Make Deps / Check Deps / Conflicts With /
URL : https://github.com/Un1q32/pfetch Replaces : None
AUR URL : https://aur.archlinux.org/packages/pfetch AUR URL : https://aur...
Groups : None First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC
Licenses : MIT Keywords : fetch
Provides : pfetch Last Modified : Tue 09 Jun 2026 10:43:50 PM UTC
Depends On : None Maintainer : yigits
Make Deps : None Popularity : 0.103440
Check Deps : None Votes : 48
Optional Deps : None Out-of-date : No
Conflicts With : pfetch-git
Maintainer : yigits
Votes : 48
Popularity : 0.10344
First Submitted : Sat, 16 Jan 2021 09:13:08
Last Modified : Tue, 9 Jun 2026 22:43:50
Out Of Date : No
-
Comparable with
pacman -Qi pfetch(section 14): one record per side, every:in one column per side (paru: 17, like pacman; yay: 31, re-padded by the merge), keys spelled like pacman's. Both, here identical:Name,Description,URL,Licenses,Groups,Provides,Depends On,Optional Deps,Conflicts With(yay alsoReplaces); different:Version.-Qionly:Architecture,Required By,Optional For,Installed Size,Packager,Build Date,Install Date,Install Reason,Install Script,Validated By. AUR only:Repository(aur),AUR URL,Make Deps,Check Deps,Maintainer,Votes,Popularity,First Submitted,Last Modified,Out Of Date(yay:Out-of-date, plusKeywords). -
Not in the AUR: paru
error: package 'no-such-package-zzz' was not found, yay-> No AUR package found for no-such-package-zzz, exit 1. AUR unreachable: the-Quaerrors above, exit 1. -
Optional dependencies are bare names in both (
Optional Deps : pipewire kdialog gnome-keyring kwalletforgoogle-chrome, whose AUR entries carry descriptions such aspipewire: WebRTC desktop sharing under Wayland): the descriptions are dropped.-Qiprints onename: description [installed]per line. -
yay wraps long values at 80 columns -
Depends On,Optional Deps, continuation lines indented to the value column (31) - wheneverCOLUMNSis unset or 80, also without any terminal (stdout and stdin not a tty);COLUMNS=0turns it off. paru 2.1.0 printed one line per field in every case tried (no terminal; an 80-column pty with stdout piped,COLUMNSunset, 0 or 80; stdout the pty itself withCOLUMNS=0). Verified 2026-10-10 withclaude-desktop2.31226.1-1 (39 dependencies),yay -Sia claude-desktopwithoutCOLUMNS=0:Depends On : alsa-lib at-spi2-core bubblewrap cairo dbus expat gcc-libs glib2 glibc gtk3 ... xdg-desktop-portal xdg-utils qemu-system-x86 edk2-ovmf Optional Deps : gnome-keyring kwallet libayatana-appindicator xdg-desktop-portal-gtk xdg-desktop-portal-kde qemu-system-aarch64 edk2-aarch64(paru:
Depends On : alsa-lib at-spi2-core bubblewrap ... edk2-ovmfon one line.) The installed side for that comparison,pacman -Qi claude-desktopof 2.9939.4-1 (a stand-in package built from the AUR's own.SRCINFOof that version, commit b7d5715, plus a dummykwalletfor[installed]):Depends Onone line withoutbubblewrap,libpipewire,qemu-system-x86,edk2-ovmf;Optional Deps : gnome-keyring: credential storage via Secret Service (GNOME)/kwallet: credential storage (KDE Plasma) [installed]/ ... one per line. These records are the unit fixtures (tests/unit/data/pacman/Qi.txt,tests/unit/data/aur/Sia-{paru,yay}-claude-desktop.txt).
15.3 -Sua: the AUR upgrade
--ignorecovers AUR packages (both):paru -Sua --noconfirm --ignore pfetch->there is nothing to do, stderrwarning: pfetch: ignoring package upgrade (1.10.0-1 => 1.11.0-1), exit 0;yay -Sua --noconfirm --ignore pfetch->-> pfetch: ignoring package upgrade (1.10.0-1 => 1.11.0-1)andthere is nothing to do(stdout), exit 0. Names that are not installed (--ignore su-dummy-x,su-dummy-y) are accepted and pfetch is upgraded.--noconfirm, stdin/dev/null: both upgrade without asking (exit 0). paru still prints:: Proceed to review? [Y/n]:and answers it; yay's menus (Packages to exclude,Packages to cleanBuild?,Diffs to show?) take their default (none). The helper installs withsudo pacman -Uitself (here NOPASSWD; otherwise sudo asks for the user's password on the terminal).- Without
--noconfirm, stdin/dev/null: paru takes the defaults at end of input and upgrades (printing the PKGBUILD as its review); yay exits 1 at its first menu (:: 1 package to upgrade/install....Excluding packages may cause partial upgrades), nothing upgraded. So--noconfirmwhenever stdin is not a terminal; on a terminal the helper keeps its prompts (paru's PKGBUILD review, yay's diff menu). - Devel:
paru -Sua --noconfirmrebuiltpfetch-gitfor thelatest-commitrow (warning: pfetch-git-r432.a906ff8-1 is up to date -- reinstalling, since only the recorded commit was faked). runuser -u bob -- env(as root) setsHOME=/home/bob,USER,LOGNAMEandMAIL(PAM) and keepsPATHand the working directory;runuseris in util-linux (the base image has it,sudoit has not).
15.4 Chosen
| need | command (as the invoking user: runuser -u "$SUDO_USER" -- env HOME=<home>, else sudo -u ...; unprivileged runs as they are; a root login without SUDO_USER: no AUR) |
|---|---|
| helper | --aur-helper=paru|yay|none / --no-aur, else paru, else yay |
| list | <helper> -Qua: rows name old new aur (flag aur, [ignored] -> held); a name pacman -Qu lists wins; exit 1 + empty stderr = none, other failures: warn, no AUR rows |
N?? |
pacman -Qi merged with <helper> -Sia PKG (section 14's merge; pacman mode: values unwrapped, lists as token sets, Optional Deps as name sets, AUR-only fields next to their counterparts) |
COLUMNS |
COLUMNS=0 for -Qua and -Sia (yay's wrapping); the upgrade keeps the terminal's |
| upgrade | after the repository step: <helper> -Sua [--noconfirm when stdin is not a terminal] [--ignore <excluded rows>] |