#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2026 Bryan Joshua Pedini # Deterministic pacman fixture for skeptical-updater E2E tests. # (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN and # FIXTURE_AUR knobs; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 # FIXTURE_SYNC=1 FIXTURE_AUR=1.) # # Creates three dummy packages installed at 1.0 and a local pacman repo # ([su-dummy], file:///srv/su-repo) that offers them at 2.0: # # su-dummy-a depends=(su-dummy-b) installed explicitly # su-dummy-b installed as a dependency (--asdeps) # su-dummy-c installed explicitly # # It also pre-wires /etc/pacman.d/ignorepkg (empty) and the matching # "Include" line inside the [options] section of /etc/pacman.conf, i.e. the # same layout that pman expects (IgnorePkg = lines). # # Run as root inside an Arch container: # docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/fixture.sh # # Environment (all optional): # FIXTURE_BUILDER tar (default) | makepkg # tar : hand-built .pkg.tar.zst (.PKGINFO + bsdtar). No network, no # fakeroot, no build user. ~0.1 s. # makepkg : real makepkg run as the unprivileged user "su-builder" via # runuser. Needs fakeroot (installed with `pacman -Sy fakeroot` # if missing, so it needs network). Slower. # FIXTURE_ISOLATE 0 (default) | 1 # 1 comments out every repo section except [su-dummy], so `pacman -Qu` # reports EXACTLY the three dummy packages (no real upgrades, no network # needed after the fixture is built). # FIXTURE_STRICT_DEP 0 (default) | 1 # 1 makes su-dummy-a 2.0 depend on "su-dummy-b>=2.0" (instead of plain # "su-dummy-b"), so excluding su-dummy-b from an upgrade (--ignore / # IgnorePkg) while upgrading su-dummy-a fails with unsatisfied deps. # FIXTURE_ORPHAN 0 (default) | 1 -> also install su-dummy-orphan 1.0 --asdeps with nothing # requiring it (a pre-existing orphan, `pacman -Qdtq` lists it) # FIXTURE_AUR 0 (default) | 1 -> also install su-dummy-aur 1.0, a foreign package (in no # repository: `pacman -Qm` lists it), and keep its 2.0 package file in # /srv/su-aur for the fake paru of tests/e2e/aur.bats; create the user # su-tester (group wheel) and make `su` passwordless for wheel (pam_wheel # trust), which the fake sudo of those tests relies on (the image has no sudo) # FIXTURE_REPO repo directory (default /srv/su-repo) # FIXTURE_SYNC 0 (default) | 1 -> run `pacman -Sy` at the end # # The fixture deliberately does NOT sync by default: the tool under test is # expected to perform the refresh itself (pacman -Sy), after which # `pacman -Qu` lists su-dummy-{a,b,c} (1.0-1 -> 2.0-1). export LC_ALL=C FIXTURE_BUILDER=${FIXTURE_BUILDER:-tar} FIXTURE_ISOLATE=${FIXTURE_ISOLATE:-0} FIXTURE_STRICT_DEP=${FIXTURE_STRICT_DEP:-0} FIXTURE_REPO=${FIXTURE_REPO:-/srv/su-repo} FIXTURE_SYNC=${FIXTURE_SYNC:-0} FIXTURE_ORPHAN=${FIXTURE_ORPHAN:-0} FIXTURE_AUR=${FIXTURE_AUR:-0} FIXTURE_REPO_NAME=su-dummy FIXTURE_EPOCH=1700000000 # fixed build date => reproducible tarballs fx_log() { printf 'fixture: %s\n' "$*" >&2; } fx_die() { fx_log "ERROR: $*"; exit 1; } # pacman wrapper: stdout dropped, the harmless "database file for X does not # exist" warnings (fresh image, no sync yet) filtered, real errors kept, exit # status preserved. fx_pacman() { local rc=0 pacman "$@" 2>&1 >/dev/null | { grep -v "^warning: database file for" >&2 || true; } || rc=$? return $rc } # fx_build_tar [depend...] # Hand-built package: only a .PKGINFO and the payload are required by pacman -U # and repo-add (no .MTREE / .BUILDINFO needed; `pacman -Qk` is happy too). fx_build_tar() { local out=$1 name=$2 ver=$3 desc=$4; shift 4 local w payload w=$(mktemp -d) payload="usr/share/su-dummy/$name" mkdir -p "$w/$payload" printf '%s\n' "$ver" >"$w/$payload/VERSION" { printf 'pkgname = %s\n' "$name" printf 'pkgbase = %s\n' "$name" printf 'pkgver = %s-1\n' "$ver" printf 'pkgdesc = %s\n' "$desc" printf 'url = https://example.invalid/su-dummy\n' printf 'builddate = %s\n' "$FIXTURE_EPOCH" printf 'packager = su-fixture \n' printf 'size = %s\n' "$(wc -c <"$w/$payload/VERSION")" printf 'arch = any\n' printf 'license = custom\n' local d for d in "$@"; do printf 'depend = %s\n' "$d"; done } >"$w/.PKGINFO" (cd "$w" && bsdtar --zstd --uid 0 --gid 0 --uname root --gname root \ -cf "$out/$name-$ver-1-any.pkg.tar.zst" .PKGINFO usr) rm -rf "$w" } # fx_build_makepkg [depend...] # Real makepkg. makepkg refuses to run as root, so run it as an unprivileged # user with `runuser -u` (no sudo needed; `su -s /bin/bash -c` also works). fx_build_makepkg() { local out=$1 name=$2 ver=$3 desc=$4; shift 4 local w depends="" d for d in "$@"; do depends+="'$d' "; done w=/home/su-builder/build/$name-$ver mkdir -p "$w" cat >"$w/PKGBUILD" </dev/null 2>&1" \ || { runuser -u su-builder -- bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor" >&2; fx_die "makepkg failed for $name"; } } fx_prepare_makepkg() { if ! command -v fakeroot >/dev/null; then fx_log "makepkg builder: installing fakeroot (needs network)" fx_pacman -Sy --noconfirm --needed fakeroot fi id su-builder >/dev/null 2>&1 || useradd --create-home su-builder } fx_build() { # fx_build [dep...] case $FIXTURE_BUILDER in tar) fx_build_tar "$@" ;; makepkg) fx_build_makepkg "$@" ;; *) fx_die "unknown FIXTURE_BUILDER=$FIXTURE_BUILDER" ;; esac } fx_main() { set -euo pipefail [ "$(id -u)" -eq 0 ] || fx_die "must run as root" local old new old=$(mktemp -d); new=$FIXTURE_REPO rm -rf "$FIXTURE_REPO"; mkdir -p "$FIXTURE_REPO" chmod 755 "$old" if [ "$FIXTURE_BUILDER" = makepkg ]; then fx_prepare_makepkg chown su-builder: "$old" "$new" # PKGDEST dirs must be writable by the build user fi # 1.0 packages (installed) and 2.0 packages (offered by the repo) fx_build "$old" su-dummy-a 1.0 'Skeptical-updater dummy package A' su-dummy-b fx_build "$old" su-dummy-b 1.0 'Skeptical-updater dummy package B (dependency of A)' fx_build "$old" su-dummy-c 1.0 'Skeptical-updater dummy package C (independent)' local a2_dep=su-dummy-b [ "$FIXTURE_STRICT_DEP" = 1 ] && a2_dep='su-dummy-b>=2.0' fx_build "$new" su-dummy-a 2.0 'Skeptical-updater dummy package A' "$a2_dep" fx_build "$new" su-dummy-b 2.0 'Skeptical-updater dummy package B (dependency of A)' fx_build "$new" su-dummy-c 2.0 'Skeptical-updater dummy package C (independent)' # Local repository database (repo-add ships with pacman) (cd "$FIXTURE_REPO" && repo-add --quiet "$FIXTURE_REPO_NAME.db.tar.gz" ./*.pkg.tar.zst) chmod -R a+rX "$FIXTURE_REPO"; chmod a+rx /srv 2>/dev/null || true # pacman.conf: register the repo (appended at the end => lowest priority) if ! grep -q "^\[$FIXTURE_REPO_NAME\]" /etc/pacman.conf; then { printf '\n[%s]\n' "$FIXTURE_REPO_NAME" printf 'SigLevel = Optional TrustAll\n' printf 'Server = file://%s\n' "$FIXTURE_REPO" } >>/etc/pacman.conf fi # pman-style ignore file, included from the [options] section # (inserted right after the FIRST "[options]" header; appending at EOF would # land in whatever section happens to be last). : >/etc/pacman.d/ignorepkg if ! grep -q '^Include *= */etc/pacman.d/ignorepkg' /etc/pacman.conf; then sed -i '0,/^\[options\]/s||[options]\nInclude = /etc/pacman.d/ignorepkg|' /etc/pacman.conf fi if [ "$FIXTURE_ISOLATE" = 1 ]; then # comment out all repos except [su-dummy]; keep [options] intact awk -v keep="$FIXTURE_REPO_NAME" ' /^\[.*\][[:space:]]*$/ { sec=$0; sub(/^\[/,"",sec); sub(/\].*$/,"",sec); active = (sec=="options" || sec==keep) } { if (sec != "" && !active && $0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) print "#" $0; else print } ' /etc/pacman.conf >/etc/pacman.conf.new && mv /etc/pacman.conf.new /etc/pacman.conf fi # Install the 1.0 packages: b as a dependency, a and c explicit. fx_pacman -U --noconfirm "$old"/su-dummy-a-1.0-1-any.pkg.tar.zst \ "$old"/su-dummy-b-1.0-1-any.pkg.tar.zst \ "$old"/su-dummy-c-1.0-1-any.pkg.tar.zst fx_pacman -D --asdeps su-dummy-b if [ "$FIXTURE_ORPHAN" = 1 ]; then fx_build "$old" su-dummy-orphan 1.0 'Skeptical-updater dummy package ORPHAN (nothing requires it)' fx_pacman -U --noconfirm --asdeps "$old"/su-dummy-orphan-1.0-1-any.pkg.tar.zst pacman -Qdtq | grep -qx su-dummy-orphan || fx_die "su-dummy-orphan is not an orphan" fx_log "pre-existing orphan: su-dummy-orphan" fi rm -rf "$old" if [ "$FIXTURE_AUR" = 1 ]; then mkdir -p /srv/su-aur [ "$FIXTURE_BUILDER" != makepkg ] || chown su-builder: /srv/su-aur fx_build /srv/su-aur su-dummy-aur 1.0 'Skeptical-updater dummy package AUR (foreign: in no repository)' fx_build /srv/su-aur su-dummy-aur 2.0 'Skeptical-updater dummy package AUR (foreign: in no repository)' chmod -R a+rX /srv/su-aur fx_pacman -U --noconfirm /srv/su-aur/su-dummy-aur-1.0-1-any.pkg.tar.zst id su-tester >/dev/null 2>&1 || useradd --create-home --groups wheel su-tester sed -i 's/^#\(auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid\)/\1/' /etc/pam.d/su grep -q '^auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid' /etc/pam.d/su || fx_die "could not enable pam_wheel trust in /etc/pam.d/su" fi if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; fi if [ "$FIXTURE_AUR" = 1 ]; then pacman -Qmq | grep -qx su-dummy-aur || fx_die "su-dummy-aur is not a foreign package" fx_log "foreign (AUR) package: su-dummy-aur 1.0; user su-tester (wheel, passwordless su)" fi fx_log "builder=$FIXTURE_BUILDER isolate=$FIXTURE_ISOLATE repo=$FIXTURE_REPO" fx_log "installed: $(pacman -Q su-dummy-a su-dummy-b su-dummy-c 2>/dev/null | tr '\n' ';')" fx_log "repo list: $(pacman-conf --repo-list | tr '\n' ' ')" } # Run when executed; do nothing when sourced (so tests can reuse fx_build_tar etc. # without inheriting `set -e`). if [ "${BASH_SOURCE[0]}" = "$0" ]; then fx_main "$@" fi