#!/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