Files
nucleic/scripts/publish-linux-vm-kernel.sh
T
2026-07-08 19:51:03 -07:00

92 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Produce (and optionally publish) a **pre-unwrapped** raw arm64 Linux kernel `Image` for the Nucleic
# macOS-guest **Linux VM base build** (docs/LINUX_VM.md, docs/LINUX_VM_SEMANTIC_AGENT.md).
#
# scripts/publish-linux-vm-kernel.sh [CODENAME] [VERSION]
# # e.g. scripts/publish-linux-vm-kernel.sh resolute 26.04
#
# Why this exists: modern Ubuntu arm64 `vmlinuz` ships as an **EFI-zboot** self-decompressing PE with a
# **zstd** payload. `VZLinuxBootLoader` can't boot that — it needs a raw arm64 `Image`. The app's
# artifact pipeline (`MacVMEngine.prepareBootableKernel`) unwraps zboot host-side, but that needs a
# `zstd` binary on the Mac (macOS has no system zstd). Publishing a pre-unwrapped Image and pointing
# `MacVMSettings.linuxKernelURL` at it removes that host dependency — the app then just downloads a raw
# Image. This script does the unwrap in Linux CI (where zstd is a package) exactly like the app would.
#
# Output: $OUT (default ./vmlinux-arm64) — an uncompressed arm64 Image, magic "ARM\x64" at byte 0x38.
#
# Publishing: pass PUBLISH=oras to push it to GHCR as an OCI artifact (mirrors
# .github/workflows/kernel-image.yml). NOTE: the MacVM download path is a plain HTTPS GET, so to consume
# it via `linuxKernelURL` you need a plain-HTTPS location (a GitHub release asset or the R2 updates
# bucket), not a GHCR package. The GHCR push here is for parity/backup; wire the plain URL you host it at.
#
# Needs: curl, zstd (or gzip), python3 — all present on an ubuntu-latest runner.
set -euo pipefail
CODENAME="${1:-resolute}"
VERSION="${2:-26.04}"
OUT="${OUT:-vmlinux-arm64}"
BASE="https://cloud-images.ubuntu.com/releases/${CODENAME}/release/unpacked"
VMLINUZ="ubuntu-${VERSION}-server-cloudimg-arm64-vmlinuz-generic"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "▸ Downloading ${VMLINUZ} …"
curl -SsL -o "$TMP/vmlinuz" "${BASE}/${VMLINUZ}"
echo "▸ Unwrapping (EFI-zboot / gzip / raw) → ${OUT} …"
python3 - "$TMP/vmlinuz" "$TMP/payload" "$TMP/comp" <<'PY'
import sys, struct
src, payload_out, comp_out = sys.argv[1], sys.argv[2], sys.argv[3]
d = open(src, 'rb').read()
# Raw arm64 Image? (magic "ARM\x64" at 0x38)
if len(d) > 0x3C and d[0x38:0x3C] == b'ARMd':
open(payload_out, 'wb').write(d); open(comp_out, 'w').write('raw'); sys.exit(0)
# Plain gzip vmlinuz (older releases).
if d[:2] == b'\x1f\x8b':
open(payload_out, 'wb').write(d); open(comp_out, 'w').write('gzip'); sys.exit(0)
# EFI-zboot: locate the "zimg" header, read payload size + compression, slice by compressor magic.
z = d.find(b'zimg')
if z < 0:
sys.stderr.write('not a recognized kernel (no raw Image, gzip, or zboot header)\n'); sys.exit(2)
size = struct.unpack_from('<I', d, z + 8)[0]
comp = d[z+20:z+52].split(b'\x00')[0].decode('ascii', 'replace')
magics = {'gzip': b'\x1f\x8b\x08', 'zstd': b'\x28\xb5\x2f\xfd', 'lz4': b'\x04\x22\x4d\x18'}
if comp not in magics:
sys.stderr.write('unsupported zboot compression: %r\n' % comp); sys.exit(3)
start = d.find(magics[comp], z)
end = start + size if size else len(d)
open(payload_out, 'wb').write(d[start:end])
open(comp_out, 'w').write(comp)
PY
COMP="$(cat "$TMP/comp")"
case "$COMP" in
raw) cp "$TMP/payload" "$OUT" ;;
gzip) gzip -dc "$TMP/payload" > "$OUT" || true ;; # trailing garbage after the stream is harmless
zstd) zstd -dc "$TMP/payload" > "$OUT" || true ;;
*) echo "unexpected compression '$COMP'" >&2; exit 3 ;;
esac
# Sanity: the result must be a raw arm64 Image (magic "ARM\x64" at 0x38).
python3 - "$OUT" <<'PY'
import sys
d = open(sys.argv[1], 'rb').read(64)
if d[0x38:0x3C] != b'ARMd':
sys.stderr.write('FAIL: output is not a raw arm64 Image (bad magic at 0x38)\n'); sys.exit(1)
print("✓ raw arm64 Image (%d bytes)" % len(open(sys.argv[1],'rb').read()))
PY
echo "▸ Wrote ${OUT} (compression was: ${COMP})"
if [ "${PUBLISH:-}" = "oras" ]; then
: "${IMAGE:?set IMAGE=ghcr.io/<owner>/nucleic-linux-vm-kernel}"
: "${TAG:?set TAG=<version>-<abi> e.g. 26.04-generic}"
echo "▸ Pushing ${IMAGE}:${TAG} via oras …"
oras push "${IMAGE}:${TAG}" \
--artifact-type application/vnd.nucleic.kernel \
"${OUT}:application/octet-stream"
fi