diff options
Diffstat (limited to 'build-aur.sh')
| -rw-r--r-- | build-aur.sh | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/build-aur.sh b/build-aur.sh new file mode 100644 index 0000000..9b83a33 --- /dev/null +++ b/build-aur.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# build-aur.sh - AUR local-repository helpers for build.sh +# +# Sourced by build.sh (not executed directly). build.sh runs as root with +# `set -euo pipefail` and provides SCRIPT_DIR, SUDO_USER, BUILD_LOG, and the +# info/warn/error helpers. The functions here build a fixed set of +# genuine-AUR packages into a local pacman repo and emit an auditable +# manifest. See docs/aur-local-repo-spec.org. +# +# The pure helpers (package lists, stanza/manifest rendering, staged +# replace, preflight) carry no side effects and are unit-tested in +# tests/unit/test_build_aur.bats. The build_aur_packages orchestrator and +# aur_manifest_append need root + network + makepkg, so they are covered by +# the build integration test and the manual verification checklist instead. + +############################# +# Package sets (single source of truth) +############################# + +# The v1 genuine-AUR build set: packages with no exact official-repo match +# whose runtime + make deps all resolve from official / archzfs / the baked +# local repo (the v1 dependency gate). paru (second helper) and +# mkinitcpio-firmware (pulls AUR firmware deps) are deferred to vNext. +# Audited 2026-06-09. This list is the one place the set is named; build.sh +# reads it for the package-list append, and the manifest records what +# actually shipped. +aur_v1_packages() { + printf '%s\n' \ + downgrade \ + yay \ + informant \ + zrepl \ + pacman-cleanup-hook \ + sanoid \ + zfs-auto-snapshot \ + topgrade \ + ventoy-bin +} + +# Official `extra`-repo packages that the audit reclassified out of the AUR. +# These are NOT built — they go straight into packages.x86_64 and install +# from the normal repos. Listed here so build.sh has one source for them. +aur_official_packages() { + printf '%s\n' \ + arch-wiki-lite \ + rate-mirrors \ + arch-audit \ + btop \ + duf \ + dust \ + procs +} + +############################# +# Pacman-config rendering +############################# + +# Render the [aur] pacman repo stanza for the given Server value, with a +# leading blank line so it appends cleanly after an existing stanza (the +# same shape as build.sh's [archzfs] block). SigLevel = Optional TrustAll: +# the repo is trusted by construction (we built it on this host); GPG +# signing is vNext. The Server differs per namespace — a build-host +# absolute file:// path for profile/pacman.conf, file:///usr/share/aur-packages +# for the live-runtime config. +aur_repo_stanza() { + local server="$1" + printf '\n[aur]\nSigLevel = Optional TrustAll\nServer = %s\n' "$server" +} + +############################# +# Manifest rendering (TSV) +############################# + +# Emit the manifest column header. Nine tab-separated columns; keep in lockstep +# with aur_manifest_row. +aur_manifest_header() { + printf 'name\tpkgbase\tfilename\tpkgver\tpkgrel\tcommit\tsource_url\ttimestamp\tsha256\n' +} + +# Format one manifest row from nine positional fields, tab-separated. Pure +# formatter so the column layout is unit-testable without a real build; +# aur_manifest_append gathers the field values and calls this. +aur_manifest_row() { + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@" +} + +############################# +# Filesystem helpers +############################# + +# Print the basename of the single *.pkg.tar.zst in $1. Returns 1 (empty +# output) when the directory holds no package file — used both to locate a +# freshly-built package and as the build-failure signal when makepkg +# produced nothing. +aur_pkgfile_name() { + local dir="$1" f + f=$(find "$dir" -maxdepth 1 -name '*.pkg.tar.zst' -print -quit 2>/dev/null) + [[ -n "$f" ]] || return 1 + basename "$f" +} + +# Staged replacement: move $staging into place at $repo_dir. Caller stages +# on the same filesystem (a sibling dir), so this is a local rename. The +# rm/mv window is not strictly atomic, but the build only calls this after +# every package built, repo-add ran, and the manifest emitted — so a +# failure earlier ships no repo, and a failure here leaves no stale repo. +# mv -T treats $repo_dir as the destination name rather than a dir to move +# into, which matters once $repo_dir has been recreated by a concurrent run. +aur_repo_replace() { + local staging="$1" repo_dir="$2" + rm -rf "$repo_dir" + mv -T "$staging" "$repo_dir" +} + +############################# +# Preflight +############################# + +# Guard the build environment before any clone/makepkg. $1 is the invoking +# user (SUDO_USER); the rest, if given, are the commands to require +# (defaults to git/makepkg/repo-add — overridable so the unit tests stay +# host-independent). Fails with a named reason on empty/root user or a +# missing command. makepkg refuses to run as root, which is why a usable +# non-root SUDO_USER is mandatory. +aur_preflight() { + local sudo_user="$1" + shift + local required=("$@") + [[ ${#required[@]} -gt 0 ]] || required=(git makepkg repo-add) + + if [[ -z "$sudo_user" ]]; then + echo "AUR build preflight: SUDO_USER is not set — run build.sh via sudo" >&2 + return 1 + fi + if [[ "$sudo_user" == "root" ]]; then + echo "AUR build preflight: invoking user is root; makepkg refuses to run as root" >&2 + return 1 + fi + local cmd + for cmd in "${required[@]}"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "AUR build preflight: required command not found: $cmd" >&2 + return 1 + fi + done + return 0 +} + +############################# +# Build orchestrator (impure) +############################# + +# Append one manifest row for the package just built into $pkgdir and +# collected into $staging. Reads the version straight from the package +# metadata (pacman -Qp) so a dash in the package name can't confuse a +# filename parse, and records the AUR commit, source URL, build timestamp, +# and SHA256. +aur_manifest_append() { + local staging="$1" pkgdir="$2" pkg="$3" + local filename commit sha256 nameversion pkgname fullver pkgver pkgrel timestamp source_url + + filename=$(aur_pkgfile_name "$pkgdir") || return 1 + commit=$(git -C "$pkgdir" rev-parse HEAD 2>/dev/null || echo unknown) + sha256=$(sha256sum "$staging/$filename" | awk '{print $1}') + nameversion=$(pacman -Qp "$staging/$filename" 2>/dev/null) + pkgname=${nameversion%% *} + fullver=${nameversion##* } + pkgver=${fullver%-*} + pkgrel=${fullver##*-} + timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + source_url="https://aur.archlinux.org/${pkg}.git" + + aur_manifest_row "$pkgname" "$pkg" "$filename" "$pkgver" "$pkgrel" \ + "$commit" "$source_url" "$timestamp" "$sha256" >> "$staging/manifest.tsv" +} + +# Build the v1 AUR set into a local pacman repo at $repo_dir (default +# $SCRIPT_DIR/aur-packages). Clones each package from the AUR and runs +# makepkg as $SUDO_USER (makepkg can't run as root), collects the built +# package, appends a manifest row, then builds the repo db with repo-add +# and atomically-ish swaps the staging dir into place. On any failure +# error() names the package + phase + log path and no repo ships. Relies on +# build.sh's info/warn/error and globals. +build_aur_packages() { + local repo_dir="${1:-${SCRIPT_DIR:-.}/aur-packages}" + local staging="${repo_dir}.staging" + local sudo_user="${SUDO_USER:-}" + local -a packages + mapfile -t packages < <(aur_v1_packages) + + aur_preflight "$sudo_user" || error "AUR build preflight failed (see above)" + + if [[ ${#packages[@]} -eq 0 ]]; then + warn "No AUR packages in the v1 set — skipping local repo creation" + return 0 + fi + + local build_dir + build_dir="$(sudo -u "$sudo_user" mktemp -d /tmp/aur-build.XXXXXX)" \ + || error "AUR build: could not create build dir as $sudo_user" + # shellcheck disable=SC2064 # expand build_dir now so RETURN cleans this dir + trap "rm -rf '$build_dir'" RETURN + + rm -rf "$staging" + mkdir -p "$staging" + aur_manifest_header > "$staging/manifest.tsv" + + local pkg + for pkg in "${packages[@]}"; do + info "Building AUR package: $pkg" + sudo -u "$sudo_user" git clone --depth 1 \ + "https://aur.archlinux.org/${pkg}.git" "$build_dir/${pkg}" \ + || error "AUR clone failed: $pkg (see ${BUILD_LOG:-build log})" + sudo -u "$sudo_user" bash -c \ + "cd '$build_dir/${pkg}' && makepkg -s --noconfirm --needed" \ + || error "AUR build failed: $pkg (makepkg; see ${BUILD_LOG:-build log})" + cp "$build_dir/${pkg}"/*.pkg.tar.zst "$staging/" \ + || error "AUR collect failed: $pkg (no package file produced)" + aur_manifest_append "$staging" "$build_dir/${pkg}" "$pkg" \ + || error "AUR manifest failed: $pkg" + done + + repo-add "$staging/aur.db.tar.gz" "$staging"/*.pkg.tar.zst \ + || error "repo-add failed (see ${BUILD_LOG:-build log})" + + aur_repo_replace "$staging" "$repo_dir" + + local count + count=$(find "$repo_dir" -maxdepth 1 -name '*.pkg.tar.zst' | wc -l) + info "AUR local repo ready: $repo_dir ($count packages)" +} |
