build: phase 3 — split image assembly and QEMU runners out of the root
build/images.zig owns everything between built binaries and a bootable volume: the FHS zig-out install tree, boot manifest + capsule, both FAT32 images, the release ISO, and their check steps. build/qemu.zig owns run-x86-64 / run-x86-64-gpu and the OVMF probing. The root build.zig (461 lines, from 1,242 pre-split) now only decides what ships: kernel + loader, the package list, the bundled boot tree, and the aggregate test step. The stale commented-out run scaffold is gone. Boot-image file list unchanged; check-fat-image and check-iso-image both green.
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
// The danos build API (docs/build-packages-plan.md): the shared user-binary
|
// The danos build API (docs/build-packages-plan.md): the shared user-binary
|
||||||
// recipe lives in the build-support package; this root build orchestrates.
|
// recipe lives in the build-support package; this root build orchestrates —
|
||||||
|
// what ships (the bundled list), the kernel + loader, and the test aggregate.
|
||||||
|
// Image assembly and the QEMU run steps live beside it in build/.
|
||||||
const build_support = @import("build-support");
|
const build_support = @import("build-support");
|
||||||
|
const images = @import("build/images.zig");
|
||||||
|
const qemu = @import("build/qemu.zig");
|
||||||
|
|
||||||
/// danos is developed against Zig 0.16.x. Pre-1.0 Zig makes breaking API changes
|
/// danos is developed against Zig 0.16.x. Pre-1.0 Zig makes breaking API changes
|
||||||
/// between minor releases, and the .zon's `minimum_zig_version` only enforces a
|
/// between minor releases, and the .zon's `minimum_zig_version` only enforces a
|
||||||
@@ -19,38 +23,6 @@ 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];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A UTC timestamp like "20260708-153045", for naming a per-run artifact so
|
|
||||||
/// repeated runs don't clobber each other's logs. Resolved when `build.zig` runs
|
|
||||||
/// (i.e. at `zig build` invocation), 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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The modules the kernel imports, gathered once so both kernel variants (the
|
/// The modules the kernel imports, gathered once so both kernel variants (the
|
||||||
/// installed one and the serial-enabled one `run-x86-64` boots) are built from
|
/// installed one and the serial-enabled one `run-x86-64` boots) are built from
|
||||||
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
|
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
|
||||||
@@ -124,43 +96,6 @@ fn addKernel(
|
|||||||
return exe;
|
return exe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One user binary and its FHS home on the boot volume (and in zig-out).
|
|
||||||
const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath };
|
|
||||||
|
|
||||||
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the
|
|
||||||
/// EFI stub, the kernel, and every user binary at its FHS path — the volume's
|
|
||||||
/// /system tree IS the system image; the EFI loader walks it at boot and builds
|
|
||||||
/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled
|
|
||||||
/// `run-x86-64` variant can bundle its own serial kernel while sharing the
|
|
||||||
/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both
|
|
||||||
/// follow the top-level -Dserial). Returns the image's LazyPath.
|
|
||||||
fn addBootImage(
|
|
||||||
b: *std.Build,
|
|
||||||
kernel_bin: std.Build.LazyPath,
|
|
||||||
efi_bin: std.Build.LazyPath,
|
|
||||||
manifest: std.Build.LazyPath,
|
|
||||||
capsule: std.Build.LazyPath,
|
|
||||||
bundled: []const BundledBinary,
|
|
||||||
) std.Build.LazyPath {
|
|
||||||
const mk_fat = b.addSystemCommand(&.{"python3"});
|
|
||||||
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
|
||||||
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
|
||||||
mk_fat.addArg("64"); // MiB
|
|
||||||
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
|
||||||
mk_fat.addFileArg(efi_bin);
|
|
||||||
mk_fat.addArg("system/kernel");
|
|
||||||
mk_fat.addFileArg(kernel_bin);
|
|
||||||
mk_fat.addArg("system/manifest");
|
|
||||||
mk_fat.addFileArg(manifest);
|
|
||||||
mk_fat.addArg("boot/system.img");
|
|
||||||
mk_fat.addFileArg(capsule);
|
|
||||||
for (bundled) |item| {
|
|
||||||
mk_fat.addArg(item.path);
|
|
||||||
mk_fat.addFileArg(item.binary);
|
|
||||||
}
|
|
||||||
return fat_image;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
ensureZigVersion();
|
ensureZigVersion();
|
||||||
|
|
||||||
@@ -271,15 +206,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
.initial_ramdisk = initial_ramdisk_module,
|
.initial_ramdisk = initial_ramdisk_module,
|
||||||
};
|
};
|
||||||
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
||||||
|
// The serial-enabled twin is what `run-x86-64` boots — built lazily (only
|
||||||
|
// when its image is requested), never installed.
|
||||||
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
||||||
|
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
||||||
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
|
||||||
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
|
||||||
// kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
|
|
||||||
// zig-out/system/services/init, and so on (see docs/README.md). The bootloader
|
|
||||||
// then loads these FHS paths off the volume.
|
|
||||||
const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
|
|
||||||
b.getInstallStep().dependOn(&kernel_install.step);
|
|
||||||
|
|
||||||
// --- the user-space binaries, every one of them a package ---
|
// --- the user-space binaries, every one of them a package ---
|
||||||
// Binary packages (docs/build-packages-plan.md, phase 2): each binary
|
// Binary packages (docs/build-packages-plan.md, phase 2): each binary
|
||||||
@@ -371,7 +301,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
// variant that omits the display stack (so the kernel's boot transcript stays
|
// variant that omits the display stack (so the kernel's boot transcript stays
|
||||||
// on screen); both are bundled at the same /etc/init.csv path.
|
// on screen); both are bundled at the same /etc/init.csv path.
|
||||||
const init_csv_source = if (diagnose) "etc/init-diagnose.csv" else "etc/init.csv";
|
const init_csv_source = if (diagnose) "etc/init-diagnose.csv" else "etc/init.csv";
|
||||||
const production_bundled = [_]BundledBinary{
|
const production_bundled = [_]images.BundledBinary{
|
||||||
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
||||||
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
||||||
.{ .path = "system/services/display", .binary = display_exe.getEmittedBin() },
|
.{ .path = "system/services/display", .binary = display_exe.getEmittedBin() },
|
||||||
@@ -402,7 +332,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
// harness signals by passing -Dtest-case=<name> for every scenario, exactly when
|
// harness signals by passing -Dtest-case=<name> for every scenario, exactly when
|
||||||
// these fixtures must be on the boot volume. Merely building this array never
|
// these fixtures must be on the boot volume. Merely building this array never
|
||||||
// forces a compile: the fixture exes build only if `bundled` (below) includes them.
|
// forces a compile: the fixture exes build only if `bundled` (below) includes them.
|
||||||
const test_bundled = [_]BundledBinary{
|
const test_bundled = [_]images.BundledBinary{
|
||||||
.{ .path = "test/system/services/vfs-test", .binary = vfstest_exe.getEmittedBin() },
|
.{ .path = "test/system/services/vfs-test", .binary = vfstest_exe.getEmittedBin() },
|
||||||
.{ .path = "test/system/services/fat-test", .binary = fat_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/fat-test", .binary = fat_test_exe.getEmittedBin() },
|
||||||
.{ .path = "test/system/services/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() },
|
.{ .path = "test/system/services/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() },
|
||||||
@@ -420,52 +350,11 @@ pub fn build(b: *std.Build) void {
|
|||||||
// A no-option build assumes neither -Dtest-case nor -Ddiagnose: it ships the
|
// A no-option build assumes neither -Dtest-case nor -Ddiagnose: it ships the
|
||||||
// production set only. Test fixtures join in only under -Dtest-case; the
|
// production set only. Test fixtures join in only under -Dtest-case; the
|
||||||
// diagnose display-omission is already handled by init_csv_source above.
|
// diagnose display-omission is already handled by init_csv_source above.
|
||||||
var bundled_list: std.ArrayListUnmanaged(BundledBinary) = .empty;
|
var bundled_list: std.ArrayListUnmanaged(images.BundledBinary) = .empty;
|
||||||
bundled_list.appendSlice(b.allocator, &production_bundled) catch @panic("OOM");
|
bundled_list.appendSlice(b.allocator, &production_bundled) catch @panic("OOM");
|
||||||
if (test_case != null) bundled_list.appendSlice(b.allocator, &test_bundled) catch @panic("OOM");
|
if (test_case != null) bundled_list.appendSlice(b.allocator, &test_bundled) catch @panic("OOM");
|
||||||
const bundled = bundled_list.items;
|
const bundled = bundled_list.items;
|
||||||
|
|
||||||
// The boot manifest: the FHS path of every bundled binary, one per line. The
|
|
||||||
// EFI loader reads THIS by name and opens each listed path by name — FAT
|
|
||||||
// name lookup is case-insensitive and firmware-portable, unlike directory
|
|
||||||
// ENUMERATION, whose returned names vary by firmware (bare 8.3 entries come
|
|
||||||
// back uppercase on some FAT drivers). The tree walk remains only as the
|
|
||||||
// loader's fallback for hand-assembled sticks without a manifest.
|
|
||||||
var manifest_text: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
for (bundled) |item| {
|
|
||||||
manifest_text.append(b.allocator, '/') catch @panic("OOM");
|
|
||||||
manifest_text.appendSlice(b.allocator, item.path) catch @panic("OOM");
|
|
||||||
manifest_text.append(b.allocator, '\n') catch @panic("OOM");
|
|
||||||
}
|
|
||||||
const manifest_files = b.addWriteFiles();
|
|
||||||
const manifest_file = manifest_files.add("manifest", manifest_text.items);
|
|
||||||
const manifest_install = b.addInstallFileWithDir(manifest_file, .prefix, "system/manifest");
|
|
||||||
b.getInstallStep().dependOn(&manifest_install.step);
|
|
||||||
|
|
||||||
// The boot capsule: the same bundled list packed into ONE file (v2
|
|
||||||
// initial_ramdisk format), because a single open + sequential read is the
|
|
||||||
// only firmware file I/O shape that is fast everywhere — a per-file tree
|
|
||||||
// walk measured MINUTES on real firmware. The loader tries this first,
|
|
||||||
// then the manifest, then the walk; the running system cannot tell the
|
|
||||||
// difference (it always receives the same in-RAM table). Derived from the
|
|
||||||
// tree in the same build graph, so the two cannot drift.
|
|
||||||
const mk_capsule = b.addSystemCommand(&.{"python3"});
|
|
||||||
mk_capsule.addFileArg(b.path("tools/pack-system-image.py"));
|
|
||||||
const capsule_img = mk_capsule.addOutputFileArg("system.img");
|
|
||||||
for (bundled) |item| {
|
|
||||||
mk_capsule.addArg(item.path);
|
|
||||||
mk_capsule.addFileArg(item.binary);
|
|
||||||
}
|
|
||||||
const capsule_install = b.addInstallFile(capsule_img, "boot/system.img");
|
|
||||||
b.getInstallStep().dependOn(&capsule_install.step);
|
|
||||||
|
|
||||||
// Install every bundled binary to its FHS home, so zig-out is a true image of
|
|
||||||
// the filesystem — the same tree make-fat-image.py lays out on the boot volume.
|
|
||||||
for (bundled) |item| {
|
|
||||||
const install = b.addInstallFileWithDir(item.binary, .prefix, item.path);
|
|
||||||
b.getInstallStep().dependOn(&install.step);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boot methods live in boot/, one per way of getting the kernel running.
|
// Boot methods live in boot/, one per way of getting the kernel running.
|
||||||
// Each is its own binary/entry (a loader is built for its own target); today
|
// Each is its own binary/entry (a loader is built for its own target); today
|
||||||
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
||||||
@@ -495,212 +384,16 @@ pub fn build(b: *std.Build) void {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
|
// Image assembly (the FHS install tree, boot manifest + capsule, both FAT32
|
||||||
// so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
|
// images, the release ISO, the check steps) and the QEMU run steps live in
|
||||||
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
// build/ — the root decides what ships, those files own how it runs.
|
||||||
b.getInstallStep().dependOn(&efi_install.step);
|
const fat_image_serial = images.addImageSteps(b, .{
|
||||||
|
.kernel = exe,
|
||||||
// --- danos-usb.img: the bootable FAT32 USB image ---
|
.kernel_serial = exe_serial,
|
||||||
// Format a real FAT32 image (the in-repo Python builder, no external tools)
|
.efi = efiexe,
|
||||||
// holding the EFI stub, the kernel, and the whole /system tree of user
|
.bundled = bundled,
|
||||||
// binaries at their FHS paths. QEMU presents this image as a USB mass-storage
|
|
||||||
// device the guest boots from (see run-x86-64 and the test harness), and the
|
|
||||||
// danos fat driver mounts the same image at /mnt/usb.
|
|
||||||
const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), manifest_file, capsule_img, bundled);
|
|
||||||
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
|
||||||
b.getInstallStep().dependOn(&fat_image_install.step);
|
|
||||||
|
|
||||||
// The image `run-x86-64` boots: identical to the flashable one but with the
|
|
||||||
// serial log sink compiled in, so a developer always gets the machine-readable
|
|
||||||
// log captured to serial0 — without baking serial into the image users flash.
|
|
||||||
// Built lazily (only when `run-x86-64` is requested), and never installed.
|
|
||||||
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
|
||||||
const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), manifest_file, capsule_img, bundled);
|
|
||||||
|
|
||||||
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
|
||||||
// with the EFI stub present (the builder's own --verify, no external tools).
|
|
||||||
const check_fat = b.addSystemCommand(&.{"python3"});
|
|
||||||
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
|
||||||
check_fat.addArg("--verify");
|
|
||||||
check_fat.addFileArg(fat_image);
|
|
||||||
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
|
|
||||||
check_fat_step.dependOn(&check_fat.step);
|
|
||||||
|
|
||||||
// --- release-x86-64: danos-x86-64.iso, the flashable release image ---
|
|
||||||
// Wrap the FAT32 boot volume in a hybrid ISO (the in-repo Python builder
|
|
||||||
// again, no xorriso/isohybrid): an ISO9660 whose El Torito EFI boot entry
|
|
||||||
// and MBR ESP partition entry both point at the embedded FAT image. One
|
|
||||||
// file then boots every way release media is consumed — flashed raw to a
|
|
||||||
// USB stick with Etcher or dd, or burned to optical media — while
|
|
||||||
// danos-usb.img stays the raw superfloppy QEMU and the test harness boot.
|
|
||||||
const mk_iso = b.addSystemCommand(&.{"python3"});
|
|
||||||
mk_iso.addFileArg(b.path("tools/make-iso-image.py"));
|
|
||||||
const iso_image = mk_iso.addOutputFileArg("danos-x86-64.iso");
|
|
||||||
mk_iso.addFileArg(fat_image);
|
|
||||||
const iso_install = b.addInstallFile(iso_image, "danos-x86-64.iso");
|
|
||||||
const release_step = b.step("release-x86-64", "Build the flashable x86-64 release ISO (zig-out/danos-x86-64.iso; flash with Etcher or dd)");
|
|
||||||
release_step.dependOn(&iso_install.step);
|
|
||||||
|
|
||||||
// `zig build check-iso-image` — the ISO builder's own --verify (mirroring
|
|
||||||
// check-fat-image): the MBR partition, the El Torito catalog, and the
|
|
||||||
// embedded FAT32 image must all agree.
|
|
||||||
const check_iso = b.addSystemCommand(&.{"python3"});
|
|
||||||
check_iso.addFileArg(b.path("tools/make-iso-image.py"));
|
|
||||||
check_iso.addArg("--verify");
|
|
||||||
check_iso.addFileArg(iso_image);
|
|
||||||
const check_iso_step = b.step("check-iso-image", "Verify the release ISO is a valid hybrid (MBR ESP partition + El Torito EFI entry)");
|
|
||||||
check_iso_step.dependOn(&check_iso.step);
|
|
||||||
|
|
||||||
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
|
||||||
// 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(
|
qemu.addRunSteps(b, fat_image_serial);
|
||||||
[]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 guest boots the self-contained FAT image (attached as USB storage below),
|
|
||||||
// not the installed FHS zig-out — see the run step's drive/device flags.
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
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",
|
|
||||||
// "-usb",
|
|
||||||
// "-device",
|
|
||||||
// "usb-ehci,id=ehci",
|
|
||||||
// "-device",
|
|
||||||
// "usb-tablet,bus=usb-bus.0",
|
|
||||||
// "-device",
|
|
||||||
// "usb-mouse,bus=ehci.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",
|
|
||||||
});
|
|
||||||
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
|
|
||||||
// scratch area — a dev/host artifact, kept out of the FHS boot volume we mount.
|
|
||||||
// (/var/log/system is reserved for the kernel's own logging system later.) 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 });
|
|
||||||
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);
|
|
||||||
|
|
||||||
// const run_cmd = b.addRunArtifact(exe);
|
|
||||||
// const run_step = b.step("run", "Run the app");
|
|
||||||
// run_step.dependOn(&run_cmd.step);
|
|
||||||
// run_cmd.step.dependOn(b.getInstallStep());
|
|
||||||
//
|
|
||||||
// if (b.args) |args| {
|
|
||||||
// run_cmd.addArgs(args);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
||||||
// and can't be executed natively, so only the shared contracts are unit-tested
|
// and can't be executed natively, so only the shared contracts are unit-tested
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
//! Boot-image assembly (docs/build-packages-plan.md, phase 3): everything
|
||||||
|
//! between "here are the built binaries" and "here is a bootable volume".
|
||||||
|
//! The FHS-shaped zig-out install tree, the boot manifest, the boot capsule,
|
||||||
|
//! the FAT32 USB image (+ its serial-enabled twin for the QEMU run steps),
|
||||||
|
//! and the release ISO — with their check steps. The root build.zig decides
|
||||||
|
//! WHAT ships (the bundled list); this file owns HOW it becomes an image.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// One user binary and its FHS home on the boot volume (and in zig-out).
|
||||||
|
pub const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath };
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
/// The installed/flashable kernel (serial follows the root -Dserial).
|
||||||
|
kernel: *std.Build.Step.Compile,
|
||||||
|
/// The serial-enabled kernel variant the `run-x86-64` image boots.
|
||||||
|
kernel_serial: *std.Build.Step.Compile,
|
||||||
|
/// The UEFI loader (BOOTX64).
|
||||||
|
efi: *std.Build.Step.Compile,
|
||||||
|
/// Every user binary and data file at its FHS path.
|
||||||
|
bundled: []const BundledBinary,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Wire up the install tree, both FAT32 boot images, the release ISO, and the
|
||||||
|
/// check steps. Returns the serial-enabled FAT image for the QEMU run steps.
|
||||||
|
pub fn addImageSteps(b: *std.Build, options: Options) std.Build.LazyPath {
|
||||||
|
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
||||||
|
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
||||||
|
// kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
|
||||||
|
// zig-out/system/services/init, and so on (see docs/README.md). The bootloader
|
||||||
|
// then loads these FHS paths off the volume.
|
||||||
|
const kernel_install = b.addInstallArtifact(options.kernel, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
|
||||||
|
b.getInstallStep().dependOn(&kernel_install.step);
|
||||||
|
|
||||||
|
// UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
|
||||||
|
// so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
|
||||||
|
const efi_install = b.addInstallArtifact(options.efi, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
||||||
|
b.getInstallStep().dependOn(&efi_install.step);
|
||||||
|
|
||||||
|
// The boot manifest: the FHS path of every bundled binary, one per line. The
|
||||||
|
// EFI loader reads THIS by name and opens each listed path by name — FAT
|
||||||
|
// name lookup is case-insensitive and firmware-portable, unlike directory
|
||||||
|
// ENUMERATION, whose returned names vary by firmware (bare 8.3 entries come
|
||||||
|
// back uppercase on some FAT drivers). The tree walk remains only as the
|
||||||
|
// loader's fallback for hand-assembled sticks without a manifest.
|
||||||
|
var manifest_text: std.ArrayListUnmanaged(u8) = .empty;
|
||||||
|
for (options.bundled) |item| {
|
||||||
|
manifest_text.append(b.allocator, '/') catch @panic("OOM");
|
||||||
|
manifest_text.appendSlice(b.allocator, item.path) catch @panic("OOM");
|
||||||
|
manifest_text.append(b.allocator, '\n') catch @panic("OOM");
|
||||||
|
}
|
||||||
|
const manifest_files = b.addWriteFiles();
|
||||||
|
const manifest_file = manifest_files.add("manifest", manifest_text.items);
|
||||||
|
const manifest_install = b.addInstallFileWithDir(manifest_file, .prefix, "system/manifest");
|
||||||
|
b.getInstallStep().dependOn(&manifest_install.step);
|
||||||
|
|
||||||
|
// The boot capsule: the same bundled list packed into ONE file (v2
|
||||||
|
// initial_ramdisk format), because a single open + sequential read is the
|
||||||
|
// only firmware file I/O shape that is fast everywhere — a per-file tree
|
||||||
|
// walk measured MINUTES on real firmware. The loader tries this first,
|
||||||
|
// then the manifest, then the walk; the running system cannot tell the
|
||||||
|
// difference (it always receives the same in-RAM table). Derived from the
|
||||||
|
// tree in the same build graph, so the two cannot drift.
|
||||||
|
const mk_capsule = b.addSystemCommand(&.{"python3"});
|
||||||
|
mk_capsule.addFileArg(b.path("tools/pack-system-image.py"));
|
||||||
|
const capsule_img = mk_capsule.addOutputFileArg("system.img");
|
||||||
|
for (options.bundled) |item| {
|
||||||
|
mk_capsule.addArg(item.path);
|
||||||
|
mk_capsule.addFileArg(item.binary);
|
||||||
|
}
|
||||||
|
const capsule_install = b.addInstallFile(capsule_img, "boot/system.img");
|
||||||
|
b.getInstallStep().dependOn(&capsule_install.step);
|
||||||
|
|
||||||
|
// Install every bundled binary to its FHS home, so zig-out is a true image of
|
||||||
|
// the filesystem — the same tree make-fat-image.py lays out on the boot volume.
|
||||||
|
for (options.bundled) |item| {
|
||||||
|
const install = b.addInstallFileWithDir(item.binary, .prefix, item.path);
|
||||||
|
b.getInstallStep().dependOn(&install.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- danos-usb.img: the bootable FAT32 USB image ---
|
||||||
|
// Format a real FAT32 image (the in-repo Python builder, no external tools)
|
||||||
|
// holding the EFI stub, the kernel, and the whole /system tree of user
|
||||||
|
// binaries at their FHS paths. QEMU presents this image as a USB mass-storage
|
||||||
|
// device the guest boots from (see run-x86-64 and the test harness), and the
|
||||||
|
// danos fat driver mounts the same image at /mnt/usb.
|
||||||
|
const fat_image = addBootImage(b, options.kernel.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);
|
||||||
|
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
||||||
|
b.getInstallStep().dependOn(&fat_image_install.step);
|
||||||
|
|
||||||
|
// The image `run-x86-64` boots: identical to the flashable one but with the
|
||||||
|
// serial log sink compiled in, so a developer always gets the machine-readable
|
||||||
|
// log captured to serial0 — without baking serial into the image users flash.
|
||||||
|
// Built lazily (only when `run-x86-64` is requested), and never installed.
|
||||||
|
const fat_image_serial = addBootImage(b, options.kernel_serial.getEmittedBin(), options.efi.getEmittedBin(), manifest_file, capsule_img, options.bundled);
|
||||||
|
|
||||||
|
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
||||||
|
// with the EFI stub present (the builder's own --verify, no external tools).
|
||||||
|
const check_fat = b.addSystemCommand(&.{"python3"});
|
||||||
|
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||||
|
check_fat.addArg("--verify");
|
||||||
|
check_fat.addFileArg(fat_image);
|
||||||
|
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
|
||||||
|
check_fat_step.dependOn(&check_fat.step);
|
||||||
|
|
||||||
|
// --- release-x86-64: danos-x86-64.iso, the flashable release image ---
|
||||||
|
// Wrap the FAT32 boot volume in a hybrid ISO (the in-repo Python builder
|
||||||
|
// again, no xorriso/isohybrid): an ISO9660 whose El Torito EFI boot entry
|
||||||
|
// and MBR ESP partition entry both point at the embedded FAT image. One
|
||||||
|
// file then boots every way release media is consumed — flashed raw to a
|
||||||
|
// USB stick with Etcher or dd, or burned to optical media — while
|
||||||
|
// danos-usb.img stays the raw superfloppy QEMU and the test harness boot.
|
||||||
|
const mk_iso = b.addSystemCommand(&.{"python3"});
|
||||||
|
mk_iso.addFileArg(b.path("tools/make-iso-image.py"));
|
||||||
|
const iso_image = mk_iso.addOutputFileArg("danos-x86-64.iso");
|
||||||
|
mk_iso.addFileArg(fat_image);
|
||||||
|
const iso_install = b.addInstallFile(iso_image, "danos-x86-64.iso");
|
||||||
|
const release_step = b.step("release-x86-64", "Build the flashable x86-64 release ISO (zig-out/danos-x86-64.iso; flash with Etcher or dd)");
|
||||||
|
release_step.dependOn(&iso_install.step);
|
||||||
|
|
||||||
|
// `zig build check-iso-image` — the ISO builder's own --verify (mirroring
|
||||||
|
// check-fat-image): the MBR partition, the El Torito catalog, and the
|
||||||
|
// embedded FAT32 image must all agree.
|
||||||
|
const check_iso = b.addSystemCommand(&.{"python3"});
|
||||||
|
check_iso.addFileArg(b.path("tools/make-iso-image.py"));
|
||||||
|
check_iso.addArg("--verify");
|
||||||
|
check_iso.addFileArg(iso_image);
|
||||||
|
const check_iso_step = b.step("check-iso-image", "Verify the release ISO is a valid hybrid (MBR ESP partition + El Torito EFI entry)");
|
||||||
|
check_iso_step.dependOn(&check_iso.step);
|
||||||
|
|
||||||
|
return fat_image_serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the
|
||||||
|
/// EFI stub, the kernel, and every user binary at its FHS path — the volume's
|
||||||
|
/// /system tree IS the system image; the EFI loader walks it at boot and builds
|
||||||
|
/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled
|
||||||
|
/// `run-x86-64` variant can bundle its own serial kernel while sharing the
|
||||||
|
/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both
|
||||||
|
/// follow the top-level -Dserial). Returns the image's LazyPath.
|
||||||
|
fn addBootImage(
|
||||||
|
b: *std.Build,
|
||||||
|
kernel_bin: std.Build.LazyPath,
|
||||||
|
efi_bin: std.Build.LazyPath,
|
||||||
|
manifest: std.Build.LazyPath,
|
||||||
|
capsule: std.Build.LazyPath,
|
||||||
|
bundled: []const BundledBinary,
|
||||||
|
) std.Build.LazyPath {
|
||||||
|
const mk_fat = b.addSystemCommand(&.{"python3"});
|
||||||
|
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||||
|
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
||||||
|
mk_fat.addArg("64"); // MiB
|
||||||
|
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
||||||
|
mk_fat.addFileArg(efi_bin);
|
||||||
|
mk_fat.addArg("system/kernel");
|
||||||
|
mk_fat.addFileArg(kernel_bin);
|
||||||
|
mk_fat.addArg("system/manifest");
|
||||||
|
mk_fat.addFileArg(manifest);
|
||||||
|
mk_fat.addArg("boot/system.img");
|
||||||
|
mk_fat.addFileArg(capsule);
|
||||||
|
for (bundled) |item| {
|
||||||
|
mk_fat.addArg(item.path);
|
||||||
|
mk_fat.addFileArg(item.binary);
|
||||||
|
}
|
||||||
|
return fat_image;
|
||||||
|
}
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
//! 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 FHS boot volume we mount.
|
||||||
|
// (/var/log/system is reserved for the kernel's own logging system later.) 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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user