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]; } pub fn build(b: *std.Build) void { ensureZigVersion(); const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Shared handoff definitions (BootInfo, Framebuffer, ...). No target is set, // so the module inherits the target of whichever binary imports it — the // freestanding kernel or the UEFI bootloader. const mod = b.addModule("danos", .{ .root_source_file = b.path("src/root.zig"), }); // Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging). // The generic kernel imports this as "arch" and never names x86_64, so a new // architecture is a matter of pointing this module at a different directory. const arch_mod = b.addModule("arch", .{ .root_source_file = b.path("src/arch/x86_64/cpu.zig"), .imports = &.{ .{ .name = "danos", .module = mod }, // paging uses the shared BootInfo/memory-map types }, }); // CPU-exception stubs — real assembly, since they need cross-symbol // jumps/calls that Zig inline asm can't express (see the file's header). arch_mod.addAssemblyFile(b.path("src/arch/x86_64/isr.s")); // Compile-time config 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 src/tests.zig)"); const build_options = b.addOptions(); build_options.addOption(?[]const u8, "test_case", test_case); const build_options_mod = build_options.createModule(); // --- 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 exe = b.addExecutable(.{ .name = "danos", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = kernel_target, .optimize = optimize, .code_model = .small, // kernel is linked in the low 2 GiB (see image_base) .red_zone = false, // interrupts would corrupt the SysV red zone .single_threaded = true, // no scheduler yet; avoids pulling in TLS/atomics .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 = "danos", .module = mod }, .{ .name = "arch", .module = arch_mod }, .{ .name = "build_options", .module = build_options_mod }, }, }), }); exe.setLinkerScript(b.path("src/arch/x86_64/linker.ld")); exe.entry = .{ .symbol_name = "_start" }; // Physical address the bootloader loads the kernel to (identity-mapped under // UEFI). Overrides Zig's default image base so the linker script's layout is // honoured; adjust here if it collides with firmware-reserved memory. exe.image_base = 0x100000; // 1 MiB b.installArtifact(exe); const efiexe = b.addExecutable(.{ .name = "BOOTX64", .root_module = b.createModule(.{ .root_source_file = b.path("src/efi.zig"), .target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .uefi, }), .optimize = optimize, .imports = &.{ .{ .name = "danos", .module = mod }, }, }), }); b.installArtifact(efiexe); // --- 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 (Arch, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first // that exists. Override with -Dovmf-code / -Dovmf-vars if yours is elsewhere. const ovmf_code = b.option( []const u8, "ovmf-code", "Path to the OVMF_CODE firmware image", ) orelse firstExisting(b.graph.io, &.{ "/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Arch "/usr/share/OVMF/OVMF_CODE_4M.fd", // Debian/Ubuntu "/usr/share/OVMF/OVMF_CODE.fd", // older Debian/Ubuntu "/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", // Fedora "/opt/homebrew/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Apple Silicon) "/usr/local/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Intel) }); const ovmf_vars = b.option( []const u8, "ovmf-vars", "Path to the OVMF_VARS firmware image (a writable copy is made)", ) orelse firstExisting(b.graph.io, &.{ "/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Arch "/usr/share/OVMF/OVMF_VARS_4M.fd", // Debian/Ubuntu "/usr/share/OVMF/OVMF_VARS.fd", // older Debian/Ubuntu "/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", // Fedora "/opt/homebrew/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Apple Silicon) "/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel) }); // Assemble an EFI System Partition layout: esp/EFI/BOOT/BOOTX64.efi const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "esp/EFI/BOOT" } }, }); // The bootloader loads the kernel by name from the volume root, so drop the // kernel ELF at esp/danos. const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "esp" } }, }); // 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", "-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); // Present the ESP directory to the guest as a FAT drive. run_efi.addArgs(&.{ "-drive", b.fmt("format=raw,file=fat:rw:{s}/esp", .{b.install_path}), "-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", }); run_efi.step.dependOn(&efi_install.step); run_efi.step.dependOn(&kernel_install.step); const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF)"); 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 module is unit-tested // here (compiled for the host rather than inheriting a freestanding target). const mod_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }), }); const run_mod_tests = b.addRunArtifact(mod_tests); const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_mod_tests.step); }