feat(aur): list and upgrade AUR packages through paru or yay on Arch

On the pacman backend, an installed AUR helper (paru, else yay;
--aur-helper=paru|yay|none, --no-aur) adds the AUR updates to the review
list. The helper never runs as root: when the tool is root through sudo it
runs as SUDO_USER (runuser -u, else sudo -u, with that user's HOME, from
that home); unprivileged it runs as is; a root login gets no AUR, with a
warning. The banner names the helper ("+ AUR via paru").

- listing: <helper> -Qua (also with --no-refresh), rows with repository
  "aur"; [ignored] is shown [held]; a failing -Qua warns and the session
  goes on with the repository rows
- every row has a marker column after the number: "*" for AUR rows, a
  space otherwise (on every backend)
- N?? merges pacman -Qi with <helper> -Sia; N?, N+, Nx stay with pacman;
  N- excludes through the helper's --ignore
- <enter>: the repository step exactly as before, then <helper> -Sua
  --ignore <excluded> as the invoking user (--noconfirm only when stdin is
  not a terminal); pacman is skipped when only AUR rows are left; a helper
  failure is reported and its status returned; --dry-run prints it
- other backends never run a helper; --aur-helper is ignored with a warning

paru 2.1.0 and yay 13.0.1 formats verified in discovery/pacman.md section
15 (proof: discovery/pacman/aur.sh). Unit tests replay the recorded output
(fake paru, yay, runuser, getent); the archlinux E2E run installs the
foreign su-dummy-aur and drives a fake paru through a fake sudo built on
passwordless su for wheel.
This commit is contained in:
bryanpedini committed 2026-10-10 15:06:18 +02:00
1 parent 875c8294bf
commit def537af58
35 files changed
+1828 -40

No files matched your search

