feat: ARM64 generic UEFI disk image (GPT + GRUB A/B)
Some checks failed
CI / Go Tests (push) Successful in 2m38s
CI / Shellcheck (push) Failing after 37s
CI / Build Go Binaries (amd64, linux, linux-amd64) (push) Failing after 1m22s
CI / Build Go Binaries (arm64, linux, linux-arm64) (push) Failing after 1m11s

Produces a UEFI-bootable raw disk image for generic ARM64 hosts (QEMU virt,
Ampere/Graviton cloud, ARM64 SBCs with UEFI). Reuses the existing 4-partition
A/B layout from x86 (EFI 256 MB FAT32 + System A 512 MB ext4 + System B 512 MB
ext4 + Data ext4 remainder).

Changes:
- build/scripts/create-disk-image.sh: TARGET_ARCH env var (amd64 default,
  arm64). Selects kernel source path, grub-mkimage target (x86_64-efi vs
  arm64-efi), EFI binary name (bootx64.efi vs BOOTAA64.EFI), grub.cfg variant,
  and whether to also install BIOS GRUB (x86 only).
- build/grub/grub-arm64.cfg: ARM64 variant of grub.cfg. Identical A/B logic;
  console=ttyAMA0+ttyS0 to cover QEMU virt PL011, Ampere PL011, and Graviton
  16550-compat.
- build/Dockerfile.builder: add grub-efi-amd64-bin, grub-efi-arm64-bin,
  grub-pc-bin, grub-common, grub2-common so the builder container can produce
  EFI images for both architectures.
- hack/dev-vm-arm64.sh: split into kernel mode (direct -kernel/-initrd, fast
  iteration) and --disk mode (UEFI firmware + GRUB + disk image, full
  integration test). Probes common UEFI firmware paths on Ubuntu/Fedora/macOS.
  Default kernel path now points at kernel-arm64-generic/Image with fallback
  to the renamed custom-kernel-rpi/Image.
- test/qemu/test-boot-arm64-disk.sh: new CI test for the full UEFI -> GRUB ->
  kernel -> stage-90 boot chain. Uses a scratch copy of the disk so grubenv
  writes don't mutate the source artifact.
- Makefile: new disk-image-arm64 target (depends on rootfs-arm64 + kernel-arm64),
  new test-boot-arm64-disk target, .PHONY + help updates.

Phase 3 scaffold is in place. First real end-to-end ARM64 build runs in the
next step on the Odroid runner — that's where we find out what's actually
broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 10:36:08 -06:00
parent d51618badb
commit 80aca5e372
6 changed files with 440 additions and 69 deletions

View File

