Files
.autoupdate/common.sh
Bryan Joshua Pedini 4defd478dc ci: prebuild the Arch environment as a container image
All six nightly jobs ran the same pacman transaction before doing any work.
The toolchain now lives in an image published to the Gitea registry as
git.bjphoster.com/aur/autoupdate:latest, rebuilt monthly since Arch is rolling
and a month-old image is a month behind.

The build job itself runs on plain ubuntu-latest -- it only drives docker. Only
the image is Arch.

Based on archlinux:base rather than base-devel: the autoupdater parses
PKGBUILDs and never compiles, so the toolchain is dead weight, 822 MB against
1.48 GB. nodejs is included because actions/checkout is a JavaScript action the
runner executes with node from inside the image.

Slimming to base initially dropped diffutils, and apply_pkgrel_rule used
diff -q. A missing binary makes the `if` fail, which reads as "changed", so
pkgrel ratcheted upward on every run -- the same silent-wrong-default class as
the empty .SRCINFO this rewrite was about, and invisible to the host test run.
The comparison is now a shell string equality with no external dependency, and
is verified against an image with no diff at all. require_tools() additionally
fails on startup for any missing binary rather than midway through.
2026-07-26 23:17:37 +02:00

214 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Shared helpers for the AUR autoupdater.
# Sourced by autoupdate.sh and the standalone scripts; never executed directly.
set -euo pipefail
# Endpoints. Overridable so the test suite can point at local bare repos
# instead of the real AUR; defaults are the production values.
AUR_SSH_BASE="${AUR_SSH_BASE:-ssh://aur@aur.archlinux.org}"
AUR_HTTPS_BASE="${AUR_HTTPS_BASE:-https://aur.archlinux.org}"
log() { printf '==> %s\n' "$*"; }
info() { printf ' -> %s\n' "$*"; }
warn() { printf 'WARNING: %s\n' "$*" >&2; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
# The old push.sh fell back to an interactive `read` when no version was passed.
# In CI that reads EOF and yields an empty version, which is how pman-helper
# committed pkgver="" and the sha256 of a zero-byte download.
# Fail up front on a missing tool rather than midway through, where an absent
# binary tends to be read as "condition false" and silently takes the wrong
# branch. `diff` going missing in a slimmer base image did exactly that: it made
# every run look like a packaging change and ratcheted pkgrel upward.
require_tools() {
local t missing=()
for t in git curl jq sha256sum awk sed grep makepkg; do
command -v "$t" >/dev/null 2>&1 || missing+=("$t")
done
[ "${#missing[@]}" -eq 0 ] || die "missing required tool(s): ${missing[*]}"
return 0
}
require_noninteractive() {
[ -t 0 ] && die "refusing to prompt: this script must never depend on interactive input"
return 0
}
# A version we are willing to write into a PKGBUILD.
VERSION_RE='^[0-9][0-9a-zA-Z._+]*$'
validate_version() {
local ver="${1-}" what="${2-version}"
[ -n "$ver" ] || die "${what} is empty"
[[ "$ver" =~ $VERSION_RE ]] || die "${what} '${ver}' does not look like a version"
return 0
}
# Read a scalar assignment out of a PKGBUILD without sourcing it.
# Handles pkgver="1.2.3", pkgver='1.2.3' and bare pkgver=1.2.3.
pkgbuild_get() {
local var="$1" file="${2:-PKGBUILD}"
sed -n -E "s/^${var}=[\"']?([^\"']*)[\"']?[[:space:]]*(#.*)?$/\1/p" "$file" | head -n1
}
# Rewrite a scalar assignment in place, preserving double-quoted style.
pkgbuild_set() {
local var="$1" val="$2" file="${3:-PKGBUILD}"
grep -qE "^${var}=" "$file" || die "no ${var}= assignment in ${file}"
sed -i -E "s|^${var}=.*|${var}=\"${val}\"|" "$file"
}
# Expand a PKGBUILD's arrays/vars by sourcing it in a subshell and printing the
# result. Safe enough: these are our own package definitions. Used instead of
# grep/sed so that ${pkgver} / ${_srcname} substitutions actually resolve.
#
# Usage: pkgbuild_expand <file> <bash snippet printing what you want>
pkgbuild_expand() {
local file="$1" snippet="$2"
(
set +euo pipefail
# Neutralise anything that would run during a real build.
# shellcheck disable=SC1090
source "$file" >/dev/null 2>&1 || true
eval "$snippet"
)
}
# Print the elements of an array variable, one per line, NUL-safe enough for
# source entries (which never contain newlines).
pkgbuild_array() {
local file="$1" name="$2"
pkgbuild_expand "$file" "printf '%s\n' \"\${${name}[@]}\" 2>/dev/null"
}
# True if the named array exists in the PKGBUILD (even if empty).
pkgbuild_has_array() {
local file="$1" name="$2"
[ "$(pkgbuild_expand "$file" "declare -p ${name} >/dev/null 2>&1 && echo yes")" = "yes" ]
}
# Replace `name=( ... )` in a PKGBUILD with the given values.
#
# This exists to replace the line-counting sed in the old freetube push.sh
# (`sednewline="n;"` accumulating `n;n;n;`), which silently corrupts the file
# whenever a source array's length changes. Matches `^name=(` through its
# closing `)`, so it handles both one-line and multi-line blocks, and appends
# the array if it is missing entirely.
replace_array() {
local file="$1" name="$2"; shift 2
local block v
if [ "$#" -eq 1 ]; then
block="${name}=(\"$1\")"
else
block="${name}=("
for v in "$@"; do block+=$'\n'" \"${v}\""; done
block+=$'\n'")"
fi
if ! grep -qE "^${name}=\(" "$file"; then
printf '%s\n' "$block" >> "$file"
return 0
fi
awk -v name="$name" -v block="$block" '
BEGIN { skip = 0 }
skip {
# Consume until the line that closes the array.
if ($0 ~ /\)[[:space:]]*$/) skip = 0
next
}
$0 ~ "^" name "=\\(" {
print block
# A one-line `name=("x")` closes on the same line; a block does not.
if ($0 !~ /\)[[:space:]]*$/) skip = 1
next
}
{ print }
' "$file" > "${file}.tmp"
mv "${file}.tmp" "$file"
}
# Delete a whole `name=( ... )` block (used to retire sha1sums/md5sums).
remove_array() {
local file="$1" name="$2"
grep -qE "^${name}=\(" "$file" || return 0
awk -v name="$name" '
BEGIN { skip = 0 }
skip { if ($0 ~ /\)[[:space:]]*$/) skip = 0; next }
$0 ~ "^" name "=\\(" { if ($0 !~ /\)[[:space:]]*$/) skip = 1; next }
{ print }
' "$file" > "${file}.tmp"
mv "${file}.tmp" "$file"
}
# Split a source entry into its rename target and its URL/path.
# "foo.AppImage::https://x/y.AppImage" -> name=foo.AppImage loc=https://x/y.AppImage
# "https://x/y.tar.gz" -> name=y.tar.gz loc=https://x/y.tar.gz
# "reflex.desktop" -> name=reflex.desktop loc=reflex.desktop
source_name() {
local e="$1"
if [[ "$e" == *"::"* ]]; then printf '%s' "${e%%::*}"; else printf '%s' "${e##*/}"; fi
}
source_loc() { local e="$1"; printf '%s' "${e##*::}"; }
is_remote() { [[ "$(source_loc "$1")" =~ ^(https?|ftp):// ]]; }
# VCS sources (git+https://..., svn+..., etc) are not fixed artefacts and
# makepkg expects a literal SKIP for them rather than a digest.
is_vcs() { [[ "$(source_loc "$1")" =~ ^(git|svn|hg|bzr)(\+|:) ]]; }
# Download a URL once per run and hand back the cached path.
#
# Downloads go to a content-addressed cache path, never to the source entry's
# `::` rename target. That matters for multi-arch: freetube's x86_64 and
# aarch64 entries both rename to freetube.AppImage, so writing to the rename
# target makes the second arch find the first arch's file and silently hash
# the wrong binary. This is the same collision that makes `makepkg -g`
# unusable for multi-arch PKGBUILDs.
download_cached() {
local url="$1" key path
[ -n "${DL_CACHE:-}" ] || die "download_cached: DL_CACHE is not set"
mkdir -p "$DL_CACHE"
key="$(printf '%s' "$url" | sha256sum | cut -c1-32)"
path="${DL_CACHE}/${key}"
if [ ! -s "$path" ]; then
info "fetching ${url}" >&2
curl --silent --show-error --location --fail --retry 3 --retry-delay 2 \
--max-time 900 -o "${path}.part" "$url" \
|| die "download failed: ${url}"
mv "${path}.part" "$path"
fi
printf '%s' "$path"
}
# Regenerate .SRCINFO and refuse to accept a broken one.
#
# This is the guard that would have prevented every observed breakage: an empty
# or inconsistent .SRCINFO is exactly what the AUR rejects, and the old script
# committed and pushed it anyway because it had no `set -e`.
regen_srcinfo() {
local dir="${1:-.}"
local out rc=0
out="$(cd "$dir" && makepkg --printsrcinfo 2>&1)" || rc=$?
if [ "$rc" -ne 0 ] || [ -z "${out//[[:space:]]/}" ]; then
printf '%s\n' "$out" >&2
die "makepkg --printsrcinfo produced no usable output (exit ${rc}).
Is this running under a real Arch makepkg? The CI job must use the
archlinux container; Ubuntu's build environment silently yields an
empty .SRCINFO, which the AUR then rejects."
fi
printf '%s\n' "$out" > "${dir}/.SRCINFO"
local pb_ver sr_ver
pb_ver="$(pkgbuild_get pkgver "${dir}/PKGBUILD")"
sr_ver="$(sed -n -E 's/^[[:space:]]*pkgver = (.*)$/\1/p' "${dir}/.SRCINFO" | head -n1)"
[ -n "$sr_ver" ] || die ".SRCINFO has no pkgver"
[ "$pb_ver" = "$sr_ver" ] \
|| die ".SRCINFO pkgver '${sr_ver}' disagrees with PKGBUILD pkgver '${pb_ver}'"
}