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