diff --git a/README.md b/README.md index 353f09a..ed81ae7 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,10 @@ channels. See [`docs/`](docs/README.md) for how each piece works. zig build ``` -Produces the UEFI bootloader (`zig-out/bin/BOOTX64.efi`) and the kernel ELF -(`zig-out/bin/kernel`). +Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume: +the UEFI bootloader at `zig-out/EFI/BOOT/BOOTX64.efi`, the kernel at +`zig-out/system/kernel`, init at `zig-out/system/services/init`, drivers under +`zig-out/system/drivers/`, and the initial-ramdisk at `zig-out/boot/`. ## Run diff --git a/boot/efi.zig b/boot/efi.zig index 5178336..c23f32f 100644 --- a/boot/efi.zig +++ b/boot/efi.zig @@ -7,16 +7,18 @@ const GraphicsOutput = uefi.protocol.GraphicsOutput; const EdidActive = uefi.protocol.edid.Active; const MemoryMapSlice = uefi.tables.MemoryMapSlice; -/// Name of the kernel ELF on the boot volume (installed to the ESP root by -/// build.zig). UEFI wants a UTF-16, null-terminated path. -const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("kernel"); +// The boot volume is the FHS-shaped zig-out (see build.zig / docs/README.md), so the +// loader reads each artifact from its addressed FHS path. UEFI paths use backslashes; +// the FAT driver walks the components itself, so no per-directory dance is needed. -/// Path of the init program on the boot volume (UEFI paths use backslashes; -/// the FAT driver walks the components itself, so no directory dance needed). -const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("sbin\\init"); +/// The kernel image: /system/kernel. +const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel"); -/// Path of the initial_ramdisk image on the boot volume (the VFS server + drivers). -const initial_ramdisk_file_name = std.unicode.utf8ToUtf16LeStringLiteral("initial-ramdisk.img"); +/// The init program: /system/services/init. +const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\services\\init"); + +/// The initial-ramdisk (the VFS server + drivers), in /boot. +const initial_ramdisk_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\initial-ramdisk.img"); /// Physical page size, and the sentinel UEFI uses to seek to end-of-file. const page_size = 4096; diff --git a/build.zig b/build.zig index a908462..7c5a4b0 100644 --- a/build.zig +++ b/build.zig @@ -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-.log"); run_efi_step.dependOn(&run_efi.step); diff --git a/docs/README.md b/docs/README.md index 1761ea0..8bd298e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -125,6 +125,25 @@ reach it *by module name*, never by a path into its files. The source tree delib what you see under `system/` in the source is what a running danos represents under `/system`. +**A sub-project is addressed by its directory; its entry point repeats the directory's +name.** `system/services/init/` contains `init.zig` (its root), and produces a binary +addressed as **`system/services/init`** — the repeated leaf resolves away: + +| Source (root file) | Addressed as (module / binary / FHS path) | +|----------------------------------------|--------------------------------------------| +| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` | +| `system/drivers/hpet/hpet.zig` | `system/drivers/hpet` → `/system/drivers/hpet` | +| `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) | + +In **source**, a sub-project is a directory so it can hold many files — the entry is +`init/init.zig`, beside it `vfs/vfs-test.zig`, `vfs/protocol.zig`, and so on. When +**addressed or installed**, that collapses to the single canonical path: the `init` +binary installs to `/system/services/init` (a file at that path), not +`/system/services/init/init`. The repeated leaf exists only in source; the directory is +the identity, the entry file is its implementation. (Same idea as a Go package being its +directory, or a macOS `.app` bundle addressed by the bundle, not the executable within.) +A sub-project's extra files are reached through the module, never as separate paths. + ``` system/ → /system danos's own internals (the self-representation) danos.zig the kernel↔user ABI contract (the `danos` module) @@ -157,7 +176,7 @@ appears in the private-ABI path. | Area | Code | |------|------| | Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` | -| Kernel entry, panic, bring-up | `system/kernel/main.zig` | +| Kernel entry, panic, bring-up | `system/kernel/kernel.zig` | | Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/danos.zig` | | Physical frame allocator | `system/kernel/pmm.zig` | | Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` | diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 2ac6dfa..5b5e678 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -137,6 +137,11 @@ single word or acronym needs no hyphen: `scheduler.zig`, `paging.zig`, `apic.zig `idt.zig`. (The module *alias* a file is imported under still follows the code conventions above — `snake_case` — because it's an identifier, not a filename.) +**A sub-project's entry point repeats its directory's name** — `init/init.zig`, +`runtime/runtime.zig`, `hpet/hpet.zig` — and the sub-project is addressed by the +*directory* (`system/services/init`, `library/runtime`), with the repeated leaf +resolving away. See the repository-layout section of [README.md](README.md). + ## Why acronyms are the line Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped diff --git a/docs/efi.md b/docs/efi.md index c70cb0d..0140b19 100644 --- a/docs/efi.md +++ b/docs/efi.md @@ -20,14 +20,17 @@ UEFI boots by looking for a FAT-formatted partition called the **EFI System Partition (ESP)** and running a file at a well-known fallback path: ``` -esp/EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64 +EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64 ``` -That's exactly the layout `build.zig` assembles. It builds `boot/efi.zig` for the -`uefi` target, installs it to `esp/EFI/BOOT/BOOTX64.efi`, and drops the kernel ELF -at `esp/kernel`. The `run-x86-64` step then points QEMU at OVMF (UEFI firmware for -virtual machines) and presents that `esp/` directory to the guest as a FAT drive. -The firmware finds `BOOTX64.efi` and runs it — that's our `main()`. +The boot volume is the **FHS-shaped `zig-out`** itself (see the repository-layout note +in [README.md](README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi` +target) to `zig-out/EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays +the rest out by FHS path: the kernel at `zig-out/system/kernel`, init at +`zig-out/system/services/init`, the initial-ramdisk at `zig-out/boot/`. The +`run-x86-64` step points QEMU at OVMF (UEFI firmware for virtual machines) and presents +`zig-out` to the guest as a FAT drive. The firmware finds `BOOTX64.efi` and runs it — +that's our `main()`, which then loads the kernel and init from their FHS paths. ## Boot services: the firmware's API @@ -164,13 +167,13 @@ the loader writes are the bytes the kernel reads. ``` power on -> UEFI firmware initialises hardware - -> finds esp/EFI/BOOT/BOOTX64.efi, runs it (our efi.zig main) + -> finds EFI/BOOT/BOOTX64.efi on the FHS volume, runs it (our efi.zig main) -> grab boot services -> queryFramebuffer (via GOP: EDID native res, setMode, describe fb) - -> loadKernel (read danos ELF, load PT_LOAD segments to 0x100000) + -> loadKernel (read system/kernel ELF, load PT_LOAD segments to 0x100000) -> exitBootServices (retry until the memory-map key holds) -> jump to e_entry, boot_info pointer in RDI - -> kernel _start (system/kernel/main.zig: framebuffer console, then halt) + -> kernel _start (system/kernel/kernel.zig: framebuffer console, then halt) ``` Bottom line: **UEFI's job is to give us a CPU, memory, and a framebuffer, then diff --git a/docs/halting.md b/docs/halting.md index b7bd322..37b2cc5 100644 --- a/docs/halting.md +++ b/docs/halting.md @@ -83,7 +83,7 @@ treats the call: signature for a kernel entry point — the bootloader jumps in and nothing ever jumps back out. -You can see the chain in `system/kernel/main.zig`: `_start` is `noreturn`, it calls +You can see the chain in `system/kernel/kernel.zig`: `_start` is `noreturn`, it calls `kmain` which is `noreturn`, which ends by calling `arch.halt()` which is `noreturn`. The "never returns" property is threaded all the way down. diff --git a/system/danos.zig b/system/danos.zig index cbaaad4..2d02c6d 100644 --- a/system/danos.zig +++ b/system/danos.zig @@ -1,5 +1,5 @@ //! Shared definitions that form the contract between a bootloader -//! (boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (system/kernel/main.zig). +//! (boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (system/kernel/kernel.zig). //! //! Both binaries import this as the "danos" module, so the handoff layout is //! defined in exactly one place. diff --git a/system/kernel/main.zig b/system/kernel/kernel.zig similarity index 100% rename from system/kernel/main.zig rename to system/kernel/kernel.zig diff --git a/test/qemu_test.py b/test/qemu_test.py index 15f471b..f5e2e2b 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -55,11 +55,14 @@ ARCHES = { "/opt/homebrew/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Apple Silicon) "/usr/local/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Intel) ], - "efi_app": ("EFI/BOOT/BOOTX64.efi", "BOOTX64.efi"), # (dest in ESP, name in zig-out/bin) - "kernel": ("kernel", "kernel"), - # Further files shipped on the ESP: the init user program and the initial_ramdisk - # (VFS server + drivers), both copied from zig-out/bin. - "extra": [("sbin/init", "init"), ("initial-ramdisk.img", "initial-ramdisk.img")], + # zig-out is a FHS-shaped image and the boot volume; the harness copies the + # boot-critical files from their FHS paths into a fresh ESP with the same + # layout. (dest in ESP, source path under zig-out) — identical here. + "efi_app": ("EFI/BOOT/BOOTX64.efi", "EFI/BOOT/BOOTX64.efi"), + "kernel": ("system/kernel", "system/kernel"), + # The init user program and the initial-ramdisk (VFS server + drivers). + "extra": [("system/services/init", "system/services/init"), + ("boot/initial-ramdisk.img", "boot/initial-ramdisk.img")], # Built as a function so we can splice in per-run paths. "qemu_args": lambda a, esp, vars_fd, serial: [ "-machine", "q35", "-m", "128M", @@ -237,14 +240,16 @@ def make_esp(arch): esp = os.path.join(WORK, "esp") if os.path.exists(esp): shutil.rmtree(esp) - efi_dest, efi_name = arch["efi_app"] - kern_dest, kern_name = arch["kernel"] + efi_dest, efi_src = arch["efi_app"] + kern_dest, kern_src = arch["kernel"] + fhs = os.path.join(REPO, "zig-out") # zig-out is the FHS image os.makedirs(os.path.join(esp, os.path.dirname(efi_dest)), exist_ok=True) - shutil.copy(os.path.join(REPO, "zig-out", "bin", efi_name), os.path.join(esp, efi_dest)) - shutil.copy(os.path.join(REPO, "zig-out", "bin", kern_name), os.path.join(esp, kern_dest)) - for dest, name in arch.get("extra", []): + os.makedirs(os.path.join(esp, os.path.dirname(kern_dest)), exist_ok=True) + shutil.copy(os.path.join(fhs, efi_src), os.path.join(esp, efi_dest)) + shutil.copy(os.path.join(fhs, kern_src), os.path.join(esp, kern_dest)) + for dest, src in arch.get("extra", []): os.makedirs(os.path.join(esp, os.path.dirname(dest)), exist_ok=True) - shutil.copy(os.path.join(REPO, "zig-out", "bin", name), os.path.join(esp, dest)) + shutil.copy(os.path.join(fhs, src), os.path.join(esp, dest)) return esp