Re-organize the source tree as a monorepo mirroring the FHS

The source layout now mirrors the runtime filesystem hierarchy
(docs/danos-file-system-hierarchy-FSH.md): what lives under system/ in the
source is what a running danos represents under /system. Each service and
driver is a sub-project directory that is its own Zig module — cross-project
references go by module name, never by a path into another project's files.

Moves (all git mv, history preserved):
- src/            -> system/            (danos internals; the self-representation)
    root.zig      -> danos.zig          (the kernel<->user contract module)
    kernel/arch/  -> kernel/architecture/   (arch -> architecture)
    device/       -> devices/           (what /system/devices reflects)
    boot/         -> /boot              (the loaders, top level)
- sbin/           -> split by role:
    init, vfs     -> system/services/<name>/<name>.zig
    hpetd, busd   -> system/drivers/<name>/<name>.zig
    vfs-test      -> system/services/vfs/vfs-test.zig  (inside the vfs project)
- lib/            -> library/runtime/   (room for other libraries beside runtime)

The VFS wire protocol becomes its own module, system/services/vfs/protocol.zig
("vfs-protocol"): the vfs sub-project exposes its interface, and the runtime's
file layer imports it by name. First instance of the "protocol module" pattern
(docs/driver-model.md); usb/block will expose theirs the same way.

Also: fix a naming-standard violation in the protocol — Op -> Operation (and
req -> request, _pad -> _padding). Docs updated: /system/services added to the
FHS doc, a repository-layout section added to the docs index, and stale source
paths swept across comments and docs.

Runtime boot paths are unchanged (the bootloader still loads /sbin/init);
aligning the runtime filesystem to the FHS is a separate follow-up. Suite 35/35
plus host tests green.
This commit is contained in:
Daniel Samson
2026-07-10 12:55:56 +01:00
parent 15b70856c9
commit 8754d4e46a
83 changed files with 334 additions and 177 deletions
+34 -24
View File
@@ -77,7 +77,7 @@ fn addUserBinary(
},
}),
});
exe.setLinkerScript(b.path("lib/user.ld"));
exe.setLinkerScript(b.path("library/runtime/user.ld"));
exe.entry = .{ .symbol_name = "_start" };
exe.image_base = 0x7000_0000_0000;
exe.use_llvm = true;
@@ -95,21 +95,21 @@ pub fn build(b: *std.Build) void {
// 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("src/root.zig"),
.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 src/configuration.zig.
// in one place instead of scattered across the tree. See system/parameters.zig.
const parameters_module = b.addModule("parameters", .{
.root_source_file = b.path("src/parameters.zig"),
.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("src/kernel/arch/x86_64/cpu.zig"),
.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
@@ -117,45 +117,55 @@ pub fn build(b: *std.Build) void {
});
// 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("src/kernel/arch/x86_64/isr.s"));
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("src/kernel/arch/x86_64/trampoline.s"));
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 src/device/platform.zig).
// the boot handoff (see system/devices/platform.zig).
const platform_module = b.addModule("platform", .{
.root_source_file = b.path("src/device/platform.zig"),
.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 user-space runtime library (a nascent libc): system_call wrappers, the
// C-convention heap, IPC helpers, the process start shim. 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.
// shared SystemCall numbers and `vfs-protocol` for the file API.
const runtime_module = b.addModule("runtime", .{
.root_source_file = b.path("lib/runtime.zig"),
.root_source_file = b.path("library/runtime/runtime.zig"),
.imports = &.{
.{ .name = "danos", .module = danos_module },
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
},
});
// The initrd container format, shared by the kernel (unpacks it) and the
// build-time packer tools/mkinitrd.zig (produces it). No dependencies.
const initrd_module = b.addModule("initrd", .{
.root_source_file = b.path("src/user/protocol/initrd.zig"),
.root_source_file = b.path("system/initrd.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 src/kernel/tests.zig)");
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();
@@ -173,7 +183,7 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "kernel",
.root_module = b.createModule(.{
.root_source_file = b.path("src/kernel/main.zig"),
.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)
@@ -192,7 +202,7 @@ pub fn build(b: *std.Build) void {
},
}),
});
exe.setLinkerScript(b.path("src/kernel/arch/x86_64/linker.ld"));
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
@@ -210,17 +220,17 @@ pub fn build(b: *std.Build) void {
// 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, "init", "sbin/init.zig");
const init_exe = addUserBinary(b, kernel_target, runtime_module, "init", "system/services/init/init.zig");
b.installArtifact(init_exe);
// --- initrd: 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 mkinitrd tool. The bootloader ferries the image to the kernel,
// which unpacks it and spawns each program (src/user/protocol/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "sbin/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "sbin/vfs-test.zig");
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "sbin/hpetd.zig");
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "sbin/busd.zig");
// which unpacks it and spawns each program (system/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "system/services/vfs/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "system/services/vfs/vfs-test.zig");
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "system/drivers/hpetd/hpetd.zig");
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "system/drivers/busd/busd.zig");
// Pack the user binaries into the initrd image with the host-side Python tool
// (the container format is trivial, and Python sidesteps std API churn). Args:
@@ -242,13 +252,13 @@ pub fn build(b: *std.Build) void {
const initrd_install = b.addInstallFile(initrd_img, "bin/initrd.img");
b.getInstallStep().dependOn(&initrd_install.step);
// Boot methods live in src/boot/, one per way of getting the kernel running.
// 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("src/boot/efi.zig"),
.root_source_file = b.path("boot/efi.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .uefi,
@@ -362,7 +372,7 @@ pub fn build(b: *std.Build) void {
// 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"),
.root_source_file = b.path("system/danos.zig"),
.target = target,
.optimize = optimize,
}),