const std = @import("std"); const builtin = @import("builtin"); /// 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(), }); } /// Build one user-space binary the same way for every program (init, and later /// the VFS server + drivers): freestanding, ReleaseSmall, `.large` code model /// (the image base is above 4 GiB — smaller models emit 32-bit relocations that /// can't reach), linked against the `runtime` runtime library with the shared user /// link script. Pinned to LLVM + LLD so the script's PHDRS (segment permissions) /// are authoritative — the kernel's W^X user-ELF loader requires exact perms. fn addUserBinary( b: *std.Build, target: std.Build.ResolvedTarget, runtime_module: *std.Build.Module, mmio_module: *std.Build.Module, xkeyboard_config_module: *std.Build.Module, acpi_ids_module: *std.Build.Module, name: []const u8, root: []const u8, ) *std.Build.Step.Compile { const exe = b.addExecutable(.{ .name = name, .root_module = b.createModule(.{ .root_source_file = b.path(root), .target = target, .optimize = .ReleaseSmall, .code_model = .large, .single_threaded = true, .sanitize_c = .off, .stack_check = false, .stack_protector = false, .imports = &.{ .{ .name = "runtime", .module = runtime_module }, // Typed volatile MMIO + memory barriers, for drivers. See library/mmio/. .{ .name = "mmio", .module = mmio_module }, // Keyboard layouts (keycode + modifiers -> keysym/character), available // to any program that wants it. See library/xkeyboard-config/. .{ .name = "xkeyboard-config", .module = xkeyboard_config_module }, // ACPI/PnP hardware-ID registry, so drivers name devices // (HardwareId.ps2_keyboard) instead of magic "_HID" strings. .{ .name = "acpi-ids", .module = acpi_ids_module }, }, }), }); exe.setLinkerScript(b.path("library/runtime/user.ld")); exe.entry = .{ .symbol_name = "_start" }; exe.image_base = 0x7000_0000_0000; exe.use_llvm = true; exe.use_lld = true; return 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, .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; } /// Assemble the bootable FAT32 image (the in-repo Python builder) holding what /// the firmware and loader need off the ESP: the EFI stub, `kernel`, `init`, and /// the initial-ramdisk. Factored so the serial-enabled `run-x86-64` variant can /// bundle its own kernel while sharing the (serial-independent) loader, init, and /// ramdisk. Returns the image's LazyPath. fn addBootImage( b: *std.Build, kernel_bin: std.Build.LazyPath, efi_bin: std.Build.LazyPath, init_bin: std.Build.LazyPath, initial_ramdisk_img: std.Build.LazyPath, ) 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/services/init"); mk_fat.addFileArg(init_bin); mk_fat.addArg("boot/initial-ramdisk.img"); mk_fat.addFileArg(initial_ramdisk_img); return fat_image; } pub fn build(b: *std.Build) void { ensureZigVersion(); const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // 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, ...) const boot_handoff_module = b.addModule("boot-handoff", .{ .root_source_file = b.path("system/boot-handoff.zig"), }); const abi_module = b.addModule("abi", .{ .root_source_file = b.path("system/abi.zig"), }); // The devices sub-project's public interface (the flat wire types), exposed as // its own module like vfs-protocol — importable by user space, unlike the // kernel-internal device model it also feeds (system/devices/device-model.zig). const device_abi_module = b.addModule("device-abi", .{ .root_source_file = b.path("system/devices/device-abi.zig"), }); // PCI class-code decoding (class/subclass/prog-IF -> names). Pure reference data, // shared by kernel discovery (the device-tree dump) and any user-space PCI tool. const pci_class_module = b.addModule("pci-class", .{ .root_source_file = b.path("system/devices/pci-class.zig"), }); // ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device // nodes. Also shared reference data. // The AML interpreter, a build module so the ring-3 acpi service can run the // same parser the kernel does (docs/discovery.md — the shared AML module). // Pure Zig, no kernel imports — one source, two builds. const aml_module = b.addModule("aml", .{ .root_source_file = b.path("system/devices/aml/aml.zig"), }); const acpi_ids_module = b.addModule("acpi-ids", .{ .root_source_file = b.path("system/devices/acpi-ids.zig"), }); // The USB device-framework wire ABI (chapter-9 set-up packets, standard + // class requests, descriptors) and the USB class-code taxonomy — the flat // reference the xHCI bus driver, the USB class drivers, and the device // manager's identity matcher all share. Pure data, like pci-class/acpi-ids. const usb_abi_module = b.addModule("usb-abi", .{ .root_source_file = b.path("system/devices/usb-abi.zig"), }); const usb_ids_module = b.addModule("usb-ids", .{ .root_source_file = b.path("system/devices/usb-ids.zig"), }); // The USB transfer protocol: what a USB class driver says to the xHCI bus // driver to drive its device (open / control / interrupt / bulk). A protocol // module like vfs-protocol, shared by the bus driver and every class driver. const usb_transfer_protocol_module = b.addModule("usb-transfer-protocol", .{ .root_source_file = b.path("system/drivers/usb-xhci-bus/usb-transfer-protocol.zig"), }); // The block-device protocol: read/write of fixed-size blocks, spoken between a // filesystem and a block driver (usb-storage). A protocol module like the rest. const block_protocol_module = b.addModule("block-protocol", .{ .root_source_file = b.path("system/services/block/protocol.zig"), }); // 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/devices/platform.zig). const platform_module = b.addModule("platform", .{ .root_source_file = b.path("system/devices/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 = "pci-class", .module = pci_class_module }, // decode PCI class codes in the device dump .{ .name = "acpi-ids", .module = acpi_ids_module }, // decode ACPI _HID names in the device dump .{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool) }, }); // The VFS wire protocol: the vfs sub-project's public interface, exposed as its // own module. Both the vfs server and the runtime's file layer (unistd/stdio) // depend on this contract by name — neither reaches into the other's files. This // is the first "protocol module" (see docs/driver-model.md); usb/block will // expose theirs the same way. const vfs_protocol_module = b.addModule("vfs-protocol", .{ .root_source_file = b.path("system/services/vfs/protocol.zig"), }); // The input wire protocol: the input service's public interface, exposed as its own // module the same way vfs-protocol is. Shared by the input service, the runtime's // `input` helper (subscribe/publish), and every source and subscriber. const input_protocol_module = b.addModule("input-protocol", .{ .root_source_file = b.path("system/services/input/protocol.zig"), }); // The danos-native user-space runtime: system_call wrappers, the C-convention // heap, IPC helpers, the process start shim, device access. This is the stable // application ABI; POSIX compatibility is a separate library on top (see below). // Compiled into every user binary (see addUserBinary), so it inherits each exe's // `.large` code model — do NOT set a target/code_model here. It imports `abi` // for the shared SystemCall numbers / mmap flags, `device-abi` for the device // types its `device` helper wraps, and re-exports `vfs-protocol` for the VFS // server. It never touches `boot-handoff` — user space has no business with the // loader↔kernel handoff. const runtime_module = b.addModule("runtime", .{ .root_source_file = b.path("library/runtime/runtime.zig"), .imports = &.{ .{ .name = "abi", .module = abi_module }, .{ .name = "device-abi", .module = device_abi_module }, .{ .name = "vfs-protocol", .module = vfs_protocol_module }, .{ .name = "input-protocol", .module = input_protocol_module }, }, }); // The device-manager protocol: hello + (M18.2) tree reports, exposed as its // own module like the other protocol modules. Imported through the runtime. const device_manager_protocol_module = b.addModule("device-manager-protocol", .{ .root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"), }); runtime_module.addImport("device-manager-protocol", device_manager_protocol_module); // The USB transfer protocol, so runtime.usb (the class-driver client) can speak // it, the way runtime.input speaks the input protocol. runtime_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module); // The block protocol, so runtime.block (the block-device client) can speak it. runtime_module.addImport("block-protocol", block_protocol_module); // The power protocol: system power's domain-named surface (docs/power.md). const power_protocol_module = b.addModule("power-protocol", .{ .root_source_file = b.path("system/services/power/protocol.zig"), }); runtime_module.addImport("power-protocol", power_protocol_module); // Typed volatile MMIO register access + memory-ordering barriers, for drivers on // top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers); // no target set, so it inherits each driver's. See library/mmio/mmio.zig. const mmio_module = b.addModule("mmio", .{ .root_source_file = b.path("library/mmio/mmio.zig"), }); // Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig // (keycode + modifiers -> keysym/character). The `layouts` tables are generated by // tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them. // No target set, so each inherits its importer's. See library/xkeyboard-config/. const xkb_layouts_module = b.addModule("layouts", .{ .root_source_file = b.path("library/xkeyboard-config/generated/layouts.zig"), }); const xkeyboard_config_module = b.addModule("xkeyboard-config", .{ .root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"), .imports = &.{ .{ .name = "layouts", .module = xkb_layouts_module }, }, }); // The initial_ramdisk container format, shared by the kernel (unpacks it) and the // build-time packer tools/make-initial-ramdisk.py (produces it). 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= 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; // --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader --- // SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff, // so we keep it: disabling it forces soft-float and makes the compiler unable // to encode the vector ops that std's formatting/runtime still emit. const kernel_target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .freestanding, .abi = .none, }); 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) --- // Built by the shared user-binary recipe (see addUserBinary): freestanding, // linked into the kernel's user region against the `runtime` runtime library, and // started in ring 3 by the kernel's user-ELF loader. const init_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig"); const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } }); b.getInstallStep().dependOn(&init_install.step); // --- initial_ramdisk: a bundle of extra user binaries (VFS server + drivers) --- // Each is built by the same user-binary recipe, then packed into one image by // the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel, // which unpacks it and spawns each program (system/initial-ramdisk.zig). const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig"); const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig"); const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); // The xHCI bus driver builds chapter-9 requests and decodes descriptors from // usb-abi, and reports each interface's (class,subclass,protocol) identity via // usb-ids.packTriple. usb_xhci_bus_exe.root_module.addImport("usb-abi", usb_abi_module); usb_xhci_bus_exe.root_module.addImport("usb-ids", usb_ids_module); usb_xhci_bus_exe.root_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module); // The USB HID class drivers: keyboard and mouse. They own no hardware — each // opens its device through runtime.usb (the transfer protocol) and publishes to // the input service. They build chapter-9 class requests from usb-abi. const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig"); usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module); const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig"); usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module); // The USB mass-storage class driver: opens its device via runtime.usb, drives it // with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`. const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig"); usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module); // The FAT filesystem server: mounts the block device and serves it into the VFS // at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively. const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig"); const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig"); const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); // The PCI bus driver decodes each function's class triple to human names in its // boot log (class/subclass/prog-IF), so pull in the shared pci-class reference. pci_bus_exe.root_module.addImport("pci-class", pci_class_module); // 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, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); const device_list_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig"); // 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). Both are placeholders until M20.1 (acpi) and the // ARM bring-up (fdt). 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_source: []const u8 = switch (discovery) { .acpi => "system/services/acpi/acpi.zig", .fdt => "system/services/fdt/fdt.zig", }; const discovery_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source); if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330. device_manager_exe.root_module.addImport("pci-class", pci_class_module); // The manager matches reported USB interfaces by their (class,subclass,protocol) // triple (usbDriverForIdentity), built from the named usb-ids codes. device_manager_exe.root_module.addImport("usb-ids", usb_ids_module); // 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 = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig"); const input_source_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig"); const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig"); const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig"); const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig"); const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig"); // Pack the user binaries into the initial_ramdisk image with the host-side Python tool // (the container format is trivial, and Python sidesteps std API churn). Args: // make-initial-ramdisk.py [ ]... — one name/file pair per binary. const mk_run = b.addSystemCommand(&.{"python3"}); mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py")); const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img"); mk_run.addArg("vfs"); mk_run.addFileArg(vfs_exe.getEmittedBin()); mk_run.addArg("vfs-test"); mk_run.addFileArg(vfstest_exe.getEmittedBin()); mk_run.addArg("ps2-bus"); mk_run.addFileArg(ps2_bus_exe.getEmittedBin()); mk_run.addArg("ps2-keyboard"); mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin()); mk_run.addArg("ps2-mouse"); mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addArg("usb-xhci-bus"); mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); mk_run.addArg("usb-hid-keyboard"); mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin()); mk_run.addArg("usb-hid-mouse"); mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin()); mk_run.addArg("usb-storage"); mk_run.addFileArg(usb_storage_exe.getEmittedBin()); mk_run.addArg("fat"); mk_run.addFileArg(fat_exe.getEmittedBin()); mk_run.addArg("fat-test"); mk_run.addFileArg(fat_test_exe.getEmittedBin()); mk_run.addArg("pci-bus"); mk_run.addFileArg(pci_bus_exe.getEmittedBin()); mk_run.addArg("crash-test"); mk_run.addFileArg(crash_test_exe.getEmittedBin()); mk_run.addArg("device-list"); mk_run.addFileArg(device_list_exe.getEmittedBin()); mk_run.addArg("discovery"); mk_run.addFileArg(discovery_exe.getEmittedBin()); mk_run.addArg("device-manager"); mk_run.addFileArg(device_manager_exe.getEmittedBin()); mk_run.addArg("input"); mk_run.addFileArg(input_exe.getEmittedBin()); mk_run.addArg("input-source"); mk_run.addFileArg(input_source_exe.getEmittedBin()); mk_run.addArg("input-test"); mk_run.addFileArg(input_test_exe.getEmittedBin()); mk_run.addArg("args-echo"); mk_run.addFileArg(args_echo_exe.getEmittedBin()); mk_run.addArg("process-test"); mk_run.addFileArg(process_test_exe.getEmittedBin()); mk_run.addArg("log-flush"); mk_run.addFileArg(log_flush_exe.getEmittedBin()); // Also install the packed binaries to their FHS homes, so zig-out is a true image // of the filesystem — even though at boot they arrive inside the initial-ramdisk. for ([_]struct { *std.Build.Step.Compile, []const u8 }{ .{ vfs_exe, "system/services" }, .{ device_manager_exe, "system/services" }, .{ input_exe, "system/services" }, .{ ps2_bus_exe, "system/drivers" }, .{ ps2_keyboard_exe, "system/drivers" }, .{ ps2_mouse_exe, "system/drivers" }, .{ usb_xhci_bus_exe, "system/drivers" }, .{ usb_hid_keyboard_exe, "system/drivers" }, .{ usb_hid_mouse_exe, "system/drivers" }, .{ usb_storage_exe, "system/drivers" }, .{ fat_exe, "system/services" }, .{ log_flush_exe, "system/services" }, }) |entry| { const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); b.getInstallStep().dependOn(&step.step); } // The initial-ramdisk itself installs to /boot (with the loaders). const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "boot/initial-ramdisk.img"); b.getInstallStep().dependOn(&initial_ramdisk_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 only the handoff contract — never the user ABI. .{ .name = "boot-handoff", .module = boot_handoff_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 exactly what the firmware and bootloader need off the ESP: the EFI // stub, the kernel, init, and the initial-ramdisk. 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(), init_exe.getEmittedBin(), initial_ramdisk_img); 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(), init_exe.getEmittedBin(), initial_ramdisk_img); // `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); // --- 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 FHS zig-out (installed above) *is* the boot volume — no separate ESP to // assemble. QEMU presents it to the guest as a FAT drive below. // 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-.log"); run_efi_step.dependOn(&run_efi.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/devices/device-abi.zig", "system/devices/pci-class.zig", // class/subclass/prog-IF name decoding "system/devices/acpi-ids.zig", // _HID name decoding "system/devices/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21) "system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings "system/devices/usb-ids.zig", // class/subclass/protocol code assignments "library/mmio/mmio.zig", // barriers assemble + registers round-trip "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly "system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode "system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes "system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian) "system/services/vfs/path.zig", // mount-prefix path matching "system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values "system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection "system/services/fat/engine.zig", // FAT read/write over a RAM-backed image }) |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 xkeyboard-config keymap tests need its generated `layouts` import wired, so they // don't fit the plain loop above. Its keycode->character assertions are the end-to-end // proof that the xkb-data -> generator -> Zig-lookup pipeline is correct. const xkb_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "layouts", .module = xkb_layouts_module }, }, }), }); test_step.dependOn(&b.addRunArtifact(xkb_tests).step); // runtime.time's Instant/Duration arithmetic. time.zig pulls in system.zig (the // syscall wrappers), which needs the `abi` module, so it doesn't fit the plain // loop above. const time_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("library/runtime/time.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "abi", .module = abi_module }, }, }), }); test_step.dependOn(&b.addRunArtifact(time_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); }