Files
kernel-agent/bin/ka-build
T
test0r dd631fd3c7 ka-build: arch makepkg wrapper + sign + publish (closes #34)
Phase-1 ka-build per umbrella #21:

1. Read manifest.lock from ka-promote output. Refuse if missing.
2. Verify each PKGBUILD-side patch in marfrit-packages still matches
   the kernel-agent-side patch by sha256 (manifest.lock is authoritative).
3. ssh-dispatch makepkg --syncdeps --noconfirm --cleanbuild to the
   manifest's build_host.primary. Native build only — no distcc
   (feedback_kernel_agent_no_distcc).
4. Pull the resulting *.pkg.tar.zst back; scp to hertz and run
   /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>.
5. Append a `build:` block to manifest.lock with built_at, host,
   per-package b2sum + size.

Flags: --dry-run (stop before makepkg), --skip-publish (build only),
--packages-repo (override default ~/src/marfrit-packages).

Out of scope (separate followups):
- Debian .deb path
- PKGBUILD template *generation* (current PKGBUILDs are hand-authored;
  ka-build verifies + stamps, doesn't author)
- distcc routing (explicitly NOT in kernel-agent flow)
- ka-build --validate-against (apply-check harness)

Tests: 6/6 pass (arg parsing, missing manifest.lock, missing PKGBUILD,
patch drift via sha256 mismatch, happy-path dry-run on fresnel).
Full-build path manually exercisable; CI integration deferred until
the sandbox supports mock build-host + mock marfrit-publish-arch.
2026-05-19 09:24:23 +02:00

200 lines
6.8 KiB
Bash
Executable File

#!/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 <host>
# ka-build <host> --packages-repo <path> # default: ~/src/marfrit-packages
# ka-build <host> --dry-run # stop after staging, don't makepkg
# ka-build <host> --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 - <<PY
import yaml, os, hashlib
from datetime import datetime, timezone
lock_path = "$lock"
out_dir = "$local_out"
build_host = "$BUILD_HOST"
skipped = $SKIP_PUBLISH
lk = yaml.safe_load(open(lock_path))
epoch = os.environ.get("SOURCE_DATE_EPOCH")
if epoch:
built_at = datetime.fromtimestamp(int(epoch), tz=timezone.utc).isoformat()
else:
built_at = datetime.now(tz=timezone.utc).isoformat()
pkgs = []
for fn in sorted(os.listdir(out_dir)):
if not fn.endswith(".pkg.tar.zst"): continue
fp = os.path.join(out_dir, fn)
b2 = hashlib.blake2b(open(fp, "rb").read()).hexdigest()
pkgs.append({"name": fn, "size": os.path.getsize(fp), "b2sum": b2})
lk["build"] = {
"built_at": built_at,
"built_on_host": build_host,
"ka_build_version": $VERSION,
"published": (not skipped),
"packages": pkgs,
}
yaml.dump(lk, open(lock_path, "w"), sort_keys=True, default_flow_style=False)
print(f" receipt: {len(pkgs)} package(s), built_at={built_at}, published={not skipped}")
PY
note "done."