#!/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") # 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