From 50f3610768d3fba386ad9a0568f645632e113529 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:43:00 +0100 Subject: [PATCH] Update build to run qemu on MacOS --- README.md | 10 ++++++---- build.zig | 35 +++++++++++++++++++++++++++++++---- test/qemu_test.py | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ceaebdc..0342156 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,13 @@ kernel heap. See [`docs/`](docs/README.md) for how each piece works. versions are rejected, because Zig makes breaking changes between releases pre-1.0. A toolchain manager such as [zvm](https://github.com/tristanisham/zvm) or `zigup` will pick up `.zig-version` automatically. -- **QEMU** (`qemu-system-x86_64`) — to run and test the kernel. +- **QEMU** (`qemu-system-x86_64`) — to run and test the kernel. On macOS, + `brew install qemu` also bundles the OVMF firmware below. - **OVMF** UEFI firmware — the `edk2-ovmf` package (Arch), `ovmf` (Debian/Ubuntu), - or `edk2-ovmf` (Fedora). The build defaults to the Arch paths - (`/usr/share/edk2/x64/OVMF_{CODE,VARS}.4m.fd`); override with `-Dovmf-code=` and - `-Dovmf-vars=` if your distro installs them elsewhere. + or `edk2-ovmf` (Fedora); on macOS it ships inside the Homebrew `qemu` formula. + Both the build and the test harness probe the known Arch/Debian/Fedora/macOS + layouts and use the first that exists, so no configuration is normally needed. + Override with `-Dovmf-code=` / `-Dovmf-vars=` (build) if yours lives elsewhere. - **Python 3** — for the QEMU integration test harness. ## Build diff --git a/build.zig b/build.zig index 3cc5d18..2ed1b74 100644 --- a/build.zig +++ b/build.zig @@ -16,6 +16,18 @@ fn ensureZigVersion() void { } } +/// Return the first path in `candidates` that exists on the build host, else the +/// first candidate as a fallback so a missing-firmware error still names a +/// concrete (and, by convention, the primary) path. Used to locate OVMF firmware +/// across distro/OS layouts without configuration. +fn firstExisting(io: std.Io, candidates: []const []const u8) []const u8 { + for (candidates) |path| { + std.Io.Dir.accessAbsolute(io, path, .{}) catch continue; + return path; + } + return candidates[0]; +} + pub fn build(b: *std.Build) void { ensureZigVersion(); @@ -105,18 +117,33 @@ pub fn build(b: *std.Build) void { b.installArtifact(efiexe); // --- run-efi: boot BOOTX64.efi in QEMU with OVMF firmware --- - // OVMF paths follow the Arch (edk2-ovmf) layout; override with -Dovmf-code - // and -Dovmf-vars if your distro installs them elsewhere. + // Firmware lives in different places per OS/distro, so probe the known + // layouts (Arch, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first + // that exists. Override with -Dovmf-code / -Dovmf-vars if yours is elsewhere. const ovmf_code = b.option( []const u8, "ovmf-code", "Path to the OVMF_CODE firmware image", - ) orelse "/usr/share/edk2/x64/OVMF_CODE.4m.fd"; + ) orelse firstExisting(b.graph.io, &.{ + "/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Arch + "/usr/share/OVMF/OVMF_CODE_4M.fd", // Debian/Ubuntu + "/usr/share/OVMF/OVMF_CODE.fd", // older Debian/Ubuntu + "/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", // Fedora + "/opt/homebrew/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Apple Silicon) + "/usr/local/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Intel) + }); const ovmf_vars = b.option( []const u8, "ovmf-vars", "Path to the OVMF_VARS firmware image (a writable copy is made)", - ) orelse "/usr/share/edk2/x64/OVMF_VARS.4m.fd"; + ) orelse firstExisting(b.graph.io, &.{ + "/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Arch + "/usr/share/OVMF/OVMF_VARS_4M.fd", // Debian/Ubuntu + "/usr/share/OVMF/OVMF_VARS.fd", // older Debian/Ubuntu + "/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", // Fedora + "/opt/homebrew/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Apple Silicon) + "/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel) + }); // Assemble an EFI System Partition layout: esp/EFI/BOOT/BOOTX64.efi const efi_install = b.addInstallArtifact(efiexe, .{ diff --git a/test/qemu_test.py b/test/qemu_test.py index 89152b1..d4ad388 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -35,8 +35,26 @@ ARCHES = { "x86_64": { "zig_flags": [], # build.zig currently pins the kernel to x86_64 "qemu": "qemu-system-x86_64", - "ovmf_code": "/usr/share/edk2/x64/OVMF_CODE.4m.fd", - "ovmf_vars": "/usr/share/edk2/x64/OVMF_VARS.4m.fd", + # Firmware lives in different places per OS/distro; resolve_firmware() + # picks the first that exists so the harness runs unconfigured on Arch, + # Debian/Ubuntu, Fedora, and macOS (Homebrew). Override by reordering or + # dropping an absolute path at the front. + "ovmf_code": [ + "/usr/share/edk2/x64/OVMF_CODE.4m.fd", # Arch + "/usr/share/OVMF/OVMF_CODE_4M.fd", # Debian/Ubuntu + "/usr/share/OVMF/OVMF_CODE.fd", # older Debian/Ubuntu + "/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", # Fedora + "/opt/homebrew/share/qemu/edk2-x86_64-code.fd", # macOS Homebrew (Apple Silicon) + "/usr/local/share/qemu/edk2-x86_64-code.fd", # macOS Homebrew (Intel) + ], + "ovmf_vars": [ + "/usr/share/edk2/x64/OVMF_VARS.4m.fd", # Arch + "/usr/share/OVMF/OVMF_VARS_4M.fd", # Debian/Ubuntu + "/usr/share/OVMF/OVMF_VARS.fd", # older Debian/Ubuntu + "/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", # Fedora + "/opt/homebrew/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Apple Silicon) + "/usr/local/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Intel) + ], "efi_app": ("EFI/BOOT/BOOTX64.efi", "BOOTX64.efi"), # (dest in ESP, name in zig-out/bin) "kernel": ("danos", "danos"), # Built as a function so we can splice in per-run paths. @@ -123,6 +141,24 @@ def make_esp(arch): return esp +def resolve_firmware(arch): + """Collapse the ovmf_code/ovmf_vars candidate lists to the first path that + exists on this machine. Mutates `arch` in place; idempotent (a resolved + string is left untouched). Exits with a clear message if none is found.""" + for key, label in (("ovmf_code", "OVMF_CODE"), ("ovmf_vars", "OVMF_VARS")): + val = arch[key] + if isinstance(val, str): + continue # already resolved on a previous call + for cand in val: + if os.path.exists(cand): + arch[key] = cand + break + else: + sys.exit(f"error: no {label} firmware image found; looked in:\n " + + "\n ".join(val) + + "\nInstall OVMF (edk2-ovmf / ovmf) or add its path above.") + + def run_case(arch, case): err = build(arch, case["name"]) if err: @@ -176,6 +212,7 @@ def main(): if not shutil.which(arch["qemu"]): print(f"error: {arch['qemu']} not found on PATH", file=sys.stderr) return 2 + resolve_firmware(arch) selected = [c for c in CASES if not args.cases or c["name"] in args.cases] os.makedirs(WORK, exist_ok=True)