#!/usr/bin/env bash # Nightly AUR package reconciler. # # usage: autoupdate.sh [...] # # env: # GIT_HOST git host holding the package repos (default git.bjphoster.com) # WORKDIR where to clone; a temp dir by default # NO_GIT_PUSH=1 do everything except push (dry run) # FORCE_REBUILD=1 bump pkgrel even with no detected change # FORCE_REFRESH=1 re-download remote sources even without a version change # # This is a RECONCILER, not a version-bump script. A new upstream release is # only one of the things that can make a package diverge from the AUR; a manual # push to origin is another, and a failed push last night is a third. All three # funnel through the same path, and the AUR reconcile at the end runs # unconditionally. set -euo pipefail _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.sh source "${_here}/common.sh" require_noninteractive require_tools GIT_HOST="${GIT_HOST:-git.bjphoster.com}" # Where the package repos live, and how the upstream version is resolved. # Both are overridable so the test suite can run hermetically against local # bare repos; the defaults are the production values. PKG_REPO_BASE="${PKG_REPO_BASE:-https://${GIT_HOST}/aur}" LATEST_VERSION_CMD="${LATEST_VERSION_CMD:-${_here}/latest-version.sh}" [ "$#" -ge 1 ] || die "usage: autoupdate.sh [...]" WORKDIR="${WORKDIR:-$(mktemp -d)}" mkdir -p "$WORKDIR" update_one() { local pkg="$1" local dir="${WORKDIR}/${pkg}" log "=== ${pkg} ===" if [ -d "$dir/.git" ]; then git -C "$dir" fetch --quiet origin git -C "$dir" reset --quiet --hard origin/HEAD else rm -rf "$dir" git clone --quiet "${PKG_REPO_BASE}/${pkg}" "$dir" \ || die "could not clone ${pkg} from ${PKG_REPO_BASE}" fi cd "$dir" [ -f PKGBUILD ] || die "${pkg} has no PKGBUILD" [ -f .autoupdate ] || die "${pkg} has no .autoupdate manifest" # shellcheck disable=SC1091 source ./.autoupdate # Per-run download cache, shared between the AppImage extraction below and # update-checksums.sh so a 100 MB AppImage is fetched at most once. export DL_CACHE="${WORKDIR}/.cache" mkdir -p "$DL_CACHE" # --- AUR reference state ------------------------------------------------- # Fetched up front: it is the reference for the pkgrel decision, not just a # push target. Comparing against the AUR rather than against local history is # what makes the pkgrel bump idempotent -- once pushed, the AUR matches and # the next run finds nothing to do. # Read-only HTTPS: establishing the reference state needs no SSH key, so # dry runs work anywhere. Only sync-aur.sh's push is authenticated. local aur_pkgbuild="" aur_pkgrel="" aur_pkgver="" aur_name aur_name="$(pkgbuild_expand PKGBUILD 'printf %s "$pkgname"')" git remote remove aur-ro >/dev/null 2>&1 || true git remote add aur-ro "${AUR_HTTPS_BASE}/${aur_name}.git" if git fetch --quiet aur-ro master 2>/dev/null; then aur_pkgbuild="${WORKDIR}/${pkg}.aur.PKGBUILD" if git show FETCH_HEAD:PKGBUILD > "$aur_pkgbuild" 2>/dev/null; then aur_pkgrel="$(pkgbuild_get pkgrel "$aur_pkgbuild")" aur_pkgver="$(pkgbuild_get pkgver "$aur_pkgbuild")" info "AUR has ${aur_pkgver}-${aur_pkgrel}" else aur_pkgbuild="" fi else info "not on the AUR yet" fi # --- upstream version ---------------------------------------------------- # A failure in here exits non-zero and is NOT mistaken for "no new version". local oldver newver oldver="$(pkgbuild_get pkgver)" # Checked explicitly rather than leaning on `set -e`, so the failure is # attributed to the lookup instead of surfacing several steps later as an # unrelated .SRCINFO complaint. newver="$("$LATEST_VERSION_CMD" .)" || die "upstream version lookup failed for ${pkg}" validate_version "$newver" "upstream version for ${pkg}" info "upstream ${newver}, local ${oldver:-}" local version_changed=0 if [ "$newver" != "$oldver" ]; then version_changed=1 log "new upstream version ${oldver:-} -> ${newver}" pkgbuild_set pkgver "$newver" # pkgrel is not touched here; apply_pkgrel_rule derives it from the AUR. fi # --- AppImage assets ----------------------------------------------------- if [ "${gui:-false}" = "true" ] && [ "$version_changed" -eq 1 ]; then extract_appimage_assets fi # --- checksums + .SRCINFO ------------------------------------------------ # Local sources are always re-hashed (free, no network) so a hand-edited # .desktop or icon self-corrects. Remote sources are only re-fetched when the # version moved, or on demand. local cs_args=() if [ "$version_changed" -eq 1 ] || [ -n "${FORCE_REFRESH:-}" ]; then cs_args+=(--remote) fi "${_here}/update-checksums.sh" "${cs_args[@]}" . regen_srcinfo . # --- pkgrel --------------------------------------------------------------- apply_pkgrel_rule # --- commit --------------------------------------------------------------- git add -A if git diff --cached --quiet; then # Nothing changed. Deliberately do NOT exit here: the AUR may still be out # of sync from a previous failed push, and an unguarded `git commit` with # nothing staged would exit non-zero and, under `set -e`, abort before the # reconcile. log "no local changes for ${pkg}" else local ver rel ver="$(pkgbuild_get pkgver)"; rel="$(pkgbuild_get pkgrel)" git commit --quiet -m "Updated ${pkg} to ${ver}-${rel}" log "committed ${pkg} ${ver}-${rel}" fi # --- publish -------------------------------------------------------------- # AUR first: if it rejects the push, origin is left unpoisoned and the whole # thing is recomputed cleanly on the next run. "${_here}/sync-aur.sh" . if [ -n "${NO_GIT_PUSH:-}" ]; then log "NO_GIT_PUSH set: not pushing ${pkg} to origin" else git push --quiet origin HEAD fi } # Derive pkgrel so that fixing a package() bug and pushing needs no manual bump. apply_pkgrel_rule() { local cur_ver cur_rel cur_ver="$(pkgbuild_get pkgver)" cur_rel="$(pkgbuild_get pkgrel)" # Nothing on the AUR to compare against (new package): push as-is. if [ -z "${aur_pkgbuild:-}" ]; then return 0 fi # pkgrel is derived purely from the AUR reference, because the AUR is what # `-N` is numbered against. If the version we are about to publish differs # from the published one, this is a new upstream version as far as the AUR is # concerned and the packaging revision restarts at 1 -- whether the bump # happened in this run or in an earlier one that failed to push. That second # case is real: reflex-appimage sat at 1.0.11-4 locally against 1.0.10-4 on # the AUR, because the old script only reset pkgrel under FORCE_REBUILD. if [ "$cur_ver" != "${aur_pkgver:-}" ]; then if [ "$cur_rel" != "1" ]; then pkgbuild_set pkgrel "1" log "new version relative to the AUR (${aur_pkgver} -> ${cur_ver}); pkgrel ${cur_rel} -> 1" regen_srcinfo . fi return 0 fi local base_rel="${aur_pkgrel:-0}" [[ "$base_rel" =~ ^[0-9]+$ ]] || base_rel=0 if [ -n "${FORCE_REBUILD:-}" ]; then pkgbuild_set pkgrel "$(( base_rel + 1 ))" log "FORCE_REBUILD: pkgrel -> $(( base_rel + 1 ))" regen_srcinfo . return 0 fi # Compare with the pkgrel line stripped, so the decision is about substantive # packaging content. An edit to a local source lands here too, because its # sha256sums entry was just regenerated. A README-only edit does not touch # the PKGBUILD and so correctly does not bump. # # Deliberately a shell string comparison rather than diff(1): diff lives in # diffutils, which archlinux:base does not ship, and a missing binary makes # the `if` fail, which reads as "changed" and ratchets pkgrel upward on every # single run. Same silent-wrong-default class of bug as the empty .SRCINFO. local local_body aur_body local_body="$(grep -v '^pkgrel=' PKGBUILD)" aur_body="$(grep -v '^pkgrel=' "$aur_pkgbuild")" if [ "$local_body" = "$aur_body" ]; then return 0 # identical packaging; nothing to do fi if [ "$cur_rel" = "$aur_pkgrel" ]; then pkgbuild_set pkgrel "$(( base_rel + 1 ))" log "packaging changed without a pkgrel bump; pkgrel -> $(( base_rel + 1 ))" regen_srcinfo . else info "pkgrel already moved by hand (${aur_pkgrel} -> ${cur_rel}); leaving it" fi } # Pull the .desktop and icon out of the AppImage so they can be shipped as # local sources. Unchanged in substance from the old push.sh. extract_appimage_assets() { local host_arr="source_$(uname -m)" entry loc img newdesktop newicon local candidates=() # A multi-arch AppImage is extracted once, against the host arch's image. if pkgbuild_has_array PKGBUILD "$host_arr"; then mapfile -t candidates < <(pkgbuild_array PKGBUILD "$host_arr") fi if [ "${#candidates[@]}" -eq 0 ]; then mapfile -t candidates < <(pkgbuild_array PKGBUILD source) fi img="" for entry in "${candidates[@]}"; do loc="$(source_loc "$entry")" if printf '%s' "${entry,,}" | grep -q "appimage" && is_remote "$entry"; then img="$(download_cached "$loc")" break fi done [ -n "$img" ] || { warn "gui=true but no AppImage source found; skipping asset extraction"; return 0; } local pkgbase pkgbase="$(pkgbuild_expand PKGBUILD 'printf %s "${_pkgname:-$pkgname}"')" newdesktop="${pkgbase}.desktop" newicon="${pkgbase}.png" local tmp; tmp="$(mktemp -d)" cp "$img" "${tmp}/app.AppImage" chmod +x "${tmp}/app.AppImage" ( cd "$tmp" && ./app.AppImage --appimage-extract >/dev/null 2>&1 ) \ || { warn "could not extract ${pkgbase} AppImage; keeping existing assets"; rm -rf "$tmp"; return 0; } if [ -f "${tmp}/squashfs-root/${desktop}" ]; then cp "${tmp}/squashfs-root/${desktop}" "$newdesktop" sed -i "/^X-AppImage-/d;/^$/d" "$newdesktop" sed -i "s|^Exec=.*|Exec=${pkgbase} %U|" "$newdesktop" info "extracted ${newdesktop}" else warn "${desktop} not found inside the AppImage" fi if [ -f "${tmp}/squashfs-root/${icon}" ]; then cp "${tmp}/squashfs-root/${icon}" "$newicon" info "extracted ${newicon}" else warn "${icon} not found inside the AppImage" fi rm -rf "$(readlink -f "${tmp}/squashfs-root" 2>/dev/null || true)" "$tmp" } # One package per process, deliberately. # # The obvious `( update_one "$pkg" ) || { rc=1; ... }` is broken: putting a # subshell in an && / || list disables `set -e` for its ENTIRE body, not just # for the subshell as a unit. update_one then sails past a failed sync-aur.sh # straight into `git push origin`, which is precisely the "origin ahead of the # AUR" state this whole tool exists to avoid -- and it happened in production. # # inner() { echo start; false; echo "REACHED CODE AFTER FAILURE"; } # ( inner ) || echo caught # prints REACHED CODE AFTER FAILURE # ( inner ) # correctly aborts # # Re-exec'ing gives each package a fresh shell whose `set -e` is intact; the # parent's AND-OR context cannot reach across a process boundary. The CI matrix # already passes exactly one package, so the loop only affects local runs. if [ -n "${AUTOUPDATE_SINGLE:-}" ]; then update_one "$1" exit 0 fi rc=0 for pkg in "$@"; do AUTOUPDATE_SINGLE=1 "$0" "$pkg" || { rc=1; warn "${pkg} FAILED"; } done exit "$rc"