172 lines
7.9 KiB
Bash
172 lines
7.9 KiB
Bash
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# pacman helper functions for skeptical-updater (proven on pacman 7.1.0 /
|
|
# archlinux:latest by discovery/pacman/checks.sh). Source this file.
|
|
#
|
|
# Conventions
|
|
# * every pacman call runs with LC_ALL=C (stable field names / messages) and
|
|
# COLUMNS=0 (never wrap -Qi/-Si output, even if COLUMNS is exported).
|
|
# * PM_OPTS is an optional array of extra global pacman options, e.g.
|
|
# PM_OPTS=(--dbpath /tmp/checkdb) to work against a temporary sync DB.
|
|
# * nothing here needs pacman-contrib, sudo, python or fakeroot.
|
|
|
|
PM_OPTS=("${PM_OPTS[@]}")
|
|
|
|
_pm() { LC_ALL=C COLUMNS=0 pacman "${PM_OPTS[@]}" "$@"; }
|
|
|
|
# ---------------------------------------------------------------- refresh ---
|
|
|
|
# Refresh the real sync DBs (root). Safe as long as it is followed by -Su
|
|
# (never `pacman -S foo` after a bare -Sy: that is the partial-upgrade case).
|
|
pm_refresh() { LC_ALL=C pacman -Sy "$@"; }
|
|
|
|
# Refresh a THROWAWAY sync DB (root, no fakeroot/pacman-contrib needed) and
|
|
# point PM_OPTS at it; the system DB under /var/lib/pacman/sync is untouched.
|
|
# The temp dir must be world-traversable (mode 755) because the sandboxed
|
|
# download user "alpm" has to write into it, mktemp -d's 0700 is not enough.
|
|
pm_refresh_tmpdb() {
|
|
local db
|
|
db=$(mktemp -d /tmp/su-checkdb.XXXXXX) || return 1
|
|
chmod 755 "$db"
|
|
ln -s "$(pacman-conf DBPath)local" "$db/local" || return 1
|
|
LC_ALL=C pacman -Sy --dbpath "$db" --logfile /dev/null >/dev/null || return 1
|
|
PM_OPTS=(--dbpath "$db")
|
|
PM_TMPDB=$db
|
|
}
|
|
|
|
# ------------------------------------------------------------ upgradable ---
|
|
|
|
# name<TAB>oldversion<TAB>newversion<TAB>repo<TAB>ignored(0|1)
|
|
# -Qu line : "name old -> new" or "name old -> new [ignored]"
|
|
# -Sl line : "repo name version [installed[: oldver]]"
|
|
# The candidate repo is the FIRST repo (config order) that carries the name,
|
|
# which is what both `-Qu` and `-S name` pick (verified with a dup in 2 repos).
|
|
# Prints nothing (and returns 1) when nothing is upgradable, like `pacman -Qu`.
|
|
pm_upgradable() {
|
|
local out
|
|
out=$(awk '
|
|
FNR == NR { if (!($2 in repo)) repo[$2] = $1; next }
|
|
{ printf "%s\t%s\t%s\t%s\t%d\n", $1, $2, $4, repo[$1], ($5 == "[ignored]") }
|
|
' <(_pm -Sl 2>/dev/null) <(_pm -Qu 2>/dev/null))
|
|
[ -n "$out" ] || return 1
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
# Effective IgnorePkg patterns (globs allowed), wherever they are configured
|
|
# (pacman.conf or an Include such as /etc/pacman.d/ignorepkg).
|
|
pm_ignored_patterns() { pacman-conf IgnorePkg; }
|
|
|
|
# ------------------------------------------------------------------ info ---
|
|
|
|
# pm_field <-Qi|-Si> <Field> <target>...
|
|
# -> "<name>\t<value>" per package; wrapped continuation lines are joined with
|
|
# one space. Use "repo/name" targets with -Si (a name in 2 repos prints twice).
|
|
# List fields keep pacman's two-space separators (split with `tr -s ' ' '\n'`);
|
|
# "None" is returned verbatim. Missing targets are skipped (the pacman error
|
|
# goes to /dev/null).
|
|
pm_field() {
|
|
local op=$1 field=$2; shift 2
|
|
_pm "$op" -- "$@" 2>/dev/null | awk -v f="$field" '
|
|
function flush() { if (name != "") print name "\t" val; name = ""; val = ""; infield = 0 }
|
|
/^[ \t]*$/ { flush(); next }
|
|
/^[^ \t]/ {
|
|
infield = 0
|
|
i = index($0, ":"); k = substr($0, 1, i - 1); sub(/[ \t]+$/, "", k)
|
|
v = substr($0, i + 1); sub(/^[ \t]+/, "", v)
|
|
if (k == "Name") name = v
|
|
else if (k == f) { val = v; infield = 1 }
|
|
next
|
|
}
|
|
infield { sub(/^[ \t]+/, ""); val = val " " $0 }
|
|
END { flush() }'
|
|
}
|
|
|
|
# One-line description of the installed package / of the upgrade candidate.
|
|
pm_desc_local() { pm_field -Qi Description "$@"; } # name<TAB>desc
|
|
pm_desc_cand() { pm_field -Si Description "$@"; } # args: repo/name
|
|
|
|
# Full dumps (verbatim pacman output, never wrapped).
|
|
pm_info_local() { _pm -Qi -- "$@"; }
|
|
pm_info_cand() { _pm -Si -- "$@"; } # args: repo/name
|
|
|
|
# Installed reverse dependencies as plain names, one per line (sorted).
|
|
pm_required_by() { pm_field -Qi 'Required By' "$1" | cut -f2- | tr -s ' ' '\n' | grep -vxE 'None|' | sort -u; }
|
|
pm_optional_for() { pm_field -Qi 'Optional For' "$1" | cut -f2- | tr -s ' ' '\n' | grep -vxE 'None|' | sort -u; }
|
|
|
|
# ------------------------------------------------------------ dependency ---
|
|
|
|
# pm_closure [-r] [-o] <pkg-or-dep>...
|
|
# Recursive closure over INSTALLED packages, one real package name per line,
|
|
# sorted, roots excluded.
|
|
# default : forward closure (Depends On, with provides/soname/version
|
|
# constraints resolved: `libfoo.so=1-64`, `glibc>=2.38`, `sh`)
|
|
# -r : reverse closure (Required By)
|
|
# -o : with -r also follow "Optional For"
|
|
# Roots may be package names or provided names. One `pacman -Qi` call,
|
|
# one awk process.
|
|
pm_closure() {
|
|
local mode=deps opt=0
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in -r) mode=rdeps ;; -o) opt=1 ;; --) shift; break ;; *) break ;; esac
|
|
shift
|
|
done
|
|
_pm -Qi 2>/dev/null | awk -v roots="$*" -v mode="$mode" -v opt="$opt" '
|
|
function norm(s) { gsub(/[ \t]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s); return (s == "None") ? "" : s }
|
|
function flush( c, a, i, p) {
|
|
if (name == "") return
|
|
real[name] = 1; dep[name] = norm(F["Depends On"]); rb[name] = norm(F["Required By"])
|
|
of[name] = norm(F["Optional For"]); pv[name] = norm(F["Provides"])
|
|
name = ""; delete F; cur = ""
|
|
}
|
|
function resolve(d, b) { b = d; sub(/[<>=].*/, "", b); if (b in real) return b; return prov[b] }
|
|
/^[ \t]*$/ { flush(); next }
|
|
/^[^ \t]/ { i = index($0, ":"); k = substr($0, 1, i - 1); sub(/[ \t]+$/, "", k)
|
|
v = substr($0, i + 1); sub(/^[ \t]+/, "", v); cur = k; F[k] = v
|
|
if (k == "Name") name = v; next }
|
|
{ sub(/^[ \t]+/, ""); F[cur] = F[cur] " " $0 }
|
|
END {
|
|
flush()
|
|
for (n in real) { c = split(pv[n], a, " "); for (i = 1; i <= c; i++) { p = a[i]; sub(/[<>=].*/, "", p); prov[p] = (p in prov) ? prov[p] " " n : n } }
|
|
nr = split(roots, r, " "); head = 0; tail = 0
|
|
for (i = 1; i <= nr; i++) { m = split(resolve(r[i]), a, " "); for (j = 1; j <= m; j++) if (!(a[j] in seen)) { seen[a[j]] = 1; isroot[a[j]] = 1; q[++tail] = a[j] } }
|
|
while (head < tail) {
|
|
n = q[++head]
|
|
if (mode == "deps") { lst = dep[n]; c = split(lst, a, " "); for (i = 1; i <= c; i++) { m = split(resolve(a[i]), b, " "); for (j = 1; j <= m; j++) if (!(b[j] in seen)) { seen[b[j]] = 1; q[++tail] = b[j] } } }
|
|
else { lst = rb[n] (opt ? " " of[n] : ""); c = split(lst, a, " "); for (i = 1; i <= c; i++) if (!(a[i] in seen)) { seen[a[i]] = 1; q[++tail] = a[i] } }
|
|
}
|
|
for (n in seen) if (!(n in isroot)) print n | "sort"
|
|
}'
|
|
}
|
|
|
|
# ------------------------------------------------------- apply / dry-run ---
|
|
|
|
# Join array elements with commas (for --ignore a,b,c).
|
|
_pm_join() { local IFS=,; printf '%s' "$*"; }
|
|
|
|
# pm_plan [pkg-to-skip...] -> "repo/name newversion" per package the upgrade
|
|
# would install (dependency order, includes NEW dependencies and respects
|
|
# IgnorePkg). Exit 1 when the exclusions make the transaction unsatisfiable.
|
|
# Needs a synced DB (run pm_refresh first); does not need root.
|
|
pm_plan() {
|
|
local ign=() out err rc
|
|
[ $# -gt 0 ] && ign=(--ignore "$(_pm_join "$@")")
|
|
err=$(mktemp)
|
|
out=$(_pm -Sup --noconfirm "${ign[@]}" --print-format '%r/%n %v' </dev/null 2>"$err"); rc=$?
|
|
if [ "$rc" -eq 0 ]; then [ -z "$out" ] || printf '%s\n' "$out"
|
|
else printf '%s\n' "$out" >&2; cat "$err" >&2; fi
|
|
rm -f "$err"; return "$rc"
|
|
}
|
|
|
|
# pm_apply [pkg-to-skip...] (root) non-interactive upgrade of the already
|
|
# refreshed DB. Use `-Su`, not `-Syu`, to apply exactly what was reviewed.
|
|
pm_apply() {
|
|
local ign=()
|
|
[ $# -gt 0 ] && ign=(--ignore "$(_pm_join "$@")")
|
|
LC_ALL=C pacman -Su --noconfirm "${ign[@]}"
|
|
}
|
|
|
|
# pm_purge <pkg>... (root) -Rns: also removes now-unneeded deps + config backups
|
|
pm_purge() { LC_ALL=C pacman -Rns --noconfirm -- "$@"; }
|