ps2-bus (bus + keyboard + mouse artifacts from one package), usb-xhci-bus, usb-hid (keyboard + mouse), usb-storage, and virtio-gpu convert on the pci-bus template. Their unit tests — PS/2 decode, HID boot reports, Bulk-Only Transport/SCSI encodings, and the virtio-gpu size checks the plan flagged as a wave carry-along — move into their packages; the root aggregate delegates. Boot-image file list unchanged.
853 lines
47 KiB
Zig
853 lines
47 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
// The danos build API (docs/build-packages-plan.md): the shared user-binary
|
|
// recipe lives in the build-support package; this root build orchestrates.
|
|
const build_support = @import("build-support");
|
|
|
|
/// 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
|
|
/// floor — so reject anything off the 0.16 line to keep the build reproducible.
|
|
fn ensureZigVersion() void {
|
|
const v = builtin.zig_version;
|
|
if (v.major != 0 or v.minor != 16) {
|
|
std.debug.print(
|
|
"danos requires Zig 0.16.x, but this is {d}.{d}.{d}. " ++
|
|
"Zig makes breaking changes between minor releases pre-1.0.\n",
|
|
.{ v.major, v.minor, v.patch },
|
|
);
|
|
std.process.exit(1);
|
|
}
|
|
}
|
|
|
|
/// 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(),
|
|
});
|
|
}
|
|
|
|
/// One user-space binary via the shared build-support recipe (freestanding,
|
|
/// ReleaseSmall, `.large` code model, root shim + user link script — see
|
|
/// build-support/build.zig for the full story). The program's file becomes the
|
|
/// `program` module; reach it through `programModule` to add per-binary imports.
|
|
fn addUserBinary(
|
|
b: *std.Build,
|
|
target: std.Build.ResolvedTarget,
|
|
default_imports: []const std.Build.Module.Import,
|
|
name: []const u8,
|
|
root: []const u8,
|
|
) *std.Build.Step.Compile {
|
|
return addUserBinaryImpl(b, target, default_imports, name, root, false);
|
|
}
|
|
|
|
/// As `addUserBinary`, but built multi-threaded (`single_threaded = false`) so real
|
|
/// atomics/TLS work — required before a binary may call `Thread.spawn`
|
|
/// (docs/threading.md). Threads are a deliberate per-binary opt-in.
|
|
fn addThreadedUserBinary(
|
|
b: *std.Build,
|
|
target: std.Build.ResolvedTarget,
|
|
default_imports: []const std.Build.Module.Import,
|
|
name: []const u8,
|
|
root: []const u8,
|
|
) *std.Build.Step.Compile {
|
|
return addUserBinaryImpl(b, target, default_imports, name, root, true);
|
|
}
|
|
|
|
fn addUserBinaryImpl(
|
|
b: *std.Build,
|
|
target: std.Build.ResolvedTarget,
|
|
default_imports: []const std.Build.Module.Import,
|
|
name: []const u8,
|
|
root: []const u8,
|
|
threaded: bool,
|
|
) *std.Build.Step.Compile {
|
|
// The shim and link script live in the kernel domain package, so name them
|
|
// through it — the same spelling build-support's userBinary uses.
|
|
const kernel_library = b.dependency("kernel", .{});
|
|
return build_support.userBinaryFromImports(b, .{
|
|
.name = name,
|
|
.root_source_file = b.path(root),
|
|
.shim_source_file = kernel_library.path("root.zig"),
|
|
.linker_script = kernel_library.path("user.ld"),
|
|
.target = target,
|
|
.default_imports = default_imports,
|
|
.threaded = threaded,
|
|
});
|
|
}
|
|
|
|
/// The `program` module of a binary built by `addUserBinary` — the module rooted
|
|
/// at the program's own source file. Per-binary imports (protocol modules, bus
|
|
/// ABIs) go here, not on the root shim: module imports are not transitive, so an
|
|
/// import added to the root would be invisible to the program's code.
|
|
fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module {
|
|
return build_support.programModule(exe);
|
|
}
|
|
|
|
/// 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
|
|
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
|
|
/// which differ per variant, so `addKernel` builds it fresh each time.
|
|
const KernelModules = struct {
|
|
boot_handoff: *std.Build.Module,
|
|
abi: *std.Build.Module,
|
|
device_abi: *std.Build.Module,
|
|
architecture: *std.Build.Module,
|
|
platform: *std.Build.Module,
|
|
parameters: *std.Build.Module,
|
|
initial_ramdisk: *std.Build.Module,
|
|
};
|
|
|
|
/// Build the freestanding x86_64 kernel ELF. Factored so we can build it twice
|
|
/// from one recipe: the installed/flashable image (serial off by default) and the
|
|
/// serial-enabled variant `run-x86-64` boots — they differ only in the `serial`
|
|
/// build option baked into `build_options`.
|
|
fn addKernel(
|
|
b: *std.Build,
|
|
kernel_target: std.Build.ResolvedTarget,
|
|
optimize: std.builtin.OptimizeMode,
|
|
modules: KernelModules,
|
|
test_case: ?[]const u8,
|
|
serial: bool,
|
|
) *std.Build.Step.Compile {
|
|
// Compile-time configuration the kernel reads as `@import("build_options")`:
|
|
// the QEMU harness's -Dtest-case, and whether the serial log sink is compiled
|
|
// in (see the -Dserial option). Built per variant since `serial` differs.
|
|
const build_options = b.addOptions();
|
|
build_options.addOption(?[]const u8, "test_case", test_case);
|
|
build_options.addOption(bool, "serial", serial);
|
|
const build_options_module = build_options.createModule();
|
|
|
|
const exe = b.addExecutable(.{
|
|
.name = "kernel",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("system/kernel/kernel.zig"),
|
|
.target = kernel_target,
|
|
.optimize = optimize,
|
|
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
|
.red_zone = false, // interrupts would corrupt the SystemV red zone
|
|
.single_threaded = false, // SMP: the big kernel lock's atomics must be real across cores
|
|
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
|
|
.stack_check = false, // stack-probe calls have no runtime to land in
|
|
.stack_protector = false,
|
|
.strip = optimize != .Debug, // DWARF info doubles the flashable image; keep it only for debug builds
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = modules.boot_handoff },
|
|
.{ .name = "abi", .module = modules.abi },
|
|
.{ .name = "device-abi", .module = modules.device_abi },
|
|
.{ .name = "architecture", .module = modules.architecture },
|
|
.{ .name = "platform", .module = modules.platform },
|
|
.{ .name = "parameters", .module = modules.parameters },
|
|
.{ .name = "build_options", .module = build_options_module },
|
|
.{ .name = "initial-ramdisk", .module = modules.initial_ramdisk },
|
|
},
|
|
}),
|
|
});
|
|
exe.setLinkerScript(b.path("system/kernel/architecture/x86_64/linker.ld"));
|
|
exe.entry = .{ .symbol_name = "_start" };
|
|
// The self-hosted linker ignores parts of the linker script (PHDRS,
|
|
// /DISCARD/, AT(), section order); the higher-half layout depends on the
|
|
// script being authoritative, so pin the kernel to LLVM + LLD.
|
|
exe.use_llvm = true;
|
|
exe.use_lld = true;
|
|
// Higher-half virtual base (matches KERNEL_VIRT_BASE in linker.ld); the
|
|
// linker's AT() clauses give each segment a low physical load address
|
|
// (.text at 1 MiB), which the loader allocates and copies into.
|
|
exe.image_base = 0xFFFFFFFF80100000;
|
|
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 {
|
|
ensureZigVersion();
|
|
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
// The library domain packages (docs/build-packages-plan.md, phase 1): each
|
|
// domain owns a build.zig/zon that wires and exports its modules, and this
|
|
// root build is a consumer — a library interface change now happens in the
|
|
// domain's own build file, not here. The per-module commentary lives with
|
|
// each domain's build.zig.
|
|
const kernel_library = b.dependency("kernel", .{});
|
|
const device_library = b.dependency("device", .{});
|
|
const client_library = b.dependency("client", .{});
|
|
const protocol_library = b.dependency("protocol", .{});
|
|
const csv_library = b.dependency("csv", .{});
|
|
const xkeyboard_config_library = b.dependency("xkeyboard-config", .{});
|
|
|
|
// The three shared contracts, each with its own audience so every import
|
|
// declares which one it speaks (no target is set, so each inherits the target of
|
|
// whichever binary imports it). See docs/coding-standards.md.
|
|
// boot-handoff : loader <-> kernel (BootInformation, framebuffer, VM layout)
|
|
// abi : kernel <-> runtime, core (SystemCall, mmap prot flags, page_size)
|
|
// device-abi : kernel <-> user, devices (DeviceDescriptor, DeviceClass, ...)
|
|
// boot-handoff stays a root module (a system/ source the loader <-> kernel
|
|
// pair speaks); abi is exported by the kernel library package (its source
|
|
// also lives in system/), device-abi by the device package.
|
|
const boot_handoff_module = b.addModule("boot-handoff", .{
|
|
.root_source_file = b.path("system/boot-handoff.zig"),
|
|
});
|
|
const abi_module = kernel_library.module("abi");
|
|
const device_abi_module = device_library.module("device-abi");
|
|
const pci_class_module = device_library.module("pci-class");
|
|
|
|
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
|
// compile-time constants, imported wherever a knob is read; keeps the trade-offs
|
|
// in one place instead of scattered across the tree. See system/parameters.zig.
|
|
const parameters_module = b.addModule("parameters", .{
|
|
.root_source_file = b.path("system/parameters.zig"),
|
|
});
|
|
|
|
// Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging).
|
|
// The generic kernel imports this as "architecture" and never names x86_64, so a new
|
|
// architecture is a matter of pointing this module at a different directory.
|
|
const architecture_module = b.addModule("architecture", .{
|
|
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module }, // paging uses BootInformation/memory-map + physicalToVirtual
|
|
.{ .name = "abi", .module = abi_module }, // paging works in page_size units
|
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
|
|
},
|
|
});
|
|
// CPU-exception stubs — real assembly, since they need cross-symbol
|
|
// jumps/calls that Zig inline asm can't express (see the file's header).
|
|
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/isr.s"));
|
|
// The AP bring-up trampoline: 16-/32-/64-bit mode-switch code that can't be
|
|
// inline asm (it runs relocated to a low page, not at its link address).
|
|
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/trampoline.s"));
|
|
|
|
// Firmware-agnostic device discovery. The generic kernel imports this as
|
|
// "platform" and asks it to enumerate hardware into a backend-neutral device
|
|
// tree, never naming ACPI (or, later, device-tree) — the same discipline the
|
|
// architecture module applies to CPU code. The backend is selected at runtime from
|
|
// the boot handoff (see system/kernel/platform.zig).
|
|
const platform_module = b.addModule("platform", .{
|
|
.root_source_file = b.path("system/kernel/platform.zig"),
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module }, // BootInformation (carries the ACPI RSDP), physicalToVirtual
|
|
.{ .name = "abi", .module = abi_module }, // acpi.zig works in page_size units
|
|
.{ .name = "device-abi", .module = device_abi_module }, // device-model's DeviceClass/ResourceKind live here
|
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
|
|
},
|
|
});
|
|
|
|
// The wire protocols some test fixtures name as per-binary extras,
|
|
// exported by the protocol package.
|
|
const device_manager_protocol_module = protocol_library.module("device-manager-protocol");
|
|
|
|
// The driver-side PCI library some test fixtures name as a per-binary
|
|
// extra. (The rest of the domain modules reach the root only through
|
|
// build-support's defaultImports below.)
|
|
const pci_module = device_library.module("pci");
|
|
|
|
// The initial_ramdisk container format, shared by the kernel (unpacks it) and
|
|
// the EFI loader (packs it in RAM from the boot volume's /system tree). No
|
|
// dependencies.
|
|
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
|
.root_source_file = b.path("system/initial-ramdisk.zig"),
|
|
});
|
|
|
|
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
|
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
|
|
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see system/kernel/tests.zig)");
|
|
// The serial-console log sink. Off by default: a real machine often has no
|
|
// working legacy COM1, and the boot log is kept in RAM (klog) and flushed to
|
|
// disk instead — serial is now only a QEMU convenience. `run-x86-64` and the
|
|
// QEMU test harness (test/qemu_test.py, which asserts on serial markers) turn
|
|
// it on; a flashable `zig build` image leaves it out. See serial.zig.
|
|
const serial = b.option(bool, "serial", "Compile the serial-console log sink into the kernel (default: off; run-x86-64 and the test harness enable it)") orelse false;
|
|
// The diagnose boot: init skips the display service (and demo), so the
|
|
// on-screen boot transcript is never suppressed — the full timestamped
|
|
// timeline stays on the screen for real-hardware debugging by eye.
|
|
const diagnose = b.option(bool, "diagnose", "Boot without the display service so the timestamped boot transcript stays on screen (real-hardware debugging)") orelse false;
|
|
|
|
// --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader ---
|
|
// (See build-support/build.zig for why SSE2 stays enabled.)
|
|
const kernel_target = build_support.freestandingTarget(b);
|
|
|
|
const kernel_modules = KernelModules{
|
|
.boot_handoff = boot_handoff_module,
|
|
.abi = abi_module,
|
|
.device_abi = device_abi_module,
|
|
.architecture = architecture_module,
|
|
.platform = platform_module,
|
|
.parameters = parameters_module,
|
|
.initial_ramdisk = initial_ramdisk_module,
|
|
};
|
|
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
|
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
|
|
|
// 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);
|
|
|
|
// --- init: the first user-space program (a system service) ---
|
|
// The default module set every user binary can import directly, drawn from
|
|
// build-support's single authoritative list so the root stanzas and the
|
|
// binary packages can never drift apart. Per-binary extras are added with
|
|
// programModule(exe).addImport.
|
|
const default_imports = build_support.defaultImports(.{
|
|
.kernel = kernel_library,
|
|
.device = device_library,
|
|
.client = client_library,
|
|
.xkeyboard_config = xkeyboard_config_library,
|
|
});
|
|
|
|
// Binary packages (docs/build-packages-plan.md, phase 2): each binary
|
|
// builds itself against the domain packages via build-support's shared
|
|
// recipe, started in ring 3 by the kernel's user-ELF loader like always;
|
|
// the root build just takes artifacts for the boot image. init receives
|
|
// the root's -Dserial as a dependency option (its liveness heartbeat is a
|
|
// serial/test-build diagnostic the QEMU harness asserts on; a flashable
|
|
// image leaves it out).
|
|
const init_exe = b.dependency("init", .{ .serial = serial }).artifact("init");
|
|
|
|
// --- the rest of the boot tree: /system services and drivers, /test fixtures ---
|
|
// Each is built by the same user-binary recipe and laid out at its FHS path on
|
|
// the boot volume (see `bundled` below). The EFI loader walks the tree at boot
|
|
// and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig).
|
|
const vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "test/system/services/vfs-test/vfs-test.zig");
|
|
// The drivers, each directory its own package: the PS/2 bus family (bus +
|
|
// keyboard + mouse from one package), the xHCI bus driver, the USB HID
|
|
// class drivers, and USB mass storage. Their unit tests ride along.
|
|
const ps2_bus_package = b.dependency("ps2-bus", .{});
|
|
const ps2_bus_exe = ps2_bus_package.artifact("ps2-bus");
|
|
const ps2_keyboard_exe = ps2_bus_package.artifact("ps2-keyboard");
|
|
const ps2_mouse_exe = ps2_bus_package.artifact("ps2-mouse");
|
|
const usb_xhci_bus_exe = b.dependency("usb-xhci-bus", .{}).artifact("usb-xhci-bus");
|
|
const usb_hid_package = b.dependency("usb-hid", .{});
|
|
const usb_hid_keyboard_exe = usb_hid_package.artifact("usb-hid-keyboard");
|
|
const usb_hid_mouse_exe = usb_hid_package.artifact("usb-hid-mouse");
|
|
const usb_storage_package = b.dependency("usb-storage", .{});
|
|
const usb_storage_exe = usb_storage_package.artifact("usb-storage");
|
|
// The FAT filesystem server and the display stack, each its own package
|
|
// (fat's and display's unit tests ride along in their packages).
|
|
const fat_package = b.dependency("fat", .{});
|
|
const fat_exe = fat_package.artifact("fat");
|
|
const display_package = b.dependency("display", .{});
|
|
const display_exe = display_package.artifact("display");
|
|
const display_demo_exe = b.dependency("display-demo", .{}).artifact("display-demo");
|
|
const virtio_gpu_package = b.dependency("virtio-gpu", .{});
|
|
const virtio_gpu_exe = virtio_gpu_package.artifact("virtio-gpu");
|
|
const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "test/system/services/shared-memory-server/shared-memory-server.zig");
|
|
const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "test/system/services/shared-memory-client/shared-memory-client.zig");
|
|
const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "test/system/services/fat-test/fat-test.zig");
|
|
// The first binary package (docs/build-packages-plan.md, phase 2): pci-bus
|
|
// builds itself against the domain packages; the root build just takes the
|
|
// artifact for the boot image.
|
|
const pci_bus_exe = b.dependency("pci-bus", .{}).artifact("pci-bus");
|
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
|
// what the driver-restart scenario drives the crash-loop cap with.
|
|
const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "test/system/services/crash-test/crash-test.zig");
|
|
programModule(crash_test_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
|
const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "test/system/services/device-list/device-list.zig");
|
|
programModule(device_list_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
|
// A test fixture: claims the pci-caps case's extra unclaimed NIC and exercises the
|
|
// driver-side PCI library surface (capabilities, MSI, MSI-X, power, FLR) against it.
|
|
const pci_cap_test_exe = addUserBinary(b, kernel_target, &default_imports, "pci-cap-test", "test/system/services/pci-cap-test/pci-cap-test.zig");
|
|
programModule(pci_cap_test_exe).addImport("pci", pci_module);
|
|
programModule(pci_cap_test_exe).addImport("pci-class", pci_class_module);
|
|
// The IOMMU-enforcement negative test: claims an unclaimed e1000e and fires a rogue
|
|
// DMA that VT-d must fault. Same PCI building blocks as pci-cap-test.
|
|
const iommu_fault_test_exe = addUserBinary(b, kernel_target, &default_imports, "iommu-fault-test", "test/system/services/iommu-fault-test/iommu-fault-test.zig");
|
|
programModule(iommu_fault_test_exe).addImport("pci", pci_module);
|
|
programModule(iommu_fault_test_exe).addImport("pci-class", pci_class_module);
|
|
// The discovery service: one swappable process per firmware
|
|
// (docs/discovery.md), bundled under the neutral ramdisk name
|
|
// "discovery" so the device manager never learns which firmware it is on.
|
|
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
|
// flattened device tree — the aarch64 target flips the default when it
|
|
// lands (docs/arm.md). Each firmware's service is its own package; both
|
|
// export an artifact named "discovery", and this option picks which one
|
|
// ships (only the chosen one is compiled).
|
|
const Discovery = enum { acpi, fdt };
|
|
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
|
const discovery_exe = switch (discovery) {
|
|
.acpi => b.dependency("acpi", .{}).artifact("discovery"),
|
|
.fdt => b.dependency("fdt", .{}).artifact("discovery"),
|
|
};
|
|
const device_manager_exe = b.dependency("device-manager", .{}).artifact("device-manager");
|
|
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
|
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
|
const input_exe = b.dependency("input", .{}).artifact("input");
|
|
const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "test/system/services/input-source/input-source.zig");
|
|
const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "test/system/services/input-test/input-test.zig");
|
|
const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "test/system/services/args-echo/args-echo.zig");
|
|
const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "test/system/services/process-test/process-test.zig");
|
|
const logger_exe = b.dependency("logger", .{}).artifact("logger");
|
|
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI
|
|
// (docs/threading.md). Built threaded so its shared-memory poll is real.
|
|
const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "test/system/services/thread-test/thread-test.zig");
|
|
|
|
// Every user binary and its FHS home on the boot volume. There is no packed
|
|
// ramdisk artifact any more: make-fat-image.py lays each binary out at this
|
|
// path on the image, and the EFI loader walks /system and /test at boot and
|
|
// builds the in-RAM initial_ramdisk table from the trees — the volume's file
|
|
// structure is the single source of truth. Entry names (and hence argv[0] and
|
|
// task names) are these paths with a leading slash. Test fixtures mirror their
|
|
// repo home: test/system/services/<name> in the source tree IS the boot path.
|
|
// init's boot service list is data (/etc/init.csv). -Ddiagnose selects the
|
|
// 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.
|
|
const init_csv_source = if (diagnose) "etc/init-diagnose.csv" else "etc/init.csv";
|
|
const production_bundled = [_]BundledBinary{
|
|
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
|
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
|
.{ .path = "system/services/display", .binary = display_exe.getEmittedBin() },
|
|
.{ .path = "system/services/display-demo", .binary = display_demo_exe.getEmittedBin() },
|
|
.{ .path = "system/services/device-manager", .binary = device_manager_exe.getEmittedBin() },
|
|
.{ .path = "system/services/input", .binary = input_exe.getEmittedBin() },
|
|
.{ .path = "system/services/discovery", .binary = discovery_exe.getEmittedBin() },
|
|
.{ .path = "system/services/logger", .binary = logger_exe.getEmittedBin() },
|
|
// A data file, not a binary: the device registry the manager reads at boot.
|
|
// Packing it under /etc makes the kernel auto-mount /etc as a read-only
|
|
// initrd tree (system/kernel/vfs.zig setInitialRamdisk), so the manager can
|
|
// fs.open("/etc/devices.csv") with no filesystem service running.
|
|
.{ .path = "etc/devices.csv", .binary = b.path("etc/devices.csv") },
|
|
// init's service list, likewise read from the kernel-served initrd /etc.
|
|
.{ .path = "etc/init.csv", .binary = b.path(init_csv_source) },
|
|
.{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/usb-xhci-bus", .binary = usb_xhci_bus_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/usb-hid-keyboard", .binary = usb_hid_keyboard_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/usb-hid-mouse", .binary = usb_hid_mouse_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/usb-storage", .binary = usb_storage_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/virtio-gpu", .binary = virtio_gpu_exe.getEmittedBin() },
|
|
.{ .path = "system/drivers/pci-bus", .binary = pci_bus_exe.getEmittedBin() },
|
|
};
|
|
// The userspace test fixtures under /test. A plain `zig build` produces a clean
|
|
// image WITHOUT them; they are bundled only for a test build — which the QEMU
|
|
// 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
|
|
// forces a compile: the fixture exes build only if `bundled` (below) includes them.
|
|
const test_bundled = [_]BundledBinary{
|
|
.{ .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/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/shared-memory-client", .binary = shared_memory_client_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/crash-test", .binary = crash_test_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/device-list", .binary = device_list_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/pci-cap-test", .binary = pci_cap_test_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/iommu-fault-test", .binary = iommu_fault_test_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/input-source", .binary = input_source_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/input-test", .binary = input_test_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/args-echo", .binary = args_echo_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/process-test", .binary = process_test_exe.getEmittedBin() },
|
|
.{ .path = "test/system/services/thread-test", .binary = thread_test_exe.getEmittedBin() },
|
|
};
|
|
// 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
|
|
// diagnose display-omission is already handled by init_csv_source above.
|
|
var bundled_list: std.ArrayListUnmanaged(BundledBinary) = .empty;
|
|
bundled_list.appendSlice(b.allocator, &production_bundled) catch @panic("OOM");
|
|
if (test_case != null) bundled_list.appendSlice(b.allocator, &test_bundled) catch @panic("OOM");
|
|
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.
|
|
// 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.
|
|
// The loader reads -Dserial too, so its boot-progress breadcrumbs (con_out,
|
|
// which firmware may mirror to a serial console) are silenced by default — a
|
|
// real-hardware boot stays quiet. Fatal-error messages ignore this and always
|
|
// show, so a failed boot still explains itself on screen. See boot/efi.zig.
|
|
const loader_options = b.addOptions();
|
|
loader_options.addOption(bool, "serial", serial);
|
|
const loader_options_module = loader_options.createModule();
|
|
const efiexe = b.addExecutable(.{
|
|
.name = "BOOTX64",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("boot/efi.zig"),
|
|
.target = b.resolveTargetQuery(.{
|
|
.cpu_arch = .x86_64,
|
|
.os_tag = .uefi,
|
|
}),
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
// The bootloader speaks the handoff contract and the ramdisk
|
|
// container it packs the /system tree into — never the user ABI.
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
|
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
|
.{ .name = "build_options", .module = loader_options_module },
|
|
},
|
|
}),
|
|
});
|
|
|
|
// 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(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
|
b.getInstallStep().dependOn(&efi_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, 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(
|
|
[]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
|
|
// and can't be executed natively, so only the shared contracts are unit-tested
|
|
// here (compiled for the host rather than inheriting a freestanding target) —
|
|
// which also compile-checks that the three-way split stays self-consistent.
|
|
const test_step = b.step("test", "Run tests");
|
|
for ([_][]const u8{
|
|
"system/boot-handoff.zig",
|
|
"system/abi.zig",
|
|
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
|
|
}) |root| {
|
|
const mod_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path(root),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
|
}
|
|
|
|
// The library domains and the binary packages own their unit tests (each
|
|
// package's standalone `zig build test` step); the root aggregate
|
|
// delegates to those steps so one command still runs everything and a
|
|
// test added inside a package can never be silently skipped here. Package
|
|
// tests are host-only, so root's -Dtarget/-Doptimize deliberately do not
|
|
// reach them.
|
|
for ([_]*std.Build.Dependency{
|
|
kernel_library,
|
|
device_library,
|
|
client_library,
|
|
protocol_library,
|
|
csv_library,
|
|
xkeyboard_config_library,
|
|
fat_package,
|
|
display_package,
|
|
ps2_bus_package,
|
|
usb_hid_package,
|
|
usb_storage_package,
|
|
virtio_gpu_package,
|
|
}) |package| {
|
|
test_step.dependOn(&package.builder.top_level_steps.get("test").?.step);
|
|
}
|
|
|
|
// The tagged kernel log ring: append/wrap/reclaim/sequence-gap behavior over
|
|
// a RAM buffer. Needs the `abi` module (record header layout), so it doesn't
|
|
// fit the plain loop above.
|
|
const log_ring_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("system/kernel/log-ring.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "abi", .module = abi_module },
|
|
},
|
|
}),
|
|
});
|
|
test_step.dependOn(&b.addRunArtifact(log_ring_tests).step);
|
|
|
|
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
|
// vendored data (offline). `fetch` (the network step) stays a manual script run.
|
|
const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" });
|
|
const gen_xkb_step = b.step("gen-xkeyboard-config", "Regenerate library/xkeyboard-config/generated from the vendored data");
|
|
gen_xkb_step.dependOn(&gen_xkb.step);
|
|
}
|