feat: ARM64 generic UEFI disk image (GPT + GRUB A/B)
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:
19
Makefile
19
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)"
|
||||
|
||||
@@ -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 \
|
||||
|
||||
86
build/grub/grub-arm64.cfg
Normal file
86
build/grub/grub-arm64.cfg
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 <project>/build/cache)
|
||||
# ROOTFS_DIR Rootfs work dir (default <project>/build/rootfs-work)
|
||||
# OUTPUT_DIR Output dir (default <project>/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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
129
test/qemu/test-boot-arm64-disk.sh
Executable file
129
test/qemu/test-boot-arm64-disk.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user