dd631fd3c7
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.
200 lines
6.8 KiB
Bash
Executable File
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."
|