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:
1 parent
875c8294bf
commit
def537af58
39 files changed
+1903
-115
No files matched your search
@@ -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>]` |
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user