diff --git a/.gitea/scripts/check-already-published.sh b/.gitea/scripts/check-already-published.sh new file mode 100755 index 000000000..1c19dc853 --- /dev/null +++ b/.gitea/scripts/check-already-published.sh @@ -0,0 +1,210 @@ +#!/bin/bash +# check-already-published.sh +# +# Decide whether a given recipe (arch/ or debian/) 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 `:` 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 (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