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.
This commit is contained in:
Executable
+199
@@ -0,0 +1,199 @@
|
||||
#!/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."
|
||||
Reference in New Issue
Block a user