#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2026 Bryan Joshua Pedini # rpm.sh - deterministic E2E fixture for RPM distros (dnf: Alma/Rocky/..., zypper: openSUSE) # Copy of discovery/rpm/fixture.sh; the only changes are the SU_FIXTURE_ISOLATE, # SU_FIXTURE_ORPHAN and SU_FIXTURE_B_AUTO knobs. # # 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. # # Optional (SU_FIXTURE_ISOLATE=1): after the build tools are installed, move the distro's # own repository (and, on zypper, service) definitions to /srv/su-disabled-repos, so that # only the su-dummy repo is left: the upgradable list is exactly su-dummy-{a,b,c} and later # refreshes/upgrades do not depend on the state of the real mirrors. # # Optional (SU_FIXTURE_ORPHAN=1): su-dummy-orphan 1.0 is installed as a dependency (of # su-dummy-orphan-parent, installed through the package manager and then removed with rpm -e), # so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded). # # Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark # remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps; on dnf # su-dummy-a and su-dummy-c also count as installed by the user (`dnf mark install`): dnf4 reports # packages installed with plain rpm as reason "unknown" (dnf5: "External User"). zypper needs # nothing for them: a package that is not in AutoInstalled is user-installed. # # 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 /discovery/rpm:/d:ro 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" # ---- 2b. optional isolation: only the su-dummy repo stays configured ---------- if [ "${SU_FIXTURE_ISOLATE:-0}" = 1 ]; then mkdir -p /srv/su-disabled-repos case $BACKEND in zypper) defs=(/etc/zypp/repos.d/*.repo /etc/zypp/services.d/*.service) ;; dnf|yum) defs=(/etc/yum.repos.d/*.repo) ;; esac for f in "${defs[@]}"; do [ -e "$f" ] || continue case ${f##*/} in su-dummy.repo) continue ;; esac mv "$f" "/srv/su-disabled-repos/${f##*/}" done log "ISOLATE: real repository definitions moved to /srv/su-disabled-repos" fi # ---- 3. build RPMs ---------------------------------------------------------- rm -rf "$TOP" "$REPO" mkdir -p "$TOP"/{BUILD,BUILDROOT,RPMS,SRPMS,SOURCES,SPECS} "$REPO" # mkspec 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 if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then build "$(mkspec su-dummy-orphan 1.0 noarch)" build "$(mkspec su-dummy-orphan-parent 1.0 noarch 'Requires: su-dummy-orphan')" 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 if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-orphan*-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 </dev/null ;; esac log "repo refresh in $((SECONDS - t))s" # ---- 6b. optional pre-existing orphan: installed as a dependency, its parent removed ---------- if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then case $BACKEND in zypper) zypper --non-interactive --quiet install su-dummy-orphan-parent >/dev/null ;; dnf|yum) "$BACKEND" -y -q install su-dummy-orphan-parent >/dev/null 2>&1 ;; esac rpm -e su-dummy-orphan-parent case $BACKEND in zypper) unneeded=$(zypper --non-interactive --quiet packages --unneeded 2>/dev/null || true) ;; dnf|yum) unneeded=$("$BACKEND" -q repoquery --unneeded 2>/dev/null || true) ;; esac rpm -q su-dummy-orphan >/dev/null || die "su-dummy-orphan is not installed" printf '%s\n' "$unneeded" | grep -q su-dummy-orphan || die "su-dummy-orphan is not an unneeded package" log "pre-existing orphan (unneeded): su-dummy-orphan" fi # ---- 6c. optional: su-dummy-b installed as a dependency (of su-dummy-a) ---------------------- if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then case $BACKEND in zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;; dnf|yum) # dnf4 spelling first, then dnf5's (which asks for confirmation) "$BACKEND" -q mark remove su-dummy-b >/dev/null 2>&1 || "$BACKEND" -q -y mark dependency su-dummy-b >/dev/null "$BACKEND" -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || "$BACKEND" -q -y mark user su-dummy-a su-dummy-c >/dev/null ;; esac log "su-dummy-b marked as installed as a dependency" fi # ---- 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)"