397 lines
19 KiB
Bash
Executable File
397 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# lib-rpm.sh - proven building blocks for RPM distros: dnf4, dnf5 (yum alias) and zypper.
|
|
#
|
|
# SOURCE this file; nothing runs on source. Every function here was exercised by proof.sh on
|
|
# almalinux:9, almalinux:10, rockylinux:9, rockylinux/rockylinux:10, opensuse/tumbleweed,
|
|
# opensuse/leap (16.0) and (dnf5 reference) fedora:latest.
|
|
#
|
|
# Runtime needs only: bash >= 4.2, rpm, sed, grep, tr, sort (GNU, for -V), join, cut, mktemp.
|
|
# NOT assumed (missing on at least one target image): awk, find, xargs, python3, xmllint, sudo,
|
|
# hostname, diff, cmp, column.
|
|
#
|
|
# All upgradable-package lists use one record format (TAB separated):
|
|
# name <TAB> old-evr <TAB> new-evr <TAB> repo <TAB> arch
|
|
# (name is the PLAIN rpm name, arch is a separate 5th column; "old-evr" is "?" if unknown)
|
|
|
|
export LC_ALL=C
|
|
|
|
# ---------------------------------------------------------------- detection
|
|
su_backend() { # -> "zypper" | "dnf" (dnf covers dnf4, dnf5 and the yum alias on EL9+)
|
|
if command -v zypper >/dev/null 2>&1; then echo zypper
|
|
elif command -v dnf >/dev/null 2>&1; then echo dnf
|
|
else return 1; fi
|
|
}
|
|
su_os_id() { ( . /etc/os-release 2>/dev/null; printf '%s' "${ID:-}" ); }
|
|
su_dnf_major() { dnf --version 2>/dev/null | grep -q '^dnf5' && echo 5 || echo 4; }
|
|
# zypper verb for "upgrade everything": Tumbleweed (and Slowroll) are rolling -> dup; Leap/SLE -> update
|
|
su_zypper_verb() {
|
|
case "$(su_os_id)" in opensuse-tumbleweed|opensuse-slowroll) echo dup ;; *) echo update ;; esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 1. refresh
|
|
# exit 0 = fresh metadata for ALL repos. dnf: 1 on any failure (we force skip_if_unavailable=0 because
|
|
# dnf5 otherwise silently ignores a dead repo and exits 0 - and `-q` hides even the warning).
|
|
# zypper: 4 = repo error, 3 = bad repo name, 6 = no repos defined.
|
|
su_refresh() {
|
|
case "$(su_backend)" in
|
|
dnf) dnf -q --refresh --setopt=skip_if_unavailable=0 makecache >/dev/null ;;
|
|
zypper) zypper --non-interactive --quiet refresh ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 2. upgradable list
|
|
# installed evr table "name.arch<TAB>evr", highest version last (installonly packages have several)
|
|
_su_installed_evr() {
|
|
rpm -qa --qf '%{NAME}.%{ARCH}\t%|EPOCH?{%{EPOCH}:}|%{VERSION}-%{RELEASE}\n' \
|
|
| LC_ALL=C sort -t $'\t' -k1,1 -k2,2V
|
|
}
|
|
|
|
# dnf (4 and 5): authoritative source = `dnf -q check-update` (the only listing that honours excludes AND
|
|
# versionlock in both majors; repoquery/list do not honour dnf4 versionlock). exit 100 = updates, 0 = none.
|
|
su_upgradable_dnf() {
|
|
local out rc na ver repo rest name arch k evr
|
|
local -A old=() oldn=()
|
|
out=$(dnf -q check-update --setopt=skip_if_unavailable=0); rc=$?
|
|
case $rc in 0) return 0 ;; 100) ;; *) return "$rc" ;; esac
|
|
while IFS=$'\t' read -r k evr; do evr=${evr#0:}; old[$k]=$evr; oldn[${k%.*}]=$evr; done < <(_su_installed_evr)
|
|
while read -r na ver repo rest; do
|
|
[ "$na" = Obsoleting ] && break # dnf4 appends an "Obsoleting Packages" section
|
|
[ -n "$repo" ] && [ -z "$rest" ] || continue # skips blank line, dnf5 "Upgrades" header, etc.
|
|
name=${na%.*}; arch=${na##*.}
|
|
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "${old[$na]:-${oldn[$name]:-?}}" "$ver" "$repo" "$arch"
|
|
done <<<"$out" | LC_ALL=C sort
|
|
}
|
|
|
|
# XML attribute helper: _su_xattr '<tag text>' attr -> $REPLY (unescaped); rc 1 if absent
|
|
_su_xattr() {
|
|
local v=${1#* $2=\"}
|
|
if [ "$v" = "$1" ]; then REPLY=; return 1; fi
|
|
v=${v%%\"*}
|
|
v=${v//</<}; v=${v//>/>}; v=${v//"/\"}; v=${v//'/\'}; v=${v//&/&}
|
|
REPLY=$v
|
|
}
|
|
|
|
# zypper: `zypper --xmlout list-updates` (locked packages are hidden; `lu --all` would show them as "vl").
|
|
# The XML is flattened with tr so each <update ...>...</update> becomes one line, then split with sed.
|
|
su_upgradable_zypper() {
|
|
local xml rc line tag name new old arch src alias
|
|
xml=$(zypper --non-interactive --xmlout list-updates); rc=$?
|
|
[ "$rc" -eq 0 ] || return "$rc"
|
|
# NB: after tr the stream has no trailing newline; GNU sed keeps it that way, so the LAST record would be
|
|
# silently dropped by a plain `while read` -> `|| [ -n "$line" ]` is required.
|
|
printf '%s\n' "$xml" | tr '\n' ' ' | sed 's/<update /\n<update /g' | while IFS= read -r line || [ -n "$line" ]; do
|
|
case $line in '<update kind="package" '*) ;; *) continue ;; esac
|
|
tag=${line%%>*}
|
|
_su_xattr "$tag" name; name=$REPLY
|
|
_su_xattr "$tag" edition; new=$REPLY
|
|
_su_xattr "$tag" edition-old; old=$REPLY
|
|
_su_xattr "$tag" arch; arch=$REPLY
|
|
src=${line#*<source }; alias=
|
|
[ "$src" != "$line" ] && { _su_xattr "$src" alias && alias=$REPLY; }
|
|
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$old" "$new" "$alias" "$arch"
|
|
done | LC_ALL=C sort
|
|
}
|
|
|
|
su_upgradable() {
|
|
case "$(su_backend)" in dnf) su_upgradable_dnf ;; zypper) su_upgradable_zypper ;; esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 3. summary + description
|
|
# su_describe PKG : line 1 = Summary, remaining lines = Description (paragraph breaks kept).
|
|
# Candidate (upgrade) version if there is one, otherwise the installed one.
|
|
su_describe() {
|
|
local pkg=$1 out
|
|
case "$(su_backend)" in
|
|
dnf)
|
|
out=$(dnf -q repoquery --upgrades --latest-limit=1 --queryformat $'%{summary}\n%{description}\n' "$pkg" 2>/dev/null)
|
|
[ -n "$out" ] || out=$(rpm -q --qf '%{SUMMARY}\n%{DESCRIPTION}\n' "$pkg" 2>/dev/null)
|
|
printf '%s\n' "$out" ;;
|
|
zypper)
|
|
out=$(zypper --non-interactive --quiet info "$pkg" 2>/dev/null) || { rpm -q --qf '%{SUMMARY}\n%{DESCRIPTION}\n' "$pkg"; return; }
|
|
printf '%s\n' "$out" | {
|
|
local l indesc=0 pend=
|
|
while IFS= read -r l; do
|
|
if [ $indesc = 1 ]; then # description lines are indented by 4 spaces;
|
|
case $l in # blank lines are real empty lines (paragraph break OR end of block)
|
|
' '*) printf '%s%s\n' "$pend" "${l# }"; pend= ;;
|
|
'') pend+=$'\n' ;;
|
|
*) break ;;
|
|
esac
|
|
else
|
|
case $l in
|
|
Summary*:*) [ -z "${sum:-}" ] && { sum=${l#*: }; printf '%s\n' "$sum"; } ;;
|
|
Description*:*) indesc=1 ;;
|
|
esac
|
|
fi
|
|
done
|
|
} ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 4/5. installed dependency graph (pure rpm)
|
|
# su_cap_names: stdin = one REQUIRENAME per line; stdout = plain capability names to look up.
|
|
# - drops rpmlib(...) pseudo-deps
|
|
# - expands rich/boolean deps "(a if b)", "(a >= 1.0 or b)", "((a and b) unless c else d)" to their
|
|
# *members*; the CONDITION of "if"/"unless" is dropped (it is not a dependency). Conditions are not
|
|
# evaluated, so "(a if b)" yields "a" even if b is not installed = safe superset.
|
|
# - names like libc.so.6(GLIBC_2.34)(64bit), perl(Foo::Bar), /bin/sh, config(x) are kept verbatim
|
|
su_cap_names() {
|
|
local cap tok w depth cond skip o c n
|
|
set -f
|
|
while IFS= read -r cap; do
|
|
case $cap in
|
|
''|rpmlib\(*) ;;
|
|
\(*)
|
|
depth=0; cond=-1; skip=0
|
|
for tok in $cap; do
|
|
w=$tok; n=0
|
|
while [ "${w:0:1}" = '(' ]; do w=${w:1}; n=$((n+1)); done # structural opens (a cap never starts with '(')
|
|
depth=$((depth+n))
|
|
o=${w//[^(]/}; c=${w//[^)]/}; n=$(( ${#c} - ${#o} )) # structural closes = surplus ')' at the end
|
|
[ "$n" -lt 0 ] && n=0
|
|
[ "$n" -gt 0 ] && w=${w%"${w: -$n}"}
|
|
if [ "$skip" = 1 ]; then skip=0
|
|
else
|
|
case $w in
|
|
if|unless) cond=$depth ;;
|
|
else) [ "$cond" = "$depth" ] && cond=-1 ;;
|
|
and|or|with|without|'') ;;
|
|
'='|'<'|'>'|'<='|'>='|'==') skip=1 ;; # next token is a version
|
|
*) [ "$cond" -lt 0 ] && printf '%s\n' "$w" ;;
|
|
esac
|
|
fi
|
|
depth=$((depth-n))
|
|
[ "$cond" -ge 0 ] && [ "$depth" -lt "$cond" ] && cond=-1
|
|
done ;;
|
|
*) printf '%s\n' "$cap" ;;
|
|
esac
|
|
done
|
|
set +f
|
|
}
|
|
|
|
# su_closure PKG...: installed packages (plain names, sorted; the roots themselves are not listed) that PKG...
|
|
# need, transitively. Breadth-first; 2 rpm calls per level. Identical results to
|
|
# `dnf repoquery --installed --requires --resolve --recursive` on dnf4 (dnf5 has no such option).
|
|
su_closure() {
|
|
local -A seen=(); local -a frontier=("$@") caps provs next out=(); local p
|
|
for p in "$@"; do seen[$p]=1; done
|
|
while [ "${#frontier[@]}" -gt 0 ]; do
|
|
mapfile -t caps < <(rpm -q --qf '[%{REQUIRENAME}\n]' "${frontier[@]}" 2>/dev/null | sort -u | su_cap_names | sort -u)
|
|
[ "${#caps[@]}" -gt 0 ] || break
|
|
mapfile -t provs < <(rpm -q --whatprovides --qf '%{NAME}\n' "${caps[@]}" 2>/dev/null | grep -v -e '^no package provides ' | sort -u)
|
|
next=()
|
|
for p in "${provs[@]}"; do
|
|
[ -n "$p" ] && [ -z "${seen[$p]:-}" ] && { seen[$p]=1; next+=("$p"); out+=("$p"); }
|
|
done
|
|
frontier=("${next[@]}")
|
|
done
|
|
[ "${#out[@]}" -gt 0 ] && printf '%s\n' "${out[@]}" | sort -u
|
|
return 0
|
|
}
|
|
|
|
# su_req_table FILE: "capability<TAB>requiring-package" for every installed package, sorted (LC_ALL=C).
|
|
# NB: inside [...] the scalar tag MUST be written %{=NAME}; plain %{NAME} makes rpm fail with
|
|
# "array iterator used with different sized arrays" for ~50% of packages (all rpm versions 4.16..6.0).
|
|
su_req_table() {
|
|
local out=$1 cap name t all
|
|
all=$(mktemp)
|
|
rpm -qa --qf '[%{REQUIRENAME}\t%{=NAME}\n]' > "$all"
|
|
{
|
|
grep -v -e '^rpmlib(' -e '^(' "$all" || true
|
|
{ grep -e '^(' "$all" || true; } | while IFS=$'\t' read -r cap name; do
|
|
su_cap_names <<<"$cap" | while IFS= read -r t; do printf '%s\t%s\n' "$t" "$name"; done
|
|
done
|
|
} | LC_ALL=C sort -u > "$out"
|
|
rm -f "$all"
|
|
}
|
|
|
|
# su_rdeps PKG: installed packages (plain names, sorted) that DIRECTLY require something PKG provides: its name,
|
|
# any Provides (sonames, virtual capabilities; versions ignored) or any file it owns (file deps like /bin/sh).
|
|
# Same result as `dnf repoquery --installed --whatrequires PKG` (verified); plain `rpm -q --whatrequires PKG`
|
|
# only matches the literal capability "PKG" and misses e.g. 51 of 52 dependents of bash on EL9.
|
|
# (Set SU_REQ_TAB=<file made by su_req_table> to reuse one table across many calls.)
|
|
su_rdeps() {
|
|
local pkg=$1 tab caps own=0
|
|
if [ -n "${SU_REQ_TAB:-}" ] && [ -s "$SU_REQ_TAB" ]; then tab=$SU_REQ_TAB; else tab=$(mktemp); su_req_table "$tab"; own=1; fi
|
|
caps=$(mktemp)
|
|
{ rpm -q --qf '[%{PROVIDENAME}\n]' "$pkg"; rpm -ql "$pkg" | grep -v -e '^(contains no files)$'; } | LC_ALL=C sort -u > "$caps"
|
|
LC_ALL=C join -t $'\t' "$caps" "$tab" | cut -f2 | LC_ALL=C sort -u | grep -v -x -F -e "$pkg" || true
|
|
rm -f "$caps"; [ "$own" = 1 ] && rm -f "$tab"
|
|
return 0
|
|
}
|
|
|
|
# su_rdeps_recursive PKG...: everything installed that (transitively) depends on PKG... (roots excluded).
|
|
# NB: "direct" dependents can be far fewer than you expect: on Tumbleweed `/bin/sh` is provided by bash-sh, not
|
|
# bash, so aaa_base/binutils/... are dependents of bash-sh (which requires bash), i.e. only in the recursive set.
|
|
su_rdeps_recursive() {
|
|
local -A seen=(); local -a frontier=("$@") next out=(); local p r SU_REQ_TAB
|
|
SU_REQ_TAB=$(mktemp); su_req_table "$SU_REQ_TAB"
|
|
for p in "$@"; do seen[$p]=1; done
|
|
while [ "${#frontier[@]}" -gt 0 ]; do
|
|
next=()
|
|
for p in "${frontier[@]}"; do
|
|
while IFS= read -r r; do
|
|
[ -n "$r" ] && [ -z "${seen[$r]:-}" ] && { seen[$r]=1; next+=("$r"); out+=("$r"); }
|
|
done < <(su_rdeps "$p")
|
|
done
|
|
frontier=("${next[@]}")
|
|
done
|
|
rm -f "$SU_REQ_TAB"
|
|
[ "${#out[@]}" -gt 0 ] && printf '%s\n' "${out[@]}" | sort -u
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 6. upgrade with per-run exclusions
|
|
# su_zypper_excluding NAME... -- zypper-args...
|
|
# zypper has no --exclude. Per-run locks WITHOUT touching /etc/zypp/locks: point libzypp at a private
|
|
# zypp.conf (env ZYPP_CONF) whose locksfile.path is a temp copy of the current locks, add the exclusions there,
|
|
# run zypper, delete the temp dir. Nothing to restore, nothing persisted, safe on Ctrl-C.
|
|
su_zypper_excluding() {
|
|
local -a ex=(); local d src locks rc
|
|
while [ $# -gt 0 ] && [ "$1" != -- ]; do ex+=("$1"); shift; done
|
|
[ "${1:-}" = -- ] && shift
|
|
d=$(mktemp -d) || return 1
|
|
src=/etc/zypp/zypp.conf; [ -r "$src" ] || src=/usr/etc/zypp/zypp.conf # Tumbleweed ships only /usr/etc
|
|
locks=$(sed -n 's/^[[:space:]]*locksfile\.path[[:space:]]*=[[:space:]]*//p' "$src" | tail -n1)
|
|
: "${locks:=/etc/zypp/locks}"
|
|
cp "$src" "$d/zypp.conf" || { rm -rf "$d"; return 1; }
|
|
if [ -e "$locks" ]; then cp "$locks" "$d/locks"; else : > "$d/locks"; fi
|
|
printf '\n[main]\nlocksfile.path = %s\n' "$d/locks" >> "$d/zypp.conf"
|
|
if [ "${#ex[@]}" -gt 0 ]; then
|
|
ZYPP_CONF=$d/zypp.conf zypper --non-interactive --quiet addlock "${ex[@]}" >/dev/null || { rm -rf "$d"; return 1; }
|
|
fi
|
|
ZYPP_CONF=$d/zypp.conf zypper --non-interactive "$@"; rc=$?
|
|
rm -rf "$d"
|
|
return $rc
|
|
}
|
|
|
|
# su_upgrade [--exclude NAME]... [-- extra-pm-args...] (non-interactive; exit status of the package manager)
|
|
# dnf : dnf -y upgrade --exclude=a,b (comma list works on dnf4 AND dnf5; repeated flags too)
|
|
# zypper : update (Leap/SLE) or dup (Tumbleweed) with temporary locks (see su_zypper_excluding)
|
|
su_upgrade() {
|
|
local -a ex=() extra=()
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--exclude) ex+=("$2"); shift 2 ;;
|
|
--) shift; extra=("$@"); break ;;
|
|
*) echo "su_upgrade: bad arg $1" >&2; return 2 ;;
|
|
esac
|
|
done
|
|
case "$(su_backend)" in
|
|
dnf)
|
|
local ox=(); if [ "${#ex[@]}" -gt 0 ]; then local IFS=,; ox=("--exclude=${ex[*]}"); fi
|
|
dnf -y --setopt=skip_if_unavailable=0 upgrade "${ox[@]}" "${extra[@]}" ;;
|
|
zypper)
|
|
su_zypper_excluding "${ex[@]}" -- "$(su_zypper_verb)" "${extra[@]}" ;;
|
|
esac
|
|
}
|
|
|
|
# su_zypper_excluding_snapshot NAME... -- zypper-args...
|
|
# Alternative recipe (what you would do if ZYPP_CONF were unavailable): add real locks to /etc/zypp/locks
|
|
# and restore the EXACT previous file afterwards. `zypper removelock NAME` is NOT a safe undo: it deletes ALL
|
|
# locks matching NAME (including a pre-existing user lock) and leaves an empty /etc/zypp/locks behind even if the
|
|
# file did not exist before. Hence: byte copy (cp -p) of the file, or "file absent" flag, restored by a trap.
|
|
# Runs in a subshell so the traps are local. Not safe against two concurrent runs (use su_zypper_excluding).
|
|
su_zypper_excluding_snapshot() (
|
|
ex=(); while [ $# -gt 0 ] && [ "$1" != -- ]; do ex+=("$1"); shift; done; [ "${1:-}" = -- ] && shift
|
|
src=/etc/zypp/zypp.conf; [ -r "$src" ] || src=/usr/etc/zypp/zypp.conf
|
|
locks=$(sed -n 's/^[[:space:]]*locksfile\.path[[:space:]]*=[[:space:]]*//p' "$src" | tail -n1); : "${locks:=/etc/zypp/locks}"
|
|
bak=$(mktemp) || exit 1; had=0; restored=0
|
|
if [ -e "$locks" ]; then cp -p -- "$locks" "$bak" || exit 1; had=1; fi
|
|
_restore() {
|
|
[ "$restored" = 1 ] && return 0; restored=1
|
|
if [ "$had" = 1 ]; then cp -p -- "$bak" "$locks"; else rm -f -- "$locks"; fi
|
|
rm -f -- "$bak"
|
|
}
|
|
trap '_restore' EXIT; trap '_restore; exit 130' INT; trap '_restore; exit 143' TERM HUP
|
|
if [ "${#ex[@]}" -gt 0 ]; then zypper --non-interactive --quiet addlock "${ex[@]}" >/dev/null || exit 1; fi
|
|
zypper --non-interactive "$@"
|
|
)
|
|
|
|
# ---------------------------------------------------------------- 7. plan preview (display only)
|
|
# su_plan [--exclude NAME]... -> section<TAB>name<TAB>old<TAB>new<TAB>repo<TAB>arch<TAB>replaces
|
|
# section = upgrade | install | remove | downgrade | reinstall | skipped | other ; old/replaces are "-" if n/a.
|
|
# Shows what `su_upgrade` would do INCLUDING new dependencies, removals and obsolete-replacements, which the
|
|
# upgradable list (check-update / list-updates) never shows. Nothing is changed.
|
|
su_plan_zypper() {
|
|
local out
|
|
out=$(su_zypper_excluding "$@" -- --xmlout "$(su_zypper_verb)" --dry-run) || return $?
|
|
# <install-summary><to-upgrade><solvable type="package" name= edition= arch= repository= edition-old= .../>...</to-upgrade><to-install>...
|
|
printf '%s\n' "$out" | tr '\n' ' ' | sed -e 's/<to-/\n<to-/g' -e 's/<solvable /\n<solvable /g' |
|
|
{
|
|
local line sec=other tag n e o a r
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
case $line in
|
|
'<to-'*) sec=${line#<to-}; sec=${sec%%>*} ;;
|
|
'<solvable type="package" '*)
|
|
tag=${line%%>*}
|
|
_su_xattr "$tag" name; n=$REPLY
|
|
_su_xattr "$tag" edition; e=$REPLY
|
|
_su_xattr "$tag" edition-old && o=$REPLY || o=-
|
|
_su_xattr "$tag" arch; a=$REPLY
|
|
_su_xattr "$tag" repository; r=$REPLY
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t-\n' "$sec" "$n" "$o" "$e" "$r" "$a" ;;
|
|
esac
|
|
done
|
|
}
|
|
}
|
|
|
|
# dnf: no machine-readable transaction output exists in dnf4; parse the (stable, non-tty) table printed by
|
|
# `dnf --assumeno upgrade`. Section headers are col-0 lines ending in ':'; rows start with one space:
|
|
# dnf4: " name arch evr repo size unit" , then optionally " replacing old.arch oldevr"
|
|
# dnf5: " name arch 0:evr repo size unit", then optionally " replacing old arch 0:oldevr repo size unit"
|
|
su_plan_dnf() {
|
|
local -a ex=(); local ox=() out l a b c d rest sec=other
|
|
local ps= pn= po=- pv= pr= pa= pp=- # pending row (printed when the next row/section/summary arrives)
|
|
while [ $# -gt 0 ]; do case $1 in --exclude) ex+=("$2"); shift 2 ;; *) shift ;; esac; done
|
|
if [ "${#ex[@]}" -gt 0 ]; then ox=("--exclude=$(IFS=,; echo "${ex[*]}")"); fi
|
|
out=$(dnf --assumeno --setopt=skip_if_unavailable=0 upgrade "${ox[@]}" 2>/dev/null) # --assumeno => rc 1 is normal
|
|
printf '%s\n' "$out" | grep -q '^Transaction Summary' || return 1
|
|
_flush() {
|
|
[ -n "$pn" ] || return 0
|
|
if [ "$ps" = upgrade ] && [ "$po" = - ]; then # dnf4 prints no "replacing" line for plain upgrades
|
|
po=$(rpm -q --qf '%|EPOCH?{%{EPOCH}:}|%{VERSION}-%{RELEASE}\n' "$pn.$pa" 2>/dev/null | LC_ALL=C sort -V | tail -n1); po=${po#0:}; : "${po:=-}"
|
|
fi
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$ps" "$pn" "$po" "$pv" "$pr" "$pa" "$pp"; pn=; po=-; pp=-
|
|
}
|
|
while IFS= read -r l; do
|
|
case $l in
|
|
'Transaction Summary'*) break ;;
|
|
[A-Z]*:) _flush
|
|
case $l in Upgrading*) sec=upgrade ;; Installing*) sec=install ;; Removing*) sec=remove ;;
|
|
Downgrading*) sec=downgrade ;; Reinstalling*) sec=reinstall ;; Skipping*) sec=skipped ;; *) sec=other ;; esac ;;
|
|
' '*)
|
|
read -r a b c d rest <<<"$l"
|
|
case $a in
|
|
Package|=*|'') ;;
|
|
replacing)
|
|
if [ -n "$d" ]; then c=${d#0:}; else b=${b%.*}; c=${c#0:}; fi # dnf5: name arch evr repo ; dnf4: name.arch evr
|
|
if [ "$sec" = upgrade ]; then po=$c; else pp=$b; fi ;;
|
|
*) _flush; ps=$sec; pn=$a; pa=$b; pv=${c#0:}; pr=$d ;;
|
|
esac ;;
|
|
esac
|
|
done <<<"$out"
|
|
_flush
|
|
}
|
|
|
|
su_plan() {
|
|
case "$(su_backend)" in dnf) su_plan_dnf "$@" ;; zypper)
|
|
local -a ex=(); while [ $# -gt 0 ]; do case $1 in --exclude) ex+=("$2"); shift 2 ;; *) shift ;; esac; done
|
|
su_plan_zypper "${ex[@]}" ;; esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 8. remove (+ unneeded dependencies)
|
|
# dnf: clean_requirements_on_remove=1 is the default on dnf4 and dnf5 -> unneeded deps go too.
|
|
# (A dependency installed via plain `rpm -i` is NOT considered unneeded; only deps installed by dnf are.)
|
|
# Exit status is 0 even if the package did not exist -> always verify with rpm -q.
|
|
# zypper: --clean-deps (-u). `remove` of an unknown name exits 104.
|
|
su_remove() {
|
|
case "$(su_backend)" in
|
|
dnf) dnf -y remove "$@" ;;
|
|
zypper) zypper --non-interactive remove --clean-deps "$@" ;;
|
|
esac
|
|
}
|