Files
bryanpedini 3af159e09a fix(show): star marker, aligned gutter and smarter AUR merge
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.
2026-10-10 16:01:51 +02:00

772 lines
45 KiB
Markdown

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