Make zig-out a FHS image, and the boot volume

`zig build` now installs into a FHS-shaped zig-out that *is* the danos filesystem
and the boot volume — no more zig-out/bin or a separate esp/:

  zig-out/EFI/BOOT/BOOTX64.efi        (firmware entry; UEFI fixes this path)
  zig-out/boot/initial-ramdisk.img
  zig-out/system/kernel               (the kernel binary)
  zig-out/system/services/init  vfs
  zig-out/system/drivers/hpet   bus

Binaries land at their addressed, leaf-collapsed paths per the sub-project
resolution rule (system/services/init/init.zig -> system/services/init); vfs, hpet,
and bus are installed to their FHS homes too, so the image is complete even though
at boot they arrive inside the initial-ramdisk.

The bootloader (boot/efi.zig) now loads each artifact from its FHS path
(system\kernel, system\services\init, boot\initial-ramdisk.img); run-x86-64 mounts
zig-out directly; the QEMU test harness assembles its ESP from the FHS zig-out.

Also renames system/kernel/main.zig -> kernel.zig so the kernel follows the
name/name.zig convention (kernel/ = ring-0 code, services/ = ring-3 OS services).
Documents the resolution rule in the repository-layout section (README + coding
standard). Suite 35/35 plus host tests green.
This commit is contained in:
Daniel Samson
2026-07-10 13:57:02 +01:00
parent ceacc6b514
commit 3d1de37d0e
10 changed files with 103 additions and 62 deletions
+34 -29
View File
@@ -202,7 +202,7 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "kernel",
.root_module = b.createModule(.{
.root_source_file = b.path("system/kernel/main.zig"),
.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)
@@ -233,14 +233,21 @@ pub fn build(b: *std.Build) void {
// (.text at 1 MiB), which the loader allocates and copies into.
exe.image_base = 0xFFFFFFFF80100000;
b.installArtifact(exe);
// 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);
// --- /sbin/init: the first user-space program ---
// --- 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, posix_module, "init", "system/services/init/init.zig");
b.installArtifact(init_exe);
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
@@ -266,9 +273,19 @@ pub fn build(b: *std.Build) void {
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");
// 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" },
.{ hpet_exe, "system/drivers" },
.{ bus_exe, "system/drivers" },
}) |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.
@@ -289,7 +306,10 @@ pub fn build(b: *std.Build) void {
}),
});
b.installArtifact(efiexe);
// 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);
// --- 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
@@ -320,21 +340,8 @@ pub fn build(b: *std.Build) void {
"/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 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 });
@@ -351,10 +358,10 @@ pub fn build(b: *std.Build) void {
});
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.
// Present the FHS zig-out to the guest as a FAT drive — it is the boot volume.
run_efi.addArgs(&.{
"-drive",
b.fmt("format=raw,file=fat:rw:{s}/esp", .{b.install_path}),
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}),
"-net",
"none",
// Emulated display advertising 1280x720 as its native (EDID preferred)
@@ -369,10 +376,8 @@ pub fn build(b: *std.Build) void {
// 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);
// The whole FHS zig-out must be installed before we mount it.
run_efi.step.dependOn(b.getInstallStep());
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);