+127 -10
View File
@@ -6,7 +6,9 @@ upgradable packages, lets the user interrogate each one, and only then runs the
native upgrade. Packages the user has explicitly acknowledged are remembered in a
statefile and never questioned again. It is one file with no runtime dependencies
beyond bash >= 4.3 and the distro's package manager, and it never reimplements
package management: every action shells out to the native tool.
package management: every action shells out to the native tool. On Arch Linux and
its derivatives, AUR packages join the review when `paru` or `yay` is installed
(see [AUR packages](#aur-packages-arch-linux-paru-or-yay)).
The specification is [docs/DESIGN.md](docs/DESIGN.md); the per-family command
research it is built on lives in [discovery/](discovery/).
@@ -27,7 +29,9 @@ on first use, not by `make install`; `make uninstall` leaves it in place.
Runtime needs: bash >= 4.3, the package manager, coreutils, sed and grep (plus awk,
which the apt and pacman backends use and which those distros always ship). `sudo`
is optional: when the tool needs root and you are not root, it re-runs itself
through `sudo` with the same arguments, or fails with a clear message.
through `sudo` with the same arguments, or fails with a clear message. On the pacman
backend `paru` or `yay` is optional too (AUR packages); the helper is run as the user
who invoked `sudo`, with `runuser` (util-linux, always there on Arch) or else `sudo -u`.
## Usage
@@ -51,6 +55,9 @@ options:
--dry-run print native commands instead of running mutating ones
--state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR)
--no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout)
--aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
```
@@ -60,7 +67,8 @@ otherwise the exit status of the package manager's upgrade step.
What needs root: the metadata refresh, the upgrade, a purge, and writing a state
directory you cannot write. `--dry-run`, `list --no-refresh`, `trusted`, `info` and
`show` work unprivileged (as long as the state directory is readable).
`show` work unprivileged (as long as the state directory is readable); there an AUR
helper simply runs as you.
### Example session
@@ -115,7 +123,8 @@ upgrade; end of input without one aborts with exit status 2).
`[held]` marks packages the package manager will not upgrade on its own: apt
holds and "kept back" packages, pacman `IgnorePkg` (`[ignored]` in `pacman -Qu`).
dnf `exclude=`/versionlock and zypper locks hide packages from the native listing,
so they do not appear at all.
so they do not appear at all. The column right after the number is a `*` for an AUR
package (pacman backend with paru or yay, see below) and a space on every other row.
### Command grammar
@@ -188,6 +197,73 @@ replacements only the pacman transaction (`-Sup`) shows, packages dnf installs t
others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list
says "0 to review, but ... reports pending changes" and `<enter>` still runs the upgrade.
### AUR packages (Arch Linux: paru or yay)
On the pacman backend (Arch and its derivatives, `pman` included) AUR packages are reviewed
with the rest when an AUR helper is installed: `paru`, else `yay`. `--aur-helper=paru|yay`
picks one (an error if it is not installed), `--aur-helper=none` or `--no-aur` turns the AUR
off. The banner says which helper is in use, the AUR rows get a `*` right after the number
and `aur` as their repository:
```
$ skeptical-updater (as a normal user: it re-runs itself through sudo)
Backend: pacman (Arch Linux) + AUR via paru
Refreshing package lists... done.
4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:
[1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy
[2]* su-dummy-aur 1.0-1 -> 2.0-1 aur
[3] su-dummy-b 1.0-1 -> 2.0-1 su-dummy
[4] su-dummy-c 1.0-1 -> 2.0-1 su-dummy
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
> 3-
excluded from this run: su-dummy-b
>
Excluded from this run: su-dummy-b
Checking the reviewed transaction (pacman -Sup)... ok.
+ cp -p -- /tmp/skeptical-updater-db.4242.AbC123/sync/su-dummy.db /var/lib/pacman/sync/
+ pacman -Su --noconfirm --ignore su-dummy-b
...
+ runuser -u alice -- env HOME=/home/alice paru -Sua --ignore su-dummy-b
...
Upgrade finished.
```
(`pman` installed: `Backend: pman (Arch Linux) + AUR via paru`.)
- **Listing**: `paru -Qua` / `yay -Qua` (`name old -> new`). The helper asks the AUR every time,
so `--no-refresh` lists the AUR rows too; when it fails (no network, the AUR is down) its
message is shown with `aur: paru -Qua failed, AUR packages not listed` and the session goes on
with the repository rows. A package in `IgnorePkg` is listed `[held]` by paru and left out by
yay. Devel (`-git`) packages are listed when the helper checks them (paru does by default, yay
with `--devel`), with `latest-commit` as the new version. A name a repository has wins over an
AUR row (a package that moved from the AUR into a repository).
- **`N?`, `N+`, `Nx`** work as for any installed package, through pacman (`pacman -Qi`,
the statefile with backend `pacman`, `pacman -Rns`). **`N??`** merges `pacman -Qi` with the
AUR record (`paru -Sia` / `yay -Sia`), `>` lines as usual: `> Version`, `> Repository : aur`,
`> Maintainer`, `> Votes`, ... (`show PKG` does the same for a package in no repository).
- **`N-`** excludes an AUR row through the helper's `--ignore`.
- **`<enter>`** upgrades the repository packages first, exactly as without the AUR (pre-check,
the reviewed databases, `pacman -Su`), then, if AUR rows are left to upgrade, runs
`paru -Sua --ignore <every excluded row>` (or yay). The helper runs only after a successful
repository step; if it fails, its exit status is the tool's (`the AUR upgrade failed: paru
exited with status 1 (the repository packages were upgraded)`). When no repository row is
left to upgrade, pacman is not run at all, only the helper. `--dry-run` prints the helper's
command line.
- **Which user**: AUR helpers must not run as root (paru refuses to build as root, makepkg too).
When the tool is root because of its own `sudo` re-exec (or any `sudo`: `SUDO_USER` names a
non-root user), every helper command runs as that user: `runuser -u "$SUDO_USER" -- env
HOME=<that user's home> paru ...` from that home (`sudo -u` when `runuser` is missing). Run
unprivileged (`list --no-refresh`, `info`, `show`, `--dry-run`) the helper runs as you. A root
login without `SUDO_USER` gets no AUR, with a warning (`aur: paru is never run as root and
there is no invoking user ...`).
- **PKGBUILD review**: when stdin is a terminal the helper gets it and asks as usual - paru's
PKGBUILD review, yay's diff menu, and `sudo` asking for your password for its `pacman -U` if
the sudo timestamp has expired meanwhile. When stdin is not a terminal (a piped session) the
helper runs with `--noconfirm`: nothing is reviewed, and its `sudo pacman -U` must work without
a password.
### Line editing and history
When stdin is a terminal, the prompt is read with bash's readline and its defaults: Left/Right
@@ -263,12 +339,14 @@ Permanent holds belong to the package manager (`apt-mark hold`, `IgnorePkg`,
|---------|---------|---------|-------------------|-------|
| apt | `apt-get update -qq -o APT::Update::Error-Mode=any` | `apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade` | temporary `apt-mark hold` of the excluded packages that are *not already held*, journalled in `holds.added` before holding; afterwards exactly those are released (also on failure, Ctrl-C, TERM, HUP, QUIT, a second signal during the restore, or a closed stdout; a journal left by `kill -9` is replayed at the start of the next root run). Holds you placed are never touched. `apt-mark` ignores `DPkg::Lock::Timeout`, so it is retried for up to 60 s while dpkg's lock is busy. | plan: `apt-get -s purge --auto-remove PKG` minus what `apt-get -s autoremove` already listed (`--auto-remove` is system-wide), then `apt-get -y purge` of exactly that set: PKG and the dependencies this purge orphans; no `autoremove` |
| pacman / pman | `pacman -Sy --dbpath <throwaway copy>` (with `local` linked to the real local database, seeded with the system's `*.db` and `*.db.sig`): listing, `q`, end of input and `--dry-run` never touch `/var/lib/pacman/sync`; errors pacman prints about it are shown | check `pacman -Sup --noconfirm --ignore ... --dbpath <throwaway>` (on failure: nothing changed, its exit status), install the reviewed `*.db`/`*.db.sig` into `/var/lib/pacman/sync` under `db.lck`, all-or-nothing, never over a newer system database (exit 2), then `pacman -Su --noconfirm` - exactly the reviewed metadata, no second sync; with `--no-refresh` plain `pacman -Su` | `--ignore a,b` | plan: `pacman -Rs --print`; then `pacman -Rns --noconfirm -- ...` |
| AUR (pacman backend, paru or yay) | none needed: `-Qua` asks the AUR every time | after the repository step: `paru -Sua` (`--noconfirm` when stdin is not a terminal), as the user who invoked sudo | the helper's `--ignore a,b` (all excluded rows) | through pacman, like any installed package |
| dnf / yum | `dnf -q --refresh --setopt=skip_if_unavailable=0 makecache` | `dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade` (the reviewed metadata; the repo glob is needed because a repo's own `metadata_expire` overrides the main one) | `--exclude=a,b` (adds to `exclude=` from the configuration) | plan: `dnf --assumeno remove PKG`; then `dnf -y remove ...` |
| zypper | `zypper --non-interactive --quiet refresh` | `zypper --non-interactive --no-refresh update` (Leap/SLE) or `dist-upgrade` (Tumbleweed/Slowroll, by `ID=` in os-release) | locks in a *private* `ZYPP_CONF`: the effective configuration (main zypp.conf plus the `zypp.conf.d` drop-ins of `/usr/etc` and `/etc`) with `locksfile.path` pointing at a copy of the effective lock file (also when a drop-in sets it), plus `addlock` of the exclusions; deleted afterwards: the real lock file is never written | plan: `zypper --xmlout remove --dry-run --clean-deps PKG`; then `zypper --non-interactive --no-refresh remove --clean-deps ...` |
Every package-manager call runs with `LC_ALL=C` (and `DEBIAN_FRONTEND=noninteractive`;
`COLUMNS=0` for pacman), with stdin from `/dev/null` so that it can never eat the
commands you pipe in. The upgradable list comes from `apt-get -s --with-new-pkgs
commands you pipe in - except an AUR helper's upgrade on a terminal, which gets the terminal for
its own prompts. The upgradable list comes from `apt-get -s --with-new-pkgs
upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman -Sl`,
`dnf -q check-update` (+ `rpm -qa` for the installed versions) and
`zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan
@@ -303,8 +381,8 @@ runs a subset of distros; `E2E_FAIL_FAST=1` stops at the first failing distro.
No root, no network, no real package manager. The applet runs with a hermetic
`PATH`: the fakes in `tests/unit/fakes/<backend>/` (`apt-get`, `apt-cache`,
`dpkg-query`, `apt-mark`, `pacman`, `pman`, `dnf`, `yum`, `rpm`, `zypper`, `sudo`)
in front of symlinks to the few system tools it needs. The fakes replay the real
`dpkg-query`, `apt-mark`, `pacman`, `pman`, `dnf`, `yum`, `rpm`, `zypper`, `sudo`, and for the
AUR `paru`, `yay`, `runuser`, `getent`) in front of symlinks to the few system tools it needs. The fakes replay the real
outputs recorded in `discovery/*.md` (data in `tests/unit/data/`), keep their state
(holds, purged packages) under the test's temp dir and log every invocation, so the
tests assert the exact commands issued. Covered: CLI parsing and exit codes, backend
@@ -320,9 +398,14 @@ database install / `-Su` sequence and stale throwaway databases, statefile locki
the `mkdir` lock fallback (EPERM owner alive, no busy loop), an inaccessible state directory, the
sudo re-exec of the file itself and its loop guard), the `N??` merge on recorded pacman, apt, dnf4
and dnf5 records (changed, identical, installed-only, candidate-only and multi-line fields, apt's
short candidate descriptions, dnf5's padding, the fallbacks), and the prompt on a real pty (`script`):
cursor editing, Up/Down history recall, the stashed line, the bell at both ends, no history file.
About 30 seconds.
short candidate descriptions, dnf5's padding, the fallbacks), the prompt on a real pty (`script`):
cursor editing, Up/Down history recall, the stashed line, the bell at both ends, no history file,
and the AUR (`tests/unit/aur.bats`, on recorded paru 2.1.0 and yay 13.0.1 output): helper detection
and overrides, the user the helper runs as (`runuser -u` / `sudo -u` with `SUDO_USER`, unprivileged,
a root login), the `*` column on pacman and apt rows, `-Qua` parsing (`[ignored]`, yay's age suffix
and root warning, devel packages, failures), `N?`/`N??`/`N-`/`N+`/`Nx` on AUR rows, the upgrade order
and arguments, an AUR-only upgrade, a failing helper, `--dry-run`, and `--noconfirm` only without a
terminal. About 35 seconds.
### End-to-end tests (`tests/e2e/`)
@@ -354,6 +437,20 @@ review is not installed), apt
holds waiting for a busy dpkg lock, zypper honouring a lock file set in a drop-in and applying
the reviewed metadata although a newer package appeared after the review.
`tests/e2e/aur.bats` covers the AUR. On archlinux the fixture (`FIXTURE_AUR=1`) also installs
`su-dummy-aur` 1.0 with `pacman -U` (a foreign package: in no repository) and keeps its 2.0
package file, creates the user `su-tester` and makes `su` passwordless for the `wheel` group;
the tests install `fixtures/fake-paru.sh` as `/usr/local/bin/paru` (it emulates paru's `-Qua`,
`-Sia` and `-Sua` for that package, offline, refusing to install as root like paru) and
`fixtures/fake-sudo.sh` as `/usr/local/bin/sudo` (the image has no sudo: `su`, with `SUDO_USER`
set like sudo does). The applet runs as `su-tester` and re-executes itself through that sudo, or
as root with `SUDO_USER=su-tester`: the `*` row with repository `aur`, paru running as su-tester
with its HOME, `N?` and `N??` on it, `N-` keeping it at 1.0 while the repository packages upgrade,
a plain `<enter>` upgrading it to 2.0 after them, an AUR-only upgrade that leaves pacman alone,
`--dry-run`, an unprivileged `list --no-refresh`, a root login (warning, no AUR) and `--no-aur`.
On the other distros it checks the space column and that no AUR helper ever runs, even with
`--aur-helper=paru` (ignored with a warning).
The matrix (`tests/docker/images.txt`):
| label | image | backend |
@@ -442,6 +539,26 @@ From the discovery reports (`discovery/*.md`), all reproduced there:
the current closures of all your trusted packages wholesale. A package shared by
several trusted roots is recorded under the first one only, so `untrust` of that
root drops it until the next `refresh`.
- **AUR** (pacman backend with paru or yay):
- The helper is run as the user named by `SUDO_USER` only: a root login (no `SUDO_USER`, or
`doas`, `run0`, `pkexec`) lists and upgrades no AUR package, with a warning.
- A piped session upgrades AUR packages with `--noconfirm`: no PKGBUILD review, and the
helper's own `sudo pacman -U` must not ask for a password (NOPASSWD, or a sudo timestamp that
is still valid). Review AUR updates on a terminal, or exclude their rows. On a terminal the
helper may ask for your password again if its sudo timestamp expired during a long review.
- The AUR listing needs the network on every run (also with `--no-refresh`); without it the
session goes on with the repository rows (warning). `N??` on an AUR row asks the AUR too.
- The helper decides what is foreign from the system's sync database, not from the reviewed
throwaway copy; a package that moved from the AUR into a repository is listed from the
repository (its AUR row is dropped).
- What the helper reports is what you get: yay leaves `IgnorePkg` packages out (paru lists them
`[held]`), devel packages are listed only when the helper checks them (paru by default, yay
with `--devel`) and show `latest-commit` as the new version; the helper's own configuration
(`paru.conf`, `yay --save`) applies to its upgrade.
- The AUR upgrade is one helper run after `pacman -Su`: a helper failure leaves the repository
packages upgraded (reported, its exit status returned). The E2E suite drives a fake paru
(offline); the formats of real paru 2.1.0 and yay 13.0.1 are verified in
`discovery/pacman.md` section 15.
- The upgradable lists show what the native listing shows: new dependencies are rows on apt
and on Tumbleweed (`-` as the old version); on pacman and dnf new dependencies,
replacements and obsoletes only appear as a note, and removals only on Tumbleweed (note);
+126
View File
@@ -9,6 +9,7 @@ Companion files (all in `discovery/pacman/`):
| `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:
@@ -30,6 +31,7 @@ docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/che
| 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):
@@ -615,3 +617,127 @@ Validated By : None
* 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.
### 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) |
| upgrade | after the repository step: `<helper> -Sua [--noconfirm when stdin is not a terminal] [--ignore <excluded rows>]` |
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# discovery/pacman/aur.sh - verify the output formats and exit codes of paru and yay that the
# AUR support of skeptical-updater relies on (-Qua, -Si/-Sia, -Sua --ignore, devel packages,
# [ignored], root refusal, AUR unreachable). NEEDS NETWORK (Arch mirrors, aur.archlinux.org,
# github.com for pfetch-git). Run as root in a throwaway container:
# docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/aur.sh
set -uo pipefail
export LC_ALL=C
U=bob
H=/home/$U
step() { printf '\n######## %s\n' "$*"; }
# as_user CMD...: run CMD as the unprivileged user, from its home, with its HOME
as_user() { (cd "$H" && runuser -u "$U" -- env HOME="$H" "$@"); }
# show [-r] CMD...: print the command, its stdout, its stderr (prefixed "2> ") and its exit status;
# as the user unless -r; escape sequences are made visible (cat -v)
show() {
local root=0 rc=0 o e
[[ $1 == -r ]] && { root=1; shift; }
o=$(mktemp) e=$(mktemp)
printf '$ %s%s\n' "$( ((root)) && echo '(root) ')" "$*"
if ((root)); then "$@" >"$o" 2>"$e" </dev/null || rc=$?; else as_user "$@" >"$o" 2>"$e" </dev/null || rc=$?; fi
cat -v "$o"
sed 's/^/2> /' "$e" | cat -v
printf '[exit %s]\n' "$rc"
rm -f "$o" "$e"
}
step "setup: base-devel git sudo, user $U (wheel, NOPASSWD)"
pacman -Syu --noconfirm --needed base-devel git sudo >/dev/null 2>&1 || { echo "pacman -Syu failed"; exit 1; }
useradd -m -G wheel "$U"
echo "$U ALL=(ALL:ALL) NOPASSWD: ALL" >/etc/sudoers.d/$U
chmod 0440 /etc/sudoers.d/$U
# aur_build NAME [older]: clone NAME from the AUR and makepkg -si it; "older" checks out the newest
# commit whose .SRCINFO pkgver differs from the current one first (an outdated install)
aur_build() {
as_user bash -c '
set -e
cd ~ && git clone -q "https://aur.archlinux.org/$1.git" && cd "$1"
if [ "${2:-}" = older ]; then
cur=$(sed -n "s/^\tpkgver = //p" .SRCINFO)
for c in $(git log --format=%h -- .SRCINFO); do
v=$(git show "$c:.SRCINFO" | sed -n "s/^\tpkgver = //p")
if [ "$v" != "$cur" ]; then git checkout -q "$c"; break; fi
done
fi
makepkg -si --noconfirm >/dev/null 2>&1
' _ "$@" || { echo "aur_build $* failed"; exit 1; }
}
# paru-bin 2.1.0 links libalpm.so.15 and does not start with pacman 7.1 (libalpm.so.16):
# "error while loading shared libraries: libalpm.so.15" (verified) - build paru from source
aur_build "${PARU_PKG:-paru}"
aur_build yay-bin
aur_build pfetch older
show paru --version
show yay --version
show -r pacman -Qm
step "runuser -u: the environment it sets"
show -r runuser -u "$U" -- env
show -r runuser -u "$U" -- id
step "a foreign package that is not in the AUR (local build)"
w=$(mktemp -d)
mkdir -p "$w/usr/share/su-local-only"
echo 1.0 >"$w/usr/share/su-local-only/VERSION"
printf 'pkgname = su-local-only\npkgbase = su-local-only\npkgver = 1.0-1\npkgdesc = local only\narch = any\nsize = 4\n' >"$w/.PKGINFO"
(cd "$w" && bsdtar --zstd -cf /tmp/su-local-only-1.0-1-any.pkg.tar.zst .PKGINFO usr)
pacman -U --noconfirm /tmp/su-local-only-1.0-1-any.pkg.tar.zst >/dev/null 2>&1
show -r pacman -Qm
step "-Qua (piped, stdin /dev/null)"
show paru -Qua
show yay -Qua
show paru -Qu
show yay -Qu
step "-Qua with IgnorePkg = pfetch"
sed -i '0,/^\[options\]/s||[options]\nIgnorePkg = pfetch|' /etc/pacman.conf
show paru -Qua
show yay -Qua
sed -i '/^IgnorePkg = pfetch$/d' /etc/pacman.conf
step "-Si / -Sia of an AUR package, compared with -Qi"
show -r env COLUMNS=0 pacman -Qi pfetch
show paru -Si pfetch
show paru -Sia pfetch
show paru -Si --aur pfetch
show yay -Si pfetch
show yay -Sia pfetch
show paru -Sia no-such-package-zzz
show yay -Sia no-such-package-zzz
show paru -Si bash
step "-Qua as root"
show -r paru -Qua
show -r yay -Qua
step "AUR unreachable (aur.archlinux.org -> 127.0.0.1)"
cp /etc/hosts /tmp/hosts.orig
echo '127.0.0.1 aur.archlinux.org' >>/etc/hosts
show paru -Qua
show yay -Qua
show paru -Sia pfetch
show yay -Sia pfetch
cat /tmp/hosts.orig >/etc/hosts
# reset_pfetch: back to the outdated pfetch built at the start (makepkg left the package file)
reset_pfetch() { pacman -U --noconfirm "$H"/pfetch/pfetch-[0-9]*.pkg.tar.zst >/dev/null 2>&1; pacman -Q pfetch; }
step "-Sua as root (pfetch outdated)"
show -r paru -Sua --noconfirm
show -r yay -Sua --noconfirm
show -r pacman -Q pfetch
step "-Sua without --noconfirm, stdin /dev/null (no terminal)"
show paru -Sua
show -r pacman -Q pfetch
reset_pfetch
show yay -Sua
show -r pacman -Q pfetch
reset_pfetch
step "-Sua --ignore (non-interactive: --noconfirm, stdin /dev/null)"
show paru -Sua --noconfirm --ignore pfetch
show -r pacman -Q pfetch
show yay -Sua --noconfirm --ignore pfetch
show -r pacman -Q pfetch
show paru -Sua --noconfirm --ignore su-dummy-x,su-dummy-y
show -r pacman -Q pfetch
reset_pfetch
show yay -Sua --noconfirm
show -r pacman -Q pfetch
step "devel (-git) packages: pfetch-git (conflicts with pfetch, which goes first)"
show -r pacman -Rns --noconfirm pfetch
show paru -S --noconfirm pfetch-git
show -r pacman -Q pfetch-git
show paru --gendb
show yay -Y --gendb
for f in "$H/.local/state/paru/devel.toml" "$H/.cache/yay/vcs.json"; do
echo "--- $f"; cat "$f"; echo
sed -i -E 's/[0-9a-f]{40}/0000000000000000000000000000000000000000/g' "$f"
echo "--- $f with every commit id replaced by zeros (an older upstream state)"; cat "$f"; echo
done
show paru -Qua
show paru -Qua --devel
show yay -Qua
show yay -Qua --devel
show paru -Sua --noconfirm
show -r pacman -Q pfetch-git
show paru -Sua --noconfirm --devel
show -r pacman -Q pfetch-git
step "-Qua with nothing to upgrade"
show paru -Qua
show yay -Qua
show paru -Qua --devel
show yay -Qua --devel
echo "ALL DONE"
+63 -4
View File
@@ -20,13 +20,44 @@ Order (first match wins, overridable with `--backend=<name>`):
| backend | detected by | notes |
|----------|-----------------------------|--------------------------------------------|
| `pacman` | `pacman` in PATH | `pman` present => label shown as `pman`, same commands; honor pacman `IgnorePkg` (`pacman -Qu` marks them `[ignored]`); the refresh syncs a throwaway `--dbpath` copy; at `<enter>` its reviewed databases are installed and applied with `pacman -Su` |
| `pacman` | `pacman` in PATH | `pman` present => label shown as `pman`, same commands; honor pacman `IgnorePkg` (`pacman -Qu` marks them `[ignored]`); the refresh syncs a throwaway `--dbpath` copy; at `<enter>` its reviewed databases are installed and applied with `pacman -Su`; AUR packages through `paru` (else `yay`) when installed, see "AUR" below |
| `apt` | `apt-get` in PATH | Debian/Ubuntu |
| `dnf` | `dnf` in PATH (else `yum`) | dnf4 (EL9) and dnf5 (EL10) differences hidden in the backend |
| `zypper` | `zypper` in PATH | `dist-upgrade` on Tumbleweed (ID=opensuse-tumbleweed), `update` on Leap; on Tumbleweed the review list is the `dist-upgrade --dry-run` plan |
Each backend implements the same function set (see "Backend contract").
### AUR (pacman backend only)
| helper | detected by | overridden by |
|--------|-------------|---------------|
| `paru` | `paru` in PATH (first) | `--aur-helper=paru` (an error when it is not in PATH) |
| `yay` | `yay` in PATH (when there is no paru) | `--aur-helper=yay` (idem) |
| none | neither installed | `--aur-helper=none`, `--no-aur` |
The helper is detected with the backend (before any sudo re-exec). The banner then reads
`Backend: pman (Arch Linux) + AUR via paru`. The other backends never look a helper up:
`--aur-helper=paru|yay` is accepted there and ignored with a warning (`--aur-helper=paru is
ignored: AUR helpers only apply to the pacman backend ...`), `--no-aur`/`--aur-helper=none` silently.
The helper never runs as root (paru refuses to install AUR packages as root, makepkg refuses to
build as root). Decided once, after the root handling (the banner decides it; the listing runs in
subshells):
| we are | the helper runs |
|--------|-----------------|
| not root (`list --no-refresh`, `info`, `show`, `--dry-run`) | as we are: `paru ARGS` |
| root, `SUDO_USER` names a user whose uid (`getent passwd`, else `/etc/passwd`) is not 0 - our sudo re-exec, or any sudo | as that user, from that user's home: `runuser -u "$SUDO_USER" -- env HOME=<home> paru ARGS`, `sudo -u "$SUDO_USER" -- env HOME=<home> ...` when there is no runuser |
| root without `SUDO_USER` (a root login), `SUDO_USER` root or unknown, neither runuser nor sudo | not at all: no AUR row, one warning (`aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded ...`) |
Helper commands (formats verified in `discovery/pacman.md` section 15, paru 2.1.0 and yay 13.0.1):
| need | command | notes |
|------|---------|-------|
| list | `<helper> -Qua` | `name old -> new`, then ` [ignored]` (paru; yay leaves `IgnorePkg` packages out) or yay's age (` [122d13h]`, ignored); devel packages show `latest-commit` as the new version; escape sequences and other lines are dropped. Exit 1 with nothing on stdout and stderr = nothing to upgrade; any other failure: the helper's stderr lines as warnings, then `aur: <helper> -Qua failed, AUR packages not listed`, and no AUR row. Asked on every run, also with `--no-refresh` (the helper always compares with the AUR RPC) |
| `N??` candidate | `<helper> -Sia PKG` | for a package in no repository (`pacman -Sl`); paru's record aligns its keys like `pacman -Qi`, yay's are wider (re-padded by the merge) |
| upgrade | `<helper> -Sua [--noconfirm] [--ignore a,b]` | see "After `<enter>`" |
## Statefile
Directory: `/var/lib/skeptical-updater/` (override: `SKEPTICAL_UPDATER_STATE_DIR`).
@@ -108,6 +139,19 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i
| `q` | quit without upgrading (exit 0) |
| `h`, `?` | help |
List format: a marker column (`-` excluded, `=` trusted with `--all`, else a space), the number in
brackets right-aligned to the widest one, then one column that is `*` for an AUR row and a space for
every other row (on every backend: the columns stay aligned), a space, then name, old version, `->`,
new version, repository (`aur` for AUR rows) and the flags (`[held]`):
```
[1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy
-[2]* su-dummy-aur 1.0-1 -> 2.0-1 aur
```
On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package; `N??` merges
`pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`.
Numbers are stable for the whole session (removed rows leave a gap, never renumber).
All tokens of a line are validated first: an unknown token or a number that is not listed
prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without
@@ -142,7 +186,7 @@ ones (dnf5: 16 installed, 15 available), the candidate lines are re-padded to th
| backend | installed record | upstream candidate |
|---------|------------------|--------------------|
| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list) |
| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list); a package in no repository: the AUR record, `<helper> -Sia PKG`, when a helper is in use (should it not merge, `pacman -Qi` is followed by the AUR record with every line marked `> `) |
| apt | `dpkg-query -s PKG` (Status, Conffiles, always the long description; `apt-cache show PKG=<installed>` gives the archive's record when that version is still listed: no Status/Conffiles, short description only); a `Status` of `not-installed`/`config-files` = not installed | `apt-cache show --no-all-versions PKG` |
| dnf | `dnf -q info --installed PKG` | `dnf -q info --upgrades PKG` (installed architecture only; `--available` also lists other architectures and, for an up-to-date package, other builds); not installed: `--available` |
| zypper | - | - (`rpm -qi` and `zypper info` share almost no keys - Version vs version-release, Arch, byte sizes, source names, description layout - and zypper cannot print the installed record by itself: `zypper info -r @System` is refused) |
@@ -188,6 +232,17 @@ After `<enter>`:
plain `pacman -Su` applies the existing databases. Throwaway databases carry their owner's
pid in the name (`/tmp/skeptical-updater-db.<pid>.XXXXXX`); those of runs that are gone are
removed at the start of the next root run.
AUR rows (pacman backend): pacman gets `--ignore` with the excluded repository rows only. When
no repository row is left to upgrade (every one excluded or `[held]`, no pending note) but AUR
rows are, (a)-(c) are skipped ("No repository package to upgrade: pacman is not run, only
paru."). Otherwise (a)-(c) run first, and only after they succeeded (d)
`<helper> -Sua --ignore <every excluded row, repository and AUR>` runs as the invoking user (see
"AUR"), when an AUR row that is neither excluded nor `[held]` remains: with stdin a terminal the
helper gets it and asks as usual (paru's PKGBUILD review, yay's menus, its sudo password);
otherwise `--noconfirm` and stdin from `/dev/null` (yay exits 1 at its first menu on a closed
stdin). A helper failure is reported as `the AUR upgrade failed: paru exited with status N (the
repository packages were upgraded)` and its status is the exit status; `--dry-run` prints the
helper's command line.
4. Restore holds/locks, exit with the package manager's exit code.
Trusted packages are upgraded silently along with everything else; they are only
@@ -215,6 +270,8 @@ options:
--dry-run print native commands instead of running mutating ones
--state-dir=DIR override /var/lib/skeptical-updater
--no-color disable ANSI colours (also honours NO_COLOR, non-tty stdout)
--aur-helper=NAME pacman backend: paru|yay|none (default: paru, else yay, when installed)
--no-aur same as --aur-helper=none
--version, --help
```
@@ -236,7 +293,8 @@ Every backend is a set of bash functions with a common prefix. All output is
```
<be>_refresh # update metadata; non-fatal on failure (warn)
<be>_upgradable # stdout: name<TAB>old<TAB>new<TAB>repo<TAB>flags (flags: "" | "held"; old "-" = new package)
<be>_upgradable # stdout: name<TAB>old<TAB>new<TAB>repo<TAB>flags (flags: space-separated words:
# "held", "aur" (pacman: an AUR row, shown with "*"); old "-" = new package)
# plus "#<note>" lines for pending work that is not a row
<be>_describe PKG # stdout: one-line summary
<be>_full_info PKG # stdout: native full dump (candidate version if available)
@@ -248,7 +306,8 @@ Every backend is a set of bash functions with a common prefix. All output is
<be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes
# (simulation, no cascade); non-zero = refused
<be>_purge PKG... # non-interactive native purge/remove of exactly the planned packages
<be>_upgrade EXCLUDED... # non-interactive native upgrade skipping EXCLUDED (handles hold/lock save+restore)
<be>_upgrade EXCLUDED... # non-interactive native upgrade skipping EXCLUDED (handles hold/lock save+restore;
# pacman: then the AUR helper, see "After <enter>")
<be>_label # "apt" / "pman" / ...
```
+263 -17
View File
@@ -52,6 +52,7 @@ opt_dry_run=0
opt_color=1
opt_state_dir=
opt_state_dir_set=0
opt_aur= # AUR helper (pacman backend): '' = detect paru, then yay; paru | yay | none
cmd=
cmd_args=()
@@ -100,6 +101,9 @@ options:
--dry-run print native commands instead of running mutating ones
--state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR)
--no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout)
--aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
@@ -120,6 +124,10 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack
h, ? this help
end of input abort without upgrading (exit 2)
A '*' right after the number marks an AUR package (pacman backend with paru or yay): <enter>
upgrades the repository packages with pacman first, then the AUR ones with the helper, run as
the user who invoked sudo; on a terminal the helper asks as usual (PKGBUILD review).
exit status: 0 ok, 1 usage/backend error, 2 aborted, otherwise the package
manager's exit status from the upgrade step.
EOF
@@ -136,7 +144,7 @@ session_help() {
pacman -Rns; purge the dependents first
N+ trust N (and its installed dependencies) forever
*+ *- trust / exclude every listed package (*- again re-includes them)
l redraw the list
l redraw the list (a '*' after the number marks an AUR package)
q quit without upgrading
h ? this help
Several tokens can be given on one line, e.g. "1? 3- 4+".
@@ -171,6 +179,16 @@ parse_args() {
opt_state_dir_set=1
shift
;;
--aur-helper=*)
opt_aur=${a#*=}
[[ -n $opt_aur ]] || usage_error "--aur-helper needs a value"
;;
--aur-helper)
(($#)) || usage_error "--aur-helper needs a value"
opt_aur=$1
shift
;;
--no-aur) opt_aur=none ;;
--all) opt_all=1 ;;
--no-refresh) opt_refresh=0 ;;
--dry-run) opt_dry_run=1 ;;
@@ -209,6 +227,10 @@ parse_args() {
*) usage_error "unknown backend '$opt_backend' (expected apt, dnf, pacman or zypper)" ;;
esac
fi
case $opt_aur in
'' | paru | yay | none) ;;
*) usage_error "unknown AUR helper '$opt_aur' (expected paru, yay or none)" ;;
esac
if ((opt_state_dir_set)); then
STATE_DIR=$opt_state_dir
else
@@ -391,7 +413,9 @@ fmt_cmd() { # the command line, shell-quoted where needed
# run_child CMD...: run CMD with stdin from /dev/null and our lock descriptors closed (a daemon
# started by a maintainer script must not inherit, and so keep, our flock).
run_child() {
run_child() { run_child_stdin "$@" </dev/null; }
# run_child_stdin CMD...: the same, but CMD keeps our stdin (an AUR helper asking on a terminal)
run_child_stdin() {
(
local v fd
for v in ${SU_LOCK[@]+"${SU_LOCK[@]}"}; do
@@ -401,7 +425,7 @@ run_child() {
fi
done
exec "$@"
) </dev/null
)
}
# run_cmd CMD...: run a MUTATING native command (output streamed); under --dry-run print
@@ -1115,7 +1139,10 @@ pacman_exit_cleanup() {
PACMAN_DBS_PENDING=0
}
pacman_init() { have pacman || die "pacman backend: pacman not found in PATH"; }
pacman_init() {
have pacman || die "pacman backend: pacman not found in PATH"
aur_detect
}
pacman_label() { if have pman; then printf 'pman'; else printf 'pacman'; fi; }
_pm() { COLUMNS=0 pacman ${PM_DB[@]+"${PM_DB[@]}"} "$@" </dev/null; }
pacman_dbpath() {
@@ -1175,6 +1202,151 @@ pacman_refresh() {
return 0
}
# ---- AUR (pacman backend only): paru or yay ---------------------------------------------------------
# discovery/pacman.md section 15. The helper lists (-Qua), describes (-Sia) and upgrades (-Sua) the
# AUR packages; everything about installed packages (N?, N+, Nx) stays with pacman. It never runs as
# root (makepkg refuses to build as root): when we are root through our sudo re-exec it runs as the
# invoking user ($SUDO_USER, with that user's HOME and from that home); unprivileged (list
# --no-refresh, info, show, --dry-run) it runs as we are; a root login (no SUDO_USER) gets no AUR.
AUR_HELPER= # paru | yay; '' = no AUR helper in use
AUR_STATE= # '' = not decided yet | on | off (aur_prepare)
AUR_RUN=() # prefix running the helper as the invoking user: runuser|sudo -u USER -- env HOME=...
AUR_HOME= # the helper's working directory (that user's home) when AUR_RUN is set
AUR_PENDING_WARN= # why the AUR is off, printed right after the banner
PW_UID='' PW_HOME='' # passwd_entry results
# aur_detect (pacman_init, i.e. before any sudo re-exec): --aur-helper/--no-aur, else paru, then yay;
# a helper asked for by name that is not installed is an error
aur_detect() {
AUR_HELPER=
case $opt_aur in
none) ;;
paru | yay)
have "$opt_aur" || die "--aur-helper=$opt_aur: $opt_aur not found in PATH"
AUR_HELPER=$opt_aur
;;
*) if have paru; then AUR_HELPER=paru; elif have yay; then AUR_HELPER=yay; fi ;;
esac
}
# passwd_entry USER: PW_UID and PW_HOME of USER (getent, else /etc/passwd); 1 if unknown
passwd_entry() {
local line='' name x uid home
if have getent; then
line=$(getent passwd "$1" 2>/dev/null) || line=''
elif [[ -r /etc/passwd ]]; then
while IFS= read -r x; do
if [[ ${x%%:*} == "$1" ]]; then
line=$x
break
fi
done </etc/passwd
fi
IFS=: read -r name x uid x x home x <<<"$line"
[[ $name == "$1" && $uid =~ ^[0-9]+$ && -n $home ]] || return 1
PW_UID=$uid PW_HOME=$home
}
# aur_prepare [quiet]: decide once how the helper runs; 1 = no AUR this run. The reason for "no AUR"
# is warned about once: right away, or with "quiet" after the banner (aur_warn_pending); never from a
# subshell (the listing runs in one: the banner decides first).
aur_prepare() {
case $AUR_STATE in on) return 0 ;; off) return 1 ;; esac
[[ -n $AUR_HELPER ]] || {
AUR_STATE=off
return 1
}
if ((SU_EUID != 0)); then
AUR_RUN=() AUR_HOME='' AUR_STATE=on
return 0
fi
local u=${SUDO_USER:-} why='' re='^[A-Za-z0-9_][A-Za-z0-9_.-]*[$]?$'
if [[ -z $u ]]; then
why="$AUR_HELPER is never run as root and there is no invoking user (SUDO_USER is not set)"
elif [[ ! $u =~ $re ]] || ! passwd_entry "$u"; then
why="the invoking user '$u' (SUDO_USER) is unknown"
elif ((PW_UID == 0)); then
why="$AUR_HELPER is never run as root and the invoking user ($u) is root"
elif have runuser; then
AUR_RUN=(runuser -u "$u" -- env HOME="$PW_HOME")
elif have sudo; then
AUR_RUN=(sudo -u "$u" -- env HOME="$PW_HOME")
else
why="neither runuser nor sudo is available to run $AUR_HELPER as $u"
fi
if [[ -n $why ]]; then
AUR_STATE=off
why="aur: $why: AUR packages are neither listed nor upgraded (run $SU_PROG through sudo from your own account, or use --no-aur)"
if [[ ${1:-} == quiet ]]; then
AUR_PENDING_WARN=$why
elif ((BASH_SUBSHELL == 0)); then
warn "$why"
fi
return 1
fi
AUR_HOME=$PW_HOME AUR_STATE=on
}
aur_warn_pending() {
[[ -z $AUR_PENDING_WARN ]] || warn "$AUR_PENDING_WARN"
AUR_PENDING_WARN=
}
# aur_exec ARGS...: run the helper (as the invoking user when we are root), stdin from /dev/null,
# our lock descriptors closed, from that user's home
aur_exec() {
(
if [[ -n $AUR_HOME ]]; then cd -- "$AUR_HOME" 2>/dev/null || cd /; fi
run_child ${AUR_RUN[@]+"${AUR_RUN[@]}"} "$AUR_HELPER" "$@"
)
}
# aur_upgradable: AUR rows (name<TAB>old<TAB>new<TAB>aur<TAB>aur[ held]) from `<helper> -Qua`:
# "name old -> new", then " [ignored]" (paru; yay leaves IgnorePkg packages out) or yay 13's time
# since the AUR change (" [122d13h]"). The helper asks the AUR RPC every time (no refresh needed).
# Exit 1 with nothing on stdout and stderr = nothing to upgrade; any other failure (the AUR is
# unreachable) is warned about with the helper's messages, and no AUR row is listed.
aur_upgradable() {
local out rc=0 errf err='' l
errf=$(mktemp "${TMPDIR:-/tmp}/$SU_PROG.XXXXXX") || errf=/dev/null
out=$(aur_exec -Qua 2>"$errf") || rc=$?
if [[ $errf != /dev/null ]]; then
err=$(cat -- "$errf")
rm -f -- "$errf"
fi
if ((rc)); then
[[ $rc -ne 1 || -n $out || -n ${err//[[:space:]]/} ]] || return 0
while IFS= read -r l; do [[ -z ${l//[[:space:]]/} ]] || warn "$AUR_HELPER: $l"; done <<<"$err"
warn "aur: $AUR_HELPER -Qua failed, AUR packages not listed"
return 0
fi
awk '{ gsub(/\033\[[0-9;]*m/, "") }
NF >= 4 && $3 == "->" && $1 ~ /^[A-Za-z0-9_+@][A-Za-z0-9_+.@-]*$/ {
printf "%s\t%s\t%s\taur\taur%s\n", $1, $2, $4, (($5 == "[ignored]") ? " held" : "")
}' <<<"$out"
}
# aur_info PKG: the AUR record of PKG, pacman-style "Key : Value" lines (paru: keys padded like
# pacman's, yay: wider; one record either way). -Sia: plain `yay -Si` only asks the repositories.
aur_info() { aur_exec -Sia "$1" 2>/dev/null; }
# aur_upgrade EXCLUDED...: `<helper> -Sua [--noconfirm] [--ignore a,b]` as the invoking user. With a
# terminal on stdin the helper gets it and asks as usual (its PKGBUILD review); otherwise
# --noconfirm, stdin from /dev/null. --dry-run prints the command line.
aur_upgrade() {
local -a c=(${AUR_RUN[@]+"${AUR_RUN[@]}"} "$AUR_HELPER" -Sua)
[[ -t 0 ]] || c+=(--noconfirm)
if (($#)); then c+=(--ignore "$(join_by , "$@")"); fi
if ((opt_dry_run)); then
say "+ $(fmt_cmd "${c[@]}")"
return 0
fi
printf '%s+ %s%s\n' "$C_DIM" "$(fmt_cmd "${c[@]}")" "$C_RESET" 2>/dev/null || true
(
if [[ -n $AUR_HOME ]]; then cd -- "$AUR_HOME" 2>/dev/null || cd /; fi
if [[ -t 0 ]]; then run_child_stdin "${c[@]}"; else run_child "${c[@]}"; fi
)
}
# discovery/pacman/lib.sh pm_upgradable: -Qu joined with the first -Sl repo; [ignored] => held.
# FILENAME == ARGV[1] (not FNR == NR): correct also when `pacman -Sl` prints nothing.
pacman_upgradable() {
@@ -1191,6 +1363,13 @@ pacman_upgradable() {
' <(printf '%s\n' "$sl") <(printf '%s\n' "$quraw"))
[[ -z $qu ]] || printf '%s\n' "$qu"
while IFS= read -r n; do [[ -n $n ]] && listed[${n%%$'\t'*}]=1; done <<<"$qu"
# AUR rows; a name the repositories list wins (a package that moved from the AUR into a
# repository, while the system's sync database the helper reads does not know that yet)
if aur_prepare; then
while IFS= read -r n; do
[[ -n $n && -z ${listed[${n%%$'\t'*}]+x} ]] && printf '%s\n' "$n"
done < <(aur_upgradable)
fi
# what the upgrade really installs: new dependencies and replacements are not in -Qu
sup=$(_pm -Sup --print-format '%n' 2>>"$errf") || sup=''
while IFS= read -r n; do
@@ -1236,20 +1415,36 @@ pacman_describe() {
[[ -n $out ]] || return 1
printf '%s\n' "$out"
}
# pacman_full_info PKG: -Qi and -Si as pacman prints them (each record ends with a blank line). A
# package in no repository: the AUR record when a helper is in use, every line marked with "> "
# (reached only when it does not merge with -Qi - paru's and yay's records do)
pacman_full_info() {
local ok=1
_pm -Qi -- "$1" 2>/dev/null && ok=0
_pm -Si -- "$1" 2>/dev/null && ok=0
local ok=1 qi si aur='' l
qi=$(_pm -Qi -- "$1" 2>/dev/null) && ok=0
si=$(_pm -Si -- "$1" 2>/dev/null) && ok=0
if [[ -z $si ]] && aur_prepare; then aur=$(aur_info "$1") || aur=''; fi
[[ -z $aur ]] || say "# lines starting with > are the AUR record ($AUR_HELPER -Sia) as it prints it"
[[ -z $qi ]] || printf '%s\n\n' "$qi"
[[ -z $si ]] || printf '%s\n\n' "$si"
if [[ -n $aur ]]; then
while IFS= read -r l; do printf '> %s\n' "$l"; done <<<"$aur"
ok=0
fi
return "$ok"
}
# pacman_installed_record PKG: -Qi. pacman_candidate_record PKG: -Si of the repository pacman would
# take it from (the first one `-Sl` lists it in, as in pacman_upgradable)
# take it from (the first one `-Sl` lists it in, as in pacman_upgradable); in no repository, the
# AUR record when a helper is in use
pacman_installed_record() { _pm -Qi -- "$1" 2>/dev/null; }
pacman_candidate_record() {
local repo
repo=$(_pm -Sl 2>/dev/null | awk -v p="$1" '$2 == p && r == "" { r = $1 } END { if (r != "") print r }') || repo=''
[[ -n $repo ]] || return 1
if [[ -n $repo ]]; then
_pm -Si -- "$repo/$1" 2>/dev/null
return
fi
aur_prepare || return 1
aur_info "$1"
}
pacman_rdepends() {
local out w
@@ -1406,7 +1601,38 @@ pacman_install_dbs() {
return 0
}
# pacman_upgrade EXCLUDED...: the repository packages with pacman (pacman_upgrade_repo), then - when
# AUR rows remain to upgrade - the AUR packages with the helper (`-Sua --ignore EXCLUDED`), only after
# a successful repository step. With nothing for pacman to do but AUR rows to upgrade, pacman is not
# run at all.
pacman_upgrade() {
local i n rc=0 n_repo=0 n_aur=0 ran=0
local -a repo_ex=()
local -A aur=()
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
((R_GONE[i] == 0)) || continue
if ((R_AUR[i])); then aur[${R_NAME[i]}]=1; fi
((R_EXCL[i] == 0)) && [[ ${R_FLAGS[i]} != held ]] || continue
if ((R_AUR[i])); then n_aur=$((n_aur + 1)); else n_repo=$((n_repo + 1)); fi
done
for n in "$@"; do [[ -n ${aur[$n]+x} ]] || repo_ex+=("$n"); done
if ((n_repo || !n_aur || ${#R_NOTES[@]})); then
pacman_upgrade_repo ${repo_ex[@]+"${repo_ex[@]}"} || return $?
ran=1
else
say "No repository package to upgrade: pacman is not run, only $AUR_HELPER."
fi
((n_aur)) || return 0
aur_upgrade "$@" || rc=$?
if ((rc)); then
UPGRADE_FAILED_MSG="the AUR upgrade failed: $AUR_HELPER exited with status $rc"
((ran == 0)) || UPGRADE_FAILED_MSG+=" (the repository packages were upgraded)"
fi
return "$rc"
}
# pacman_upgrade_repo EXCLUDED...: the reviewed repository transaction (see the top of this section)
pacman_upgrade_repo() {
local rc=0 real tmp out
local -a ign=()
if (($#)); then ign=(--ignore "$(join_by , "$@")"); fi
@@ -1982,27 +2208,33 @@ zypper_upgrade() {
# commands
# ===================================================================================================
banner() {
local pretty
local pretty via=''
pretty=$(os_release_field PRETTY_NAME)
say "${C_BOLD}Backend: $(be label)${pretty:+ ($pretty)}${C_RESET}"
# pacman: the AUR helper in use (decided here, once: the listing runs in subshells)
if [[ $BACKEND == pacman ]] && aur_prepare quiet; then via=" + AUR via $AUR_HELPER"; fi
say "${C_BOLD}Backend: $(be label)${pretty:+ ($pretty)}$via${C_RESET}"
if [[ $BACKEND == pacman ]]; then aur_warn_pending; fi
}
maybe_refresh() { if ((opt_refresh)); then be refresh || true; fi; }
# rows of the review list: R_* arrays indexed 0..n-1, sorted by name; R_NOTES: what the backend
# reports beyond the rows (new dependencies, removals, pending changes it does not list)
R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_NUM=() R_GONE=() R_EXCL=() R_NOTES=()
# R_FLAGS: what is shown in brackets ("held"); R_AUR: 1 for an AUR package (flag "aur" from the
# backend; shown as a "*" right after the number)
R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_AUR=() R_NUM=() R_GONE=() R_EXCL=() R_NOTES=()
N_IDX=() # number -> row index
N_MAX=0
load_rows() {
local out rc=0 line name old new repo flags i n=0
local out rc=0 line name old new repo flags i n=0 w aur shown
local -a fw=()
out=$(be upgradable | sort -t $'\t' -k1,1 -u) || rc=$?
if ((rc)); then
err "could not list the upgradable packages ($(be label) exited $rc)"
exit 1
fi
R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_NUM=() R_GONE=() R_EXCL=() N_IDX=() R_NOTES=()
R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_AUR=() R_NUM=() R_GONE=() R_EXCL=() N_IDX=() R_NOTES=()
while IFS= read -r line; do
[[ -n $line ]] || continue
if [[ $line == '#'* ]]; then
@@ -2011,7 +2243,12 @@ load_rows() {
fi
IFS=$US read -r name old new repo flags <<<"${line//$'\t'/$US}"
[[ -n $name ]] || continue
R_NAME+=("$name") R_OLD+=("${old:--}") R_NEW+=("${new:--}") R_REPO+=("${repo:--}") R_FLAGS+=("$flags")
aur=0 shown=''
read -r -a fw <<<"$flags" || true
for w in ${fw[@]+"${fw[@]}"}; do
if [[ $w == aur ]]; then aur=1; else shown+=${shown:+ }$w; fi
done
R_NAME+=("$name") R_OLD+=("${old:--}") R_NEW+=("${new:--}") R_REPO+=("${repo:--}") R_FLAGS+=("$shown") R_AUR+=("$aur")
R_GONE+=(0) R_EXCL+=(0)
done <<<"$out"
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
@@ -2039,7 +2276,7 @@ row_visible() { # row_visible IDX
}
print_list() {
local i total=0 trusted=0 review=0 namew=4 oldw=3 neww=3 numw marker line flags repo note
local i total=0 trusted=0 review=0 namew=4 oldw=3 neww=3 numw marker aurm line flags repo note
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
((R_GONE[i] == 0)) || continue
total=$((total + 1))
@@ -2065,11 +2302,14 @@ print_list() {
elif is_trusted "${R_NAME[i]}"; then
marker="${C_DIM}=${C_RESET}"
fi
# an AUR package: "*" right after the bracket, every other row a space there
aurm=' '
((R_AUR[i] == 0)) || aurm="${C_YELLOW}*${C_RESET}"
repo=${R_REPO[i]}
[[ $repo != - ]] || repo=
flags=
[[ -z ${R_FLAGS[i]} ]] || flags=" ${C_YELLOW}[${R_FLAGS[i]}]${C_RESET}"
printf -v line '%s%*s %s%-*s%s %-*s -> %s%-*s%s %s%s' "$marker" "$numw" "[${R_NUM[i]}]" \
printf -v line '%s%*s%s %s%-*s%s %-*s -> %s%-*s%s %s%s' "$marker" "$numw" "[${R_NUM[i]}]" "$aurm" \
"$C_BOLD" "$namew" "${R_NAME[i]}" "$C_RESET" "$oldw" "${R_OLD[i]}" \
"$C_GREEN" "$neww" "${R_NEW[i]}" "$C_RESET" "$repo" "$flags"
line=${line%"${line##*[! ]}"}
@@ -2445,6 +2685,7 @@ session_toggle_all() {
}
SESSION_PROCEED=0
UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own
# session_line LINE: validate every token first (a typo never executes half a line), then run them
session_line() {
@@ -2553,6 +2794,8 @@ cmd_interactive() {
be upgrade ${excl[@]+"${excl[@]}"} || rc=$?
if ((rc == 0)); then
if ((opt_dry_run)); then say "Dry run: nothing was changed."; else say "Upgrade finished."; fi
elif [[ -n $UPGRADE_FAILED_MSG ]]; then
err "$UPGRADE_FAILED_MSG"
else
err "the upgrade failed: $(be label) exited with status $rc"
upgrade_hint "$rc" ${excl[@]+"${excl[@]}"}
@@ -2590,6 +2833,9 @@ main() {
fi
detect_backend
ensure_root
if [[ $BACKEND != pacman && ($opt_aur == paru || $opt_aur == yay) ]]; then
warn "--aur-helper=$opt_aur is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)"
fi
recover_leftovers
case $cmd in
'') cmd_interactive ;;
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# E2E: AUR packages. pacman backend (archlinux): su-dummy-aur 1.0 is a foreign package (fixture
# FIXTURE_AUR=1), the fake paru (fixtures/fake-paru.sh) offers it at 2.0 and installs it through
# the fake sudo (fixtures/fake-sudo.sh: su, passwordless for wheel). The applet runs either as the
# test user su-tester, re-executing itself through that sudo, or as root with SUDO_USER=su-tester
# (what the re-exec leaves behind). Rows: [1] su-dummy-a [2]* su-dummy-aur [3] su-dummy-b
# [4] su-dummy-c. Other backends: the marker column is a space and no AUR helper ever runs.
load helpers
PARU_LOG=/tmp/su-aur-paru.log
AUR_OLD=/srv/su-aur/su-dummy-aur-1.0-1-any.pkg.tar.zst
setup_file() {
e2e_setup_file
if [[ $BE == pacman ]]; then
install -m 0755 "$BATS_TEST_DIRNAME/fixtures/fake-paru.sh" /usr/local/bin/paru
install -m 0755 "$BATS_TEST_DIRNAME/fixtures/fake-sudo.sh" /usr/local/bin/sudo
else
# a paru that only records that it ran (it never may: AUR helpers are for pacman only)
printf '#!/bin/sh\necho "$*" >>%s\n' "$PARU_LOG" >/usr/local/bin/paru
chmod 0755 /usr/local/bin/paru
fi
}
teardown_file() {
rm -f /usr/local/bin/paru "$PARU_LOG"
if [[ $BE == pacman ]]; then rm -f /usr/local/bin/sudo; fi
}
setup() {
e2e_reset
if [[ $BE == pacman ]]; then
pacman -U --noconfirm "$AUR_OLD" >/dev/null 2>&1
[[ $(pkg_ver su-dummy-aur) == 1.0 ]]
fi
: >"$PARU_LOG"
chmod 0666 "$PARU_LOG"
}
# as_tester ARGS...: run skeptical-updater as the unprivileged su-tester (its TMPDIR: the suite's
# is root's); a run that needs root re-executes itself through the fake sudo
as_tester() { runuser -u su-tester -- env TMPDIR=/tmp HOME=/home/su-tester skeptical-updater "$@"; }
pacman_only() { [[ $BE == pacman ]] || skip "pacman backend only"; }
@test "AUR: the row is listed with * and repo aur; paru ran as the invoking user after the sudo re-exec" {
pacman_only
run as_tester list
assert_success
assert_line --index 0 "Backend: pacman ($(pretty_name)) + AUR via paru"
assert_line 'Refreshing package lists... done.'
assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:'
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
assert_line ' [3] su-dummy-b 1.0-1 -> 2.0-1 su-dummy'
assert_line ' [4] su-dummy-c 1.0-1 -> 2.0-1 su-dummy'
run cat "$PARU_LOG"
assert_output 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Qua'
}
@test "AUR: N? and N?? on the AUR row (pacman -Qi; merged with paru -Sia)" {
pacman_only
SUDO_USER=su-tester run skeptical-updater --no-refresh <<<$'2?\n2??\nq'
assert_success
assert_output --partial "$(printf '%s\n' \
'su-dummy-aur: Skeptical-updater dummy package AUR (foreign: in no repository)' \
'reason: explicitly installed' \
'required by: nothing')"
assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks'
assert_output --partial "$(printf '%s\n' 'Version : 1.0-1' '> Version : 2.0-1')"
assert_line 'Install Reason : Explicitly installed'
assert_line '> Repository : aur'
assert_line '> Maintainer : su-fixture'
assert_line '> AUR URL : https://aur.archlinux.org/packages/su-dummy-aur'
run grep -c 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Sia su-dummy-aur' "$PARU_LOG"
assert_output 1
}
@test "AUR: N- keeps su-dummy-aur at 1.0 while the repository packages upgrade; paru is not run" {
pacman_only
SUDO_USER=su-tester run skeptical-updater <<<$'2-\n'
assert_success
assert_line 'Excluded from this run: su-dummy-aur'
assert_line 'Upgrade finished.'
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
[[ $(pkg_ver su-dummy-aur) == 1.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)"
run grep -c -- '-Sua' "$PARU_LOG"
assert_output 0
}
@test "AUR: a plain <enter> upgrades su-dummy-aur to 2.0 with paru, after the repository packages" {
pacman_only
run as_tester <<<''
assert_success
assert_line 'Excluded from this run: none'
assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm'
assert_line 'Upgrade finished.'
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
[[ $(pkg_ver su-dummy-aur) == 2.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)"
# the repository step came first: su-dummy-a was 2.0 already when paru -Sua started
run grep -A1 -- ' -Sua --noconfirm$' "$PARU_LOG"
assert_line --index 0 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Sua --noconfirm'
assert_line --index 1 ' (at -Sua: su-dummy-a 2.0-1)'
run skeptical-updater list --no-refresh --no-aur
assert_output --partial 'Nothing to upgrade.'
SUDO_USER=su-tester run skeptical-updater list --no-refresh
assert_output --partial 'Nothing to upgrade.'
}
@test "AUR: only the AUR row left to upgrade: pacman is not run, only paru" {
pacman_only
# (the files only: `..` changes with the db.lck of paru's pacman -U)
before=$(ls -l --time-style=full-iso /var/lib/pacman/sync)
SUDO_USER=su-tester run skeptical-updater <<<$'1- 3- 4-\n'
assert_success
assert_line 'No repository package to upgrade: pacman is not run, only paru.'
assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c'
refute_line --regexp '^\+ pacman '
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
[[ $(pkg_ver su-dummy-aur) == 2.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)"
[[ $(ls -l --time-style=full-iso /var/lib/pacman/sync) == "$before" ]] || fail "the sync database changed although pacman was not run"
}
@test "AUR: --dry-run prints paru's command line (as the invoking user) and changes nothing" {
pacman_only
SUDO_USER=su-tester run skeptical-updater --dry-run --no-refresh <<<$'3-\n'
assert_success
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm --ignore su-dummy-b'
assert_line 'Dry run: nothing was changed.'
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-aur) == 1.0 ]]
run grep -c -- '-Sua' "$PARU_LOG"
assert_output 0
}
@test "AUR: unprivileged list --no-refresh runs paru directly, as that user" {
pacman_only
run as_tester list --no-refresh
assert_success
assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
run cat "$PARU_LOG"
assert_output --regexp '^su-tester HOME=/home/su-tester PWD=[^ ]+ -Qua$'
}
@test "AUR: a root login (no SUDO_USER): skipped with a warning, the repository rows listed, paru never run" {
pacman_only
run skeptical-updater list --no-refresh
assert_success
assert_line --index 0 "Backend: pacman ($(pretty_name))"
assert_line --index 1 'skeptical-updater: warning: aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded (run skeptical-updater through sudo from your own account, or use --no-aur)'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
refute_output --partial 'su-dummy-aur'
[[ ! -s $PARU_LOG ]]
}
@test "AUR: --no-aur lists the repository rows only and never runs paru" {
pacman_only
run as_tester --no-aur list --no-refresh
assert_success
assert_line --index 0 "Backend: pacman ($(pretty_name))"
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
refute_output --partial 'su-dummy-aur'
[[ ! -s $PARU_LOG ]]
}
@test "other backends: the marker column is a space; no AUR helper is ever run, --aur-helper is ignored" {
[[ $BE != pacman ]] || skip "not the pacman backend"
run skeptical-updater list --no-refresh
assert_success
assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
run skeptical-updater --aur-helper=paru list --no-refresh
assert_success
assert_line "skeptical-updater: warning: --aur-helper=paru is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)"
assert_line --regexp '^ \[3\] su-dummy-c '
run skeptical-updater --dry-run --no-refresh <<<''
assert_success
refute_output --partial 'paru'
[[ ! -s $PARU_LOG ]] || fail "paru ran: $(cat "$PARU_LOG")"
}
+3 -2
View File
@@ -27,8 +27,9 @@ install -m 0755 skeptical-updater /usr/local/bin/skeptical-updater
if command -v pacman >/dev/null 2>&1; then
family=pacman
# FIXTURE_SYNC=1: a system whose sync database was synced once (--no-refresh lists from it)
FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1 bash tests/e2e/fixtures/pacman.sh
# FIXTURE_SYNC=1: a system whose sync database was synced once (--no-refresh lists from it);
# FIXTURE_AUR=1: the foreign package su-dummy-aur and the user su-tester for tests/e2e/aur.bats
FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1 FIXTURE_AUR=1 bash tests/e2e/fixtures/pacman.sh
elif command -v apt-get >/dev/null 2>&1; then
family=apt
SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1 bash tests/e2e/fixtures/apt.sh
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Fake paru for the AUR E2E tests (tests/e2e/aur.bats installs it as /usr/local/bin/paru on the
# pacman backend): emulates paru 2.1.0 (discovery/pacman.md section 15) for su-dummy-aur, the
# foreign package of the fixture (FIXTURE_AUR=1), whose 2.0 "AUR build" is
# /srv/su-aur/su-dummy-aur-2.0-1-any.pkg.tar.zst. No network.
# -Qua "su-dummy-aur 1.0-1 -> 2.0-1" (+ " [ignored]" when IgnorePkg names it) while an
# older version is installed; otherwise nothing, exit 1
# -Sia NAME paru's AUR record of su-dummy-aur 2.0-1 (pacman-style "Key : Value" lines)
# -Sua [--noconfirm] [--ignore a,b]
# installs the 2.0 package with `sudo pacman -U --noconfirm`, unless su-dummy-aur is
# ignored or up to date; refuses to run as root like paru ("can't install AUR package
# as root")
# Every call is appended to /tmp/su-aur-paru.log as "<user> HOME=<home> PWD=<dir> <args>"; -Sua
# also logs the installed su-dummy-a then (the repository step must have upgraded it before).
set -uo pipefail
LOG=/tmp/su-aur-paru.log
PKG=su-dummy-aur NEW=2.0-1 FILE=/srv/su-aur/su-dummy-aur-2.0-1-any.pkg.tar.zst
printf '%s HOME=%s PWD=%s %s\n' "$(id -un)" "${HOME-}" "$PWD" "$*" >>"$LOG"
installed() { pacman -Q "$PKG" 2>/dev/null | cut -d' ' -f2; }
ignored_by_conf() { pacman-conf IgnorePkg 2>/dev/null | grep -qx "$PKG"; }
case ${1:-} in
-Qua)
v=$(installed)
[[ -n $v && $v != "$NEW" ]] || exit 1
if ignored_by_conf; then echo "$PKG $v -> $NEW [ignored]"; else echo "$PKG $v -> $NEW"; fi
;;
-Sia)
if [[ ${2:-} != "$PKG" ]]; then
echo "error: package '${2:-}' was not found" >&2
exit 1
fi
cat <<REC
Repository : aur
Name : $PKG
Version : $NEW
Description : Skeptical-updater dummy package AUR (foreign: in no repository)
URL : https://example.invalid/su-dummy
AUR URL : https://aur.archlinux.org/packages/$PKG
Groups : None
Licenses : custom
Provides : None
Depends On : None
Make Deps : None
Check Deps : None
Optional Deps : None
Conflicts With : None
Maintainer : su-fixture
Votes : 7
Popularity : 0.01
First Submitted : Tue, 14 Nov 2023 22:13:20
Last Modified : Tue, 14 Nov 2023 22:13:20
Out Of Date : No
REC
;;
-Sua)
shift
ign=''
while (($#)); do
case $1 in
--ignore)
ign=${2:-}
shift
;;
esac
shift
done
echo " (at -Sua: $(pacman -Q su-dummy-a 2>/dev/null))" >>"$LOG"
echo ':: Looking for devel upgrades...'
if ((EUID == 0)); then
echo ':: Resolving dependencies...'
echo "error: can't install AUR package as root" >&2
exit 1
fi
v=$(installed)
if [[ ,$ign, == *,"$PKG",* ]] || ignored_by_conf; then
echo " there is nothing to do"
echo "warning: $PKG: ignoring package upgrade ($v => $NEW)" >&2
exit 0
fi
if [[ -z $v || $v == "$NEW" ]]; then
echo " there is nothing to do"
exit 0
fi
printf '\nAur (1) Old Version New Version Make Only\naur/%s %s %s No\n\n' "$PKG" "$v" "$NEW"
sudo pacman -U --noconfirm "$FILE"
;;
*)
echo "fake paru: unhandled: $*" >&2
exit 99
;;
esac
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Fake sudo for the AUR E2E tests (tests/e2e/aur.bats installs it as /usr/local/bin/sudo on the
# pacman backend): archlinux:latest has no sudo and the E2E run is offline. Runs
# "sudo [--] COMMAND..." as root through su, which the fixture (FIXTURE_AUR=1) makes passwordless
# for the members of wheel (pam_wheel trust), with the variables sudo sets for the command:
# SUDO_USER, SUDO_UID and SUDO_GID. Used for the applet's own re-exec and by the fake paru.
[[ ${1:-} != -- ]] || shift
if (($# == 0)); then
echo "usage: sudo [--] command..." >&2
exit 1
fi
# shellcheck disable=SC2016 # $1... belong to the root shell started by su
exec su -s /bin/bash root -c 'exec env SUDO_USER="$1" SUDO_UID="$2" SUDO_GID="$3" "${@:4}"' -- sudo "$(id -un)" "$(id -u)" "$(id -g)" "$@"
+26 -2
View File
@@ -3,8 +3,9 @@
# Copyright (C) 2026 Bryan Joshua Pedini
# Deterministic pacman fixture for skeptical-updater E2E tests.
# (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN
# knob; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1.)
# (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN and
# FIXTURE_AUR knobs; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1
# FIXTURE_SYNC=1 FIXTURE_AUR=1.)
#
# Creates three dummy packages installed at 1.0 and a local pacman repo
# ([su-dummy], file:///srv/su-repo) that offers them at 2.0:
@@ -37,6 +38,11 @@
# IgnorePkg) while upgrading su-dummy-a fails with unsatisfied deps.
# FIXTURE_ORPHAN 0 (default) | 1 -> also install su-dummy-orphan 1.0 --asdeps with nothing
# requiring it (a pre-existing orphan, `pacman -Qdtq` lists it)
# FIXTURE_AUR 0 (default) | 1 -> also install su-dummy-aur 1.0, a foreign package (in no
# repository: `pacman -Qm` lists it), and keep its 2.0 package file in
# /srv/su-aur for the fake paru of tests/e2e/aur.bats; create the user
# su-tester (group wheel) and make `su` passwordless for wheel (pam_wheel
# trust), which the fake sudo of those tests relies on (the image has no sudo)
# FIXTURE_REPO repo directory (default /srv/su-repo)
# FIXTURE_SYNC 0 (default) | 1 -> run `pacman -Sy` at the end
#
@@ -52,6 +58,7 @@ FIXTURE_STRICT_DEP=${FIXTURE_STRICT_DEP:-0}
FIXTURE_REPO=${FIXTURE_REPO:-/srv/su-repo}
FIXTURE_SYNC=${FIXTURE_SYNC:-0}
FIXTURE_ORPHAN=${FIXTURE_ORPHAN:-0}
FIXTURE_AUR=${FIXTURE_AUR:-0}
FIXTURE_REPO_NAME=su-dummy
FIXTURE_EPOCH=1700000000 # fixed build date => reproducible tarballs
@@ -208,7 +215,24 @@ fx_main() {
fi
rm -rf "$old"
if [ "$FIXTURE_AUR" = 1 ]; then
mkdir -p /srv/su-aur
[ "$FIXTURE_BUILDER" != makepkg ] || chown su-builder: /srv/su-aur
fx_build /srv/su-aur su-dummy-aur 1.0 'Skeptical-updater dummy package AUR (foreign: in no repository)'
fx_build /srv/su-aur su-dummy-aur 2.0 'Skeptical-updater dummy package AUR (foreign: in no repository)'
chmod -R a+rX /srv/su-aur
fx_pacman -U --noconfirm /srv/su-aur/su-dummy-aur-1.0-1-any.pkg.tar.zst
id su-tester >/dev/null 2>&1 || useradd --create-home --groups wheel su-tester
sed -i 's/^#\(auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid\)/\1/' /etc/pam.d/su
grep -q '^auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid' /etc/pam.d/su ||
fx_die "could not enable pam_wheel trust in /etc/pam.d/su"
fi
if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; fi
if [ "$FIXTURE_AUR" = 1 ]; then
pacman -Qmq | grep -qx su-dummy-aur || fx_die "su-dummy-aur is not a foreign package"
fx_log "foreign (AUR) package: su-dummy-aur 1.0; user su-tester (wheel, passwordless su)"
fi
fx_log "builder=$FIXTURE_BUILDER isolate=$FIXTURE_ISOLATE repo=$FIXTURE_REPO"
fx_log "installed: $(pacman -Q su-dummy-a su-dummy-b su-dummy-c 2>/dev/null | tr '\n' ';')"
+1 -1
View File
@@ -17,7 +17,7 @@ rpm) if command -v zypper >/dev/null 2>&1; then BE=zypper; else BE=dnf; fi ;;
*) BE=unknown ;;
esac
export STATE_DIR STATE_FILE FAMILY BE
unset SKEPTICAL_UPDATER_STATE_DIR NO_COLOR
unset SKEPTICAL_UPDATER_STATE_DIR NO_COLOR SUDO_USER
# shellcheck source=/dev/null
pretty_name() { (. /etc/os-release && printf '%s' "$PRETTY_NAME"); }
+551
View File
@@ -0,0 +1,551 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# AUR packages on the pacman backend, through the fake paru/yay (tests/unit/fakes/paru) replaying
# the output recorded in discovery/pacman.md section 15: helper detection and overrides, the user
# the helper runs as (runuser/sudo -u, SUDO_USER, a root login), the "*" marker column, -Qua
# parsing and failures, N? / N?? / N- / N+ / Nx on an AUR row, and the upgrade (repository
# packages with pacman first, then `-Sua --ignore ...`; --noconfirm only without a terminal).
# Rows with the fake paru: [1]* pfetch [2] su-dummy-a [3]* su-dummy-aur [4] su-dummy-b
# [5] su-dummy-c (pfetch and su-dummy-aur are foreign: in no repository).
# each @test runs in its own subshell: per-test exports are intended
# shellcheck disable=SC2030,SC2031
load test_helper
setup() {
common_setup
use_fakes paru pacman runuser getent
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch
# root through our sudo re-exec, invoked by alice (fake getent: uid 1000, home below)
export SUDO_USER=alice
ALICE_HOME=$FAKE_DIR/home/alice
mkdir -p "$ALICE_HOME"
AS_ALICE="runuser -u alice -- env HOME=$ALICE_HOME"
}
ROW1=' [1]* pfetch 1.10.0-1 -> 1.11.0-1 aur'
ROW2=' [2] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
ROW3=' [3]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
ROW4=' [4] su-dummy-b 1.0-1 -> 2.0-1 su-dummy'
ROW5=' [5] su-dummy-c 1.0-1 -> 2.0-1 su-dummy'
# upgrade_steps: the upgrade's steps from the fake log, in order (throwaway --dbpath normalised)
upgrade_steps() {
grep -E '^(pacman -Sup --noconfirm|pacman -Su |real sync at -Su|(runuser|sudo) -u .* -Sua|paru -Sua|yay -Sua)' "$FAKE_LOG" |
sed -E 's#--dbpath /tmp/skeptical-updater-db\.[^ ]+#--dbpath DB#'
}
# ---- detection and overrides -------------------------------------------------------------------
@test "detection: paru first; the banner names the helper" {
use_fakes paru yay pacman runuser getent
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru'
log_has 'paru -Qua'
log_lacks '^yay'
}
@test "detection: yay when paru is not installed" {
use_fakes yay pacman runuser getent
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay'
log_has "$AS_ALICE yay -Qua"
assert_line "$ROW1"
assert_line "$ROW3"
}
@test "pman: the banner reads pman (Arch Linux) + AUR via paru" {
use_fakes pman paru pacman runuser getent
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pman (Arch Linux) + AUR via paru'
}
@test "--aur-helper=yay picks yay although paru is installed" {
use_fakes paru yay pacman runuser getent
run skup --aur-helper=yay list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay'
log_has 'yay -Qua'
log_lacks '^paru'
run skup --aur-helper yay list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay'
}
@test "--no-aur and --aur-helper=none: no helper runs, no AUR row, plain banner" {
for opt in --no-aur --aur-helper=none; do
: >"$FAKE_LOG"
run skup "$opt" list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux)'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
refute_output --partial 'pfetch'
log_lacks '^(paru|yay|runuser)'
done
}
@test "no helper installed: no AUR, plain banner, every row still has the marker column" {
use_fakes pacman runuser getent
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux)'
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
log_lacks '^runuser'
}
@test "--aur-helper names a helper that is not installed: error (exit 1), nothing runs" {
use_fakes paru pacman runuser getent
run skup --aur-helper=yay list --no-refresh
assert_failure 1
assert_output --partial 'error: --aur-helper=yay: yay not found in PATH'
[[ -z $(cat "$FAKE_LOG") ]]
}
@test "--aur-helper: an unknown or empty value is a usage error" {
run skup --aur-helper=trizen list
assert_failure 1
assert_output --partial "unknown AUR helper 'trizen' (expected paru, yay or none)"
run skup --aur-helper= list
assert_failure 1
assert_output --partial '--aur-helper needs a value'
run skup list --aur-helper
assert_failure 1
assert_output --partial '--aur-helper needs a value'
[[ -z $(cat "$FAKE_LOG") ]]
}
@test "apt: no helper is ever run, even with paru in PATH; --aur-helper is ignored with a warning" {
use_fakes apt paru runuser getent
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-debian12
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
refute_output --partial 'aur'
run skup --aur-helper=paru list --no-refresh
assert_success
assert_output --partial 'warning: --aur-helper=paru is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)'
run skup --no-aur list --no-refresh
assert_success
refute_output --partial 'warning'
log_lacks '^(paru|yay|runuser)'
}
# ---- the user the helper runs as ---------------------------------------------------------------
@test "root through sudo: the helper runs as SUDO_USER with runuser -u, that user's HOME, from that home" {
run skup list --no-refresh
assert_success
log_has "$AS_ALICE paru -Qua"
log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty"
}
@test "root through sudo without runuser: sudo -u USER -- env HOME=..." {
use_fakes paru pacman sudo getent
run skup list --no-refresh
assert_success
log_has "sudo -u alice -- env HOME=$ALICE_HOME paru -Qua"
log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty"
assert_line "$ROW1"
}
@test "unprivileged (list --no-refresh): the helper runs directly, as we are" {
export SKEPTICAL_UPDATER_EUID=1000
unset SUDO_USER
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru'
log_has 'paru -Qua'
log_lacks '^(runuser|sudo)'
assert_line "$ROW1"
}
@test "a root login (no SUDO_USER): AUR skipped with a warning after the banner; repository rows listed" {
unset SUDO_USER
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux)'
assert_line --index 1 'skeptical-updater: warning: aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded (run skeptical-updater through sudo from your own account, or use --no-aur)'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
[[ $(grep -c 'warning: aur:' <<<"$output") -eq 1 ]]
log_lacks '^(paru|runuser)'
}
@test "SUDO_USER root (or another uid 0 name) or an unknown user: AUR skipped with a warning" {
for u in root toor; do
SUDO_USER=$u run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux)'
assert_output --partial "warning: aur: paru is never run as root and the invoking user ($u) is root"
done
SUDO_USER=nosuchuser run skup list --no-refresh
assert_success
assert_output --partial "warning: aur: the invoking user 'nosuchuser' (SUDO_USER) is unknown"
log_lacks '^(paru|runuser)'
}
@test "a root login: the interactive upgrade runs pacman only, no helper" {
unset SUDO_USER
run skup --no-refresh <<<''
assert_success
log_has 'pacman -Su --noconfirm'
log_lacks '^(paru|runuser)'
}
# ---- listing ------------------------------------------------------------------------------------
@test "the * marker: right after the bracket of AUR rows, a space there on the others; columns aligned" {
run skup list --no-refresh
assert_success
assert_line '5 upgradable, 0 trusted (hidden, use --all to show), 5 to review:'
assert_output --partial "$(printf '%s\n' "$ROW1" "$ROW2" "$ROW3" "$ROW4" "$ROW5")"
}
@test "the marker column stays aligned with the - and = markers" {
write_state "$HEADER" "pacman${T}su-dummy-aur${T}user${T}2026-10-09T09:40:12Z"
run skup --all --no-refresh <<<$'1-\nl\nq'
assert_success
assert_line '-[1]* pfetch 1.10.0-1 -> 1.11.0-1 aur'
assert_line '=[3]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
assert_line ' [2] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
}
@test "--no-refresh still lists the AUR rows (the helper always asks the AUR)" {
run skup list --no-refresh
assert_success
log_lacks '^pacman -Sy'
log_has "$AS_ALICE paru -Qua"
assert_line "$ROW3"
}
@test "with a refresh too: AUR rows next to the rows of the reviewed databases" {
run skup list
assert_success
assert_line 'Refreshing package lists... done.'
assert_line "$ROW1"
assert_line "$ROW2"
}
@test "-Qua failing (AUR unreachable): the helper's message and a warning; repository rows listed" {
FAKE_AUR_QUA=fail run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru'
assert_output --partial 'warning: paru: error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect'
assert_output --partial 'warning: aur: paru -Qua failed, AUR packages not listed'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
refute_output --partial '*'
}
@test "yay -Qua failing: its multi-line message is shown without the blank lines" {
use_fakes yay pacman runuser getent
FAKE_AUR_QUA=fail run skup list --no-refresh
assert_success
assert_output --partial 'warning: yay: -> 1 error occurred:'
assert_output --partial 'dial tcp 127.0.0.1:443: connect: connection refused'
assert_output --partial 'warning: aur: yay -Qua failed, AUR packages not listed'
refute_line 'skeptical-updater: warning: yay: '
}
@test "-Qua with nothing to upgrade (exit 1, silent) is not a failure" {
FAKE_AUR_QUA=none run skup list --no-refresh
assert_success
refute_output --partial 'warning'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
}
@test "paru marks IgnorePkg packages [ignored] (shown [held]); yay leaves them out" {
FAKE_AUR_QUA=ignored run skup list --no-refresh
assert_success
assert_line ' [1]* pfetch 1.10.0-1 -> 1.11.0-1 aur [held]'
use_fakes yay pacman runuser getent
FAKE_AUR_QUA=ignored run skup list --no-refresh
assert_success
refute_output --partial 'pfetch'
assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
}
@test "yay: the age suffix ([122d13h]) is no flag; its colored root warning on stdout is no row" {
use_fakes yay pacman runuser getent
for q in fixture noise; do
FAKE_AUR_QUA=$q run skup list --no-refresh
assert_success
assert_output --partial "$(printf '%s\n' "$ROW1" "$ROW2" "$ROW3")"
refute_output --partial 'Avoid'
refute_output --partial '122d13h'
done
}
@test "devel packages: the new version is latest-commit (paru by default, yay with --devel)" {
for h in paru yay; do
use_fakes "$h" pacman runuser getent
FAKE_AUR_QUA=devel run skup list --no-refresh
assert_success
assert_line --regexp '^ \[1\]\* pfetch-git +r432\.a906ff8-1 +-> +latest-commit +aur$'
done
}
@test "a name a repository lists wins over the AUR row (moved from the AUR into a repository)" {
FAKE_AUR_QUA=dup run skup list --no-refresh
assert_success
assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$'
refute_output --partial '9.0-1'
}
# ---- N? N?? N+ Nx on an AUR row ---------------------------------------------------------------
@test "N? on an AUR row: description, install reason and dependents from pacman -Qi" {
run skup --no-refresh <<<$'1?\nq'
assert_success
assert_output --partial "$(printf '%s\n' '> 1?' \
'pfetch: A pretty system information tool written in POSIX sh.' \
'reason: explicitly installed' \
'required by: nothing')"
log_lacks 'paru -Si'
}
@test "N?? on an AUR row: pacman -Qi merged with paru -Sia (same key column)" {
run skup --no-refresh <<<$'1??\nq'
assert_success
log_has "$AS_ALICE paru -Sia pfetch"
assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks'
assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')"
assert_line 'Install Reason : Explicitly installed'
assert_line 'Description : A pretty system information tool written in POSIX sh.'
[[ $(count_lines 'Description : A pretty system information tool written in POSIX sh.') -eq 1 ]]
assert_output --partial "$(printf '%s\n' \
'> Repository : aur' \
'> AUR URL : https://aur.archlinux.org/packages/pfetch' \
'> Make Deps : None' \
'> Check Deps : None' \
'> 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')"
}
@test "N?? with yay: its wider key column is re-padded to pacman's" {
use_fakes yay pacman runuser getent
run skup --no-refresh <<<$'1??\nq'
assert_success
log_has "$AS_ALICE yay -Sia pfetch"
assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')"
assert_line '> Repository : aur'
assert_line '> Keywords : fetch'
assert_line '> First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC'
assert_line '> Out-of-date : No'
refute_output --regexp 'Repository {10,}:'
}
@test "N?? when the AUR has no record of it: the installed record alone" {
run skup --no-refresh <<<$'3??\nq'
assert_success
assert_line 'Name : su-dummy-aur'
assert_line 'Install Reason : Explicitly installed'
refute_line --regexp '^> [A-Z#]'
log_has "$AS_ALICE paru -Sia su-dummy-aur"
}
@test "N?? when the AUR record does not merge: -Qi, then the AUR record with every line marked >" {
FAKE_AUR_SIA=twice run skup --no-refresh <<<$'1??\nq'
assert_success
assert_line '# lines starting with > are the AUR record (paru -Sia) as it prints it'
refute_line --partial '# lines starting with > come from the upstream candidate'
assert_line 'Version : 1.10.0-1'
[[ $(count_lines '> Version : 1.11.0-1') -eq 2 ]]
[[ $(count_lines '> Repository : aur') -eq 2 ]]
assert_line 'Install Reason : Explicitly installed'
}
@test "show PKG for an AUR package: the same merge, the helper decided on the spot" {
run skup show pfetch
assert_success
assert_line '> Repository : aur'
assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')"
export SKEPTICAL_UPDATER_EUID=1000
unset SUDO_USER
: >"$FAKE_LOG"
run skup show pfetch
assert_success
assert_line '> Maintainer : yigits'
log_has 'paru -Sia pfetch'
log_lacks '^runuser'
}
@test "show of an AUR package as a root login: one warning, the installed record" {
unset SUDO_USER
run skup show pfetch
assert_success
assert_line 'Name : pfetch'
refute_line --regexp '^> '
[[ $(grep -c 'warning: aur:' <<<"$output") -eq 1 ]]
log_lacks '^paru'
}
@test "N+ on an AUR row trusts it like any installed package (backend pacman)" {
run skup --no-refresh <<<$'1+\nl\nq'
assert_success
assert_line 'trusted pfetch'
assert_line '5 upgradable, 1 trusted (hidden, use --all to show), 4 to review:'
run cat "$STATE_FILE"
assert_line --regexp "^pacman${T}pfetch${T}user${T}${DATE_RE}\$"
}
@test "Nx on an AUR row purges it with pacman -Rns; the row leaves the list" {
run skup --no-refresh <<<$'1x\ny\nl\nq'
assert_success
log_has 'pacman -Rs --print --print-format %n -- pfetch'
assert_line 'Purge pfetch now? [y/N] y'
log_has 'pacman -Rns --noconfirm -- pfetch'
assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:'
[[ $(grep -c '^ \[1\]\* pfetch' <<<"$output") -eq 1 ]] || fail "row 1 is still listed after the purge"
assert_line ' [3]* su-dummy-aur 1.0-1 -> 2.0-1 aur'
}
# ---- the upgrade --------------------------------------------------------------------------------
@test "<enter>: the repository step first (pre-check, databases, -Su), then the helper as the invoking user" {
run skup <<<''
assert_success
assert_line 'Upgrade finished.'
run upgrade_steps
assert_output "$(printf '%s\n' \
'pacman -Sup --noconfirm --dbpath DB --print-format %n %v' \
'pacman -Su --noconfirm' \
'real sync at -Su: reviewed 1 / no-sig' \
"$AS_ALICE paru -Sua --noconfirm" \
'paru -Sua --noconfirm')"
log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty"
}
@test "N- on an AUR row: the helper's --ignore; pacman never sees AUR names" {
run skup --no-refresh <<<$'1-\n'
assert_success
assert_line 'Excluded from this run: pfetch'
log_has 'pacman -Su --noconfirm'
assert_line "+ $AS_ALICE paru -Sua --noconfirm --ignore pfetch"
log_has 'paru -Sua --noconfirm --ignore pfetch'
log_lacks '^pacman .*--ignore'
}
@test "N- on a repository row: pacman --ignore and the helper's --ignore both get it" {
run skup --no-refresh <<<$'2-\n1-\n'
assert_success
log_has 'pacman -Su --noconfirm --ignore su-dummy-a'
log_has "$AS_ALICE paru -Sua --noconfirm --ignore pfetch,su-dummy-a"
}
@test "every AUR row excluded: the helper is not run" {
run skup --no-refresh <<<$'1-\n3-\n'
assert_success
log_has 'pacman -Su --noconfirm'
log_lacks '-Sua'
}
@test "only AUR rows left to upgrade: pacman is not run, only the helper" {
run skup <<<$'2- 4- 5-\n'
assert_success
assert_line 'No repository package to upgrade: pacman is not run, only paru.'
log_lacks '^pacman -Su'
log_lacks '^pacman -Sup --noconfirm'
log_lacks '^real sync'
log_has "$AS_ALICE paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c"
assert_line 'Upgrade finished.'
}
@test "no AUR row at all: exactly the repository upgrade as before" {
FAKE_AUR_QUA=none run skup --no-refresh <<<''
assert_success
log_has 'pacman -Su --noconfirm'
log_lacks '-Sua'
}
@test "trusted (hidden) AUR rows are upgraded by the helper too" {
write_state "$HEADER" "pacman${T}pfetch${T}user${T}2026-10-09T09:40:12Z" "pacman${T}su-dummy-aur${T}user${T}2026-10-09T09:40:12Z"
# listed: [1] su-dummy-a [2] su-dummy-b [3] su-dummy-c (both AUR rows trusted: hidden)
run skup --no-refresh <<<$'1- 2- 3-\n'
assert_success
assert_line 'No repository package to upgrade: pacman is not run, only paru.'
log_has "$AS_ALICE paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c"
}
@test "an [ignored] AUR row alone does not make pacman stand aside" {
FAKE_AUR_QUA=ignored run skup --no-refresh <<<$'2- 3- 4- 5-\n'
assert_success
log_has 'pacman -Su --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c'
log_lacks '-Sua'
}
@test "the helper failing after a successful repository step: reported, its exit status returned" {
FAKE_AUR_SUA_RC=1 run skup --no-refresh <<<''
assert_failure 1
log_has 'pacman -Su --noconfirm'
assert_output --partial 'error: the AUR upgrade failed: paru exited with status 1 (the repository packages were upgraded)'
refute_output --partial 'hint:'
refute_output --partial 'Upgrade finished.'
}
@test "the helper failing in an AUR-only upgrade: its exit status" {
FAKE_AUR_SUA_RC=4 run skup --no-refresh <<<$'2- 4- 5-\n'
assert_failure 4
assert_output --partial 'error: the AUR upgrade failed: paru exited with status 4'
refute_output --partial 'repository packages were upgraded'
}
@test "a failing repository step: the helper is not run" {
FAKE_PACMAN_SU_RC=1 run skup --no-refresh <<<''
assert_failure 1
assert_output --partial 'the upgrade failed: pacman exited with status 1'
log_lacks '-Sua'
FAKE_PACMAN_SUP_RC=1 run skup <<<'2-'$'\n'
assert_failure 1
log_lacks '-Sua'
}
@test "--dry-run prints the helper's command line (root through sudo: as the invoking user)" {
run skup --dry-run --no-refresh <<<$'1-\n'
assert_success
assert_line '+ pacman -Su --noconfirm'
assert_line "+ $AS_ALICE paru -Sua --noconfirm --ignore pfetch"
assert_line 'Dry run: nothing was changed.'
log_lacks '-Sua'
log_lacks '^pacman -Su '
}
@test "--dry-run unprivileged: the helper's plain command line" {
export SKEPTICAL_UPDATER_EUID=1000
unset SUDO_USER
run skup --dry-run <<<''
assert_success
assert_line '+ paru -Sua --noconfirm'
log_lacks '-Sua'
log_lacks '^(runuser|sudo)'
}
@test "on a terminal: no --noconfirm, the helper gets the terminal (its PKGBUILD review)" {
pty_skup $'3-\r\r' --no-refresh
assert_success
log_has "$AS_ALICE paru -Sua --ignore su-dummy-aur"
log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=tty"
log_lacks 'noconfirm --ignore su-dummy-aur'
log_has 'pacman -Su --noconfirm'
}
@test "help and --help mention the * marker and the options" {
run skup --no-refresh <<<$'h\nq'
assert_success
assert_line " l redraw the list (a '*' after the number marks an AUR package)"
run skup --help
assert_success
assert_line --partial '--aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or'
assert_line --partial '--no-aur same as --aur-helper=none'
assert_output --partial "A '*' right after the number marks an AUR package"
}
+1
View File
@@ -0,0 +1 @@
error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect: tcp connect error: Connection refused (os error 111): error trying to connect: tcp connect error: Connection refused (os error 111): tcp connect error: Connection refused (os error 111): Connection refused (os error 111)
+4
View File
@@ -0,0 +1,4 @@
-> 1 error occurred:
* request failed: Get "https://aur.archlinux.org/rpc?arg%5B%5D=paru&arg%5B%5D=pfetch&arg%5B%5D=su-dummy-aur&type=info&v=5": dial tcp 127.0.0.1:443: connect: connection refused
+1
View File
@@ -0,0 +1 @@
pfetch-git r432.a906ff8-1 -> latest-commit
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1
su-dummy-b 1.0-1 -> 9.0-1
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1
su-dummy-aur 1.0-1 -> 2.0-1
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1 [ignored]
su-dummy-aur 1.0-1 -> 2.0-1
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1
su-dummy-aur 1.0-1 -> 2.0-1
+1
View File
@@ -0,0 +1 @@
pfetch-git r432.a906ff8-1 -> latest-commit [2299d6h]
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1 [122d13h]
su-dummy-b 1.0-1 -> 9.0-1 [1d2h]
+2
View File
@@ -0,0 +1,2 @@
pfetch 1.10.0-1 -> 1.11.0-1 [122d13h]
su-dummy-aur 1.0-1 -> 2.0-1 [3d4h]
+1
View File
@@ -0,0 +1 @@
su-dummy-aur 1.0-1 -> 2.0-1 [3d4h]
+3
View File
@@ -0,0 +1,3 @@
 -> Avoid running yay as root/sudo.
pfetch 1.10.0-1 -> 1.11.0-1 [122d13h]
su-dummy-aur 1.0-1 -> 2.0-1 [3d4h]
+21
View File
@@ -0,0 +1,21 @@
Repository : aur
Name : pfetch
Version : 1.11.0-1
Description : A pretty system information tool written in POSIX sh.
URL : https://github.com/Un1q32/pfetch
AUR URL : https://aur.archlinux.org/packages/pfetch
Groups : None
Licenses : MIT
Provides : pfetch
Depends On : None
Make Deps : None
Check Deps : None
Optional Deps : None
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
+23
View File
@@ -0,0 +1,23 @@
Repository : aur
Name : pfetch
Version : 1.11.0-1
Description : A pretty system information tool written in POSIX sh.
URL : https://github.com/Un1q32/pfetch
Licenses : MIT
Groups : None
Provides : pfetch
Depends On : None
Optional Deps : None
Make Deps : None
Check Deps : None
Conflicts With : pfetch-git
Replaces : None
AUR URL : https://aur.archlinux.org/packages/pfetch
First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC
Keywords : fetch
Last Modified : Tue 09 Jun 2026 10:43:50 PM UTC
Maintainer : yigits
Popularity : 0.103440
Votes : 48
Out-of-date : No
+44
View File
@@ -288,3 +288,47 @@ Install Date : Sun Oct 4 00:00:00 2026
Install Reason : Installed as a dependency for another package
Install Script : No
Validated By : Signature
Name : pfetch
Version : 1.10.0-1
Description : A pretty system information tool written in POSIX sh.
Architecture : any
URL : https://github.com/Un1q32/pfetch
Licenses : MIT
Groups : None
Provides : pfetch
Depends On : None
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : pfetch-git
Replaces : None
Installed Size : 81.50 KiB
Packager : Unknown Packager
Build Date : Sat Oct 10 12:42:02 2026
Install Date : Sat Oct 10 12:42:04 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : su-dummy-aur
Version : 1.0-1
Description : Skeptical-updater dummy package AUR (foreign: in no repository)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake getent (unit tests): "getent passwd NAME" for alice (uid 1000, home $FAKE_DIR/home/alice),
# root and toor (uid 0); exit 2 (no such key) for anyone else
[[ ${1:-} == passwd ]] || exit 1
case ${2:-} in
alice) echo "alice:x:1000:1000:Alice:${FAKE_DIR:?}/home/alice:/bin/bash" ;;
root) echo "root:x:0:0::/root:/bin/bash" ;;
toor) echo "toor:x:0:0:root by another name:/root:/bin/bash" ;;
*) exit 2 ;;
esac
+1 -1
View File
@@ -145,7 +145,7 @@ case $op in
fi
case $tg in
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;; # b was installed as a dependency of a
su-dummy-c | glib2 | expat | ca-certificates-mozilla) echo "$tg" ;;
su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) echo "$tg" ;; # (the last two: foreign, AUR)
*)
echo "error: target not found: $tg" >&2
exit 1
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake paru (unit tests; fakes/yay/yay runs this file as yay): replays the output of paru 2.1.0
# and yay 13.0.1 recorded in discovery/pacman.md section 15 (tests/unit/data/aur/) and logs every
# call, followed by "<helper> as: HOME=... PWD=... stdin=tty|notty" (where and how it runs).
# FAKE_AUR_QUA selects what -Qua prints: fixture (default: pfetch and su-dummy-aur), ignored
# (pfetch in IgnorePkg: paru marks it [ignored], yay leaves it out), devel (a -git package whose
# upstream moved: "-> latest-commit"), dup (an AUR row for su-dummy-b, which a repository has),
# noise (yay's colored root warning on stdout), none (nothing to upgrade: exit 1, no output) or
# fail (the AUR is unreachable: exit 1 with the recorded error). Packages purged by the fake pacman
# are not listed. -Sia PKG prints data/aur/Sia-<helper>-PKG.txt (else the helper's not-found
# error, exit 1), twice with FAKE_AUR_SIA=twice (a record that cannot be merged); -Sua exits
# FAKE_AUR_SUA_RC (default 0).
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
if [[ -t 0 ]]; then stdin='tty'; else stdin='notty'; fi
printf '%s as: HOME=%s PWD=%s stdin=%s\n' "$FAKE_NAME" "${HOME-}" "$PWD" "$stdin" >>"$FAKE_LOG"
D=$FAKE_DATA/aur
case ${1:-} in
-Qua)
case ${FAKE_AUR_QUA:-fixture} in
none) exit 1 ;;
fail)
cat "$D/Qua-fail-$FAKE_NAME.err" >&2
exit 1
;;
esac
out=$(
while IFS= read -r line; do
fake_purged "${line%% *}" || printf '%s\n' "$line"
done <"$D/Qua-$FAKE_NAME-${FAKE_AUR_QUA:-fixture}.txt"
)
[[ -n $out ]] || exit 1
printf '%s\n' "$out"
;;
-Sia)
if [[ -f $D/Sia-$FAKE_NAME-${2:-}.txt ]]; then
cat "$D/Sia-$FAKE_NAME-$2.txt"
[[ ${FAKE_AUR_SIA:-} != twice ]] || cat "$D/Sia-$FAKE_NAME-$2.txt"
exit 0
fi
if [[ $FAKE_NAME == yay ]]; then
echo " -> No AUR package found for ${2:-}" >&2
else
echo "error: package '${2:-}' was not found" >&2
fi
exit 1
;;
-Sua) exit "$(fake_rc FAKE_AUR_SUA_RC 0)" ;;
*)
echo "fake $FAKE_NAME: unhandled: $*" >&2
exit 99
;;
esac
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake runuser (unit tests): logs "runuser -u USER -- CMD..." and runs CMD (as the same user)
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
if [[ ${1:-} != -u || ${3:-} != -- ]]; then
echo "fake runuser: unexpected arguments: $*" >&2
exit 98
fi
shift 3
exec "$@"
+6 -1
View File
@@ -2,8 +2,13 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake sudo (unit tests): logs the command it was asked to run, runs nothing
# fake sudo (unit tests): logs the command it was asked to run; runs nothing (the re-exec of the
# applet), except "sudo -u USER -- CMD...", which runs CMD (as the same user: an AUR helper)
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
if [[ ${1:-} == -u && ${3:-} == -- ]]; then
shift 3
exec "$@"
fi
exit 0
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake yay (unit tests): the fake paru, replaying yay's recorded output instead
FAKE_AS=yay exec "${BASH_SOURCE[0]%/*}/../paru/paru" "$@"
+1 -1
View File
@@ -368,7 +368,7 @@ EOF
assert_line --regexp '^ \[5\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$'
# epoch kept on both sides
assert_line --regexp '^ \[6\] vim-minimal +2:8\.2\.2637-26\.el9_8\.21 +-> +2:8\.2\.2637-26\.el9_8\.23 +baseos$'
refute_line --regexp '\] su-dummy-new'
refute_line --regexp '\] +su-dummy-new'
refute_output --partial 'su-dummy-old'
refute_output --partial 'Obsoleting'
assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new'
+1 -1
View File
@@ -36,7 +36,7 @@ common_setup() {
export TMPDIR=$BATS_TEST_TMPDIR
export FAKE_PACMAN_DBPATH=$FAKE_DIR/pacman-db
mkdir -p "$FAKE_PACMAN_DBPATH/sync" "$FAKE_PACMAN_DBPATH/local"
unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT
unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT SUDO_USER
use_fakes apt
}