Files
2026-10-10 10:37:53 +02:00

212 lines
8.6 KiB
Bash

#!/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.
#
# 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 = <pkg> 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_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_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 <outdir> <name> <pkgver> <pkgdesc> [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 <su-fixture@example.invalid>\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 <outdir> <name> <pkgver> <pkgdesc> [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" <<EOF
pkgname=$name
pkgver=$ver
pkgrel=1
pkgdesc='$desc'
arch=('any')
url='https://example.invalid/su-dummy'
license=('custom')
options=('!debug' '!strip') # arch=any payload: no debugedit/binutils needed
depends=($depends)
package() {
install -Dm644 /dev/stdin "\$pkgdir/usr/share/su-dummy/\$pkgname/VERSION" <<<"\$pkgver"
}
EOF
chown -R su-builder: "$w"
runuser -u su-builder -- env \
PKGDEST="$out" SOURCE_DATE_EPOCH="$FIXTURE_EPOCH" \
PACKAGER='su-fixture <su-fixture@example.invalid>' \
bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor >/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 <outdir> <name> <ver> <desc> [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
rm -rf "$old"
if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; 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