@@ -1,64 +1,163 @@
#!/bin/bash
# dev-vm-arm64.sh — Launch ARM64 QEMU VM for development
#
# Uses qemu-system-aarch64 with -machine virt to emulate an ARM64 system.
# This is useful for testing ARM64/RPi builds on x86_64 hosts.
# Two modes:
#
# Default (direct kernel boot — fast iteration):
# qemu loads the kernel Image + initramfs directly via -kernel/-initrd.
# Skips bootloader, UEFI firmware, and disk image entirely.
# Use this for kernel and init-script changes.
#
# --disk (full UEFI boot — integration testing):
# qemu boots the .arm64.img disk image via UEFI firmware -> GRUB -> kernel.
# Exercises the full boot chain. Use this when changing the disk image
# layout, GRUB config, or anything that touches the EFI partition.
#
# Usage:
# ./hack/dev-vm-arm64.sh # Use default kernel + initramfs
# ./hack/dev-vm-arm64.sh <kernel> <initramfs> # Specify custom paths
# ./hack/dev-vm-arm64.sh --debug # Enable debug logging
# ./hack/dev-vm-arm64.sh --shell # Drop to emergency shell
# ./hack/dev-vm-arm64.sh # direct kernel boot (default)
# ./hack/dev-vm-arm64.sh --disk # full UEFI boot from built image
# ./hack/dev-vm-arm64.sh --debug # enable kubesolo.debug
# ./hack/dev-vm-arm64.sh --shell # drop to emergency shell
# ./hack/dev-vm-arm64.sh --disk /path/to.img # boot a specific disk image
# ./hack/dev-vm-arm64.sh <kernel> <initramfs> # direct boot with custom files
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION="$(cat "$PROJECT_ROOT/VERSION")"
MODE="kernel" # kernel | disk
VMLINUZ=""
INITRD=""
DISK_IMAGE=""
EXTRA_APPEND=""
# Parse arguments
for arg in "$@"; do
case "$arg" in
--shell) EXTRA_APPEND="$EXTRA_APPEND kubesolo.shell" ;;
--debug) EXTRA_APPEND="$EXTRA_APPEND kubesolo.debug" ;;
*)
if [ -z "$VMLINUZ" ]; then
VMLINUZ="$arg"
elif [ -z "$INITRD" ]; then
INITRD="$arg"
while [ $# -gt 0 ]; do
case "$1" in
--shell) EXTRA_APPEND="$EXTRA_APPEND kubesolo.shell"; shift ;;
--debug) EXTRA_APPEND="$EXTRA_APPEND kubesolo.debug"; shift ;;
--disk)
MODE="disk"
shift
# Optional next-arg as disk image path
if [ $# -gt 0 ] && [ -f "$1" ]; then
DISK_IMAGE="$1"
shift
fi
;;
*)
if [ "$MODE" = "kernel" ] && [ -z "$VMLINUZ" ]; then
VMLINUZ="$1"
elif [ "$MODE" = "kernel" ] && [ -z "$INITRD" ]; then
INITRD="$1"
fi
shift
;;
esac
done
# Defaults
VMLINUZ="${VMLINUZ:-$PROJECT_ROOT/build/cache/custom-kernel-arm64/Image}"
# ---------------------------------------------------------------------------
# UEFI firmware probe (used for --disk mode)
# ---------------------------------------------------------------------------
find_uefi_firmware() {
local candidates=(
/usr/share/qemu-efi-aarch64/QEMU_EFI.fd
/usr/share/AAVMF/AAVMF_CODE.fd
/usr/share/edk2/aarch64/QEMU_EFI.fd
/usr/share/qemu/edk2-aarch64-code.fd
/opt/homebrew/share/qemu/edk2-aarch64-code.fd
/usr/local/share/qemu/edk2-aarch64-code.fd
)
for f in "${candidates[@]}"; do
[ -f "$f" ] && echo "$f" && return 0
done
return 1
}
# ---------------------------------------------------------------------------
# mkfs.ext4 probe (kernel mode creates a scratch data disk)
# ---------------------------------------------------------------------------
find_mkfs_ext4() {
if command -v mkfs.ext4 >/dev/null 2>&1; then
echo "mkfs.ext4"
elif [ -x "/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4" ]; then
echo "/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4"
elif [ -x "/usr/local/opt/e2fsprogs/sbin/mkfs.ext4" ]; then
echo "/usr/local/opt/e2fsprogs/sbin/mkfs.ext4"
fi
}
# ===========================================================================
# Disk mode: boot the built .arm64.img through UEFI firmware + GRUB
# ===========================================================================
if [ "$MODE" = "disk" ]; then
DISK_IMAGE="${DISK_IMAGE:-$PROJECT_ROOT/output/kubesolo-os-${VERSION}.arm64.img}"
if [ ! -f "$DISK_IMAGE" ]; then
echo "ERROR: Disk image not found: $DISK_IMAGE"
echo " Run 'make disk-image-arm64' to build it."
exit 1
fi
UEFI_FW="$(find_uefi_firmware || true)"
if [ -z "$UEFI_FW" ]; then
echo "ERROR: No ARM64 UEFI firmware found."
echo " Install one of:"
echo " apt install qemu-efi-aarch64 # Debian/Ubuntu"
echo " dnf install edk2-aarch64 # Fedora/RHEL"
echo " brew install qemu # macOS (bundled)"
exit 1
fi
# Pad UEFI firmware variable store to 64 MiB if QEMU expects pflash sizing.
# Most ARM64 EFI .fd files are 64 MB; if yours is smaller, QEMU may refuse.
echo "==> Launching ARM64 QEMU (UEFI disk boot)..."
echo " Firmware: $UEFI_FW"
echo " Disk: $DISK_IMAGE"
echo ""
echo " K8s API: localhost:6443"
echo " SSH: localhost:2222"
echo " Press Ctrl+A X to exit QEMU"
echo ""
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a72 \
-m 2048 \
-smp 2 \
-nographic \
-bios "$UEFI_FW" \
-drive "file=$DISK_IMAGE,format=raw,if=virtio,media=disk" \
-net "nic,model=virtio" \
-net "user,hostfwd=tcp::6443-:6443,hostfwd=tcp::2222-:22"
exit 0
fi
# ===========================================================================
# Kernel mode (default): direct -kernel / -initrd, fast iteration
# ===========================================================================
VMLINUZ="${VMLINUZ:-$PROJECT_ROOT/build/cache/kernel-arm64-generic/Image}"
INITRD="${INITRD:-$PROJECT_ROOT/build/rootfs-work/kubesolo-os.gz}"
# Verify files exist
# Fallback: previous-generation RPi kernel cache, in case someone hasn't yet
# rebuilt under v0.3 paths.
if [ ! -f "$VMLINUZ" ] && [ -f "$PROJECT_ROOT/build/cache/custom-kernel-rpi/Image" ]; then
VMLINUZ="$PROJECT_ROOT/build/cache/custom-kernel-rpi/Image"
echo "==> Note: falling back to RPi kernel ($VMLINUZ)"
fi
if [ ! -f "$VMLINUZ" ]; then
echo "ERROR: Kernel not found: $VMLINUZ"
echo " Run 'make kernel-arm64' to build the ARM64 kernel."
echo " Run 'make kernel-arm64' (generic) or 'make kernel-rpi' to build a kernel."
exit 1
fi
if [ ! -f "$INITRD" ]; then
echo "ERROR: Initrd not found: $INITRD"
echo " Run 'make initramfs' to build the initramfs."
echo " Run 'make rootfs-arm64' to build the initramfs."
exit 1
fi
# Find mkfs.ext4
MKFS_EXT4=""
if command -v mkfs.ext4 >/dev/null 2>&1; then
MKFS_EXT4="mkfs.ext4"
elif [ -x "/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4" ]; then
MKFS_EXT4="/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4"
elif [ -x "/usr/local/opt/e2fsprogs/sbin/mkfs.ext4" ]; then
MKFS_EXT4="/usr/local/opt/e2fsprogs/sbin/mkfs.ext4"
fi
MKFS_EXT4="$(find_mkfs_ext4)"
if [ -z "$MKFS_EXT4" ]; then
echo "ERROR: mkfs.ext4 not found. Install e2fsprogs:"
if [ "$(uname)" = "Darwin" ]; then
@@ -70,13 +169,12 @@ if [ -z "$MKFS_EXT4" ]; then
exit 1
fi
# Create data disk
DATA_DISK="$(mktemp /tmp/kubesolo-arm64-data-XXXXXX).img"
dd if=/dev/zero of="$DATA_DISK" bs=1M count=1024 2>/dev/null
"$MKFS_EXT4" -q -L KSOLODATA "$DATA_DISK" 2>/dev/null
trap 'rm -f "$DATA_DISK"' EXIT
echo "==> Launching ARM64 QEMU VM..."
echo "==> Launching ARM64 QEMU (direct kernel boot)..."
echo " Kernel: $VMLINUZ"
echo " Initrd: $INITRD"
echo " Data: $DATA_DISK"