f8d1257d35
CI currently rebuilds every recipe on every push. distcc-avahi hasn't
changed in weeks but still burns runner-time. Add a small bash helper
that takes a recipe dir (arch/<n> or debian/<n>), resolves the expected
pool URL on packages.reauktion.de, and prints `skip=1` or `skip=0`.
Live-tested against all 14 recipes. Sources PKGBUILDs in a sandboxed
subshell so epoch=, ${_pkgver/-/}, and pkgname=() arrays resolve
correctly. For debian/*, extracts top-level PKGVER/PKGREL lines with
an awk guard that rejects command-subst and embedded-command
assignments (avoids false-positive matches against HEREDOC-quoted
dkms.conf content).
Wiring into build.yml lands in the next commit.
211 lines
6.9 KiB
Bash
Executable File
211 lines
6.9 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")
|
|
first_letter="${pkg_name:0:1}"
|
|
|
|
url="${REPO_BASE}/debian/pool/main/${first_letter}/${pkg_name}/${pkg_name}_${ver_full}_${file_arch}.deb"
|
|
code=$(http_head "$url")
|
|
if [ "$code" = "200" ]; then
|
|
emit 1
|
|
fi
|
|
emit 0
|
|
;;
|
|
|
|
*)
|
|
echo "error: unsupported ecosystem '$ecosystem' (recipe-dir=$RECIPE_DIR)" >&2
|
|
emit 0
|
|
;;
|
|
esac
|