Follow-up to the monorepo re-org. Suite 35/35 plus host tests green. POSIX compatibility is now its own library, library/posix/ (unistd, stdio), layered strictly over the runtime — it calls the runtime's IPC/heap, never system calls directly. The runtime is now POSIX-free (the danos-native application ABI). The VFS wire protocol is danos-native throughout (Stat -> FileStatus, .stat -> .status, O_CREAT -> create); the POSIX layer maps the POSIX spellings at the boundary. The coding standard's ABI-name exception is scoped to one place: a file is allowed POSIX spellings only if it lives under library/posix/ — everywhere else, danos naming with no exception. Naming fixes, all mechanical: - initrd -> initial-ramdisk: the source file, the module, the tool (make-initial-ramdisk.py), the artifact (initial-ramdisk.img, including the bootloader's load path), and the identifiers. - system/kernel/device-service.zig -> devices-broker.zig: it is ring-0 kernel code (the trusted device table + claim capability), not a ring-3 service. The future user-space device *manager* (policy) will live in system/services/. - Dropped the daemon `d` suffix: hpetd -> hpet, busd -> bus. A driver lives in system/drivers/, so the folder already says what it is; encoding the role in the name too is redundant. The coding standard drops that exception. - system/devices/aml/interp.zig -> interpreter.zig (the type was already Interpreter).
405 lines
19 KiB
Zig
405 lines
19 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];
|
|
}
|
|
|
|
/// 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,
|
|
posix_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 },
|
|
// POSIX/C compatibility layer, available to any program that wants it
|
|
// (danos-native code uses `runtime` directly). See library/posix/.
|
|
.{ .name = "posix", .module = posix_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;
|
|
}
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
ensureZigVersion();
|
|
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
// Shared handoff definitions (BootInformation, Framebuffer, ...). No target is set,
|
|
// so the module inherits the target of whichever binary imports it — the
|
|
// freestanding kernel or the UEFI bootloader.
|
|
const danos_module = b.addModule("danos", .{
|
|
.root_source_file = b.path("system/danos.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 = "danos", .module = danos_module }, // paging uses the shared BootInformation/memory-map types
|
|
.{ .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 = "danos", .module = danos_module }, // BootInformation (carries the ACPI RSDP)
|
|
.{ .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 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 `danos`
|
|
// for the shared SystemCall numbers and re-exports `vfs-protocol` for the VFS
|
|
// server.
|
|
const runtime_module = b.addModule("runtime", .{
|
|
.root_source_file = b.path("library/runtime/runtime.zig"),
|
|
.imports = &.{
|
|
.{ .name = "danos", .module = danos_module },
|
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
},
|
|
});
|
|
|
|
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
|
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
|
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
|
// and library/posix/posix.zig.
|
|
const posix_module = b.addModule("posix", .{
|
|
.root_source_file = b.path("library/posix/posix.zig"),
|
|
.imports = &.{
|
|
.{ .name = "runtime", .module = runtime_module },
|
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
.{ .name = "danos", .module = danos_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=<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)");
|
|
const build_options = b.addOptions();
|
|
build_options.addOption(?[]const u8, "test_case", test_case);
|
|
const build_options_module = 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 = "kernel",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("system/kernel/main.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 = "danos", .module = danos_module },
|
|
.{ .name = "architecture", .module = architecture_module },
|
|
.{ .name = "platform", .module = platform_module },
|
|
.{ .name = "parameters", .module = parameters_module },
|
|
.{ .name = "build_options", .module = build_options_module },
|
|
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
|
},
|
|
}),
|
|
});
|
|
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;
|
|
|
|
b.installArtifact(exe);
|
|
|
|
// --- /sbin/init: the first user-space program ---
|
|
// 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, posix_module, "init", "system/services/init/init.zig");
|
|
b.installArtifact(init_exe);
|
|
|
|
// --- 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, posix_module, "vfs", "system/services/vfs/vfs.zig");
|
|
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
|
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "hpet", "system/drivers/hpet/hpet.zig");
|
|
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, "bus", "system/drivers/bus/bus.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 <out> [<name> <file>]... — 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("hpet");
|
|
mk_run.addFileArg(hpet_exe.getEmittedBin());
|
|
mk_run.addArg("bus");
|
|
mk_run.addFileArg(bus_exe.getEmittedBin());
|
|
|
|
// Install the image to zig-out/bin (so the QEMU test harness picks it up like
|
|
// the other binaries). The run-x86-64 ESP install is added below.
|
|
const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "bin/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.
|
|
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 = &.{
|
|
.{ .name = "danos", .module = danos_module },
|
|
},
|
|
}),
|
|
});
|
|
|
|
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 (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)
|
|
});
|
|
|
|
// 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/kernel.
|
|
const kernel_install = b.addInstallArtifact(exe, .{
|
|
.dest_dir = .{ .override = .{ .custom = "esp" } },
|
|
});
|
|
// The bootloader loads init from sbin/init on the same volume.
|
|
const init_install = b.addInstallArtifact(init_exe, .{
|
|
.dest_dir = .{ .override = .{ .custom = "esp/sbin" } },
|
|
});
|
|
// ...and the initial_ramdisk (VFS server + drivers) from the volume root.
|
|
const initial_ramdisk_esp_install = b.addInstallFile(initial_ramdisk_img, "esp/initial-ramdisk.img");
|
|
|
|
// 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",
|
|
});
|
|
// Always capture the guest's serial0 (the kernel's machine-readable log) to a
|
|
// timestamped file under zig-out, so each run leaves its own log behind.
|
|
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ b.install_path, timestamp(b) });
|
|
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
|
run_efi.step.dependOn(&efi_install.step);
|
|
run_efi.step.dependOn(&kernel_install.step);
|
|
run_efi.step.dependOn(&init_install.step);
|
|
run_efi.step.dependOn(&initial_ramdisk_esp_install.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/run-x86-64-serial0-<timestamp>.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 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("system/danos.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);
|
|
}
|