You've already forked .autoupdate
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.
214 lines
7.6 KiB
Bash
Executable File
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}'"
|
|
}
|