Files
marfrit-packages/.gitea/scripts/check-already-published.sh
T
marfrit 7570d2daab check-already-published.sh: use dpkg vercmp for debian (skip when pool >= source)
Old logic curl-HEAD-checked an exact pkgname_pkgver-pkgrel_arch.deb URL.
That breaks when source PKGREL has rolled back below pools current head:
reprepro rejects the lower-version upload but our exact-URL check still
404s, so CI rebuilds the package on EVERY push without ever updating
the pool — endless rebuild trap.

New logic parses the canonical Packages index (what apt consults) and
uses dpkg --compare-versions to skip iff pool >= source.

Pool view today:
- ffmpeg-v4l2-request-fourier: pool=2:8.1+rfourier+gb57fbbe-4 vs
                               source=2:8.1+rfourier+gb57fbbe-2
                               4 >= 2 → skip (was: 404 → rebuild)
- mpv-fourier:                 pool=1:0.41.0+rfourier-3 vs
                               source=1:0.41.0+rfourier-1
                               3 >= 1 → skip (was: 404 → rebuild)

set +o pipefail before the curl|awk pipeline — awks early `exit`
closes the pipe, curl gets SIGPIPE and returns 23 ("Failure writing
output"), which under our pipefail/-e header aborts the script with
empty pool_ver. Localised pipefail-off keeps the global -euo behaviour
elsewhere.
2026-05-20 16:37:30 +02:00

231 lines
8.1 KiB
Bash
Executable File

#!/bin/bash
# check-already-published.sh <recipe-dir>
#
# Decide whether a given recipe (arch/<name> or debian/<name>) is already
# present in https://packages.reauktion.de/. Emits exactly one line to
# stdout:
#
# skip=1 — package with this version-pkgrel-arch tuple already lives in
# the pool; CI should short-circuit.
# skip=0 — file is missing or HEAD failed; CI should build + publish.
#
# Design notes:
# * For Arch recipes we source the PKGBUILD in a clean subshell so
# shell expansions (epoch=, ${_pkgver/-/}, pkgname=() arrays) resolve
# naturally. Only the first element of pkgname[] is checked — split
# packages share one source tarball / one build, so any-one-missing
# forces the full rebuild anyway.
# * For Debian recipes we extract the bare top-level PKGVER= /
# PKGREL= assignments (plus any other top-level VAR=value lines they
# reference) via grep and re-evaluate them in an isolated subshell —
# sourcing the entire build-deb.sh would run curl/tar/dpkg-deb
# against a tempdir we don't want to materialise here.
# * Epoch handling differs by ecosystem: Arch keeps `<epoch>:` in the
# pool filename, Debian/reprepro strips it.
# * curl --head with -f maps non-2xx to non-zero exit, which is what we
# want — 404 means "build it". -L follows mirrors. --max-time caps
# the worst-case latency per HEAD.
set -euo pipefail
REPO_BASE="${REPO_BASE:-https://packages.reauktion.de}"
HEAD_TIMEOUT="${HEAD_TIMEOUT:-15}"
RECIPE_DIR="${1:?usage: $0 <recipe-dir> (e.g. arch/distcc-avahi or debian/lmcp)}"
# Resolve relative to repo root if a leading path is passed; allow
# both `arch/foo` and absolute paths.
if [ ! -d "$RECIPE_DIR" ]; then
echo "error: recipe dir not found: $RECIPE_DIR" >&2
exit 2
fi
ecosystem="${RECIPE_DIR%%/*}"
http_head() {
local url="$1"
curl -sS -L --max-time "$HEAD_TIMEOUT" -o /dev/null \
-w '%{http_code}' --head "$url" || echo "000"
}
emit() {
# one-line GITHUB_OUTPUT-compatible kv
echo "skip=$1"
exit 0
}
case "$ecosystem" in
arch)
pkgbuild="$RECIPE_DIR/PKGBUILD"
[ -f "$pkgbuild" ] || { echo "error: $pkgbuild missing" >&2; exit 2; }
# Source in a fresh bash to capture variables. Some PKGBUILDs run
# functions or call commands at top level — keep this fast by
# restricting PATH and trapping side effects.
eval "$(
bash --noprofile --norc -c "
set +e
# Stub out anything that might shell out; we only need variable
# assignments to land.
cd '$RECIPE_DIR'
source ./PKGBUILD >/dev/null 2>&1 || true
# pkgname may be array; print first element.
if declare -p pkgname 2>/dev/null | grep -q 'declare -a'; then
first_name=\"\${pkgname[0]}\"
else
first_name=\"\$pkgname\"
fi
if declare -p arch 2>/dev/null | grep -q 'declare -a'; then
first_arch=\"\${arch[0]}\"
else
first_arch=\"\$arch\"
fi
printf 'PB_NAME=%q\n' \"\$first_name\"
printf 'PB_VER=%q\n' \"\$pkgver\"
printf 'PB_REL=%q\n' \"\$pkgrel\"
printf 'PB_EPOCH=%q\n' \"\${epoch:-}\"
printf 'PB_ARCH=%q\n' \"\$first_arch\"
"
)"
if [ -z "${PB_NAME:-}" ] || [ -z "${PB_VER:-}" ] || [ -z "${PB_REL:-}" ]; then
echo "error: failed to parse PKGBUILD ($RECIPE_DIR)" >&2
emit 0
fi
# Pool arch:
# arch=('any') → any
# arch=('aarch64' 'x86_64') → aarch64 (we publish for both, but the
# aarch64 artifact is the canonical CI build)
# arch=('aarch64') → aarch64
case "$PB_ARCH" in
any) pool_arch=any ;;
*) pool_arch=aarch64 ;;
esac
# Version string with optional epoch (epoch:pkgver-pkgrel).
if [ -n "${PB_EPOCH:-}" ]; then
ver_full="${PB_EPOCH}:${PB_VER}-${PB_REL}"
else
ver_full="${PB_VER}-${PB_REL}"
fi
# Pool URL path (arch keeps any/aarch64 split; 'any' lands in the
# aarch64 dir per current marfrit layout — both arches share the
# blob via the publish-to-both-arches step in build.yml).
pool_dir="arch/aarch64"
base_url="${REPO_BASE}/${pool_dir}/${PB_NAME}-${ver_full}-${pool_arch}.pkg.tar"
for ext in zst xz gz; do
code=$(http_head "${base_url}.${ext}")
if [ "$code" = "200" ]; then
emit 1
fi
done
emit 0
;;
debian)
bd="$RECIPE_DIR/build-deb.sh"
ctrl="$RECIPE_DIR/control"
[ -f "$bd" ] || { echo "error: $bd missing" >&2; exit 2; }
# Pull top-level `VAR=value` lines until we've passed PKGREL, and
# only those whose RHS is safe to re-evaluate (no command
# substitution `$(...)`, no escaped `\$`, no embedded commands like
# `DESTDIR=... meson ...`). This deliberately undershoots: we just
# need PKGVER/PKGREL plus any version vars they reference. Anything
# else (HERE=$(readlink ...), KERNELVER=\$(uname -r) inside a
# HEREDOC, etc.) gets dropped.
assigns=$(awk '
/^[A-Z_][A-Z0-9_]*=/ {
# split into LHS and RHS
eq = index($0, "=")
lhs = substr($0, 1, eq - 1)
rhs = substr($0, eq + 1)
# strip inline `# comment`
hash = index(rhs, "#")
if (hash > 1 && substr(rhs, hash-1, 1) == " ") rhs = substr(rhs, 1, hash - 2)
# reject lines with command-subst or escaped-dollar or naked commands
if (rhs ~ /\$\(/) next
if (rhs ~ /\\\$/) next
if (rhs ~ / [a-z]/) next # e.g. `DESTDIR="$ROOT" meson ...`
print lhs "=" rhs
if (lhs == "PKGREL") exit
}
' "$bd")
eval "$(
bash --noprofile --norc -c "
set +e
$assigns
printf 'PKGVER=%q\n' \"\${PKGVER:-}\"
printf 'PKGREL=%q\n' \"\${PKGREL:-}\"
"
)"
if [ -z "${PKGVER:-}" ] || [ -z "${PKGREL:-}" ]; then
echo "error: failed to parse PKGVER/PKGREL from $bd" >&2
emit 0
fi
# Strip epoch (`N:` prefix) — debian pool filenames omit it.
ver_no_epoch="${PKGVER#*:}"
# If PKGVER had no colon, ${PKGVER#*:} returns PKGVER unchanged (bash quirk:
# the pattern must match for the prefix to be stripped). Guard explicitly.
case "$PKGVER" in
*:*) : ;;
*) ver_no_epoch="$PKGVER" ;;
esac
ver_full="${ver_no_epoch}-${PKGREL}"
# Architecture: parse control's `Architecture:` field.
if [ ! -f "$ctrl" ]; then
# Some recipes ship debian/control instead of ./control
ctrl="$RECIPE_DIR/debian/control"
fi
ctrl_arch=$(grep -m1 '^Architecture:' "$ctrl" 2>/dev/null | awk '{print $2}')
case "$ctrl_arch" in
all) file_arch=all ;;
arm64|any) file_arch=arm64 ;;
amd64) file_arch=amd64 ;;
*) file_arch=arm64 ;; # conservative default
esac
pkg_name=$(basename "$RECIPE_DIR")
# Compare against the canonical Packages index (what apt actually
# consults). reprepro refuses lower-version uploads, so checking
# only an exact source-pkgrel URL produces an endless-rebuild trap
# whenever source PKGREL has rolled back below pool head. We skip
# if pools published version >= source version-tuple.
source_full="${ver_full}"
if [ -n "${PKGVER#*:}" ] && [ "${PKGVER}" != "${PKGVER#*:}" ]; then
# PKGVER had an epoch — keep it for dpkg --compare-versions.
source_full="${PKGVER}-${PKGREL}"
fi
# Determine suite: most recipes publish to both bookworm and trixie;
# checking trixie is sufficient (changelogs share Distribution).
suite="trixie"
pkg_arch_label="$file_arch"
[ "$file_arch" = "all" ] && pkg_arch_label="all"
packages_url="${REPO_BASE}/debian/dists/${suite}/main/binary-arm64/Packages"
[ "$file_arch" = "amd64" ] && packages_url="${REPO_BASE}/debian/dists/${suite}/main/binary-amd64/Packages"
pool_ver=$(set +o pipefail; curl -sS --max-time "$HEAD_TIMEOUT" "$packages_url" 2>/dev/null | awk -v p="$pkg_name" '$1=="Package:" && $2==p {found=1; next} found && $1=="Version:" {print $2; exit}')
if [ -n "$pool_ver" ] && command -v dpkg >/dev/null && dpkg --compare-versions "$pool_ver" ge "$source_full"; then
echo "pool has $pool_ver >= source $source_full" >&2
emit 1
fi
echo "pool has $pool_ver, source wants $source_full — build" >&2
emit 0
;;
*)
echo "error: unsupported ecosystem '$ecosystem' (recipe-dir=$RECIPE_DIR)" >&2
emit 0
;;
esac