diff options
| -rwxr-xr-x | archsetup | 14 | ||||
| -rwxr-xr-x | scripts/audit-packages.sh | 135 | ||||
| -rw-r--r-- | tests/audit-packages/test_audit_packages.py | 127 |
3 files changed, 270 insertions, 6 deletions
@@ -796,15 +796,17 @@ install_gpu_drivers() { if $detected_amd; then display "task" "AMD GPU detected (via modalias) - installing drivers" - pacman_install mesa + pacman_install mesa # includes VAAPI drivers (libva-mesa-driver was folded in) pacman_install xf86-video-amdgpu pacman_install vulkan-radeon - pacman_install libva-mesa-driver # hardware video acceleration fi if $detected_nvidia; then display "task" "NVIDIA GPU detected (via modalias) - installing drivers" - pacman_install nvidia-dkms # DKMS version for kernel flexibility + # nvidia-dkms left the repos; nvidia-open-dkms is the packaged driver + # (Turing and newer — pre-Turing cards need an AUR legacy variant, + # see the NVIDIA preflight task). + pacman_install nvidia-open-dkms pacman_install nvidia-utils pacman_install nvidia-settings pacman_install libva-nvidia-driver # hardware video acceleration @@ -1675,8 +1677,8 @@ hyprland() { aur_install pyprland # scratchpads, magnify, expose (fixes special workspace issues) pacman_install waybar # status bar pacman_install fuzzel # app launcher (native Wayland, pinentry support) - pacman_install swww # wallpaper daemon - aur_install waypaper # wallpaper GUI (swww frontend) + pacman_install awww # wallpaper daemon (swww successor; provides swww) + aur_install waypaper # wallpaper GUI (awww backend) aur_install wlogout-git # logout menu pacman_install grim # screenshot pacman_install slurp # region select @@ -1933,7 +1935,7 @@ desktop_environment() { aur_install "$software" done - pacman_install libappindicator-gtk3 # required by some applets + pacman_install libayatana-appindicator # appindicator support (libappindicator-gtk3 successor) # Browsers diff --git a/scripts/audit-packages.sh b/scripts/audit-packages.sh new file mode 100755 index 0000000..f7af19f --- /dev/null +++ b/scripts/audit-packages.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# audit-packages.sh — verify every package archsetup installs still exists +# at its declared source, and flag packages that moved between the official +# repos and the AUR. +# +# Extraction covers direct `pacman_install pkg` / `aur_install pkg` calls +# and `for software in a b c; do` loop lists (backslash continuations +# included). Variable arguments ("$software") are the loop plumbing, not +# packages, and are skipped. +# +# Checks: +# official package missing from sync dbs -> MOVED TO AUR (if AUR has it) +# or MISSING EVERYWHERE +# AUR package gone from the AUR -> NOW IN OFFICIAL (if repos +# have it) or MISSING EVERYWHERE +# +# Usage: audit-packages.sh [--list] [installer-script] +# --list print the extracted package lists and exit (no network) +# default audit; exit 1 when anything is missing or moved +# +# Env overrides (tests): AUDIT_PACMAN, AUDIT_CURL. + +set -u + +PACMAN="${AUDIT_PACMAN:-pacman}" +CURL="${AUDIT_CURL:-curl}" + +mode=audit +case "${1:-}" in + --list) mode=list; shift ;; +esac + +script="${1:-$(dirname "$0")/../archsetup}" +if [ ! -r "$script" ]; then + echo "audit-packages: cannot read installer script: $script" >&2 + exit 2 +fi + +# Extract package names per source. Loop lists are attributed to the +# install command used inside the loop body. +extract() { + awk ' + # Collect a `for software in ...` list (may span backslash-continued + # lines); attribute it when the loop body shows which installer runs. + /for[ \t]+[A-Za-z_]+[ \t]+in[ \t]/ { + line = $0 + sub(/^.*[ \t]in[ \t]+/, "", line) + listpkgs = "" + while (1) { + stop = (line ~ /;[ \t]*do([ \t]|$)/) + sub(/;[ \t]*do.*$/, "", line) + gsub(/\\/, "", line) + listpkgs = listpkgs " " line + if (stop) break + if ((getline line) <= 0) break + } + pending = listpkgs + next + } + /^[ \t]*(pacman_install|aur_install)[ \t]/ { + cmd = ($0 ~ /pacman_install/) ? "official" : "aur" + arg = $2 + gsub(/["'\'']/, "", arg) + if (arg ~ /^\$/) { + # loop plumbing — emit the pending for-list under this source + n = split(pending, w, /[ \t]+/) + for (i = 1; i <= n; i++) if (w[i] != "") print cmd, w[i] + pending = "" + } else if (arg != "") { + print cmd, arg + } + } + ' "$script" | sort -u +} + +pairs=$(extract) +official=$(echo "$pairs" | awk '$1 == "official" { print $2 }') +aur=$(echo "$pairs" | awk '$1 == "aur" { print $2 }') + +if [ "$mode" = list ]; then + echo "OFFICIAL ($(echo "$official" | grep -c .)):" + echo "$official" | sed 's/^/ /' + echo "AUR ($(echo "$aur" | grep -c .)):" + echo "$aur" | sed 's/^/ /' + exit 0 +fi + +# One batched AUR RPC query answers existence for every AUR-relevant name. +aur_query() { + # args: package names; prints the names the AUR knows + local args="" p + for p in "$@"; do args="$args&arg[]=$p"; done + "$CURL" -sm 20 "https://aur.archlinux.org/rpc/v5/info?${args#&}" 2>/dev/null \ + | tr ',' '\n' | sed -n 's/.*"Name":[[:space:]]*"\([^"]*\)".*/\1/p' +} + +# shellcheck disable=SC2086 +aur_known=$(aur_query $official $aur) + +in_repos() { "$PACMAN" -Si "$1" >/dev/null 2>&1; } +in_aur() { echo "$aur_known" | grep -qx "$1"; } + +moved_to_aur="" now_official="" missing="" +for p in $official; do + in_repos "$p" && continue + if in_aur "$p"; then moved_to_aur="$moved_to_aur $p"; else missing="$missing $p"; fi +done +for p in $aur; do + if in_aur "$p"; then + in_repos "$p" && now_official="$now_official $p (also in repos)" + continue + fi + if in_repos "$p"; then now_official="$now_official $p"; else missing="$missing $p"; fi +done + +rc=0 +if [ -n "$missing" ]; then + echo "MISSING EVERYWHERE (not in repos, not in AUR):" + for p in $missing; do echo " $p"; done + rc=1 +fi +if [ -n "$moved_to_aur" ]; then + echo "MOVED TO AUR (pacman_install will fail; switch to aur_install):" + for p in $moved_to_aur; do echo " $p"; done + rc=1 +fi +if [ -n "$now_official" ]; then + echo "NOW IN OFFICIAL (aur_install works but pacman_install is cleaner):" + echo "$now_official" | tr ' ' '\n' | grep -v '^$' | sed 's/^/ /' + rc=1 +fi +total=$(( $(echo "$official" | grep -c .) + $(echo "$aur" | grep -c .) )) +echo "---" +echo "$total packages audited" +exit $rc diff --git a/tests/audit-packages/test_audit_packages.py b/tests/audit-packages/test_audit_packages.py new file mode 100644 index 0000000..5adb26d --- /dev/null +++ b/tests/audit-packages/test_audit_packages.py @@ -0,0 +1,127 @@ +"""Tests for scripts/audit-packages.sh. + +The auditor extracts every package archsetup installs — direct +pacman_install/aur_install calls plus `for software in ...` loop lists — +then verifies each against the right source: official packages must exist +in the sync databases, AUR packages in the AUR. Packages that moved +between sources are flagged in both directions. Tests run the real script +against a fixture installer snippet, a fake pacman, and a fake curl that +serves controlled AUR RPC JSON; no network, no real pacman db. + +Run from repo root: + make test-unit (or python3 -m unittest tests.audit-packages.test_audit_packages) +""" + +import json +import os +import shutil +import stat +import subprocess +import tempfile +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +SCRIPT = os.path.join(REPO_ROOT, "scripts/audit-packages.sh") + +FIXTURE = """ +pacman_install good-official # exists in repos +pacman_install gone-official # vanished from repos, not in AUR +pacman_install moved-to-aur # left repos, lives in AUR now +aur_install good-aur # exists in AUR +aur_install gone-aur # vanished from AUR +aur_install now-official # promoted into the repos +aur_install "$software" # variable — not a literal package +for software in loop-pkg-a loop-pkg-b \\ + loop-pkg-c; do + pacman_install "$software" +done +""" + + +class AuditHarness(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="audit-pkgs-test-") + self.fixture = os.path.join(self.tmp, "installer-snippet") + with open(self.fixture, "w") as f: + f.write(FIXTURE) + + # Fake pacman: -Si exits 0 only for the packages "in the repos". + official = ["good-official", "now-official", + "loop-pkg-a", "loop-pkg-b", "loop-pkg-c"] + self.fake_pacman = os.path.join(self.tmp, "pacman") + self._stub(self.fake_pacman, ( + 'case "$2" in\n' + + "".join(f" {p}) exit 0 ;;\n" for p in official) + + ' *) exit 1 ;;\nesac\n' + )) + + # Fake curl: serves AUR RPC v5 info JSON for the packages "in the AUR". + aur = ["good-aur", "moved-to-aur"] + rpc = json.dumps({"results": [{"Name": p} for p in aur]}) + self.fake_curl = os.path.join(self.tmp, "curl") + self._stub(self.fake_curl, f"printf '%s' '{rpc}'\nexit 0\n") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def _stub(self, path, body): + with open(path, "w") as f: + f.write("#!/bin/sh\n" + body) + os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) + + def run_audit(self, *args): + env = os.environ.copy() + env["AUDIT_PACMAN"] = self.fake_pacman + env["AUDIT_CURL"] = self.fake_curl + return subprocess.run( + [SCRIPT, *args, self.fixture], + env=env, capture_output=True, text=True, timeout=20, + ) + + +class TestParser(AuditHarness): + def test_list_mode_extracts_direct_and_loop_packages(self): + r = self.run_audit("--list") + self.assertEqual(r.returncode, 0, msg=r.stderr) + for p in ("good-official", "gone-official", "moved-to-aur", + "loop-pkg-a", "loop-pkg-b", "loop-pkg-c"): + self.assertIn(p, r.stdout) + for p in ("good-aur", "gone-aur", "now-official"): + self.assertIn(p, r.stdout) + + def test_variable_args_are_not_packages(self): + r = self.run_audit("--list") + self.assertNotIn("$software", r.stdout) + + +class TestAudit(AuditHarness): + def test_clean_packages_pass_silently(self): + r = self.run_audit() + self.assertNotIn("good-official", r.stdout) + self.assertNotIn("good-aur", r.stdout) + + def test_missing_everywhere_reported(self): + r = self.run_audit() + self.assertNotEqual(r.returncode, 0) + self.assertIn("gone-official", r.stdout) + self.assertIn("gone-aur", r.stdout) + + def test_movers_flagged_in_both_directions(self): + r = self.run_audit() + self.assertIn("moved-to-aur", r.stdout) + self.assertIn("now-official", r.stdout) + + def test_clean_fixture_exits_zero(self): + clean = os.path.join(self.tmp, "clean-snippet") + with open(clean, "w") as f: + f.write("pacman_install good-official\naur_install good-aur\n") + env = os.environ.copy() + env["AUDIT_PACMAN"] = self.fake_pacman + env["AUDIT_CURL"] = self.fake_curl + r = subprocess.run([SCRIPT, clean], env=env, + capture_output=True, text=True, timeout=20) + self.assertEqual(r.returncode, 0, msg=r.stdout + r.stderr) + + +if __name__ == "__main__": + unittest.main() |
