177 lines
8.0 KiB
Zig
177 lines
8.0 KiB
Zig
//! The QEMU run steps (docs/build-packages-plan.md, phase 3): `run-x86-64`
|
|
//! boots the serial-enabled FAT image via UEFI/OVMF; `run-x86-64-gpu` adds a
|
|
//! virtio-gpu adapter for the native-present display path. OVMF firmware is
|
|
//! probed across distro/OS layouts (-Dovmf-code / -Dovmf-vars override).
|
|
|
|
const std = @import("std");
|
|
|
|
/// Wire up the `run-x86-64` and `run-x86-64-gpu` steps around the given
|
|
/// serial-enabled boot image (the guest boots that self-contained image
|
|
/// attached as USB storage, not the installed FHS zig-out).
|
|
pub fn addRunSteps(b: *std.Build, fat_image_serial: std.Build.LazyPath) void {
|
|
// Firmware lives in different places per OS/distro, so probe the known
|
|
// layouts (Architecture, 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 firstExisting(b.graph.io, &.{
|
|
"/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Architecture
|
|
"/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 firstExisting(b.graph.io, &.{
|
|
"/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Architecture
|
|
"/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)
|
|
});
|
|
|
|
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
|
|
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
|
const vars_out = vars_copy.addOutputFileArg("OVMF_VARS.4m.fd");
|
|
|
|
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
|
|
// scratch area — a dev/host artifact, kept out of the boot volume we mount.
|
|
// (/system/logs on the volume belongs to the guest's own logger.) One
|
|
// timestamped file per run.
|
|
const log_dir = b.fmt("{s}/qemu-test", .{b.install_path});
|
|
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
|
|
|
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
|
const run_efi = b.addSystemCommand(&.{
|
|
"qemu-system-x86_64",
|
|
"-device",
|
|
"qemu-xhci,id=xhci",
|
|
"-device",
|
|
"usb-mouse,bus=xhci.0",
|
|
"-device",
|
|
"usb-kbd,bus=xhci.0",
|
|
"-machine",
|
|
"q35",
|
|
"-m",
|
|
"128M",
|
|
"-drive",
|
|
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
|
|
});
|
|
run_efi.addArg("-drive");
|
|
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
|
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
|
|
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
|
|
// The serial-enabled variant, so serial0 carries the log for this dev boot.
|
|
run_efi.addArg("-drive");
|
|
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
|
|
run_efi.addArgs(&.{
|
|
"-device",
|
|
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
|
"-net",
|
|
"none",
|
|
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
|
// resolution, so the kernel's native-resolution switch has something to
|
|
// find. `-vga none` avoids a second, default adapter.
|
|
"-vga",
|
|
"none",
|
|
"-device",
|
|
"VGA,edid=on,xres=1280,yres=720",
|
|
});
|
|
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
|
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
|
// We boot the self-contained `fat_image_serial` (added as a file arg above, so
|
|
// it's already a dependency) — not the installed FHS zig-out — so `run-x86-64`
|
|
// builds only the serial kernel, never the flashable one. Just make the serial
|
|
// scratch dir first.
|
|
run_efi.step.dependOn(&make_log_dir.step);
|
|
|
|
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
|
run_efi_step.dependOn(&run_efi.step);
|
|
|
|
// --- run-x86-64-gpu: the same boot plus a virtio-gpu adapter ---
|
|
// The VGA device still supplies the boot (GOP) framebuffer the compositor starts
|
|
// on; the virtio-gpu function is discovered by the device-manager stack, its
|
|
// driver announces a shared scanout, and the compositor upgrades off the GOP
|
|
// floor to fenced, tear-free native presents (docs/display-v2.md).
|
|
// This is the interactive twin of the `display-native` test case, and 512M
|
|
// matches it (the whole driver stack + the compositor's surfaces at once).
|
|
// QEMU shows one head per adapter: pick the virtio-gpu head in the View menu
|
|
// to watch the native output.
|
|
const run_gpu = b.addSystemCommand(&.{
|
|
"qemu-system-x86_64",
|
|
"-device",
|
|
"qemu-xhci,id=xhci",
|
|
"-device",
|
|
"usb-mouse,bus=xhci.0",
|
|
"-device",
|
|
"usb-kbd,bus=xhci.0",
|
|
"-machine",
|
|
"q35",
|
|
"-m",
|
|
"512M",
|
|
"-drive",
|
|
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
|
|
});
|
|
run_gpu.addArg("-drive");
|
|
run_gpu.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
|
run_gpu.addArg("-drive");
|
|
run_gpu.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
|
|
run_gpu.addArgs(&.{
|
|
"-device",
|
|
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
|
"-net",
|
|
"none",
|
|
"-vga",
|
|
"none",
|
|
"-device",
|
|
"VGA,edid=on,xres=1280,yres=720",
|
|
"-device",
|
|
"virtio-gpu-pci",
|
|
});
|
|
const gpu_serial_log = b.fmt("{s}/run-x86-64-gpu-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
|
run_gpu.addArgs(&.{ "-serial", b.fmt("file:{s}", .{gpu_serial_log}) });
|
|
run_gpu.step.dependOn(&make_log_dir.step);
|
|
|
|
const run_gpu_step = b.step("run-x86-64-gpu", "Boot in QEMU with a virtio-gpu adapter: the compositor upgrades to fenced (tear-free) native presents; watch the virtio-gpu head in QEMU's View menu");
|
|
run_gpu_step.dependOn(&run_gpu.step);
|
|
}
|
|
|
|
/// 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];
|
|
}
|
|
|
|
/// A UTC timestamp like "20260708-153045", for naming a per-run artifact so
|
|
/// repeated runs don't clobber each other's logs. Resolved when `zig build`
|
|
/// runs, which is moments before QEMU launches.
|
|
fn timestamp(b: *std.Build) []const u8 {
|
|
const ns = std.Io.Clock.now(.real, b.graph.io).nanoseconds;
|
|
const secs: u64 = @intCast(@divFloor(ns, std.time.ns_per_s));
|
|
const es = std.time.epoch.EpochSeconds{ .secs = secs };
|
|
const yd = es.getEpochDay().calculateYearDay();
|
|
const md = yd.calculateMonthDay();
|
|
const ds = es.getDaySeconds();
|
|
return b.fmt("{d:0>4}{d:0>2}{d:0>2}-{d:0>2}{d:0>2}{d:0>2}", .{
|
|
yd.year,
|
|
md.month.numeric(),
|
|
@as(u32, md.day_index) + 1,
|
|
ds.getHoursIntoDay(),
|
|
ds.getMinutesIntoHour(),
|
|
ds.getSecondsIntoMinute(),
|
|
});
|
|
}
|