#!/usr/bin/env bash # ka-build — render PKGBUILD from manifest.lock, build native on host, # sign+publish via marfrit-publish-arch on hertz. # # Phase-1 (issue #34): arch makepkg wrapper. Debian path deferred. # # Usage: # ka-build # ka-build --packages-repo # default: ~/src/marfrit-packages # ka-build --dry-run # stop after staging, don't makepkg # ka-build --skip-publish # build only, don't push to hertz # # Exit codes: # 0 success (pkg built + published) # 2 missing input (manifest.lock, PKGBUILD, ssh target) # 3 patch drift (resolved.sha256 != PKGBUILD-side file sha256) # 4 makepkg / sign / publish failure # 5 manifest parse error set -euo pipefail VERSION=1 die() { echo "ka-build: error: $1" >&2; exit "${2:-1}"; } note() { echo "ka-build: $1"; } # Defaults PACKAGES_REPO="${KA_PACKAGES_REPO:-${HOME}/src/marfrit-packages}" DRY_RUN=0 SKIP_PUBLISH=0 HOST="" while [ $# -gt 0 ]; do case "$1" in --packages-repo) PACKAGES_REPO="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; --skip-publish) SKIP_PUBLISH=1; shift ;; --version) echo "ka-build version $VERSION"; exit 0 ;; -h|--help) sed -n '1,30p' "$0" | grep -E '^# ' | sed 's/^# //'; exit 0 ;; -*) die "unknown flag: $1" ;; *) [ -z "$HOST" ] && HOST="$1" || die "extra arg: $1"; shift ;; esac done [ -n "$HOST" ] || die "host is required" 2 [ -d "$PACKAGES_REPO" ] || die "--packages-repo not found: $PACKAGES_REPO" 2 # Locate kernel-agent repo root (where bin/ + fleet/ live) script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$script_dir/.." && pwd)" [ -d "$REPO_ROOT/fleet" ] || die "fleet/ not found relative to $script_dir" 2 manifest="$REPO_ROOT/fleet/${HOST}.yaml" [ -f "$manifest" ] || die "no manifest for host '$HOST': $manifest" 2 # Read fields from manifest via python (yaml in bash is masochism) py_read() { python3 -c " import sys, yaml, os m = yaml.safe_load(open('$manifest')) keys = '$1'.split('.') v = m for k in keys: if not isinstance(v, dict) or k not in v: sys.exit('missing key: $1') v = v[k] print(v) " } PKG_NAME="$(py_read package.name)" BASELINE_REF="$(py_read baseline.ref)" BUILD_HOST="$(py_read build_host.primary)" # Locate the most recent ka-promote output build_dir_root="${KA_BUILD_DIR:-$REPO_ROOT/build}" promote_out="${build_dir_root}/${HOST}/${BASELINE_REF}" lock="${promote_out}/manifest.lock" cumulative="${promote_out}/cumulative.patch" [ -f "$lock" ] || die "no manifest.lock at $lock — run 'ka-promote $HOST' first" 2 [ -f "$cumulative" ] || die "no cumulative.patch at $cumulative — run 'ka-promote $HOST' first" 2 # Locate the PKGBUILD pkg_dir="${PACKAGES_REPO}/arch/${PKG_NAME}" pkgbuild="${pkg_dir}/PKGBUILD" [ -f "$pkgbuild" ] || die "no PKGBUILD at $pkgbuild (expected from manifest package.name)" 2 note "host=$HOST pkg=$PKG_NAME baseline=$BASELINE_REF build_host=$BUILD_HOST" note "PKGBUILD: $pkgbuild" note "manifest.lock: $lock" # Refuse if PKGBUILD-side patches drifted from kernel-agent patches/. # manifest.lock.resolved_patches[].sha256 must match PKGBUILD-dir-side # files of the same basename. (If a patch is in resolved but missing from # PKGBUILD dir, fail loud — operator needs to sync.) note "verifying patch consistency between kernel-agent and marfrit-packages..." drift=0 while IFS=$'\t' read -r basename expected_sha; do pkg_side="${pkg_dir}/${basename}" if [ ! -f "$pkg_side" ]; then echo " MISSING in PKGBUILD dir: $basename" >&2 drift=1; continue fi actual_sha=$(sha256sum "$pkg_side" | cut -d' ' -f1) if [ "$actual_sha" != "$expected_sha" ]; then echo " DRIFT: $basename (expected $expected_sha, got $actual_sha)" >&2 drift=1 fi done < <(python3 -c " import yaml, sys, os lk = yaml.safe_load(open('$lock')) for r in lk['resolved_patches']: bn = os.path.basename(r['include']) print(f\"{bn}\t{r['sha256']}\") ") [ "$drift" -eq 0 ] || die "patches differ between kernel-agent and marfrit-packages — sync first" 3 note "patches OK ($(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))") files)" if [ "$DRY_RUN" -eq 1 ]; then note "--dry-run: stopping before makepkg" exit 0 fi # Stage build dir on the build host via ssh note "staging build on ${BUILD_HOST}..." remote_stage="/tmp/ka-build-${HOST}-$$" ssh "${BUILD_HOST}" "mkdir -p '$remote_stage'" rsync -a "${pkg_dir}/" "${BUILD_HOST}:${remote_stage}/" # Run makepkg natively note "running makepkg --syncdeps --noconfirm --cleanbuild on ${BUILD_HOST}..." ssh "${BUILD_HOST}" "cd '$remote_stage' && makepkg --syncdeps --noconfirm --cleanbuild --skipchecksums" \ || die "makepkg failed on ${BUILD_HOST}" 4 # Fetch built packages note "fetching .pkg.tar.zst from ${BUILD_HOST}..." local_out="${promote_out}/pkgs" mkdir -p "$local_out" rsync -av "${BUILD_HOST}:${remote_stage}/*.pkg.tar.zst" "$local_out/" 2>&1 | tail -5 # Compute b2sums pkg_b2sum_list=$(cd "$local_out" && for p in *.pkg.tar.zst; do [ -f "$p" ] || continue printf '%s %s\n' "$(b2sum "$p" | cut -d' ' -f1)" "$p" done) note "built packages:" echo "$pkg_b2sum_list" | sed 's/^/ /' # Publish via hertz marfrit-publish-arch (unless --skip-publish) if [ "$SKIP_PUBLISH" -eq 0 ]; then note "publishing to packages.reauktion.de/arch/aarch64/..." for p in "$local_out"/*.pkg.tar.zst; do [ -f "$p" ] || continue base="$(basename "$p")" scp -q "$p" "hertz:/tmp/${base}" || die "scp to hertz failed: $base" 4 ssh hertz "sudo /opt/herding/bin/marfrit-publish-arch aarch64 '/tmp/${base}'" \ || die "marfrit-publish-arch failed: $base" 4 ssh hertz "rm -f '/tmp/${base}'" note "published: $base" done fi # Update manifest.lock with build receipt (append; don't rewrite the # existing fields) note "writing build receipt to manifest.lock..." python3 - <