From 80aca5e37270ce83c15e472331f650f28f6bda6a Mon Sep 17 00:00:00 2001 From: Adolfo Delorenzo Date: Thu, 14 May 2026 10:36:08 -0600 Subject: [PATCH] feat: ARM64 generic UEFI disk image (GPT + GRUB A/B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Makefile | 19 +++- build/Dockerfile.builder | 5 + build/grub/grub-arm64.cfg | 86 +++++++++++++++ build/scripts/create-disk-image.sh | 106 +++++++++++++------ hack/dev-vm-arm64.sh | 164 +++++++++++++++++++++++------ test/qemu/test-boot-arm64-disk.sh | 129 +++++++++++++++++++++++ 6 files changed, 440 insertions(+), 69 deletions(-) create mode 100644 build/grub/grub-arm64.cfg create mode 100755 test/qemu/test-boot-arm64-disk.sh diff --git a/Makefile b/Makefile index 5b8dad3..acc8fcd 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ .PHONY: all fetch kernel build-cloudinit build-update-agent build-cross rootfs initramfs \ - iso disk-image oci-image rpi-image \ + iso disk-image disk-image-arm64 oci-image rpi-image \ kernel-arm64 kernel-rpi rootfs-arm64 rootfs-arm64-rpi \ test-boot test-k8s test-persistence test-deploy test-storage test-security test-all \ - test-boot-arm64 test-cloudinit test-update-agent \ + test-boot-arm64 test-boot-arm64-disk test-cloudinit test-update-agent \ bench-boot bench-resources \ dev-vm dev-vm-shell dev-vm-arm64 quick docker-build shellcheck \ kernel-audit clean distclean help @@ -88,6 +88,11 @@ rootfs-arm64: build-cross @echo "==> Packing generic ARM64 initramfs..." $(BUILD_DIR)/scripts/pack-initramfs.sh +disk-image-arm64: rootfs-arm64 kernel-arm64 + @echo "==> Creating generic ARM64 disk image (UEFI + GRUB A/B)..." + TARGET_ARCH=arm64 $(BUILD_DIR)/scripts/create-disk-image.sh + @echo "==> Built: $(OUTPUT_DIR)/$(OS_NAME)-$(VERSION).arm64.img" + # ============================================================================= # ARM64 Raspberry Pi targets (RPi-patched kernel, firmware blobs, SD card) # ============================================================================= @@ -144,9 +149,13 @@ test-security: iso test/integration/test-security-hardening.sh $(OUTPUT_DIR)/$(OS_NAME)-$(VERSION).iso test-boot-arm64: - @echo "==> Testing ARM64 boot in QEMU..." + @echo "==> Testing ARM64 boot in QEMU (direct kernel)..." test/qemu/test-boot-arm64.sh +test-boot-arm64-disk: disk-image-arm64 + @echo "==> Testing ARM64 UEFI disk boot in QEMU..." + test/qemu/test-boot-arm64-disk.sh $(OUTPUT_DIR)/$(OS_NAME)-$(VERSION).arm64.img + test-all: test-boot test-k8s test-persistence # Cloud-init Go tests @@ -266,6 +275,7 @@ help: @echo "Build targets (ARM64 generic — UEFI / cloud / SBCs):" @echo " make kernel-arm64 Build mainline ARM64 kernel from kernel.org LTS" @echo " make rootfs-arm64 Prepare generic ARM64 rootfs (mainline kernel modules)" + @echo " make disk-image-arm64 Create UEFI-bootable A/B GPT disk image (.arm64.img)" @echo "" @echo "Build targets (ARM64 Raspberry Pi):" @echo " make kernel-rpi Build RPi kernel from raspberrypi/linux" @@ -283,7 +293,8 @@ help: @echo " make test-update-agent Run update agent Go unit tests" @echo " make test-update A/B update cycle integration test" @echo " make test-rollback Forced rollback integration test" - @echo " make test-boot-arm64 ARM64 boot test in QEMU aarch64" + @echo " make test-boot-arm64 ARM64 boot test (direct kernel, fast)" + @echo " make test-boot-arm64-disk ARM64 full UEFI disk-boot test" @echo " make test-all Run core tests (boot + k8s + persistence)" @echo " make test-integ Run full integration suite" @echo " make bench-boot Benchmark boot performance (3 runs)" diff --git a/build/Dockerfile.builder b/build/Dockerfile.builder index f843ae5..6ccd2e9 100644 --- a/build/Dockerfile.builder +++ b/build/Dockerfile.builder @@ -18,6 +18,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ file \ flex \ genisoimage \ + grub-common \ + grub-efi-amd64-bin \ + grub-efi-arm64-bin \ + grub-pc-bin \ + grub2-common \ gzip \ isolinux \ iptables \ diff --git a/build/grub/grub-arm64.cfg b/build/grub/grub-arm64.cfg new file mode 100644 index 0000000..13c70f4 --- /dev/null +++ b/build/grub/grub-arm64.cfg @@ -0,0 +1,86 @@ +# KubeSolo OS — GRUB Configuration (ARM64) +# A/B partition boot with automatic rollback. +# +# Same A/B logic as build/grub/grub.cfg; only the console parameters differ +# (ARM64 PL011 / 16550-compat UART rather than x86 ttyS0). +# +# Partition layout: +# (hd0,gpt1) — EFI/Boot (256 MB, FAT32) — contains GRUB + grubenv +# (hd0,gpt2) — System A (512 MB, ext4) — vmlinuz + kubesolo-os.gz +# (hd0,gpt3) — System B (512 MB, ext4) — vmlinuz + kubesolo-os.gz +# (hd0,gpt4) — Data (remaining, ext4) — persistent K8s state + +set default=0 +set timeout=3 + +load_env + +# --- A/B Rollback Logic (identical to amd64 grub.cfg) --- + +if [ "${boot_success}" != "1" ]; then + if [ "${boot_counter}" = "0" ]; then + if [ "${active_slot}" = "A" ]; then + set active_slot=B + else + set active_slot=A + fi + save_env active_slot + set boot_counter=3 + save_env boot_counter + else + if [ "${boot_counter}" = "3" ]; then + set boot_counter=2 + elif [ "${boot_counter}" = "2" ]; then + set boot_counter=1 + elif [ "${boot_counter}" = "1" ]; then + set boot_counter=0 + fi + save_env boot_counter + fi +fi + +set boot_success=0 +save_env boot_success + +if [ "${active_slot}" = "A" ]; then + set root='(hd0,gpt2)' + set slot_label="System A" +else + set root='(hd0,gpt3)' + set slot_label="System B" +fi + +# --- ARM64 console string --- +# Covers QEMU virt (ttyAMA0), Ampere/RPi-equivalent PL011 (ttyAMA0), and +# Graviton/16550-compat (ttyS0). Last `console=` becomes the system console. + +menuentry "KubeSolo OS (${slot_label})" { + echo "Booting KubeSolo OS from ${slot_label}..." + echo "Boot counter: ${boot_counter}, Boot success: ${boot_success}" + linux /vmlinuz kubesolo.data=LABEL=KSOLODATA console=ttyAMA0,115200 console=ttyS0,115200 quiet + initrd /kubesolo-os.gz +} + +menuentry "KubeSolo OS (${slot_label}) — Debug Mode" { + echo "Booting KubeSolo OS (debug) from ${slot_label}..." + linux /vmlinuz kubesolo.data=LABEL=KSOLODATA kubesolo.debug console=ttyAMA0,115200 console=ttyS0,115200 + initrd /kubesolo-os.gz +} + +menuentry "KubeSolo OS — Emergency Shell" { + echo "Booting to emergency shell..." + linux /vmlinuz kubesolo.shell console=ttyAMA0,115200 console=ttyS0,115200 + initrd /kubesolo-os.gz +} + +menuentry "KubeSolo OS — Boot Other Slot" { + if [ "${active_slot}" = "A" ]; then + set root='(hd0,gpt3)' + echo "Booting from System B (passive)..." + else + set root='(hd0,gpt2)' + echo "Booting from System A (passive)..." + fi + linux /vmlinuz kubesolo.data=LABEL=KSOLODATA kubesolo.debug console=ttyAMA0,115200 console=ttyS0,115200 + initrd /kubesolo-os.gz +} diff --git a/build/scripts/create-disk-image.sh b/build/scripts/create-disk-image.sh index e90ea16..e79ac0c 100755 --- a/build/scripts/create-disk-image.sh +++ b/build/scripts/create-disk-image.sh @@ -6,28 +6,61 @@ # Part 2: System A (512 MB, ext4) — vmlinuz + kubesolo-os.gz (active) # Part 3: System B (512 MB, ext4) — vmlinuz + kubesolo-os.gz (passive) # Part 4: Data (remaining, ext4) — persistent K8s state +# +# Supports both x86_64 (default) and ARM64 generic UEFI targets. ARM64 RPi +# uses a different image format — see build/scripts/create-rpi-image.sh. +# +# Environment: +# TARGET_ARCH amd64 (default) or arm64 +# IMG_SIZE_MB Image size in MB (default 4096) +# CACHE_DIR Build cache (default /build/cache) +# ROOTFS_DIR Rootfs work dir (default /build/rootfs-work) +# OUTPUT_DIR Output dir (default /output) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" ROOTFS_DIR="${ROOTFS_DIR:-$PROJECT_ROOT/build/rootfs-work}" +CACHE_DIR="${CACHE_DIR:-$PROJECT_ROOT/build/cache}" OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT/output}" VERSION="$(cat "$PROJECT_ROOT/VERSION")" OS_NAME="kubesolo-os" +TARGET_ARCH="${TARGET_ARCH:-amd64}" -IMG_OUTPUT="$OUTPUT_DIR/${OS_NAME}-${VERSION}.img" IMG_SIZE_MB="${IMG_SIZE_MB:-4096}" # 4 GB default (larger for A/B) -VMLINUZ="$ROOTFS_DIR/vmlinuz" +# --- Arch-specific paths --- +case "$TARGET_ARCH" in + amd64) + IMG_OUTPUT="$OUTPUT_DIR/${OS_NAME}-${VERSION}.img" + VMLINUZ="$ROOTFS_DIR/vmlinuz" + GRUB_CFG="$PROJECT_ROOT/build/grub/grub.cfg" + GRUB_TARGET="x86_64-efi" + GRUB_EFI_BIN="bootx64.efi" + GRUB_INSTALL_BIOS=true + ;; + arm64) + IMG_OUTPUT="$OUTPUT_DIR/${OS_NAME}-${VERSION}.arm64.img" + VMLINUZ="$CACHE_DIR/kernel-arm64-generic/Image" + GRUB_CFG="$PROJECT_ROOT/build/grub/grub-arm64.cfg" + GRUB_TARGET="arm64-efi" + GRUB_EFI_BIN="BOOTAA64.EFI" + GRUB_INSTALL_BIOS=false + ;; + *) + echo "ERROR: TARGET_ARCH must be 'amd64' or 'arm64' (got: $TARGET_ARCH)" + exit 1 + ;; +esac + INITRAMFS="$ROOTFS_DIR/kubesolo-os.gz" -GRUB_CFG="$PROJECT_ROOT/build/grub/grub.cfg" GRUB_ENV_DEFAULTS="$PROJECT_ROOT/build/grub/grub-env-defaults" for f in "$VMLINUZ" "$INITRAMFS" "$GRUB_CFG" "$GRUB_ENV_DEFAULTS"; do [ -f "$f" ] || { echo "ERROR: Missing $f"; exit 1; } done -echo "==> Creating ${IMG_SIZE_MB}MB disk image with A/B partitions..." +echo "==> Creating ${IMG_SIZE_MB}MB ${TARGET_ARCH} disk image with A/B partitions..." mkdir -p "$OUTPUT_DIR" # Create sparse image @@ -161,35 +194,44 @@ else mv "$GRUBENV_FILE.tmp" "$GRUBENV_FILE" fi -# Install GRUB EFI binary if available -if command -v grub-mkimage >/dev/null 2>&1; then - grub-mkimage -O x86_64-efi -o "$MNT_EFI/EFI/BOOT/bootx64.efi" \ - -p /boot/grub \ - part_gpt ext2 fat normal linux echo all_video test search \ - search_fs_uuid search_label configfile loadenv \ - 2>/dev/null || echo " WARN: grub-mkimage failed — use QEMU -bios flag" -elif command -v grub2-mkimage >/dev/null 2>&1; then - grub2-mkimage -O x86_64-efi -o "$MNT_EFI/EFI/BOOT/bootx64.efi" \ - -p /boot/grub \ - part_gpt ext2 fat normal linux echo all_video test search \ - search_fs_uuid search_label configfile loadenv \ - 2>/dev/null || echo " WARN: grub2-mkimage failed — use QEMU -bios flag" +# Install GRUB EFI binary +# Modules required: part_gpt + fat (boot partition), ext2 (system A/B), +# normal + linux + echo + configfile + loadenv (boot menu + grubenv), +# search_* (locate partitions by label). +# all_video + test are x86-specific (DRM init); leave them out on arm64. +if [ "$TARGET_ARCH" = "arm64" ]; then + GRUB_MODULES="part_gpt ext2 fat normal linux echo test search search_fs_uuid search_label configfile loadenv" else - echo " WARN: grub-mkimage not found — EFI boot image not created" - echo " Install grub2-tools or use QEMU -kernel/-initrd flags" + GRUB_MODULES="part_gpt ext2 fat normal linux echo all_video test search search_fs_uuid search_label configfile loadenv" fi -# For BIOS boot: install GRUB i386-pc modules if available -if command -v grub-install >/dev/null 2>&1; then - grub-install --target=i386-pc --boot-directory="$MNT_EFI/boot" \ - --no-floppy "$LOOP" 2>/dev/null || { - echo " WARN: BIOS GRUB install failed — EFI-only or use QEMU -kernel" - } -elif command -v grub2-install >/dev/null 2>&1; then - grub2-install --target=i386-pc --boot-directory="$MNT_EFI/boot" \ - --no-floppy "$LOOP" 2>/dev/null || { - echo " WARN: BIOS GRUB install failed — EFI-only or use QEMU -kernel" - } +# shellcheck disable=SC2086 # GRUB_MODULES is intentionally word-split +if command -v grub-mkimage >/dev/null 2>&1; then + grub-mkimage -O "$GRUB_TARGET" -o "$MNT_EFI/EFI/BOOT/$GRUB_EFI_BIN" \ + -p /boot/grub $GRUB_MODULES \ + || echo " WARN: grub-mkimage failed — use QEMU -bios flag" +elif command -v grub2-mkimage >/dev/null 2>&1; then + grub2-mkimage -O "$GRUB_TARGET" -o "$MNT_EFI/EFI/BOOT/$GRUB_EFI_BIN" \ + -p /boot/grub $GRUB_MODULES \ + || echo " WARN: grub2-mkimage failed — use QEMU -bios flag" +else + echo " WARN: grub-mkimage not found — EFI boot image not created" + echo " Install grub-efi-${TARGET_ARCH}-bin or use QEMU -kernel/-initrd flags" +fi + +# For BIOS boot: install GRUB i386-pc modules (x86 only — ARM64 is UEFI-only). +if [ "$GRUB_INSTALL_BIOS" = "true" ]; then + if command -v grub-install >/dev/null 2>&1; then + grub-install --target=i386-pc --boot-directory="$MNT_EFI/boot" \ + --no-floppy "$LOOP" 2>/dev/null || { + echo " WARN: BIOS GRUB install failed — EFI-only or use QEMU -kernel" + } + elif command -v grub2-install >/dev/null 2>&1; then + grub2-install --target=i386-pc --boot-directory="$MNT_EFI/boot" \ + --no-floppy "$LOOP" 2>/dev/null || { + echo " WARN: BIOS GRUB install failed — EFI-only or use QEMU -kernel" + } + fi fi # --- System A Partition (active) --- @@ -213,9 +255,9 @@ done sync echo "" -echo "==> Disk image created: $IMG_OUTPUT" +echo "==> ${TARGET_ARCH} disk image created: $IMG_OUTPUT" echo " Size: $(du -h "$IMG_OUTPUT" | cut -f1)" -echo " Part 1 (KSOLOEFI): GRUB + A/B boot config" +echo " Part 1 (KSOLOEFI): GRUB ($GRUB_TARGET) + A/B boot config" echo " Part 2 (KSOLOA): System A — kernel + initramfs (active)" echo " Part 3 (KSOLOB): System B — kernel + initramfs (passive)" echo " Part 4 (KSOLODATA): Persistent K8s state" diff --git a/hack/dev-vm-arm64.sh b/hack/dev-vm-arm64.sh index 29b2600..40be3f2 100755 --- a/hack/dev-vm-arm64.sh +++ b/hack/dev-vm-arm64.sh @@ -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 # 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 # 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" diff --git a/test/qemu/test-boot-arm64-disk.sh b/test/qemu/test-boot-arm64-disk.sh new file mode 100755 index 0000000..526ae34 --- /dev/null +++ b/test/qemu/test-boot-arm64-disk.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# test-boot-arm64-disk.sh — Boot the ARM64 .arm64.img via UEFI + GRUB and +# verify the init system reaches stage 90. +# +# This is the full-stack integration test: UEFI firmware -> GRUB -> kernel -> +# initramfs -> staged init. Contrast with test-boot-arm64.sh which skips the +# bootloader and loads kernel/initramfs directly. +# +# Exit 0 = PASS, Exit 1 = FAIL. +# +# Usage: ./test/qemu/test-boot-arm64-disk.sh [disk.img] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +VERSION="$(cat "$PROJECT_ROOT/VERSION")" + +DISK_IMAGE="${1:-$PROJECT_ROOT/output/kubesolo-os-${VERSION}.arm64.img}" +TIMEOUT=180 + +echo "==> ARM64 UEFI Disk Boot Test" +echo " Disk image: $DISK_IMAGE" +echo " Timeout: ${TIMEOUT}s" + +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 + +if ! command -v qemu-system-aarch64 >/dev/null 2>&1; then + echo "ERROR: qemu-system-aarch64 not found." + echo " apt install qemu-system-arm # Debian/Ubuntu" + echo " dnf install qemu-system-aarch64 # Fedora/RHEL" + exit 1 +fi + +# --- Locate UEFI firmware --- +UEFI_FW="" +for candidate in \ + /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 +do + if [ -f "$candidate" ]; then + UEFI_FW="$candidate" + break + fi +done + +if [ -z "$UEFI_FW" ]; then + echo "ERROR: No ARM64 UEFI firmware found." + echo " apt install qemu-efi-aarch64" + exit 1 +fi + +echo " UEFI fw: $UEFI_FW" + +# Copy disk image to a scratch file so the test doesn't mutate the source. +# UEFI will write to grubenv on the EFI partition; we don't want to bake those +# changes into the canonical build artifact. +SCRATCH_DISK=$(mktemp /tmp/kubesolo-arm64-disk-test-XXXXXX.img) +SERIAL_LOG=$(mktemp /tmp/kubesolo-arm64-disk-serial-XXXXXX.log) +QEMU_PID="" + +cleanup() { + [ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null || true + rm -f "$SCRATCH_DISK" "$SERIAL_LOG" +} +trap cleanup EXIT + +cp --reflink=auto "$DISK_IMAGE" "$SCRATCH_DISK" 2>/dev/null || cp "$DISK_IMAGE" "$SCRATCH_DISK" + +# --- Launch QEMU --- +qemu-system-aarch64 \ + -machine virt \ + -cpu cortex-a72 \ + -m 2048 \ + -smp 2 \ + -nographic \ + -bios "$UEFI_FW" \ + -drive "file=$SCRATCH_DISK,format=raw,if=virtio,media=disk" \ + -net nic,model=virtio \ + -net user \ + -serial "file:$SERIAL_LOG" & +QEMU_PID=$! + +echo " Waiting for boot (PID $QEMU_PID)..." +ELAPSED=0 +SUCCESS=0 +while [ "$ELAPSED" -lt "$TIMEOUT" ]; do + if grep -q "\[kubesolo-init\] \[OK\] Stage 90-kubesolo.sh complete" "$SERIAL_LOG" 2>/dev/null; then + SUCCESS=1 + break + fi + if grep -q "KubeSolo is running" "$SERIAL_LOG" 2>/dev/null; then + SUCCESS=1 + break + fi + if ! kill -0 "$QEMU_PID" 2>/dev/null; then + echo "" + echo "==> FAIL: QEMU exited prematurely" + echo " Last 30 lines of serial output:" + tail -30 "$SERIAL_LOG" 2>/dev/null || echo " (no output)" + exit 1 + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) + printf "\r Elapsed: %ds / %ds" "$ELAPSED" "$TIMEOUT" +done +echo "" + +kill "$QEMU_PID" 2>/dev/null || true +wait "$QEMU_PID" 2>/dev/null || true +QEMU_PID="" + +if [ "$SUCCESS" = "1" ]; then + echo "==> ARM64 UEFI Disk Boot Test PASSED (${ELAPSED}s)" + exit 0 +fi + +echo "==> ARM64 UEFI Disk Boot Test FAILED (timeout ${TIMEOUT}s)" +echo "" +echo "==> Last 50 lines of serial output:" +tail -50 "$SERIAL_LOG" 2>/dev/null || echo " (no output)" +exit 1