first commit
This commit is contained in:
commit
1354db6c39
90 files changed
+13123
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# closure-dpkg.awk - dependency closure (Pre-Depends + Depends) of installed packages, computed
|
||||
# from the dpkg database only (exactly the dependencies of the INSTALLED versions; virtual
|
||||
# packages are resolved through the Provides of installed packages; every installed alternative
|
||||
# of an "a | b" group is followed).
|
||||
# usage: dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' \
|
||||
# | awk -F'\t' -v roots="pkg1 pkg2" -f closure-dpkg.awk
|
||||
# output: plain package names, one per line, roots included (only if installed), BFS order
|
||||
function trim(s) { gsub(/^[ ]+|[ ]+$/, "", s); return s }
|
||||
substr($1, 2, 1) ~ /[nc]/ { next } # not-installed / config-files-only
|
||||
{
|
||||
inst[$2] = 1
|
||||
deps[$2] = $3 "," $4
|
||||
n = split($5, pv, ",")
|
||||
for (i = 1; i <= n; i++) {
|
||||
p = trim(pv[i]); sub(/ .*/, "", p); sub(/:.*/, "", p)
|
||||
if (p != "") prov[p] = prov[p] " " $2
|
||||
}
|
||||
}
|
||||
END {
|
||||
nr = split(roots, r, " ")
|
||||
for (i = 1; i <= nr; i++) if (inst[r[i]] && !seen[r[i]]) { seen[r[i]] = 1; q[++t] = r[i] }
|
||||
for (h = 1; h <= t; h++) {
|
||||
p = q[h]; print p
|
||||
m = split(deps[p], d, ",")
|
||||
for (j = 1; j <= m; j++) {
|
||||
na = split(d[j], alt, "|")
|
||||
for (k = 1; k <= na; k++) {
|
||||
a = alt[k]
|
||||
gsub(/\([^)]*\)/, "", a); gsub(/\[[^]]*\]/, "", a); gsub(/<[^>]*>/, "", a)
|
||||
a = trim(a); sub(/:(any|native)$/, "", a)
|
||||
if (a == "") continue
|
||||
if (inst[a] && !seen[a]) { seen[a] = 1; q[++t] = a }
|
||||
np = split(prov[a], pp, " ")
|
||||
for (x = 1; x <= np; x++) if (!seen[pp[x]]) { seen[pp[x]] = 1; q[++t] = pp[x] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fixture.sh - APT e2e fixture for skeptical-updater (Debian/Ubuntu containers).
|
||||
#
|
||||
# Builds a local *flat* APT repository at /srv/su-repo containing dummy packages
|
||||
# and installs the old (1.0) versions, so that after `apt-get update` the package
|
||||
# manager reports deterministic upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0 -> 2.0 (Depends: su-dummy-b, ships a conffile)
|
||||
# su-dummy-b 1.0 -> 2.0 (dependency of su-dummy-a)
|
||||
# su-dummy-c 1.0 -> 2.0 (standalone)
|
||||
#
|
||||
# Optional extras (SU_FIXTURE_EXTRA=1) for "kept back" / removal / virtual-package
|
||||
# scenarios:
|
||||
# su-dummy-d 1.0 -> 2.0 2.0 adds a NEW dependency (su-dummy-newdep, not installed)
|
||||
# => plain `upgrade` keeps it back, `--with-new-pkgs upgrade`
|
||||
# and `dist-upgrade` install it
|
||||
# su-dummy-e 1.0 -> 2.0 2.0 Conflicts: su-dummy-f (installed, no 2.0 exists)
|
||||
# => only `dist-upgrade` can do it (removes su-dummy-f) - except with apt 3.x
|
||||
# solver3 (ubuntu:latest = 26.04) which refuses to remove the manually
|
||||
# installed su-dummy-f and keeps su-dummy-e back even for dist-upgrade
|
||||
# su-dummy-f 1.0 installed, never upgradable
|
||||
# su-dummy-w 1.0 -> 2.0 Depends: su-virtual | su-dummy-alt (virtual + alternative)
|
||||
# su-dummy-v 1.0 Provides: su-virtual
|
||||
# (+ su-dummy-newdep 1.0 and su-dummy-alt 1.0: only in the repo, never pre-installed)
|
||||
#
|
||||
# Environment knobs (all optional):
|
||||
# SU_REPO_DIR=/srv/su-repo where the repo is created (wiped first; must match /srv/su-*, /tmp/su-*, /var/tmp/su-*)
|
||||
# SU_OLD_DIR=/srv/su-old where the 1.0 .debs are kept (NOT in the repo)
|
||||
# SU_FIXTURE_INDEXER=shell shell | scanpackages | ftparchive
|
||||
# shell = no extra tools needed (dpkg-deb -f + sha256sum)
|
||||
# scanpackages = dpkg-scanpackages (installs dpkg-dev if missing)
|
||||
# ftparchive = apt-ftparchive (installs apt-utils if missing)
|
||||
# SU_FIXTURE_OFFLINE=1 disable the distro's real sources (hermetic: no network at all)
|
||||
# SU_FIXTURE_EXTRA=1 add the extra scenario packages listed above
|
||||
# SU_FIXTURE_BAD=1 add su-dummy-bad: 1.0 installed, 2.0 has a postinst that fails
|
||||
# (apt-get upgrade then exits 100) - for error-path tests
|
||||
#
|
||||
# Must run as root inside a throwaway container, e.g.:
|
||||
# docker run --rm -i -v "$PWD/discovery/apt:/d:ro" debian:12 bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
REPO_DIR=${SU_REPO_DIR:-/srv/su-repo}
|
||||
OLD_DIR=${SU_OLD_DIR:-/srv/su-old}
|
||||
INDEXER=${SU_FIXTURE_INDEXER:-shell}
|
||||
OFFLINE=${SU_FIXTURE_OFFLINE:-0}
|
||||
EXTRA=${SU_FIXTURE_EXTRA:-0}
|
||||
BAD=${SU_FIXTURE_BAD:-0}
|
||||
SOURCES_FILE=/etc/apt/sources.list.d/su-repo.list
|
||||
T0=$(date +%s)
|
||||
|
||||
log() { printf '[fixture] %s\n' "$*"; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
command -v apt-get >/dev/null || die "apt-get not found (not a Debian/Ubuntu image?)"
|
||||
command -v dpkg-deb >/dev/null || die "dpkg-deb not found"
|
||||
case $INDEXER in shell|scanpackages|ftparchive) ;; *) die "bad SU_FIXTURE_INDEXER=$INDEXER" ;; esac
|
||||
# the directories are wiped with rm -rf: only accept obviously fixture-owned locations
|
||||
for d in "$REPO_DIR" "$OLD_DIR"; do
|
||||
case $d in /srv/su-*|/tmp/su-*|/var/tmp/su-*) ;; *) die "refusing to wipe '$d' (must match /srv/su-*, /tmp/su-* or /var/tmp/su-*)" ;; esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------- build debs
|
||||
# mkdeb NAME VERSION OUTDIR [extra control line]...
|
||||
# If $MKDEB_POSTINST is set it becomes the body of the package's postinst script.
|
||||
mkdeb() {
|
||||
local name=$1 ver=$2 out=$3 root line
|
||||
shift 3
|
||||
root=$(mktemp -d)
|
||||
mkdir -p "$root/DEBIAN" "$root/usr/share/su-dummy" "$root/etc/su-dummy"
|
||||
{
|
||||
printf 'Package: %s\nVersion: %s\nSection: misc\nPriority: optional\n' "$name" "$ver"
|
||||
printf 'Architecture: all\nMaintainer: Skeptical Updater Fixture <su-fixture@example.invalid>\n'
|
||||
for line in "$@"; do printf '%s\n' "$line"; done
|
||||
printf 'Description: dummy package %s for skeptical-updater tests\n' "$name"
|
||||
printf ' This package only exists to exercise the package manager.\n'
|
||||
printf ' .\n'
|
||||
printf ' It ships one data file and one conffile (version %s).\n' "$ver"
|
||||
} > "$root/DEBIAN/control"
|
||||
echo "$ver" > "$root/usr/share/su-dummy/$name"
|
||||
echo "version=$ver" > "$root/etc/su-dummy/$name.conf"
|
||||
echo "/etc/su-dummy/$name.conf" > "$root/DEBIAN/conffiles"
|
||||
if [ -n "${MKDEB_POSTINST:-}" ]; then
|
||||
printf '#!/bin/sh\n%s\n' "$MKDEB_POSTINST" > "$root/DEBIAN/postinst"
|
||||
chmod 755 "$root/DEBIAN/postinst"
|
||||
fi
|
||||
mkdir -p "$out"
|
||||
# -Zgzip: readable by every dpkg in the supported images; --root-owner-group: files owned by root
|
||||
dpkg-deb --root-owner-group -Zgzip --build "$root" "$out/${name}_${ver}_all.deb" >/dev/null
|
||||
rm -rf "$root"
|
||||
}
|
||||
|
||||
rm -rf "$REPO_DIR" "$OLD_DIR"
|
||||
mkdir -p "$REPO_DIR" "$OLD_DIR"
|
||||
|
||||
# old versions (installed straight away with dpkg, NOT published in the repo)
|
||||
mkdeb su-dummy-a 1.0 "$OLD_DIR" 'Depends: su-dummy-b'
|
||||
mkdeb su-dummy-b 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-c 1.0 "$OLD_DIR"
|
||||
# new versions (published in the repo)
|
||||
mkdeb su-dummy-a 2.0 "$REPO_DIR" 'Depends: su-dummy-b (>= 2.0)'
|
||||
mkdeb su-dummy-b 2.0 "$REPO_DIR"
|
||||
mkdeb su-dummy-c 2.0 "$REPO_DIR"
|
||||
|
||||
if [ "$EXTRA" = 1 ]; then
|
||||
mkdeb su-dummy-d 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-d 2.0 "$REPO_DIR" 'Depends: su-dummy-newdep'
|
||||
mkdeb su-dummy-newdep 1.0 "$REPO_DIR" # only in the repo, never pre-installed
|
||||
mkdeb su-dummy-e 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-e 2.0 "$REPO_DIR" 'Conflicts: su-dummy-f'
|
||||
mkdeb su-dummy-f 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-v 1.0 "$OLD_DIR" 'Provides: su-virtual'
|
||||
mkdeb su-dummy-alt 1.0 "$REPO_DIR" # alternative target, not installed
|
||||
mkdeb su-dummy-w 1.0 "$OLD_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
mkdeb su-dummy-w 2.0 "$REPO_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
fi
|
||||
|
||||
if [ "$BAD" = 1 ]; then
|
||||
mkdeb su-dummy-bad 1.0 "$OLD_DIR"
|
||||
MKDEB_POSTINST='[ "$1" = configure ] && { echo "su-dummy-bad: postinst failing on purpose" >&2; exit 1; }; exit 0' \
|
||||
mkdeb su-dummy-bad 2.0 "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- real sources
|
||||
if [ "$OFFLINE" = 1 ]; then
|
||||
mkdir -p /srv/su-disabled-sources
|
||||
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list; do
|
||||
[ -e "$f" ] || continue
|
||||
case $f in "$SOURCES_FILE") continue ;; esac
|
||||
mv "$f" "/srv/su-disabled-sources/$(basename "$f")"
|
||||
done
|
||||
log "OFFLINE: real sources moved to /srv/su-disabled-sources"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- indexing
|
||||
install_tool() { # install_tool PACKAGE : needs the real mirrors
|
||||
[ "$OFFLINE" = 1 ] && die "SU_FIXTURE_INDEXER=$INDEXER needs network to install $1; use shell with OFFLINE"
|
||||
log "installing $1 (needs network)"
|
||||
apt-get update -qq >/dev/null
|
||||
apt-get install -y -qq --no-install-recommends "$1" >/dev/null
|
||||
}
|
||||
|
||||
index_shell() {
|
||||
local f
|
||||
: > Packages
|
||||
for f in ./*.deb; do
|
||||
f=${f#./}
|
||||
dpkg-deb -f "$f" >> Packages
|
||||
printf 'Filename: ./%s\nSize: %s\nMD5sum: %s\nSHA256: %s\n\n' \
|
||||
"$f" "$(stat -c %s "$f")" "$(md5sum < "$f" | cut -d' ' -f1)" "$(sha256sum < "$f" | cut -d' ' -f1)" >> Packages
|
||||
done
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_scanpackages() {
|
||||
command -v dpkg-scanpackages >/dev/null || install_tool dpkg-dev
|
||||
dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
|
||||
# dpkg-scanpackages does not write a Release file; make a minimal one
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_ftparchive() {
|
||||
command -v apt-ftparchive >/dev/null || install_tool apt-utils
|
||||
apt-ftparchive packages . > Packages
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=su-fixture \
|
||||
-o APT::FTPArchive::Release::Label=su-fixture \
|
||||
-o APT::FTPArchive::Release::Suite=su-repo \
|
||||
-o APT::FTPArchive::Release::Codename=su-repo \
|
||||
release . > Release
|
||||
}
|
||||
|
||||
( cd "$REPO_DIR" && "index_$INDEXER" )
|
||||
log "repo $REPO_DIR indexed with '$INDEXER': $(grep -c '^Package:' "$REPO_DIR/Packages") packages"
|
||||
|
||||
echo "deb [trusted=yes] file:$REPO_DIR ./" > "$SOURCES_FILE"
|
||||
|
||||
# ---------------------------------------------------------------- install old versions
|
||||
# --force-confnew + </dev/null: never block on a conffile prompt if the fixture is re-run in a dirty container
|
||||
dpkg --force-confnew -i "$OLD_DIR"/*.deb >/dev/null </dev/null
|
||||
log "installed (old): $(dpkg-query -W -f '${db:Status-Abbrev} ${Package}=${Version}\n' 'su-dummy-*' 2>/dev/null | awk '$1 == "ii" {printf "%s ", $2}')"
|
||||
|
||||
# ---------------------------------------------------------------- refresh + verify
|
||||
# drop stale lists of our own repo first: apt compares mtimes of file: indexes with 1s resolution,
|
||||
# so a repo regenerated within the same second would otherwise be seen as "unchanged"
|
||||
rm -f /var/lib/apt/lists/"$(printf '%s' "$REPO_DIR" | tr / _)"_*
|
||||
apt-get update -qq >/dev/null
|
||||
|
||||
fail=0
|
||||
upg=$(apt list --upgradable 2>/dev/null | awk -F/ '/^su-dummy-/{print $1}' | sort | tr '\n' ' ')
|
||||
for p in su-dummy-a su-dummy-b su-dummy-c; do
|
||||
case " $upg" in *" $p "*) ;; *) log "MISSING from upgradable: $p"; fail=1 ;; esac
|
||||
done
|
||||
sim=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk '/^Inst su-dummy-(a|b|c) /{n++} END{print n+0}')
|
||||
[ "$sim" -ge 3 ] || { log "simulated upgrade only covers $sim of 3 core packages"; fail=1; }
|
||||
[ "$fail" -eq 0 ] || die "fixture verification failed"
|
||||
|
||||
log "upgradable dummy packages: $upg"
|
||||
real=$(apt list --upgradable 2>/dev/null | awk -F/ 'NR>1 && $1 !~ /^su-dummy-/{print $1}' | tr '\n' ' ')
|
||||
if [ -n "$real" ]; then
|
||||
log "NOTE: real-world upgradable packages present in this base image: $real"
|
||||
else
|
||||
log "NOTE: no real-world upgradable packages in this base image right now"
|
||||
fi
|
||||
log "OK in $(( $(date +%s) - T0 ))s (image: $(. /etc/os-release; echo "$PRETTY_NAME"))"
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# parse-inst.awk - parse the "Inst" lines of `LC_ALL=C apt-get -s <verb>` into TSV.
|
||||
# usage: LC_ALL=C apt-get -s --with-new-pkgs upgrade | awk -f parse-inst.awk
|
||||
# output: name<TAB>oldversion<TAB>newversion<TAB>repo
|
||||
# oldversion is "-" for packages that are newly installed (no previous version);
|
||||
# repo is the raw origin list, e.g. "Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security"
|
||||
# (may be empty for packages coming from a local .deb). POSIX awk, tested with mawk 1.3.4.
|
||||
/^Inst / {
|
||||
name = $2
|
||||
rest = $0
|
||||
sub(/^Inst [^ ]+ /, "", rest)
|
||||
old = "-"
|
||||
if (substr(rest, 1, 1) == "[") { # "[old] (new origins [arch])"
|
||||
old = rest
|
||||
sub(/^\[/, "", old)
|
||||
sub(/\].*$/, "", old)
|
||||
sub(/^\[[^]]*\] /, "", rest)
|
||||
}
|
||||
sub(/^\(/, "", rest) # "new origins [arch]) ..."
|
||||
new = rest
|
||||
sub(/ .*$/, "", new)
|
||||
sub(/^[^ ]+ ?/, "", rest) # "origins [arch]) ..."
|
||||
i = index(rest, "])")
|
||||
repo = substr(rest, 1, i - 1) # "origins [arch"
|
||||
sub(/ ?\[[^[]*$/, "", repo) # drop " [arch"
|
||||
printf "%s\t%s\t%s\t%s\n", name, old, new, repo
|
||||
}
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# smoke.sh - proves that the command recipes documented in discovery/apt.md work, on top of fixture.sh.
|
||||
# docker run --rm -i --network none -v "$PWD/discovery/apt:/d:ro" debian:12 bash /d/smoke.sh
|
||||
# Runs fixture.sh in hermetic mode (SU_FIXTURE_OFFLINE=1, SU_FIXTURE_EXTRA=1) and then asserts.
|
||||
set -uo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
fails=0
|
||||
ok() { printf 'PASS %s\n' "$1"; }
|
||||
bad() { printf 'FAIL %s\n got: %s\n expected: %s\n' "$1" "$2" "$3"; fails=$((fails + 1)); }
|
||||
check() { # check LABEL GOT EXPECTED
|
||||
if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "$2" "$3"; fi
|
||||
}
|
||||
ver() { dpkg-query -W -f '${Version}' "$1" 2>/dev/null; }
|
||||
|
||||
SU_FIXTURE_OFFLINE=1 SU_FIXTURE_EXTRA=1 bash "$HERE/fixture.sh" >/dev/null || { echo "fixture failed"; exit 2; }
|
||||
echo "image: $(. /etc/os-release; echo "$PRETTY_NAME") apt: $(apt-get --version | head -1)"
|
||||
|
||||
# ---- 1. refresh: exit code is 0 and the unreachable-source case is made fatal by Error-Mode=any
|
||||
apt-get update -qq -o APT::Update::Error-Mode=any >/dev/null 2>&1; check "update exit code" "$?" "0"
|
||||
|
||||
# ---- 2. upgradable list (Inst parse) + kept-back parse + apt list cross-check
|
||||
list=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk -f "$HERE/parse-inst.awk" | sort)
|
||||
check "Inst parse su-dummy-a" "$(printf '%s\n' "$list" | grep '^su-dummy-a')" "$(printf 'su-dummy-a\t1.0\t2.0\tsu-fixture:su-repo')"
|
||||
check "Inst parse new package (old=-)" "$(printf '%s\n' "$list" | grep '^su-dummy-newdep')" "$(printf 'su-dummy-newdep\t-\t1.0\tsu-fixture:su-repo')"
|
||||
kept=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk '/^The following packages have been kept back:/{k=1;next} k&&/^ /{for(i=1;i<=NF;i++)print $i;next} {k=0}' | tr '\n' ' ')
|
||||
check "kept-back parse" "$kept" "su-dummy-e "
|
||||
alist=$(apt list --upgradable 2>/dev/null | sed -nE 's#^([^/]+)/([^ ]+) ([^ ]+) ([^ ]+) \[upgradable from: ([^]]+)\]$#\1\t\5\t\3\t\2#p' | grep '^su-dummy-a')
|
||||
check "apt list parse" "$alist" "$(printf 'su-dummy-a\t1.0\t2.0\tsu-repo')"
|
||||
|
||||
# ---- 3. description
|
||||
desc=$(apt-cache show --no-all-versions su-dummy-a | awk '
|
||||
/^Description(-[a-z][a-z](_[A-Z][A-Z])?)?:/ { p=1; sub(/^[^:]*: ?/,""); print; next }
|
||||
p && /^ / { l=substr($0,2); print (l=="." ? "" : l); next }
|
||||
p { exit }')
|
||||
check "description first line" "$(printf '%s\n' "$desc" | head -1)" "dummy package su-dummy-a for skeptical-updater tests"
|
||||
check "description line count" "$(printf '%s\n' "$desc" | wc -l | tr -d ' ')" "4"
|
||||
|
||||
# ---- 4. rdepends / closure
|
||||
RD="--installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances"
|
||||
rd=$(apt-cache rdepends $RD su-dummy-b | awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u | tr '\n' ' ')
|
||||
check "rdepends su-dummy-b" "$rd" "su-dummy-a "
|
||||
NI="--no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances"
|
||||
installed_names() { dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\n' | awk -F'\t' 'substr($1,2,1) !~ /[nc]/ {print $2}' | sort -u; }
|
||||
clos=$(comm -12 <(apt-cache depends --recurse $NI su-dummy-w | awk '/^[^ <]/' | sort -u) <(installed_names) | tr '\n' ' ')
|
||||
check "closure su-dummy-w via virtual (no --installed)" "$clos" "su-dummy-v su-dummy-w "
|
||||
clos_flag=$(apt-cache depends --recurse --installed $NI su-dummy-w | awk '/^[^ <]/' | sort -u | tr '\n' ' ')
|
||||
check "closure su-dummy-w WITH --installed (documented defect: provider lost)" "$clos_flag" "su-dummy-w "
|
||||
clos_dpkg=$(dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' | awk -F'\t' -v roots="su-dummy-w" -f "$HERE/closure-dpkg.awk" | sort -u | tr '\n' ' ')
|
||||
check "closure su-dummy-w via dpkg awk" "$clos_dpkg" "su-dummy-v su-dummy-w "
|
||||
# real package: base-files Pre-Depends on the virtual 'awk' (provided by the installed mawk)
|
||||
real_dpkg=$(dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' | awk -F'\t' -v roots="base-files" -f "$HERE/closure-dpkg.awk" | sort -u | tr '\n' ' ')
|
||||
case " $real_dpkg" in *" mawk "*) ok "closure-dpkg base-files reaches mawk through virtual awk" ;; *) bad "closure-dpkg base-files reaches mawk" "$real_dpkg" "... mawk ..." ;; esac
|
||||
real_flag=$(apt-cache depends --recurse --installed $NI base-files | awk '/^[^ <]/' | sort -u | tr '\n' ' ')
|
||||
case " $real_flag" in *" mawk "*) bad "apt-cache --recurse --installed base-files lacks mawk (documented defect)" "$real_flag" "no mawk" ;; *) ok "apt-cache --recurse --installed base-files lacks mawk (documented defect)" ;; esac
|
||||
|
||||
# ---- 5. hold / exclusion recipe + upgrade + restore
|
||||
SU_STATE_DIR=$(mktemp -d); SU_HOLDS_ADDED=$SU_STATE_DIR/holds.added
|
||||
su_holds_begin() {
|
||||
mkdir -p "$SU_STATE_DIR" || return 1
|
||||
local held p; held=$(apt-mark showhold) || return 1
|
||||
: > "$SU_HOLDS_ADDED"
|
||||
for p in "$@"; do
|
||||
grep -qxF -- "$p" <<<"$held" && continue
|
||||
echo "$p" >> "$SU_HOLDS_ADDED"
|
||||
apt-mark hold "$p" >/dev/null || return 1
|
||||
done
|
||||
}
|
||||
su_holds_restore() {
|
||||
[ -f "$SU_HOLDS_ADDED" ] || return 0
|
||||
apt-mark showhold | sort -u | comm -12 - <(sort -u "$SU_HOLDS_ADDED") | xargs -r apt-mark unhold >/dev/null
|
||||
rm -f "$SU_HOLDS_ADDED"
|
||||
}
|
||||
apt-mark hold su-dummy-alt >/dev/null # pre-existing admin hold: must survive untouched
|
||||
before=$(apt-mark showhold | sort | tr '\n' ' ')
|
||||
su_holds_begin su-dummy-c su-dummy-alt
|
||||
check "hold set during run" "$(apt-mark showhold | sort | tr '\n' ' ')" "su-dummy-alt su-dummy-c "
|
||||
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 \
|
||||
--with-new-pkgs upgrade >/dev/null 2>&1; check "upgrade exit code" "$?" "0"
|
||||
check "held pkg skipped (c)" "$(ver su-dummy-c)" "1.0"
|
||||
check "a upgraded" "$(ver su-dummy-a)" "2.0"; check "b upgraded" "$(ver su-dummy-b)" "2.0"
|
||||
check "new dep installed via --with-new-pkgs" "$(ver su-dummy-newdep)" "1.0"
|
||||
check "kept back e untouched" "$(ver su-dummy-e)" "1.0"
|
||||
su_holds_restore
|
||||
check "hold set restored exactly" "$(apt-mark showhold | sort | tr '\n' ' ')" "$before"
|
||||
# crash recovery: journal survives, admin edits in between are respected
|
||||
su_holds_begin su-dummy-b su-dummy-e; apt-mark hold su-dummy-w >/dev/null; apt-mark unhold su-dummy-alt >/dev/null # (no restore = simulated kill -9)
|
||||
su_holds_restore
|
||||
check "crash recovery releases only our holds, keeps admin edits" "$(apt-mark showhold | sort | tr '\n' ' ')" "su-dummy-w "
|
||||
apt-mark unhold su-dummy-w >/dev/null
|
||||
apt-get -y --with-new-pkgs upgrade >/dev/null 2>&1; check "second upgrade exit code" "$?" "0"
|
||||
check "c upgraded after restore" "$(ver su-dummy-c)" "2.0"
|
||||
noop=$(apt-get -y --with-new-pkgs upgrade 2>&1); rc=$?
|
||||
check "nothing-to-do exit code" "$rc" "0"
|
||||
check "nothing-to-do summary" "$(printf '%s\n' "$noop" | grep -E '^[0-9]+ upgraded')" "0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded."
|
||||
|
||||
# ---- 6. purge + autoremove
|
||||
apt-mark auto su-dummy-b >/dev/null
|
||||
apt-get -y purge su-dummy-a >/dev/null 2>&1; check "purge exit code" "$?" "0"
|
||||
check "a purged (no dpkg status)" "$(dpkg-query -W -f '${db:Status-Abbrev}' su-dummy-a 2>/dev/null)" ""
|
||||
check "autoremove --purge candidate" "$(apt-get -s autoremove --purge | awk '/^Purg /{print $2}' | tr '\n' ' ')" "su-dummy-b "
|
||||
apt-get -y autoremove --purge >/dev/null 2>&1; check "autoremove exit code" "$?" "0"
|
||||
check "b gone" "$(dpkg-query -W -f '${db:Status-Abbrev}' su-dummy-b 2>/dev/null)" ""
|
||||
check "conffile of b gone" "$(ls /etc/su-dummy/su-dummy-b.conf 2>&1 | grep -c 'No such file')" "1"
|
||||
|
||||
rm -rf "$SU_STATE_DIR"
|
||||
[ "$fails" -eq 0 ] && echo "ALL PASSED" || { echo "$fails FAILED"; exit 1; }
|
||||
Reference in new issue
Block a user