223 lines
9.4 KiB
Bash
Executable File
223 lines
9.4 KiB
Bash
Executable File
#!/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
|
|
# knob; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=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 = <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_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_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_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
|
|
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_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
|