first commit
This commit is contained in:
commit
1354db6c39
90 files changed
+13123
No files matched your search
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fixture.sh - deterministic E2E fixture for RPM distros (dnf: Alma/Rocky/..., zypper: openSUSE)
|
||||
#
|
||||
# Builds three dummy packages with rpmbuild, installs version 1.0 of each, publishes
|
||||
# version 2.0 in a local file:// repo and registers that repo, so that the package
|
||||
# manager reports exactly these upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0-1 -> 2.0-1 (noarch, Requires: su-dummy-b)
|
||||
# su-dummy-b 1.0-1 -> 2.0-1 (noarch)
|
||||
# su-dummy-c 1.0-1 -> 2.0-1 (ARCH-SPECIFIC, so the ".x86_64" suffix path is exercised)
|
||||
#
|
||||
# Optional (SU_FIXTURE_EXTRAS=1):
|
||||
# - installs su-dummy-d 1.0 (NOT upgradable): Requires su-dummy-a, /bin/sh (file dep),
|
||||
# "su-dummy-b >= 1.0" (versioned), "(su-dummy-c if su-dummy-b)" (rich dep) and rpmlib(...)
|
||||
# -> for reverse-dep / dependency-closure tests;
|
||||
# - publishes su-dummy-r 1.0 (Requires su-dummy-s) and su-dummy-s 1.0 in the repo, NOT installed
|
||||
# -> for install/remove/--clean-deps tests.
|
||||
#
|
||||
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
|
||||
#
|
||||
# Usage (inside the container, as root): bash fixture.sh
|
||||
# docker run --rm -i -v <project>/discovery/rpm:/d:ro <image> bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
T0=$SECONDS
|
||||
log() { printf '[fixture] %s\n' "$*" >&2; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
|
||||
# ---- 1. backend detection ---------------------------------------------------
|
||||
BACKEND=
|
||||
if command -v zypper >/dev/null 2>&1; then BACKEND=zypper
|
||||
elif command -v dnf >/dev/null 2>&1; then BACKEND=dnf
|
||||
elif command -v yum >/dev/null 2>&1; then BACKEND=yum
|
||||
else die "neither zypper nor dnf/yum found"; fi
|
||||
log "backend=$BACKEND"
|
||||
|
||||
TOP=/var/tmp/su-fixture # rpmbuild topdir
|
||||
REPO=/var/lib/su-dummy-repo # file:// repo (only 2.0 packages)
|
||||
SPEC_DIR=$TOP/SPECS
|
||||
|
||||
# ---- 2. build tools ---------------------------------------------------------
|
||||
t=$SECONDS
|
||||
case $BACKEND in
|
||||
zypper) zypper --non-interactive --quiet install --no-recommends rpm-build createrepo_c >/dev/null ;;
|
||||
dnf|yum) "$BACKEND" -y -q install --setopt=install_weak_deps=False rpm-build createrepo_c >/dev/null ;;
|
||||
esac
|
||||
command -v rpmbuild >/dev/null || die "rpmbuild missing after install"
|
||||
command -v createrepo_c >/dev/null || die "createrepo_c missing after install"
|
||||
log "build tools installed in $((SECONDS - t))s"
|
||||
|
||||
# ---- 3. build RPMs ----------------------------------------------------------
|
||||
rm -rf "$TOP" "$REPO"
|
||||
mkdir -p "$TOP"/{BUILD,BUILDROOT,RPMS,SRPMS,SOURCES,SPECS} "$REPO"
|
||||
|
||||
# mkspec <name> <version> <arch|noarch> <extra spec header lines...>
|
||||
mkspec() {
|
||||
local name=$1 ver=$2 arch=$3; shift 3
|
||||
local f=$SPEC_DIR/$name-$ver.spec
|
||||
{
|
||||
echo "Name: $name"
|
||||
echo "Version: $ver"
|
||||
echo "Release: 1"
|
||||
echo "Summary: skeptical-updater dummy $name (v$ver)"
|
||||
echo "License: MIT"
|
||||
[ "$arch" = noarch ] && echo "BuildArch: noarch"
|
||||
local l; for l in "$@"; do echo "$l"; done
|
||||
echo "%global debug_package %{nil}"
|
||||
echo "%description"
|
||||
echo "Dummy package $name version $ver for skeptical-updater tests."
|
||||
echo ""
|
||||
echo "Second paragraph of $name $ver (blank line above on purpose)."
|
||||
echo "%install"
|
||||
echo "mkdir -p %{buildroot}/usr/share/su-dummy"
|
||||
echo "echo '$name $ver' > %{buildroot}/usr/share/su-dummy/$name"
|
||||
echo "%files"
|
||||
echo "/usr/share/su-dummy/$name"
|
||||
} > "$f"
|
||||
printf '%s\n' "$f"
|
||||
}
|
||||
|
||||
build() { rpmbuild -bb --quiet --define "_topdir $TOP" "$1" >/dev/null 2>"$TOP/rpmbuild.err" \
|
||||
|| { cat "$TOP/rpmbuild.err" >&2; die "rpmbuild failed for $1"; }; }
|
||||
|
||||
t=$SECONDS
|
||||
for v in 1.0 2.0; do
|
||||
build "$(mkspec su-dummy-a $v noarch 'Requires: su-dummy-b')"
|
||||
build "$(mkspec su-dummy-b $v noarch)"
|
||||
build "$(mkspec su-dummy-c $v arch)"
|
||||
done
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
build "$(mkspec su-dummy-s 1.0 noarch)" # published in the repo, NOT installed
|
||||
build "$(mkspec su-dummy-r 1.0 noarch 'Requires: su-dummy-s')" # (used to test remove/--clean-deps)
|
||||
build "$(mkspec su-dummy-d 1.0 noarch \
|
||||
'Requires: su-dummy-a' \
|
||||
'Requires: /bin/sh' \
|
||||
'Requires: su-dummy-b >= 1.0' \
|
||||
'Requires: (su-dummy-c if su-dummy-b)' \
|
||||
'Requires: rpmlib(CompressedFileNames)')"
|
||||
fi
|
||||
log "rpmbuild done in $((SECONDS - t))s"
|
||||
|
||||
# ---- 4. install 1.0 (rpm directly: distro-agnostic, resets to 1.0 on re-run) ----
|
||||
shopt -s nullglob
|
||||
v1=( "$TOP"/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm )
|
||||
[ "${#v1[@]}" -eq 3 ] || die "expected 3 built 1.0 rpms, got ${#v1[@]}"
|
||||
rpm -Uvh --oldpackage --replacepkgs "${v1[@]}" >/dev/null
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
rpm -Uvh --oldpackage --replacepkgs "$TOP"/RPMS/noarch/su-dummy-d-1.0-1.*.rpm >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 5. publish 2.0 as a local repo -------------------------------------------
|
||||
v2=( "$TOP"/RPMS/*/su-dummy-[abc]-2.0-1.*.rpm )
|
||||
[ "${#v2[@]}" -eq 3 ] || die "expected 3 built 2.0 rpms, got ${#v2[@]}"
|
||||
cp "${v2[@]}" "$REPO"/
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-[rs]-1.0-1.*.rpm "$REPO"/; fi
|
||||
createrepo_c --quiet "$REPO" >/dev/null
|
||||
|
||||
# ---- 6. register repo ---------------------------------------------------------
|
||||
case $BACKEND in
|
||||
zypper)
|
||||
zypper --non-interactive --quiet removerepo su-dummy >/dev/null 2>&1 || true
|
||||
zypper --non-interactive --quiet addrepo --no-gpgcheck --no-check --refresh "file://$REPO" su-dummy >/dev/null
|
||||
t=$SECONDS
|
||||
zypper --non-interactive --quiet refresh su-dummy >/dev/null
|
||||
;;
|
||||
dnf|yum)
|
||||
cat > /etc/yum.repos.d/su-dummy.repo <<REPOEOF
|
||||
[su-dummy]
|
||||
name=skeptical-updater dummy packages
|
||||
baseurl=file://$REPO
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
repo_gpgcheck=0
|
||||
metadata_expire=0
|
||||
REPOEOF
|
||||
t=$SECONDS
|
||||
"$BACKEND" -q makecache --repo su-dummy >/dev/null
|
||||
;;
|
||||
esac
|
||||
log "repo refresh in $((SECONDS - t))s"
|
||||
|
||||
# ---- 7. verify: the three must be listed as upgradable --------------------------
|
||||
want="su-dummy-a su-dummy-b su-dummy-c"
|
||||
case $BACKEND in
|
||||
zypper) listing=$(zypper --non-interactive --quiet list-updates 2>&1 || true) ;;
|
||||
dnf|yum) listing=$("$BACKEND" -q check-update 2>&1 || true) ;;
|
||||
esac
|
||||
bad=0
|
||||
for p in $want; do
|
||||
printf '%s\n' "$listing" | grep -Eq "(^|[| ])$p([. ]|$)" || { log "NOT listed as upgradable: $p"; bad=1; }
|
||||
done
|
||||
if [ "$bad" -ne 0 ]; then printf '%s\n' "$listing" >&2; die "fixture verification failed"; fi
|
||||
log "OK: su-dummy-{a,b,c} 1.0 installed and 2.0 offered by repo 'su-dummy' (total ${SECONDS}s, since start $((SECONDS - T0))s)"
|
||||
Executable
+396
@@ -0,0 +1,396 @@
|
||||
#!/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
|
||||
}
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# proof.sh - runs lib-rpm.sh against the fixture and asserts the behaviour documented in rpm.md.
|
||||
# docker run --rm -i -v <project>/discovery/rpm:/d:ro <image> bash /d/proof.sh
|
||||
# Prints PASS/FAIL lines + sample output; exit code = number of failures.
|
||||
export LC_ALL=C
|
||||
D=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
now() { printf '%s' "${EPOCHREALTIME/./}"; } # microseconds
|
||||
T_START=$(now)
|
||||
FAILS=0
|
||||
pass() { printf 'PASS %s\n' "$1"; }
|
||||
fail() { printf 'FAIL %s\n' "$1"; FAILS=$((FAILS+1)); }
|
||||
check() { local name=$1; shift; if "$@" >/dev/null 2>&1; then pass "$name"; else fail "$name"; fi; }
|
||||
eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1: got [$2] want [$3]"; fi; }
|
||||
exec 3>&1 # fd 3 = real stdout, so tm's timing is not swallowed by redirections
|
||||
tm() { local t0=$(now); "$@"; local rc=$?; printf ' (took %d ms)\n' $(( ($(now)-t0)/1000 )) >&3; return $rc; }
|
||||
sec() { printf '\n== %s\n' "$*"; }
|
||||
|
||||
sec "fixture"
|
||||
t0=$(now); SU_FIXTURE_EXTRAS=1 bash "$D/fixture.sh" 2>&1 | sed 's/^/ /' | tail -4; [ "${PIPESTATUS[0]}" -eq 0 ] || { echo "FIXTURE FAILED"; exit 99; }
|
||||
FIXTURE_MS=$(( ($(now)-t0)/1000 ))
|
||||
. "$D/lib-rpm.sh"
|
||||
. /etc/os-release
|
||||
BACKEND=$(su_backend)
|
||||
echo "image=$PRETTY_NAME ID=$ID backend=$BACKEND dnf_major=$([ $BACKEND = dnf ] && su_dnf_major || echo -) zypper_verb=$([ $BACKEND = zypper ] && su_zypper_verb || echo -)"
|
||||
|
||||
sec "1. refresh"
|
||||
t0=$(now); su_refresh; rc=$?; REFRESH_MS=$(( ($(now)-t0)/1000 )); eq "su_refresh exit code" "$rc" 0; echo " (took $REFRESH_MS ms)"
|
||||
|
||||
sec "2. upgradable list (name old new repo arch)"
|
||||
t0=$(now); su_upgradable > /tmp/up.tsv; rc=$?; LIST_MS=$(( ($(now)-t0)/1000 )); eq "su_upgradable exit code" "$rc" 0
|
||||
echo " (took $LIST_MS ms; $(wc -l </tmp/up.tsv) rows) first 5 rows:"; head -5 /tmp/up.tsv | cat -A | sed 's/^/ /'
|
||||
for p in a b c; do
|
||||
arch=noarch; [ $p = c ] && arch=$(rpm -q --qf '%{ARCH}' su-dummy-c)
|
||||
grep -q -x -F "su-dummy-$p"$'\t'"1.0-1"$'\t'"2.0-1"$'\t'"su-dummy"$'\t'"$arch" /tmp/up.tsv && pass "row su-dummy-$p 1.0-1 -> 2.0-1 [su-dummy] $arch" || fail "row su-dummy-$p missing"
|
||||
done
|
||||
check "no su-dummy-d (not upgradable)" test "$(grep -c '^su-dummy-d' /tmp/up.tsv)" -eq 0
|
||||
check "no '.arch' suffix in name column" test "$(cut -f1 /tmp/up.tsv | grep -c '\.\(noarch\|x86_64\|aarch64\)$')" -eq 0
|
||||
|
||||
sec "3. summary + description (candidate if upgradable, else installed)"
|
||||
su_describe su-dummy-a > /tmp/d_a.txt; echo "--- su-dummy-a (candidate):"; sed 's/^/ /' /tmp/d_a.txt
|
||||
eq "candidate summary" "$(head -n1 /tmp/d_a.txt)" "skeptical-updater dummy su-dummy-a (v2.0)"
|
||||
check "candidate description has 2nd paragraph" grep -q 'Second paragraph of su-dummy-a 2.0' /tmp/d_a.txt
|
||||
check "paragraph break preserved" test "$(sed -n 3p /tmp/d_a.txt)" = ""
|
||||
su_describe su-dummy-d > /tmp/d_d.txt; echo "--- su-dummy-d (installed only):"; sed 's/^/ /' /tmp/d_d.txt
|
||||
eq "installed-only summary" "$(head -n1 /tmp/d_d.txt)" "skeptical-updater dummy su-dummy-d (v1.0)"
|
||||
rpm -qi su-dummy-a | sed -n '/^Summary/p' | sed 's/^/ rpm -qi: /'
|
||||
|
||||
sec "4. installed reverse dependencies"
|
||||
eq "rdeps su-dummy-b" "$(su_rdeps su-dummy-b | tr '\n' ' ')" "su-dummy-a su-dummy-d "
|
||||
eq "rdeps su-dummy-a" "$(su_rdeps su-dummy-a | tr '\n' ' ')" "su-dummy-d "
|
||||
eq "rdeps su-dummy-c (only via rich dep (c if b))" "$(su_rdeps su-dummy-c | tr '\n' ' ')" "su-dummy-d "
|
||||
eq "rdeps su-dummy-d (leaf)" "$(su_rdeps su-dummy-d | tr '\n' ' ')" ""
|
||||
n_mine=$(su_rdeps bash | wc -l); n_naive=$(rpm -q --qf '%{NAME}\n' --whatrequires bash 2>/dev/null | sort -u | wc -l)
|
||||
echo " bash: su_rdeps=$n_mine naive 'rpm -q --whatrequires bash'=$n_naive"
|
||||
provider_sh=$(rpm -q --whatprovides --qf '%{NAME}\n' /bin/sh | head -n1)
|
||||
check "su_rdeps($provider_sh) finds su-dummy-d via file dep /bin/sh" bash -c ". $D/lib-rpm.sh; su_rdeps $provider_sh | grep -qx su-dummy-d"
|
||||
[ "$n_mine" -gt "$n_naive" ] && pass "su_rdeps(bash) > naive rpm --whatrequires" || fail "su_rdeps(bash) not > naive"
|
||||
if [ "$BACKEND" = dnf ]; then
|
||||
dnf -q repoquery --installed --whatrequires bash --queryformat $'%{name}\n' 2>/dev/null | sed '/^$/d' | sort -u > /tmp/native_rd.txt
|
||||
su_rdeps bash > /tmp/mine_rd.txt; eq "su_rdeps(bash) == dnf repoquery --installed --whatrequires" "$(cat /tmp/mine_rd.txt | tr '\n' ' ')" "$(cat /tmp/native_rd.txt | tr '\n' ' ')"
|
||||
else
|
||||
zypper -q -n search --installed-only -t package --requires-pkg bash 2>/dev/null | grep -E '^[iv ][+ ]? *\|' | cut -d'|' -f2 | tr -d ' ' | grep -v -x Name | sort -u > /tmp/native_rd.txt
|
||||
su_rdeps bash > /tmp/mine_rd.txt; su_rdeps_recursive bash > /tmp/mine_rrd.txt
|
||||
echo " zypper se --requires-pkg bash : $(wc -l </tmp/native_rd.txt) pkgs"; echo " su_rdeps bash (direct) : $(wc -l </tmp/mine_rd.txt) pkgs: $(tr '\n' ' ' </tmp/mine_rd.txt)"
|
||||
echo " su_rdeps_recursive bash : $(wc -l </tmp/mine_rrd.txt) pkgs"
|
||||
echo " native minus recursive : $(LC_ALL=C comm -23 /tmp/native_rd.txt /tmp/mine_rrd.txt | tr '\n' ' ')"
|
||||
fi
|
||||
eq "su_rdeps_recursive(su-dummy-b)" "$(su_rdeps_recursive su-dummy-b | tr '\n' ' ')" "su-dummy-a su-dummy-d "
|
||||
eq "su_rdeps_recursive(su-dummy-d) (leaf)" "$(su_rdeps_recursive su-dummy-d | tr '\n' ' ')" ""
|
||||
|
||||
sec "5. recursive installed dependency closure"
|
||||
tm su_closure su-dummy-d > /tmp/clo_d.txt
|
||||
echo " closure(su-dummy-d): $(wc -l </tmp/clo_d.txt) pkgs: $(head -c 300 /tmp/clo_d.txt | tr '\n' ' ')..."
|
||||
for p in su-dummy-a su-dummy-b su-dummy-c glibc; do grep -qx "$p" /tmp/clo_d.txt && pass "closure(d) contains $p" || fail "closure(d) missing $p"; done
|
||||
provider_sh=$(rpm -q --whatprovides --qf '%{NAME}\n' /bin/sh | head -n1); grep -qx "$provider_sh" /tmp/clo_d.txt && pass "closure(d) contains $provider_sh (provider of file dep /bin/sh)" || fail "closure(d) missing $provider_sh"
|
||||
! grep -qx 'su-dummy-d' /tmp/clo_d.txt && pass "closure(d) excludes root" || fail "closure(d) contains root"
|
||||
grep -q 'rpmlib' /tmp/clo_d.txt && fail "closure contains rpmlib" || pass "no rpmlib(...) leakage"
|
||||
eq "closure(su-dummy-a)" "$(su_closure su-dummy-a | tr '\n' ' ')" "su-dummy-b "
|
||||
if [ "$BACKEND" = dnf ] && [ "$(su_dnf_major)" = 4 ]; then
|
||||
dnf -q repoquery --installed --requires --resolve --recursive --queryformat $'%{name}\n' su-dummy-d 2>/dev/null | sed '/^$/d' | sort -u > /tmp/clo_native.txt
|
||||
eq "closure(d) == dnf4 repoquery --requires --resolve --recursive" "$(tr '\n' ' ' </tmp/clo_d.txt)" "$(tr '\n' ' ' </tmp/clo_native.txt)"
|
||||
fi
|
||||
if [ "$BACKEND" = dnf ] && [ "$(su_dnf_major)" = 5 ]; then
|
||||
dnf -q repoquery --installed --requires --resolve --recursive su-dummy-d >/dev/null 2>&1; echo " dnf5 repoquery --requires --resolve --recursive -> rc=$? (unsupported)"
|
||||
fi
|
||||
|
||||
sec "6. upgrade with per-run exclusion (a,b excluded) -- c must upgrade, a,b must stay"
|
||||
if [ $BACKEND = dnf ]; then RESTRICT=(--disablerepo='*' --enablerepo=su-dummy); else RESTRICT=(); fi
|
||||
LOCKS_BEFORE=$( [ -e /etc/zypp/locks ] && cksum < /etc/zypp/locks || echo absent )
|
||||
tm su_upgrade --exclude su-dummy-a --exclude su-dummy-b -- "${RESTRICT[@]}" > /tmp/up_ex.log 2>&1; rc=$?
|
||||
eq "exit code of excluded upgrade" "$rc" 0
|
||||
eq "versions after excluded upgrade" "$(rpm -q --qf '%{NAME}=%{VERSION} ' su-dummy-a su-dummy-b su-dummy-c)" "su-dummy-a=1.0 su-dummy-b=1.0 su-dummy-c=2.0 "
|
||||
if [ $BACKEND = zypper ]; then
|
||||
LOCKS_AFTER=$( [ -e /etc/zypp/locks ] && cksum < /etc/zypp/locks || echo absent )
|
||||
eq "/etc/zypp/locks untouched ($LOCKS_BEFORE)" "$LOCKS_AFTER" "$LOCKS_BEFORE"
|
||||
check "no leftover temp zypp dirs" bash -c 'ls /tmp | grep -q "^tmp\." && exit 1 || exit 0'
|
||||
fi
|
||||
su_upgradable | cut -f1,2,3 | grep '^su-dummy' | sed 's/^/ still upgradable: /'
|
||||
check "a and b still listed as upgradable" bash -c ". $D/lib-rpm.sh; su_upgradable | cut -f1 | grep -c '^su-dummy-[ab]$' | grep -qx 2"
|
||||
echo "--- now without exclusions"
|
||||
tm su_upgrade -- "${RESTRICT[@]}" > /tmp/up_all.log 2>&1; rc=$?; eq "exit code of plain upgrade" "$rc" 0
|
||||
eq "versions after full upgrade" "$(rpm -q --qf '%{NAME}=%{VERSION} ' su-dummy-a su-dummy-b su-dummy-c)" "su-dummy-a=2.0 su-dummy-b=2.0 su-dummy-c=2.0 "
|
||||
check "nothing left for su-dummy-*" test "$(su_upgradable | grep -c '^su-dummy')" -eq 0
|
||||
|
||||
sec "7. remove (+ unneeded deps)"
|
||||
if [ $BACKEND = dnf ]; then dnf -y -q install su-dummy-r >/dev/null 2>&1; else zypper -n -q install su-dummy-r >/dev/null 2>&1; fi
|
||||
eq "installed r and s" "$(rpm -q --qf '%{NAME} ' su-dummy-r su-dummy-s)" "su-dummy-r su-dummy-s "
|
||||
su_remove su-dummy-r >/tmp/rm.log 2>&1; rc=$?; eq "remove exit code" "$rc" 0
|
||||
eq "r AND its auto-installed dep s removed" "$(rpm -q su-dummy-r su-dummy-s 2>&1 | sed 's/ is not installed//;s/^package //' | tr '\n' ' ')" "su-dummy-r su-dummy-s "
|
||||
su_remove su-dummy-a >/tmp/rm2.log 2>&1
|
||||
eq "rpm-installed dep b NOT removed with a (not marked auto-installed)" "$(rpm -q su-dummy-b)" "su-dummy-b-2.0-1.noarch"
|
||||
su_remove no-such-pkg-xyz >/tmp/rm3.log 2>&1; echo " remove of unknown pkg -> exit $?"
|
||||
|
||||
sec "8. fixture re-run resets state"
|
||||
SU_FIXTURE_EXTRAS=1 bash "$D/fixture.sh" >/dev/null 2>&1; su_upgradable > /tmp/up2.tsv
|
||||
check "after re-run a,b,c upgradable again" test "$(grep -c '^su-dummy-[abc] 1.0-1 2.0-1' /tmp/up2.tsv)" -eq 3
|
||||
|
||||
sec "9. plan preview (display only) incl. an obsoleting package"
|
||||
TOP=/var/tmp/su-fixture; REPO=/var/lib/su-dummy-repo
|
||||
mkp() { # mkp name ver [extra spec header lines...] -> builds noarch rpm, echoes path
|
||||
local n=$1 v=$2; shift 2; local f=$TOP/SPECS/$n-$v.spec
|
||||
{ echo "Name: $n"; echo "Version: $v"; echo "Release: 1"; echo "Summary: dummy $n"; echo "License: MIT"; echo "BuildArch: noarch"
|
||||
local l; for l in "$@"; do echo "$l"; done
|
||||
echo '%description'; echo "d"; echo '%install'; echo 'mkdir -p %{buildroot}/usr/share/su-dummy'; echo "echo $n > %{buildroot}/usr/share/su-dummy/$n"
|
||||
echo '%files'; echo "/usr/share/su-dummy/$n"; } > "$f"
|
||||
rpmbuild -bb --quiet --define "_topdir $TOP" "$f" >/dev/null 2>&1; ls "$TOP"/RPMS/noarch/$n-$v-1.*.rpm
|
||||
}
|
||||
rpm -Uvh "$(mkp su-dummy-old 1.0)" >/dev/null
|
||||
cp "$(mkp su-dummy-new 1.0 'Obsoletes: su-dummy-old < 2' 'Provides: su-dummy-old = 2')" "$REPO"/ && createrepo_c --quiet --update "$REPO" >/dev/null
|
||||
su_refresh
|
||||
su_plan --exclude su-dummy-a > /tmp/plan.tsv; rc=$?; eq "su_plan exit code" "$rc" 0
|
||||
echo " su_plan --exclude su-dummy-a (section name old new repo arch replaces), su-dummy rows only:"; grep -E $'\tsu-dummy-' /tmp/plan.tsv | sed 's/^/ /'
|
||||
check "plan: b upgrade 1.0-1 -> 2.0-1" grep -q -x -F $'upgrade\tsu-dummy-b\t1.0-1\t2.0-1\tsu-dummy\tnoarch\t-' /tmp/plan.tsv
|
||||
check "plan: excluded su-dummy-a is absent" test "$(grep -c $'\tsu-dummy-a\t' /tmp/plan.tsv)" -eq 0
|
||||
check "plan: obsoleting install of su-dummy-new is visible" grep -q $'^install\tsu-dummy-new\t' /tmp/plan.tsv
|
||||
echo " (upgradable list does NOT show the obsoleter:) $(su_upgradable | cut -f1 | grep -c '^su-dummy-new$') row(s) for su-dummy-new"
|
||||
rpm -e su-dummy-old >/dev/null 2>&1
|
||||
|
||||
if [ $BACKEND = zypper ]; then
|
||||
sec "10. zypper lock save/restore recipe (su_zypper_excluding_snapshot)"
|
||||
zypper -n -q addlock -m "user lock" su-dummy-d >/dev/null
|
||||
sum() { [ -e /etc/zypp/locks ] && { cksum < /etc/zypp/locks; stat -c %Y /etc/zypp/locks; } | tr '\n' ' ' || echo absent; }
|
||||
B=$(sum); echo " before: $B"
|
||||
su_zypper_excluding_snapshot su-dummy-a su-dummy-b -- "$(su_zypper_verb)" --dry-run > /tmp/snap.log 2>&1; rc=$?
|
||||
eq "snapshot recipe exit code (dry-run)" "$rc" 0
|
||||
eq "locks file byte-identical + same mtime after normal run" "$(sum)" "$B"
|
||||
check "user lock still present afterwards" grep -q 'solvable_name: su-dummy-d' /etc/zypp/locks
|
||||
check "temporary locks are gone" test "$(grep -c 'solvable_name: su-dummy-[ab]$' /etc/zypp/locks)" -eq 0
|
||||
echo "--- why not addlock + removelock: a pre-existing user lock on the same name is destroyed"
|
||||
zypper -n -q addlock su-dummy-a >/dev/null # the 'user' lock on su-dummy-a
|
||||
zypper -n -q removelock su-dummy-a >/dev/null
|
||||
check "NAIVE removelock destroyed the user's own lock on su-dummy-a (why we snapshot)" test "$(grep -c 'solvable_name: su-dummy-a$' /etc/zypp/locks)" -eq 0
|
||||
zypper -n -q addlock su-dummy-a >/dev/null; B3=$(sum)
|
||||
su_zypper_excluding_snapshot su-dummy-a -- "$(su_zypper_verb)" --dry-run >/dev/null 2>&1
|
||||
eq "snapshot recipe keeps pre-existing lock on su-dummy-a" "$(sum)" "$B3"
|
||||
echo "--- file absent before => absent after (removelock would leave a 0-byte file)"
|
||||
rm -f /etc/zypp/locks
|
||||
su_zypper_excluding_snapshot su-dummy-a -- "$(su_zypper_verb)" --dry-run >/dev/null 2>&1
|
||||
eq "locks file absent after" "$(sum)" absent
|
||||
echo "--- SIGTERM in the middle of the run => trap restores"
|
||||
zypper -n -q addlock -m "user lock" su-dummy-d >/dev/null; B4=$(sum)
|
||||
zypper() { if [ "$3" = "$(su_zypper_verb)" ] || [ "$2" = "$(su_zypper_verb)" ]; then kill -TERM $BASHPID; sleep 5; else command zypper "$@"; fi; }
|
||||
su_zypper_excluding_snapshot su-dummy-a su-dummy-b -- "$(su_zypper_verb)" > /dev/null 2>&1; rc=$?
|
||||
unset -f zypper
|
||||
eq "killed run exit code (143 = 128+SIGTERM)" "$rc" 143
|
||||
eq "locks restored after SIGTERM" "$(sum)" "$B4"
|
||||
fi
|
||||
|
||||
TOTAL_MS=$(( ($(now)-T_START)/1000 ))
|
||||
printf '\n== timings: fixture=%d ms refresh=%d ms list=%d ms total=%d ms\n' "$FIXTURE_MS" "$REFRESH_MS" "$LIST_MS" "$TOTAL_MS"
|
||||
printf '== RESULT: %d failure(s)\n' "$FAILS"
|
||||
exit "$FAILS"
|
||||
Reference in new issue
Block a user