218 lines
9.0 KiB
Zig
218 lines
9.0 KiB
Zig
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=<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 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);
|
|
}
|