const std = @import("std"); const builtin = @import("builtin"); // Hard pin: FlashOS uses inline asm, freestanding aarch64, custom linker // scripts and patchable-function-entry hooks that are sensitive to Zig // compiler changes. Bumping is a deliberate act — install the new Zig, // raise REQUIRED_ZIG_VERSION here and `minimum_zig_version` in // build.zig.zon, fix any breakage, commit. The .zigversion file mirrors // this for version managers (zigup / zvm / anyzig). const REQUIRED_ZIG_VERSION = std.SemanticVersion{ .major = 0, .minor = 16, .patch = 0 }; comptime { const v = builtin.zig_version; const r = REQUIRED_ZIG_VERSION; if (v.major != r.major or v.minor != r.minor or v.patch != r.patch) { @compileError(std.fmt.comptimePrint( "FlashOS requires Zig {d}.{d}.{d} exactly. Found Zig {d}.{d}.{d}. " ++ "To upgrade: bump REQUIRED_ZIG_VERSION in build.zig and " ++ "minimum_zig_version in build.zig.zon, then fix breakage.", .{ r.major, r.minor, r.patch, v.major, v.minor, v.patch }, )); } } // Native Zig build for the FlashOS kernel (AArch64; rpi4b + virt boards). // // Layout: // * src/start.zig — root, comptime-imports every kernel module // * arch/aarch64/*.S, *.inc — CPU-ISA core: boot/entry/sched/timer/etc. // * src/*.S — machine-independent asm (symbol table, trace) // * src/board//* — per-board driver bag + linker script // * user_space/init_main.flash — pid1.elf root, staged into the initramfs // * src/board//linker.ld — per-board link script (.initramfs section) // // The build produces: // * kernel8.img — raw binary loaded by the GPU bootloader (or QEMU `-kernel`) // * armstub8.bin — small EL3→EL1 bootstrap shim (rpi4b only) // // Optional `populate-syms` step runs nm on the linked ELF, regenerates // src/symbol_area.S via scripts/generate_syms.zig, then relinks so the // trace/ksyms machinery has a real symbol table to look up. const Board = enum { rpi4b, virt }; // Host-test wiring helper. Covers all three call patterns the suite // uses (shared-stub leaf, shared-stub + named imports, per-target stub // + imports) and returns the created test Module so a caller can reuse // it as a named-import target downstream — e.g. wait_queue's test // module is also pipe's "wait_queue" import. const HostTest = struct { src: []const u8, // When set, the test compiles this generated source instead of b.path(src). // Used for Flash-transpiled modules whose .zig lives in the build cache (a // composed WriteFiles directory) rather than on disk; `src` stays the // human-readable label. src_lazy: ?std.Build.LazyPath = null, stubs: ?*std.Build.Step.Compile = null, extra_stubs: []const *std.Build.Step.Compile = &.{}, imports: []const struct { name: []const u8, mod: *std.Build.Module, } = &.{}, }; // Set from the -Dcoverage option in build(); read by addHostTest below. var host_tests_use_llvm = false; // Set from the -Dtest-filter option in build(); read by addHostTest below. When // non-null, only tests whose name contains this substring run (zig test filter). var host_test_filter: ?[]const u8 = null; // Source files of every host test addHostTest wires, in registration order. // The `test` step's final tally (scripts/test_tally.sh) counts the `test "…"` // blocks across exactly these files, so the printed pass count is derived from // the build graph itself and can never drift from the suite. Fixed cap — there // are ~60 host tests; bump if the suite ever outgrows it. var host_test_srcs: [256][]const u8 = undefined; var host_test_n: usize = 0; fn addHostTest(b: *std.Build, step: *std.Build.Step, cfg: HostTest) *std.Build.Module { const m = b.createModule(.{ .root_source_file = if (cfg.src_lazy) |lp| lp else b.path(cfg.src), .target = b.graph.host, .optimize = .Debug, }); if (cfg.stubs) |s| m.addObject(s); for (cfg.extra_stubs) |s| m.addObject(s); for (cfg.imports) |imp| m.addImport(imp.name, imp.mod); const t = b.addTest(.{ .root_module = m, .use_llvm = if (host_tests_use_llvm) true else null, .filters = if (host_test_filter) |f| &.{f} else &.{}, }); step.dependOn(&b.addRunArtifact(t).step); host_test_srcs[host_test_n] = cfg.src; host_test_n += 1; return m; } // Set from the -Dflashc option in build(); read by addFlashSource below. var flashc_path: []const u8 = "flashc"; // Flash transpile helper. Registers a flashc run step (Flash -> Zig) and // returns the generated .zig as a LazyPath usable as a module root. The // .flash file is the source of truth: the generated Zig lands in the // build cache and is never committed. The step always re-runs // (has_side_effects): flashc is an external binary the cache cannot // fingerprint, so a stale cached output could otherwise green a boot // that no longer matches its source. fn addFlashSource(b: *std.Build, src: []const u8) std.Build.LazyPath { const run = b.addSystemCommand(&.{flashc_path}); run.setName(b.fmt("flashc {s}", .{src})); run.addFileArg(b.path(src)); run.addArg("-o"); const out = run.addOutputFileArg(b.fmt("{s}.zig", .{std.fs.path.stem(src)})); run.has_side_effects = true; return out; } pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .freestanding, .abi = .none, // Force +strict-align so LLVM never widens a byte copy or a // >16-byte by-value return into a NEON `str q` aimed at an // only-8-aligned slot. Those stores fault under SCTLR_EL1.A on real // silicon (data abort, DFSC 0x21) while sailing through QEMU's // lenient TCG. Covers the kernel and every freestanding EL0 program // that shares this target, so the whole class is closed at codegen // instead of with per-site `align(16)` / volatile dodges. .cpu_features_add = std.Target.aarch64.featureSet(&.{.strict_align}), }); // Default .ReleaseSmall keeps the kernel inside its symbol/image // budget, but it also compiles out the integer-overflow and // bounds-check traps: a missed overflow becomes silent UB instead of a // panic. Deliberate ceiling — arithmetic on untrusted input carries // explicit checks at the source (the ELF p_vaddr/p_memsz range+wrap // guards in src/elf.zig, the clusterLba fail-closed guard in // src/fat32.zig). Pass -Doptimize=ReleaseSafe to restore the traps. const optimize: std.builtin.OptimizeMode = b.option( std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size", ) orelse .ReleaseSmall; const board: Board = b.option( Board, "board", "Target board (rpi4b | virt)", ) orelse .rpi4b; // Expose the active board to Zig source via @import("build_options"). // src/board.zig switches on this at comptime to alias each driver // module to the right `src/board//*.zig`. const build_options = b.addOptions(); build_options.addOption(Board, "board", board); // Project version, single-sourced from build.zig.zon (.version). Flows to // fsh via build_options so the homescreen banner never hardcodes it: a // release bumps build.zig.zon and the shell line follows automatically. build_options.addOption([]const u8, "version", @import("build.zig.zon").version); // Opt-in fork tracing: prints a `created pid N at ` line on every // fork. Off by default so normal and CI boots read clean; flip on with // `-Dverbose-fork` when debugging the scheduler / process lifecycle. const verbose_fork = b.option( bool, "verbose-fork", "Print a 'created pid N at ' line on every fork (debug)", ) orelse false; build_options.addOption(bool, "verbose_fork", verbose_fork); // CI auto-login seed (default OFF — secure by default). PID 1 (pid1.elf) // injects `flash\nflash\n` into the console RX ring before exec'ing // /bin/login so the unattended QEMU boot watchdog authenticates with no // interactive typist (run_qemu_test.sh feeds QEMU ` Zig). Modules ported to // Flash (*.flash) transpile at build time via addFlashSource; the // pinned compiler revision lives in flash-toolchain.lock. The default // expects that checkout at ~/Flash, built with `zig build`. flashc_path = b.option( []const u8, "flashc", "Path to the flashc transpiler binary (default: ~/Flash/zig-out/bin/flashc-stage1)", ) orelse blk: { const home = b.graph.environ_map.get("HOME") orelse break :blk "flashc-stage1"; break :blk b.pathJoin(&.{ home, "Flash", "zig-out", "bin", "flashc-stage1" }); }; // ---- hygiene checks (trailing space, hard tabs, lowercase hex) ---- const hygiene_step = b.step("check-hygiene", "Fail on whitespace or hex-literal regressions"); const whitespace_check = b.addSystemCommand(&.{ "sh", "scripts/check_whitespace_hygiene.sh" }); hygiene_step.dependOn(&whitespace_check.step); const hex_check = b.addSystemCommand(&.{ "sh", "scripts/check_hex_hygiene.sh" }); hygiene_step.dependOn(&hex_check.step); // Shared syscall ID constants — single source of truth for the // kernel-side dispatch table (src/sys.zig) and the user-side // wrappers (user_space/kernel_tests.zig). Exposed as a named module // because Zig 0.16 forbids `@import` reaching outside the importing // module's root directory. // lib/syscall_defs.flash is the source of truth; flashc transpiles it to // Zig at build time via addFlashSource. The generated .zig is shared by // the kernel/userspace module here and the host-test module below, so the // transpile runs once. const syscall_defs_src = addFlashSource(b, "lib/syscall_defs.flash"); const syscall_defs_mod = b.createModule(.{ .root_source_file = syscall_defs_src, .target = target, .optimize = optimize, }); // console_ui — shared terminal look (status tags, ANSI palette, the // boot-success marker, and the line/stage/banner renderers). Pure and // target-agnostic (no .target, like shadow_mod): the one source compiles // into every console-drawing binary — the kernel boot log and the // userspace tools — so the whole system restyles from a single file. // Output is routed through a caller-supplied Sink, so it depends on // neither kernel internals nor flibc. Added to consumers below // (kernel_mod, fsh_mod); unused until a call site @imports it, so staging // it leaves every image byte-identical. // console_ui is a multi-file Flash module: console_ui.flash re-exports its // palette / tags / screen siblings through relative imports. flashc // transpiles one file at a time, so compose the generated .zig into a single // directory where each `@import("palette.zig")` sibling resolves — the same // per-stage WriteFiles composition the Flash toolchain uses for its own // std/selfhost modules. lib/console_ui/*.flash is the source of truth. const console_ui_dir = b.addWriteFiles(); const console_ui_files = [_][]const u8{ "palette", "tags", "screen", "console_ui" }; var console_ui_root: std.Build.LazyPath = undefined; var console_ui_screen_src: std.Build.LazyPath = undefined; for (console_ui_files) |name| { const gen = addFlashSource(b, b.fmt("lib/console_ui/{s}.flash", .{name})); const dest = console_ui_dir.addCopyFile(gen, b.fmt("{s}.zig", .{name})); if (std.mem.eql(u8, name, "console_ui")) console_ui_root = dest; if (std.mem.eql(u8, name, "screen")) console_ui_screen_src = dest; } const console_ui_mod = b.createModule(.{ .root_source_file = console_ui_root, }); // User-space virtual address layout (text/data/heap/stack bases + // per-region permission bits). Kernel-only consumer for now — // src/fork.zig (prepare_move_to_user_elf) and src/mm_user.zig // (map_page, do_data_abort) share the constants. Same module-level // exposure pattern as syscall_defs_mod. const user_layout_src = addFlashSource(b, "src/user_layout.flash"); const user_layout_mod = b.createModule(.{ .root_source_file = user_layout_src, .target = target, .optimize = optimize, }); // TaskStruct/CoreContext/etc. layout module. Already implicitly // imported by kernel-root modules via `@import("task_layout.zig")`, // but the named modules (wait_queue, pipe) need // an explicit named import to keep task_layout.zig from being // pulled into two sibling named modules through relative paths // (which Zig 0.16 rejects as "file exists in two modules"). const task_layout_src = addFlashSource(b, "src/task_layout.flash"); const task_layout_mod = b.createModule(.{ .root_source_file = task_layout_src, .target = target, .optimize = optimize, }); // WaitQueue API. Named module so both kernel and // host-test builds reach it via `@import("wait_queue")` — the host // test wiring at the bottom of this file mirrors this for the // pipe.zig test root. The one flashc transpile (wait_queue_src) is // shared across the kernel root and both test roots below. const wait_queue_src = addFlashSource(b, "src/wait_queue.flash"); const wait_queue_mod = b.createModule(.{ .root_source_file = wait_queue_src, .target = target, .optimize = optimize, }); wait_queue_mod.addImport("task_layout", task_layout_mod); // Anonymous-pipe module. Pulls in wait_queue for // the blocking read/write paths; kernel-only for now (future work // generalises to a tagged ?*File once the FS lands). The one flashc // transpile (pipe_src) is shared across the kernel root and both // test roots below. const pipe_src = addFlashSource(b, "src/pipe.flash"); const pipe_mod = b.createModule(.{ .root_source_file = pipe_src, .target = target, .optimize = optimize, }); pipe_mod.addImport("wait_queue", wait_queue_mod); pipe_mod.addImport("task_layout", task_layout_mod); // Initramfs parser module. Pure-data newc cpio // walker with linker-provided section bounds; no external imports // needed in freestanding (the host-test build flips a comptime // branch onto fixture globals — see src/initramfs.flash). Ported to // Flash; the one flashc transpile is shared with the host-test builds // below (src_lazy), including initramfs_backend's test module. const initramfs_src = addFlashSource(b, "src/initramfs.flash"); const initramfs_mod = b.createModule(.{ .root_source_file = initramfs_src, .target = target, .optimize = optimize, }); // ELF64 header + program-header parser. The loader in src/fork.zig // imports it as the named module "elf". Ported to Flash and promoted // to a named module: the generated .zig lives in the flashc cache, so // fork.zig's former file-relative @import("elf.zig") could no longer // resolve. Imports user_layout for the TEXT_BASE / DATA_BASE / // STACK_LOW bounds the validators pin against. const elf_src = addFlashSource(b, "src/elf.flash"); const elf_mod = b.createModule(.{ .root_source_file = elf_src, .target = target, .optimize = optimize, }); elf_mod.addImport("user_layout", user_layout_mod); // Physical page allocator (bitmap over the MALLOC pool). A leaf // module — its only dependencies are assembly-side externs // (memzero / main_output*). Ported to Flash and promoted to a named // module: the generated .zig lives in the flashc cache, so // start.zig's former file-relative @import("page_alloc.zig") could // no longer resolve. The one transpile is shared with the host-test // build below (src_lazy). const page_alloc_src = addFlashSource(b, "src/page_alloc.flash"); const page_alloc_mod = b.createModule(.{ .root_source_file = page_alloc_src, .target = target, .optimize = optimize, }); // User-page mapping + page-table walk + the EL0 fault handlers. // Ported to Flash; flashc transpiles it via addFlashSource. start.zig // pulls the export fns into the ELF with `_ = @import("mm_user")`; // sys.zig / fork.zig reach copy_from_user / map_page / do_data_abort // through C-ABI `extern fn`. The one transpile is shared with the // host-test build below (src_lazy). const mm_user_src = addFlashSource(b, "src/mm_user.flash"); const mm_user_mod = b.createModule(.{ .root_source_file = mm_user_src, .target = target, .optimize = optimize, }); mm_user_mod.addImport("task_layout", task_layout_mod); mm_user_mod.addImport("user_layout", user_layout_mod); // File handle module. Owns the open_files // lifetime helpers (alloc / unref / fdAlloc / fdGet / fdClose / // dupAll / closeAll). Imports task_layout for TaskStruct + File // (which lives in task_layout.zig to break the circular import // with the typed `open_files: [_]?*File` slot). const file_mod = b.createModule(.{ .root_source_file = b.path("src/file.zig"), .target = target, .optimize = optimize, }); file_mod.addImport("task_layout", task_layout_mod); // The one flashc transpile (fdtable_src) is shared across the kernel // root and both test roots below. const fdtable_src = addFlashSource(b, "src/fdtable.flash"); const fdtable_mod = b.createModule(.{ .root_source_file = fdtable_src, .target = target, .optimize = optimize, }); fdtable_mod.addImport("task_layout", task_layout_mod); fdtable_mod.addImport("pipe", pipe_mod); fdtable_mod.addImport("file", file_mod); // VFS dispatch layer. 1-bit superblock tag + // two-slot mount table; imports `file` for the File type its // vtable signatures reference. Host-test wiring for vfs.zig lives // at the bottom of this file. const vfs_mod = b.createModule(.{ .root_source_file = b.path("src/vfs.zig"), .target = target, .optimize = optimize, }); vfs_mod.addImport("file", file_mod); // vfs.zig re-exports the shared Dirent ABI type for the // readdir vtable signature. vfs_mod.addImport("syscall_defs", syscall_defs_mod); // Initramfs VFS backend. Thin wrapper turning // initramfs.zig's locate/read/seek into a VfsOps vtable — kept // separate from initramfs.zig so the parser stays VFS-agnostic // and host-testable in isolation. // Transpiled from src/initramfs_backend.flash; shared with the // host-test build below (src_lazy). const initramfs_backend_src = addFlashSource(b, "src/initramfs_backend.flash"); const initramfs_backend_mod = b.createModule(.{ .root_source_file = initramfs_backend_src, .target = target, .optimize = optimize, }); initramfs_backend_mod.addImport("initramfs", initramfs_mod); initramfs_backend_mod.addImport("vfs", vfs_mod); initramfs_backend_mod.addImport("file", file_mod); // Block-device abstraction. Single global // `sd_dev` vtable that the FAT32 backend reads + writes // through; the board layer (src/board//emmc2.zig) // populates `read_fn` / `write_fn` post-init. No tests // (pure data + one extern struct). const block_dev_src = addFlashSource(b, "src/block_dev.flash"); const block_dev_mod = b.createModule(.{ .root_source_file = block_dev_src, .target = target, .optimize = optimize, }); // SDHCI command encoder + CSD parser. // Named module so the rpi4b BCM2711 EMMC2 driver // (src/board/rpi4b/emmc2.zig) can `@import("sdhci_cmd")` // for the CMD0..ACMD41 encodings and parseCsdV2. Ported to Flash; // flashc transpiles it via addFlashSource and the one transpile is // shared with the host-test build below (src_lazy). const sdhci_cmd_src = addFlashSource(b, "src/sdhci_cmd.flash"); const sdhci_cmd_mod = b.createModule(.{ .root_source_file = sdhci_cmd_src, .target = target, .optimize = optimize, }); // VideoCore mailbox — property-tag message construction + parsing. // Pure data; the rpi4b board side // (src/board/rpi4b/mailbox.zig) wraps it with the MMIO doorbell so // the EMMC2 driver can read the firmware-set base clock and derive // a safe SDHCI divider. Ported to Flash; flashc transpiles it via // addFlashSource and the one transpile is shared with the host-test // build below (src_lazy). const mailbox_src = addFlashSource(b, "src/mailbox.flash"); const mailbox_mod = b.createModule(.{ .root_source_file = mailbox_src, .target = target, .optimize = optimize, }); // USB descriptor set + SETUP decode (DWC2 gadget). Pure data; the // rpi4b board side (src/board/rpi4b/usb.zig) imports it as // "usb_descriptors". Ported to Flash; flashc transpiles it via // addFlashSource and the one transpile is shared with the host-test // build below (src_lazy). const usb_descriptors_src = addFlashSource(b, "src/usb_descriptors.flash"); const usb_descriptors_mod = b.createModule(.{ .root_source_file = usb_descriptors_src, .target = target, .optimize = optimize, }); // Bulk-IN TX byte-ring for the DWC2 CDC-ACM gadget. Pure // data + logic; src/board/rpi4b/usb.zig imports it as "usb_tx_ring" // and keeps only the MMIO FIFO push. Ported to Flash; flashc transpiles // it via addFlashSource and the one transpile is shared with the // host-test build below (src_lazy). const usb_tx_ring_src = addFlashSource(b, "src/usb_tx_ring.flash"); const usb_tx_ring_mod = b.createModule(.{ .root_source_file = usb_tx_ring_src, .target = target, .optimize = optimize, }); // Per-board driver leaves (src/board//*). Ported to Flash, these // can no longer be reached by src/board.zig's relative `@import( // "board//x.zig")` — the generated .zig lives in the build cache. // Each is promoted to a named module; board.zig's comptime switch selects // the active board's prong via the named import below. The non-selected // prong is dead comptime code, so registering both boards' leaves here is // harmless (the unused module is never compiled). const virt_timer_src = addFlashSource(b, "src/board/virt/timer.flash"); const virt_timer_mod = b.createModule(.{ .root_source_file = virt_timer_src, .target = target, .optimize = optimize, }); const virt_gpio_src = addFlashSource(b, "src/board/virt/gpio.flash"); const virt_gpio_mod = b.createModule(.{ .root_source_file = virt_gpio_src, .target = target, .optimize = optimize, }); const virt_power_src = addFlashSource(b, "src/board/virt/power.flash"); const virt_power_mod = b.createModule(.{ .root_source_file = virt_power_src, .target = target, .optimize = optimize, }); const virt_mailbox_src = addFlashSource(b, "src/board/virt/mailbox.flash"); const virt_mailbox_mod = b.createModule(.{ .root_source_file = virt_mailbox_src, .target = target, .optimize = optimize, }); const virt_usb_src = addFlashSource(b, "src/board/virt/usb.flash"); const virt_usb_mod = b.createModule(.{ .root_source_file = virt_usb_src, .target = target, .optimize = optimize, }); const virt_emmc2_src = addFlashSource(b, "src/board/virt/emmc2.flash"); const virt_emmc2_mod = b.createModule(.{ .root_source_file = virt_emmc2_src, .target = target, .optimize = optimize, }); virt_emmc2_mod.addImport("block_dev", block_dev_mod); // FDT parser — a sibling of uart/irq, not a board.zig switch prong; // virt_uart and virt_irq reach it as the named "virt_dtb" import. const virt_dtb_src = addFlashSource(b, "src/board/virt/dtb.flash"); const virt_dtb_mod = b.createModule(.{ .root_source_file = virt_dtb_src, .target = target, .optimize = optimize, }); const virt_uart_src = addFlashSource(b, "src/board/virt/uart.flash"); const virt_uart_mod = b.createModule(.{ .root_source_file = virt_uart_src, .target = target, .optimize = optimize, }); virt_uart_mod.addImport("virt_dtb", virt_dtb_mod); // Kernel-log byte-ring (overwrite-oldest) backing dmesg. Pure // data + logic; src/utilc.zig tees main_output into it and src/sys.zig // snapshots it for sys_klog_read — both reach the one `klog` global // through this single named module. Imports syscall_defs for KLOG_SIZE // (the ring capacity is ABI-shared with userland dmesg). Ported to Flash; // flashc transpiles it via addFlashSource and the one transpile is shared // with the host-test build below (src_lazy). const klog_ring_src = addFlashSource(b, "src/klog_ring.flash"); const klog_ring_mod = b.createModule(.{ .root_source_file = klog_ring_src, .target = target, .optimize = optimize, }); klog_ring_mod.addImport("syscall_defs", syscall_defs_mod); // utilc — kernel utility exports (hex render, byte mem*, UART tee, // panic). Ported to Flash; flashc transpiles it to Zig at build time // via addFlashSource. start.zig pulls the symbols into the ELF with // `_ = @import("utilc")` (named, like sched/execve); every other // consumer reaches them through a C-ABI `extern fn` declaration, so the // import graph needs only this one named module. The one transpile is // shared with the host-test build below (src_lazy). const utilc_src = addFlashSource(b, "src/utilc.flash"); const utilc_mod = b.createModule(.{ .root_source_file = utilc_src, .target = target, .optimize = optimize, }); utilc_mod.addImport("task_layout", task_layout_mod); utilc_mod.addImport("klog_ring", klog_ring_mod); // sha256 — SHA-256 / HMAC / PBKDF2 / constant-time compare. // Target-agnostic (no .target) so both the freestanding kernel and the // host-side gen_shadow tool import the one source. Pure, no imports. // // Always ReleaseSmall, even in Debug kernel builds: sys_authenticate // runs the PBKDF2 → HMAC → SHA-256 chain on the per-task kernel stack // (the 4 KiB TaskStruct page, ~2.4 KiB usable), and Debug-mode frames // (no register allocation, 256-byte compress W-array + value-copied // hasher states per level) overflow that budget — the overflow lands in // the TaskStruct tail and silently corrupts the credential fields. // ReleaseSmall keeps the deepest chain comfortably inside the page (and // makes the boot-path KDF an order of magnitude faster under QEMU TCG). // The module is pure wrapping arithmetic (+%), so no Debug safety // checks are lost that the host-test target (its own Debug module) // doesn't still run. Ported to Flash; flashc transpiles it via // addFlashSource and the one transpile is shared with the host-test // build below (src_lazy). The .ReleaseSmall pin stays on this module. const sha256_src = addFlashSource(b, "src/sha256.flash"); const sha256_mod = b.createModule(.{ .root_source_file = sha256_src, .optimize = .ReleaseSmall, }); // shadow — /etc/shadow line parser + hex decoder. Pure. Ported to // Flash; the one transpile is shared with the host-test build below. const shadow_src = addFlashSource(b, "src/shadow.flash"); const shadow_mod = b.createModule(.{ .root_source_file = shadow_src, }); // perm — Unix discretionary access check. Pure decision // function (checkAccess) shared by the syscall-layer enforcement // sites; the truth-table host test below is the gate. const perm_src = addFlashSource(b, "src/perm.flash"); const perm_mod = b.createModule(.{ .root_source_file = perm_src, }); // overlay — FAT32 permission-overlay parser. Pure parse + // lookup consumed by fat32_backend (PERMS.TAB -> per-file mode/uid/gid). const overlay_src = addFlashSource(b, "src/overlay.flash"); const overlay_mod = b.createModule(.{ .root_source_file = overlay_src, }); // pwfile — /etc/passwd parser. Pure name/uid lookups shared // by the kernel (sys_passwd authorization), /bin/login, and fsh's // whoami builtin. const pwfile_src = addFlashSource(b, "src/pwfile.flash"); const pwfile_mod = b.createModule(.{ .root_source_file = pwfile_src, }); // hwrng — kernel entropy source (timer-backed SplitMix64 fallback). // Ported to Flash; flashc transpiles it via addFlashSource. start.zig // pulls hwrng_init into the ELF with `_ = @import("hwrng")` and sys.zig // reaches fill()/Source through the same named module; kernel.zig calls // hwrng_init via a C-ABI `extern fn`. console_ui supplies the boot Sink. // The one transpile is shared with the host-test build below (src_lazy). const hwrng_src = addFlashSource(b, "src/hwrng.flash"); const hwrng_mod = b.createModule(.{ .root_source_file = hwrng_src, .target = target, .optimize = optimize, }); hwrng_mod.addImport("console_ui", console_ui_mod); // generic_timer — generic ARM timer driver (absolute CNTP_CVAL cadence). // Ported to Flash; flashc transpiles it via addFlashSource. start.zig // pulls generic_timer_init / handle_generic_timer into the ELF with // `_ = @import("generic_timer")`; kernel.zig calls generic_timer_init via // a C-ABI `extern fn` and irq.S reaches handle_generic_timer by symbol. // Pure externs — no module imports. const generic_timer_src = addFlashSource(b, "src/generic_timer.flash"); const generic_timer_mod = b.createModule(.{ .root_source_file = generic_timer_src, .target = target, .optimize = optimize, }); // FAT32 on-disk layout decode + cluster/FAT/dir helpers. // Pure data-shape module — no VFS / file / page // imports; takes the BlockDev vtable by runtime pointer so the // host tests can swap in an in-memory fake. // fat32_backend.zig consumes this module to wire the real VfsOps. // Ported to Flash; the one flashc transpile is shared with the // host-test builds below (src_lazy), including fat32_backend's test // module. const fat32_src = addFlashSource(b, "src/fat32.flash"); const fat32_mod = b.createModule(.{ .root_source_file = fat32_src, .target = target, .optimize = optimize, }); fat32_mod.addImport("block_dev", block_dev_mod); // FAT32 VFS backend. Wraps fat32.zig's // on-disk decode in the real VfsOps vtable; replaces the earlier // fat32_stub. // Ported to Flash; the one flashc transpile is shared with the // host-test build below (src_lazy). const fat32_backend_src = addFlashSource(b, "src/fat32_backend.flash"); const fat32_backend_mod = b.createModule(.{ .root_source_file = fat32_backend_src, .target = target, .optimize = optimize, }); fat32_backend_mod.addImport("fat32", fat32_mod); fat32_backend_mod.addImport("vfs", vfs_mod); fat32_backend_mod.addImport("file", file_mod); fat32_backend_mod.addImport("block_dev", block_dev_mod); // Permission overlay: PERMS.TAB parse + lookup. fat32_backend_mod.addImport("overlay", overlay_mod); // Console RX layer. 256-byte ring + WaitQueue // backing the unified console read. Same named-module wiring as wait_queue // / pipe so the kernel build and the host-test build share one // task_layout Module instance. // The one flashc transpile (console_src) is shared across the kernel // root and the host-test root below. const console_src = addFlashSource(b, "src/console.flash"); const console_mod = b.createModule(.{ .root_source_file = console_src, .target = target, .optimize = optimize, }); console_mod.addImport("wait_queue", wait_queue_mod); console_mod.addImport("task_layout", task_layout_mod); // Scheduler module. Promoted from a relative-path // import to a named module so sys.zig can `@import("sched")` and call // the pure helpers (pick_next_running / refill_counters / // zombify_and_wake_parent) without re-declaring extern signatures. // Imports pipe + task_layout because sched.zig consumes both // (pipe.closeAll in do_wait_impl; TaskStruct from task_layout). // Ported to Flash; the one transpile feeds the kernel module and // sched's own host test below (src_lazy). const sched_src = addFlashSource(b, "src/sched.flash"); const sched_mod = b.createModule(.{ .root_source_file = sched_src, .target = target, .optimize = optimize, }); sched_mod.addImport("task_layout", task_layout_mod); sched_mod.addImport("fdtable", fdtable_mod); // Pure cwd-aware path-resolution helper. Hosts // joinResolve, the single non-recursive `.` / `..` collapse shared // by sys_chdir, sys_openFile, and execveKernel. Pure — no imports, // no externs — so the freestanding kernel module and the host-test // module reach the same source through this single named module. // Ported to Flash; the one transpile feeds the kernel module, the // execve host-test "path" alias, and path's own host test below. const path_src = addFlashSource(b, "src/path.flash"); const path_mod = b.createModule(.{ .root_source_file = path_src, .target = target, .optimize = optimize, }); // Path-resolved ELF loader. Ported to Flash; flashc transpiles it via // addFlashSource. The one transpile is shared between the kernel module // and the execve host-test below (src_lazy). start.zig pulls execve_impl // into the ELF; fork imports execve for the ArgvBlock type. const execve_src = addFlashSource(b, "src/execve.flash"); const execve_mod = b.createModule(.{ .root_source_file = execve_src, .target = target, .optimize = optimize, }); // Kernel-build imports for execveKernel (path-resolve + PT_LOAD stream). // The host-test build (build.zig below) wires src/execve.flash with no // kernel imports; the comptime is_kernel guard keeps these out of host // analysis. execve_mod.addImport("task_layout", task_layout_mod); execve_mod.addImport("vfs", vfs_mod); execve_mod.addImport("user_layout", user_layout_mod); execve_mod.addImport("path", path_mod); // Permission gate: exec-intent check + the EACCES constant. execve_mod.addImport("perm", perm_mod); execve_mod.addImport("syscall_defs", syscall_defs_mod); // Process creation + move-to-user ELF loader. Ported to Flash; flashc // transpiles it via addFlashSource. start.zig pulls the export fns // (copy_process_impl / prepare_move_to_user_elf / move_to_user_elf_argv) // into the ELF with `_ = @import("fork")`; sys.zig / kernel.zig reach // them through C-ABI `extern fn`. fork imports execve for the ArgvBlock // type (execve itself reaches back through the exported trampoline, so // there is no import cycle). The one transpile is shared with the // host-test build below (src_lazy). const fork_src = addFlashSource(b, "src/fork.flash"); const fork_mod = b.createModule(.{ .root_source_file = fork_src, .target = target, .optimize = optimize, }); fork_mod.addImport("task_layout", task_layout_mod); fork_mod.addImport("fdtable", fdtable_mod); fork_mod.addImport("user_layout", user_layout_mod); fork_mod.addImport("elf", elf_mod); fork_mod.addImport("execve", execve_mod); fork_mod.addImport("build_options", build_options_mod); // Syscall dispatch table + handlers. Ported to Flash; flashc transpiles // it via addFlashSource. Moved from a relative `@import("sys.zig")` in // start.zig to a named module — the generated .zig lives in the build // cache, so the path import no longer resolves. start.zig force-includes // it (`_ = @import("sys")`) so the dispatch table + every export fn land // in the ELF; entry.S reaches sys_call_table by symbol and kernel.flash // calls sys_call_table_relocate through a C-ABI `extern fn`. The board // driver bag is not imported here — sys reaches its three board entry // points through C-ABI trampolines in src/start.zig (the kernel root // module, which imports the board bag as a named module). No host test: // sys.zig carries no test blocks. const sys_src = addFlashSource(b, "src/sys.flash"); const sys_mod = b.createModule(.{ .root_source_file = sys_src, .target = target, .optimize = optimize, }); sys_mod.addImport("syscall_defs", syscall_defs_mod); sys_mod.addImport("task_layout", task_layout_mod); sys_mod.addImport("user_layout", user_layout_mod); sys_mod.addImport("pipe", pipe_mod); sys_mod.addImport("console", console_mod); sys_mod.addImport("sched", sched_mod); sys_mod.addImport("vfs", vfs_mod); sys_mod.addImport("file", file_mod); sys_mod.addImport("fdtable", fdtable_mod); sys_mod.addImport("path", path_mod); sys_mod.addImport("klog_ring", klog_ring_mod); sys_mod.addImport("sha256", sha256_mod); sys_mod.addImport("shadow", shadow_mod); sys_mod.addImport("perm", perm_mod); sys_mod.addImport("pwfile", pwfile_mod); sys_mod.addImport("hwrng", hwrng_mod); // Boot sequence + main loop. Ported to Flash; flashc transpiles it via // addFlashSource. Moved from a relative `@import("kernel.zig")` in // start.zig to a named module — the generated .zig lives in the build // cache, so the path import no longer resolves. start.zig force-includes // it (`_ = @import("kernel")`) so kernel_main_impl + the other export fns // land in the ELF; boot.S/entry.S reach kernel_main by symbol. The board // driver bag is not imported here — kernel reaches its board entry points // through C-ABI trampolines in src/start.zig (the kernel root imports the // board bag as a named module). No host test: kernel.zig carries no tests. const kernel_src = addFlashSource(b, "src/kernel.flash"); const kernel_kmod = b.createModule(.{ .root_source_file = kernel_src, .target = target, .optimize = optimize, }); kernel_kmod.addImport("initramfs", initramfs_mod); kernel_kmod.addImport("initramfs_backend", initramfs_backend_mod); kernel_kmod.addImport("fat32_backend", fat32_backend_mod); kernel_kmod.addImport("fdtable", fdtable_mod); kernel_kmod.addImport("task_layout", task_layout_mod); kernel_kmod.addImport("console_ui", console_ui_mod); kernel_kmod.addImport("build_options", build_options_mod); // rpi4b board driver leaves promoted to Flash named modules (same // pattern as the virt set above). gpio/timer/power/uart are board.zig // switch prongs; rpi4b_mailbox is the VideoCore MMIO doorbell consumed // by the rpi4b emmc2/usb board drivers (it can't take the name // "mailbox" — that's the pure message-layout data module it imports). // Created here so rpi4b_uart can reach console_mod and rpi4b_mailbox // can reach mailbox_mod. const rpi4b_gpio_src = addFlashSource(b, "src/board/rpi4b/gpio.flash"); const rpi4b_gpio_mod = b.createModule(.{ .root_source_file = rpi4b_gpio_src, .target = target, .optimize = optimize, }); const rpi4b_timer_src = addFlashSource(b, "src/board/rpi4b/timer.flash"); const rpi4b_timer_mod = b.createModule(.{ .root_source_file = rpi4b_timer_src, .target = target, .optimize = optimize, }); const rpi4b_power_src = addFlashSource(b, "src/board/rpi4b/power.flash"); const rpi4b_power_mod = b.createModule(.{ .root_source_file = rpi4b_power_src, .target = target, .optimize = optimize, }); const rpi4b_mailbox_src = addFlashSource(b, "src/board/rpi4b/mailbox.flash"); const rpi4b_mailbox_mod = b.createModule(.{ .root_source_file = rpi4b_mailbox_src, .target = target, .optimize = optimize, }); rpi4b_mailbox_mod.addImport("mailbox", mailbox_mod); const rpi4b_uart_src = addFlashSource(b, "src/board/rpi4b/uart.flash"); const rpi4b_uart_mod = b.createModule(.{ .root_source_file = rpi4b_uart_src, .target = target, .optimize = optimize, }); rpi4b_uart_mod.addImport("console", console_mod); const rpi4b_emmc2_src = addFlashSource(b, "src/board/rpi4b/emmc2.flash"); const rpi4b_emmc2_mod = b.createModule(.{ .root_source_file = rpi4b_emmc2_src, .target = target, .optimize = optimize, }); rpi4b_emmc2_mod.addImport("sdhci_cmd", sdhci_cmd_mod); rpi4b_emmc2_mod.addImport("block_dev", block_dev_mod); rpi4b_emmc2_mod.addImport("mailbox", mailbox_mod); rpi4b_emmc2_mod.addImport("rpi4b_mailbox", rpi4b_mailbox_mod); const rpi4b_usb_src = addFlashSource(b, "src/board/rpi4b/usb.flash"); const rpi4b_usb_mod = b.createModule(.{ .root_source_file = rpi4b_usb_src, .target = target, .optimize = optimize, }); rpi4b_usb_mod.addImport("usb_descriptors", usb_descriptors_mod); rpi4b_usb_mod.addImport("usb_tx_ring", usb_tx_ring_mod); rpi4b_usb_mod.addImport("mailbox", mailbox_mod); rpi4b_usb_mod.addImport("rpi4b_mailbox", rpi4b_mailbox_mod); rpi4b_usb_mod.addImport("console", console_mod); // Trace-cluster named modules (kept in Zig; the profiler is opt-in and // not on the boot contract). Promoted from kernel_mod-relative imports so // the Flash-sourced IRQ drivers can reach the sampler by name — a named // module can no longer be pulled in through a relative path once its // source moves to the build cache. ksyms is force-imported by start.zig // in every build (its symbol table backs the trace machinery); sampler + // fp_walk are referenced only by the IRQ handlers' -Dtrace seam, so the // default build never compiles them (the import sits in a dead comptime // branch). Promoting all three to named modules keeps ksyms a member of // exactly one module instead of landing in both kernel_mod and sampler. const ksyms_mod = b.createModule(.{ .root_source_file = b.path("src/trace/ksyms.zig"), .target = target, .optimize = optimize, }); const fp_walk_mod = b.createModule(.{ .root_source_file = b.path("src/trace/fp_walk.zig"), .target = target, .optimize = optimize, }); const sampler_mod = b.createModule(.{ .root_source_file = b.path("src/trace/sampler.zig"), .target = target, .optimize = optimize, }); sampler_mod.addImport("task_layout", task_layout_mod); sampler_mod.addImport("ksyms", ksyms_mod); sampler_mod.addImport("fp_walk", fp_walk_mod); // Per-board interrupt controllers — GICv2 (GIC-400) on rpi4b, GICv3 on // virt — promoted to Flash named modules. board.zig's irq prong selects // the active one; the non-selected prong is dead comptime code. Both // reach the -Dtrace sampler by name (trace_sampler seam in handle_irq); // the default build leaves that import in a dead comptime branch, so no // trace module is compiled and the kernel image is byte-identical. const rpi4b_irq_src = addFlashSource(b, "src/board/rpi4b/irq.flash"); const rpi4b_irq_mod = b.createModule(.{ .root_source_file = rpi4b_irq_src, .target = target, .optimize = optimize, }); rpi4b_irq_mod.addImport("console", console_mod); rpi4b_irq_mod.addImport("rpi4b_usb", rpi4b_usb_mod); rpi4b_irq_mod.addImport("build_options", build_options_mod); rpi4b_irq_mod.addImport("task_layout", task_layout_mod); rpi4b_irq_mod.addImport("sampler", sampler_mod); const virt_irq_src = addFlashSource(b, "src/board/virt/irq.flash"); const virt_irq_mod = b.createModule(.{ .root_source_file = virt_irq_src, .target = target, .optimize = optimize, }); virt_irq_mod.addImport("virt_dtb", virt_dtb_mod); virt_irq_mod.addImport("virt_uart", virt_uart_mod); virt_irq_mod.addImport("console", console_mod); virt_irq_mod.addImport("build_options", build_options_mod); virt_irq_mod.addImport("task_layout", task_layout_mod); virt_irq_mod.addImport("sampler", sampler_mod); // board: comptime indirection that aliases each driver slot to the active // board's leaf module. Ported to Flash; flashc transpiles it via // addFlashSource. Moved from a relative `@import("board.zig")` in start.zig // to a named module — the generated .zig lives in the build cache, so the // path import no longer resolves. start.zig force-imports it (`_ = // board.uart` etc.) so each driver's `export fn` decls land in the ELF. // Both boards' leaf modules are imported here; board.zig's comptime switch // on build_options.board selects the active set, so the linker only keeps // the chosen prong. const board_src = addFlashSource(b, "src/board.flash"); const board_mod = b.createModule(.{ .root_source_file = board_src, .target = target, .optimize = optimize, }); board_mod.addImport("build_options", build_options_mod); board_mod.addImport("virt_uart", virt_uart_mod); board_mod.addImport("rpi4b_uart", rpi4b_uart_mod); board_mod.addImport("virt_gpio", virt_gpio_mod); board_mod.addImport("rpi4b_gpio", rpi4b_gpio_mod); board_mod.addImport("virt_timer", virt_timer_mod); board_mod.addImport("rpi4b_timer", rpi4b_timer_mod); board_mod.addImport("virt_irq", virt_irq_mod); board_mod.addImport("rpi4b_irq", rpi4b_irq_mod); board_mod.addImport("virt_emmc2", virt_emmc2_mod); board_mod.addImport("rpi4b_emmc2", rpi4b_emmc2_mod); board_mod.addImport("virt_usb", virt_usb_mod); board_mod.addImport("rpi4b_usb", rpi4b_usb_mod); board_mod.addImport("virt_power", virt_power_mod); board_mod.addImport("rpi4b_power", rpi4b_power_mod); board_mod.addImport("virt_mailbox", virt_mailbox_mod); board_mod.addImport("rpi4b_mailbox", rpi4b_mailbox_mod); // ---- kernel executable ---- const kernel_mod = b.createModule(.{ .root_source_file = b.path("src/start.zig"), .target = target, .optimize = optimize, .strip = false, // keep symbols so `populate-syms` can nm the ELF .unwind_tables = .none, // NOTE: -Dtrace deliberately does NOT force -fno-omit-frame-pointer. // arch/aarch64/boot.S uses x29 as a scratch LR stash during early boot, and the // per-task kernel stack is only ~2.4 KiB; reserving x29 as a frame // pointer kernel-wide corrupts the boot and trips a safety panic (it // wild-branches under ReleaseSmall). The sampler therefore walks the // FP chain best-effort (whatever frames LLVM kept) and always emits // the leaf PC, which needs no frame pointer. .omit_frame_pointer = null, }); const kernel = b.addExecutable(.{ .name = "kernel8.elf", .root_module = kernel_mod, }); kernel.step.dependOn(hygiene_step); const asm_files = [_][]const u8{ "arch/aarch64/boot.S", "arch/aarch64/entry.S", "arch/aarch64/utils.S", "arch/aarch64/mm.S", "arch/aarch64/sched.S", "arch/aarch64/irq.S", "arch/aarch64/generic_timer.S", "src/symbol_area.S", "src/trace/hook.S", "src/trace/patchable_trampolines.S", }; for (asm_files) |path| { kernel_mod.addAssemblyFile(b.path(path)); } // Board-specific assembly: per-board boot quirks (and any future // timer init etc.) live under src/board//. virt additionally // ships a Linux arm64 image header so UEFI/GRUB can identify the // kernel binary in Phase B; rpi4b's firmware loads kernel8.img raw // and does not expect the header. const board_asm_files: []const []const u8 = if (board == .virt) &.{ "image_header.S", "boot_quirks.S" } else &.{"boot_quirks.S"}; for (board_asm_files) |path| { kernel_mod.addAssemblyFile(b.path(b.fmt("src/board/{s}/{s}", .{ @tagName(board), path }))); } // The kernel .S files use `#include "asm_defs.inc"`, which now lives // alongside them under arch/aarch64/. That bridge header in turn pulls in // `board_asm_defs.inc` from the active board's directory — both search // paths are added below so the ISA + per-board layout resolves. src/ is // kept on the path for any remaining top-level .S includes. kernel_mod.addIncludePath(b.path("arch/aarch64")); kernel_mod.addIncludePath(b.path("src")); kernel_mod.addIncludePath(b.path(b.fmt("src/board/{s}", .{@tagName(board)}))); // -Dtrace: define FLASHOS_TRACE for the C-preprocessed .S files (the .S // extension routes them through clang's preprocessor, the same path that // resolves their #include "asm_defs.inc"). entry.S keys its one extra // `mov x0, sp` on this; absent the macro that instruction is not emitted, // so the default kernel image is byte-identical. if (trace) kernel_mod.addCMacro("FLASHOS_TRACE", "1"); kernel_mod.addImport("build_options", build_options_mod); kernel_mod.addImport("syscall_defs", syscall_defs_mod); kernel_mod.addImport("user_layout", user_layout_mod); kernel_mod.addImport("task_layout", task_layout_mod); kernel_mod.addImport("wait_queue", wait_queue_mod); kernel_mod.addImport("pipe", pipe_mod); kernel_mod.addImport("fdtable", fdtable_mod); kernel_mod.addImport("console", console_mod); kernel_mod.addImport("sched", sched_mod); kernel_mod.addImport("sys", sys_mod); kernel_mod.addImport("execve", execve_mod); kernel_mod.addImport("path", path_mod); kernel_mod.addImport("initramfs", initramfs_mod); kernel_mod.addImport("elf", elf_mod); kernel_mod.addImport("file", file_mod); kernel_mod.addImport("vfs", vfs_mod); kernel_mod.addImport("initramfs_backend", initramfs_backend_mod); kernel_mod.addImport("fat32_backend", fat32_backend_mod); kernel_mod.addImport("fat32", fat32_mod); kernel_mod.addImport("block_dev", block_dev_mod); kernel_mod.addImport("page_alloc", page_alloc_mod); kernel_mod.addImport("mm_user", mm_user_mod); kernel_mod.addImport("fork", fork_mod); kernel_mod.addImport("sdhci_cmd", sdhci_cmd_mod); kernel_mod.addImport("mailbox", mailbox_mod); kernel_mod.addImport("usb_descriptors", usb_descriptors_mod); kernel_mod.addImport("usb_tx_ring", usb_tx_ring_mod); // Per-board driver leaves promoted to named modules (see comment above // their createModule). board.zig's comptime switch picks the active set. kernel_mod.addImport("virt_timer", virt_timer_mod); kernel_mod.addImport("virt_gpio", virt_gpio_mod); kernel_mod.addImport("virt_power", virt_power_mod); kernel_mod.addImport("virt_usb", virt_usb_mod); kernel_mod.addImport("virt_emmc2", virt_emmc2_mod); // virt_dtb + virt_uart: board.zig's uart prong selects virt_uart; the // virt IRQ controller (virt_irq) reaches both by name (a sibling of // uart/irq, not its own board.zig prong). kernel_mod.addImport("virt_dtb", virt_dtb_mod); kernel_mod.addImport("virt_uart", virt_uart_mod); // rpi4b board leaves: gpio/timer/power/uart are board.zig prongs; // rpi4b_mailbox is reached by the still-Zig rpi4b emmc2/usb drivers. kernel_mod.addImport("rpi4b_gpio", rpi4b_gpio_mod); kernel_mod.addImport("rpi4b_timer", rpi4b_timer_mod); kernel_mod.addImport("rpi4b_power", rpi4b_power_mod); kernel_mod.addImport("rpi4b_mailbox", rpi4b_mailbox_mod); kernel_mod.addImport("rpi4b_uart", rpi4b_uart_mod); kernel_mod.addImport("rpi4b_emmc2", rpi4b_emmc2_mod); kernel_mod.addImport("rpi4b_usb", rpi4b_usb_mod); // Per-board IRQ controllers (board.zig's irq prong) + the unconditional // ksyms force-import (start.zig pulls its symbol table into every image). // sampler/fp_walk are not added here: only the IRQ modules' -Dtrace seam // references the sampler, so kernel_mod never imports them directly. kernel_mod.addImport("rpi4b_irq", rpi4b_irq_mod); kernel_mod.addImport("virt_irq", virt_irq_mod); // board: the comptime driver-alias bag (src/board.flash). start.zig // force-imports it so each active driver's export fns reach the linker. kernel_mod.addImport("board", board_mod); kernel_mod.addImport("ksyms", ksyms_mod); kernel_mod.addImport("klog_ring", klog_ring_mod); kernel_mod.addImport("utilc", utilc_mod); kernel_mod.addImport("sha256", sha256_mod); kernel_mod.addImport("shadow", shadow_mod); kernel_mod.addImport("perm", perm_mod); kernel_mod.addImport("hwrng", hwrng_mod); // sys_passwd authorization: uid -> login-name lookup against // /etc/passwd (the same parser /bin/login and fsh's whoami import). kernel_mod.addImport("pwfile", pwfile_mod); // console_ui: shared terminal look for the boot log. Staged but not yet // @imported by any kernel source, so the kernel image stays byte-identical // until the migration call sites land. kernel_mod.addImport("console_ui", console_ui_mod); kernel_mod.addImport("generic_timer", generic_timer_mod); kernel_mod.addImport("kernel", kernel_kmod); // ---- hello.elf — payload for [TEST] exec-elf ---- // Built as a standalone aarch64-freestanding ET_EXEC, staged into // the initramfs at /test/hello.elf. The exec-elf scenario opens it // via sys_openFile, reads it into an EL0 buffer, and hands the // bytes to sys_exec. Source is Flash (tools/hello.flash) — flashc // transpiles it to Zig at build time via addFlashSource. const hello_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/hello.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); const hello = b.addExecutable(.{ .name = "hello.elf", .root_module = hello_mod, }); hello.pie = false; // ET_EXEC, not ET_DYN — the loader rejects PIE. hello.bundle_compiler_rt = false; // Tiny p_align so LLD doesn't pad the file out to a page-sized // offset — the ELF loader caps blob_size at PAGE_SIZE because it // snapshots the blob into one kernel page. p_vaddr is still // 0x1000-aligned via the linker script's `. = 0x100000`, which is // what FlashOS's page-grain mapper actually requires; p_align only // governs the ELF spec's `p_vaddr ≡ p_offset (mod p_align)` rule, // and the kernel loader does not enforce p_align. hello.link_z_max_page_size = 0x80; hello.link_z_common_page_size = 0x80; // Custom linker script: stock LLD output splits .eh_frame_hdr / // .eh_frame into a separate LOAD segment ahead of .text, which // pushes .text to a non-page-aligned VA. The script collapses to // a single R+X PT_LOAD and discards the unwind / dyn metadata. hello.setLinkerScript(b.path("tools/hello_linker.ld")); hello.entry = .disabled; // ENTRY(_start) lives in the linker script // ---- stackbomb.elf — payload for [TEST] stack-overflow ---- // Same recipe as hello.elf, swapping the source for a payload that // recurses without termination. The kernel's do_data_abort detects // the guard-zone fault, prints a kernel-side diagnostic and zombies // the task; the parent's sys_wait reaps it so the per-process page // balance returns to baseline (which is what the harness verifies). // Source is Flash (tools/stackbomb.flash) — flashc transpiles it to // Zig at build time via addFlashSource. const stackbomb_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/stackbomb.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); stackbomb_mod.addImport("user_layout", user_layout_mod); const stackbomb = b.addExecutable(.{ .name = "stackbomb.elf", .root_module = stackbomb_mod, }); stackbomb.pie = false; stackbomb.bundle_compiler_rt = false; stackbomb.link_z_max_page_size = 0x80; stackbomb.link_z_common_page_size = 0x80; // The hello linker script is a generic single-PT_LOAD layout — // reuse it verbatim. If the two payloads ever need different // section discards or VA bases, fork into tools/stackbomb_linker.ld. stackbomb.setLinkerScript(b.path("tools/hello_linker.ld")); stackbomb.entry = .disabled; // ---- flibc — userland mini-libc, ELF-demo dependency ---- // Userland mini-libc: SVC wrappers, printf/puts on sys_writeConsole, // bump allocator over sys_brk/sbrk, fork/wait/exit/execve. Exposed // as a named module so ELF demos (and future fsh / coreutils // payloads) can `addImport("flibc", flibc_mod)` and stay one // `@import` deep. Pulls in syscall_defs for the SVC IDs — same // module the kernel and the kernel_tests user-side wrappers consume. // flibc is a multi-file module: flibc.zig pulls its siblings in by relative // import. As the port advances each module flips from a copied-verbatim .zig // to a flashc-generated one; both land in a single composed WriteFiles // directory so every `@import("sibling.zig")` resolves there (the same // composition console_ui uses above). Until a module is ported it is copied // straight from the source tree; ported modules are transpiled from .flash. const flibc_dir = b.addWriteFiles(); const flibc_flash = [_][]const u8{ "heap", "io", "keys", "completion", "pager", "readline", "syscalls", "process", "execvp", "flibc" }; const flibc_zig = [_][]const u8{}; // Every composed sibling's in-dir LazyPath, keyed by module name. A flibc // host test compiles the in-dir copy (where each `@import("sibling.zig")` // resolves) rather than the on-disk file, which breaks the moment a sibling // flips from a copied .zig to a flashc-generated one. var flibc_srcs = std.StringHashMap(std.Build.LazyPath).init(b.allocator); for (flibc_flash) |name| { const gen = addFlashSource(b, b.fmt("user_space/lib/flibc/{s}.flash", .{name})); flibc_srcs.put(name, flibc_dir.addCopyFile(gen, b.fmt("{s}.zig", .{name}))) catch @panic("OOM"); } for (flibc_zig) |name| { const dest = flibc_dir.addCopyFile( b.path(b.fmt("user_space/lib/flibc/{s}.zig", .{name})), b.fmt("{s}.zig", .{name}), ); flibc_srcs.put(name, dest) catch @panic("OOM"); } const flibc_mod = b.createModule(.{ .root_source_file = flibc_srcs.get("flibc").?, .target = target, .optimize = .ReleaseSmall, }); flibc_mod.addImport("syscall_defs", syscall_defs_mod); // ---- flibc_demo.elf — payload for [TEST] flibc ---- // Same recipe as hello.elf / stackbomb.elf, swapping the source for // a flibc-driven body: printf("flibc hello %d\n", 42), malloc 32 B, // pattern write+verify, exit. The forked linker script // (tools/flibc_demo_linker.ld) folds .rodata / .data / .bss into the // single R+X PT_LOAD so flibc's state-free heap design carries // through to a one-segment ELF that once fit inside the retired // loader's PAGE_SIZE snapshot cap. Source is Flash // (tools/flibc_demo.flash) — flashc transpiles it to Zig at build time // via addFlashSource. const flibc_demo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/flibc_demo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); flibc_demo_mod.addImport("flibc", flibc_mod); const flibc_demo = b.addExecutable(.{ .name = "flibc_demo.elf", .root_module = flibc_demo_mod, }); flibc_demo.pie = false; flibc_demo.bundle_compiler_rt = false; flibc_demo.link_z_max_page_size = 0x80; flibc_demo.link_z_common_page_size = 0x80; flibc_demo.setLinkerScript(b.path("tools/flibc_demo_linker.ld")); flibc_demo.entry = .disabled; // ---- argv_echo.elf — payload for [TEST] execve ---- // Same recipe as flibc_demo.elf, but its entry is the flibc _start // argc/argv shim (user_space/lib/flibc/start.flash) rather than a bespoke // _start, and it carries a 4 KiB .rodata PAD so the linked ELF crosses // one page — proving sys_execve's PT_LOAD streaming path loads payloads // the long-retired PAGE_SIZE snapshot cap could not. The shim lives // in its own module (not flibc/process.zig) because flibc.zig re-exports // process into every flibc program, and Zig 0.16 rejects two _start // exports in one compilation; argv_echo opts in via addImport below plus // the `link "flibc_start"` in argv_echo.flash. Source is Flash — // flashc transpiles it to Zig at build time via addFlashSource. const flibc_start_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "user_space/lib/flibc/start.flash"), .target = target, .optimize = .ReleaseSmall, }); // Freestanding memcpy / memset / strlen for payloads that actually // exercise execvp / the tokenizer / per-arg length scans — LLVM // lowers those loops to libcalls that bundle_compiler_rt=false leaves // unprovided. Opt-in (imported only by fsh / echo / cat), so the // payloads that dodge the idiom (argv_echo, flibc_demo) stay lean. const flibc_mem_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "user_space/lib/flibc/mem.flash"), .target = target, .optimize = .ReleaseSmall, }); const argv_echo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/argv_echo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); argv_echo_mod.addImport("flibc", flibc_mod); argv_echo_mod.addImport("flibc_start", flibc_start_mod); const argv_echo = b.addExecutable(.{ .name = "argv_echo.elf", .root_module = argv_echo_mod, }); argv_echo.pie = false; argv_echo.bundle_compiler_rt = false; argv_echo.link_z_max_page_size = 0x80; argv_echo.link_z_common_page_size = 0x80; argv_echo.setLinkerScript(b.path("tools/argv_echo_linker.ld")); argv_echo.entry = .disabled; // ---- fsh.elf — the FlashOS shell (/bin/fsh) ---- // Same recipe as argv_echo.elf (flibc _start argc/argv shim entry, // pie=false, ReleaseSmall, strip, own single R+X PT_LOAD linker // script — no PAD; fsh need not cross a page). fsh and its pure // tokenizer are both Flash now; fsh.flash imports the tokenizer as a // relative sibling, so the two flashc-generated files are composed into // one WriteFiles directory (the same composition console_ui / flibc use) // for that @import("tokenize.zig") to resolve. The tokenizer is // host-tested separately in the test section below. // Staged into the initramfs at /bin/fsh and exec'd by the PID-1 // hand-off after the harness tally; the boot watchdog keys on fsh's // homescreen marker as the success signal. (The in-harness // [TEST] fsh scenario is disabled — see user_space/kernel_tests.zig.) const fsh_dir = b.addWriteFiles(); const tokenize_gen = addFlashSource(b, "user_space/fsh/tokenize.flash"); _ = fsh_dir.addCopyFile(tokenize_gen, "tokenize.zig"); const fsh_mod = b.createModule(.{ .root_source_file = fsh_dir.addCopyFile(addFlashSource(b, "user_space/fsh/fsh.flash"), "fsh.zig"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); fsh_mod.addImport("flibc", flibc_mod); fsh_mod.addImport("flibc_start", flibc_start_mod); // whoami builtin: uid -> login-name lookup against // /etc/passwd via the same parser the kernel and /bin/login use. fsh_mod.addImport("pwfile", pwfile_mod); // console_ui: shared terminal look for the homescreen/prompt. fsh renders // its homescreen banner through it, fed the build_options version below. fsh_mod.addImport("console_ui", console_ui_mod); // build_options carries the project version (from build.zig.zon) into the // homescreen banner — single source, no hardcoded version in fsh. fsh_mod.addOptions("build_options", build_options); // fsh is the first payload to actually exercise execvp + the // tokenizer's @memcpy, so LLVM lowers those to memcpy / strlen // libcalls; flibc_mem supplies the freestanding providers. fsh_mod.addImport("flibc_mem", flibc_mem_mod); const fsh = b.addExecutable(.{ .name = "fsh.elf", .root_module = fsh_mod, }); fsh.pie = false; fsh.bundle_compiler_rt = false; fsh.link_z_max_page_size = 0x80; fsh.link_z_common_page_size = 0x80; fsh.setLinkerScript(b.path("tools/fsh_linker.ld")); fsh.entry = .disabled; // ---- echo.elf / cat.elf — minimal coreutils ---- // Same recipe as fsh.elf (flibc _start shim, // flibc_mem, pie=false, ReleaseSmall, strip) over a shared // single-PT_LOAD linker script. Staged at /bin/echo and /bin/cat; // exercised interactively via fsh (the `echo hi | cat` acceptance). // The coreutil set also carries ls / meminfo / forkbomb. echo / cat / // ls source from Flash (tools/{echo,cat,ls}.flash) — flashc transpiles // them to Zig at build time via addFlashSource. const echo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/echo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); echo_mod.addImport("flibc", flibc_mod); echo_mod.addImport("flibc_start", flibc_start_mod); echo_mod.addImport("flibc_mem", flibc_mem_mod); const echo = b.addExecutable(.{ .name = "echo.elf", .root_module = echo_mod, }); echo.pie = false; echo.bundle_compiler_rt = false; echo.link_z_max_page_size = 0x80; echo.link_z_common_page_size = 0x80; echo.setLinkerScript(b.path("tools/coreutil_linker.ld")); echo.entry = .disabled; const cat_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/cat.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); cat_mod.addImport("flibc", flibc_mod); cat_mod.addImport("flibc_start", flibc_start_mod); cat_mod.addImport("flibc_mem", flibc_mem_mod); // EACCES-aware diagnostic: cat names the permission denial. cat_mod.addImport("syscall_defs", syscall_defs_mod); const cat = b.addExecutable(.{ .name = "cat.elf", .root_module = cat_mod, }); cat.pie = false; cat.bundle_compiler_rt = false; cat.link_z_max_page_size = 0x80; cat.link_z_common_page_size = 0x80; cat.setLinkerScript(b.path("tools/coreutil_linker.ld")); cat.entry = .disabled; // ---- grep.elf — line-pattern search coreutil ---- // grep [-i] PATTERN [FILE...]: streams each input (a FILE, or fd 0 with // none) line by line and writes the lines containing PATTERN to fd 1. The // matcher is the pure tools/grep_match.flash (host-tested below); this ELF // is only the driver (flag/argv parsing, open/read, line assembly). Same // recipe as cat (flibc _start shim, flibc_mem, pie=false, ReleaseSmall, // strip, shared coreutil_linker.ld). Staged at /bin/grep; kept out of the // CI FSH_SCRIPT so the free-page baseline stays deterministic. const grep_match_gen = addFlashSource(b, "tools/grep_match.flash"); const grep_match_mod = b.createModule(.{ .root_source_file = grep_match_gen, .target = target, .optimize = .ReleaseSmall, }); const grep_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/grep.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); grep_mod.addImport("flibc", flibc_mod); grep_mod.addImport("flibc_start", flibc_start_mod); grep_mod.addImport("flibc_mem", flibc_mem_mod); // EACCES-aware diagnostic: grep names the permission denial, like cat. grep_mod.addImport("syscall_defs", syscall_defs_mod); // Pure, host-tested substring matcher. grep_mod.addImport("grep_match", grep_match_mod); const grep = b.addExecutable(.{ .name = "grep.elf", .root_module = grep_mod, }); grep.pie = false; grep.bundle_compiler_rt = false; grep.link_z_max_page_size = 0x80; grep.link_z_common_page_size = 0x80; grep.setLinkerScript(b.path("tools/coreutil_linker.ld")); grep.entry = .disabled; // ---- cp.elf / mv.elf / rm.elf — file-management coreutils ---- // The consumers of the FAT32 write ABI: cp creates + copies (SYS_CREATE), // rm removes (SYS_UNLINK), mv renames same-dir (SYS_RENAME) with a // cp+unlink fallback for cross-dir. Same recipe as cat (flibc _start shim, // flibc_mem, pie=false, ReleaseSmall, strip, shared coreutil_linker.ld). // Staged at /bin/{cp,mv,rm}; kept out of the CI FSH_SCRIPT (they mutate the // FAT32 card, which only exists on real hardware) so the free-page baseline // stays deterministic. const cp_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/cp.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); cp_mod.addImport("flibc", flibc_mod); cp_mod.addImport("flibc_start", flibc_start_mod); cp_mod.addImport("flibc_mem", flibc_mem_mod); const cp = b.addExecutable(.{ .name = "cp.elf", .root_module = cp_mod, }); cp.pie = false; cp.bundle_compiler_rt = false; cp.link_z_max_page_size = 0x80; cp.link_z_common_page_size = 0x80; cp.setLinkerScript(b.path("tools/coreutil_linker.ld")); cp.entry = .disabled; const mv_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/mv.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); mv_mod.addImport("flibc", flibc_mod); mv_mod.addImport("flibc_start", flibc_start_mod); mv_mod.addImport("flibc_mem", flibc_mem_mod); const mv = b.addExecutable(.{ .name = "mv.elf", .root_module = mv_mod, }); mv.pie = false; mv.bundle_compiler_rt = false; mv.link_z_max_page_size = 0x80; mv.link_z_common_page_size = 0x80; mv.setLinkerScript(b.path("tools/coreutil_linker.ld")); mv.entry = .disabled; const rm_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/rm.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); rm_mod.addImport("flibc", flibc_mod); rm_mod.addImport("flibc_start", flibc_start_mod); rm_mod.addImport("flibc_mem", flibc_mem_mod); const rm = b.addExecutable(.{ .name = "rm.elf", .root_module = rm_mod, }); rm.pie = false; rm.bundle_compiler_rt = false; rm.link_z_max_page_size = 0x80; rm.link_z_common_page_size = 0x80; rm.setLinkerScript(b.path("tools/coreutil_linker.ld")); rm.entry = .disabled; // ---- ls.elf — directory-listing coreutil ---- // The first consumer of sys_readdir (slot 37): loops readdir(path, i) // 0.. and writes each basename (a trailing '/' for DT_DIR) to fd 1. // Same recipe as echo / cat (flibc _start shim, flibc_mem, pie=false, // ReleaseSmall, strip, shared coreutil_linker.ld). Staged at /bin/ls; // exercised by `ls /bin` in FSH_SCRIPT + [TEST] readdir in the stage- // closing commit. const ls_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/ls.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); ls_mod.addImport("flibc", flibc_mod); ls_mod.addImport("flibc_start", flibc_start_mod); ls_mod.addImport("flibc_mem", flibc_mem_mod); const ls = b.addExecutable(.{ .name = "ls.elf", .root_module = ls_mod, }); ls.pie = false; ls.bundle_compiler_rt = false; ls.link_z_max_page_size = 0x80; ls.link_z_common_page_size = 0x80; ls.setLinkerScript(b.path("tools/coreutil_linker.ld")); ls.entry = .disabled; // ---- dmesg.elf — kernel-log dumper coreutil ---- // The consumer of sys_klog_read (slot 38): one snapshot of the kernel // log ring (src/klog_ring.zig) written to fd 1, so the boot log is // readable over the USB-C console without the Mini-UART adapter. Same // recipe as ls / cat / echo (flibc _start shim, flibc_mem, pie=false, // ReleaseSmall, strip, shared coreutil_linker.ld). Staged at /bin/dmesg; // Pi-interactive surface — the CI harness asserts the ring + syscall // directly via [TEST] klog, the way meminfo / forkbomb stay out of the // FSH_SCRIPT. Source is Flash (tools/dmesg.flash) — flashc transpiles it // to Zig at build time via addFlashSource. const dmesg_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/dmesg.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); dmesg_mod.addImport("flibc", flibc_mod); dmesg_mod.addImport("flibc_start", flibc_start_mod); dmesg_mod.addImport("flibc_mem", flibc_mem_mod); const dmesg = b.addExecutable(.{ .name = "dmesg.elf", .root_module = dmesg_mod, }); dmesg.pie = false; dmesg.bundle_compiler_rt = false; dmesg.link_z_max_page_size = 0x80; dmesg.link_z_common_page_size = 0x80; dmesg.setLinkerScript(b.path("tools/coreutil_linker.ld")); dmesg.entry = .disabled; // ---- meminfo.elf / forkbomb.elf — demo coreutils ---- // meminfo is the standalone /bin form of fsh's `free` built-in (one // sys_dump_free line); forkbomb is a capped (N=16) fork/reap leak // detector that never approaches OOM. Both print via the legacy slot-0 // console write and are Pi-interactive only — kept out of the CI // FSH_SCRIPT (meminfo's live value breaks the baseline count; forkbomb // must not approach exhaustion while OOM still panics today). Same // recipe as echo / cat / ls. meminfo's source is Flash // (tools/meminfo.flash) — flashc transpiles it to Zig at build time via // addFlashSource. const meminfo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/meminfo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); meminfo_mod.addImport("flibc", flibc_mod); meminfo_mod.addImport("flibc_start", flibc_start_mod); meminfo_mod.addImport("flibc_mem", flibc_mem_mod); const meminfo = b.addExecutable(.{ .name = "meminfo.elf", .root_module = meminfo_mod, }); meminfo.pie = false; meminfo.bundle_compiler_rt = false; meminfo.link_z_max_page_size = 0x80; meminfo.link_z_common_page_size = 0x80; meminfo.setLinkerScript(b.path("tools/coreutil_linker.ld")); meminfo.entry = .disabled; // forkbomb's source is Flash (tools/forkbomb.flash) — flashc // transpiles it to Zig at build time via addFlashSource. const forkbomb_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/forkbomb.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); forkbomb_mod.addImport("flibc", flibc_mod); forkbomb_mod.addImport("flibc_start", flibc_start_mod); forkbomb_mod.addImport("flibc_mem", flibc_mem_mod); const forkbomb = b.addExecutable(.{ .name = "forkbomb.elf", .root_module = forkbomb_mod, }); forkbomb.pie = false; forkbomb.bundle_compiler_rt = false; forkbomb.link_z_max_page_size = 0x80; forkbomb.link_z_common_page_size = 0x80; forkbomb.setLinkerScript(b.path("tools/coreutil_linker.ld")); forkbomb.entry = .disabled; // ---- sysinfo.elf — one-shot system summary coreutil ---- // First consumer of the console_ui screen-layer kv() renderer (the // full-screen-navigation scaffold): prints the FlashOS version, the // logged-in user, and the free-page count as aligned key/value rows, then // exits. Imports console_ui for kv(), pwfile for the uid -> name lookup, and // build_options for the version (single-sourced from build.zig.zon). Same // recipe as ls / meminfo (flibc _start shim, flibc_mem, pie=false, // ReleaseSmall, strip, shared coreutil_linker.ld). Staged at /bin/sysinfo; // kept out of the CI FSH_SCRIPT like meminfo (its free-page value is live). // Source is Flash (tools/sysinfo.flash) — flashc transpiles it to Zig at // build time via addFlashSource. const sysinfo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/sysinfo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); sysinfo_mod.addImport("flibc", flibc_mod); sysinfo_mod.addImport("flibc_start", flibc_start_mod); sysinfo_mod.addImport("flibc_mem", flibc_mem_mod); sysinfo_mod.addImport("pwfile", pwfile_mod); sysinfo_mod.addImport("console_ui", console_ui_mod); sysinfo_mod.addOptions("build_options", build_options); const sysinfo = b.addExecutable(.{ .name = "sysinfo.elf", .root_module = sysinfo_mod, }); sysinfo.pie = false; sysinfo.bundle_compiler_rt = false; sysinfo.link_z_max_page_size = 0x80; sysinfo.link_z_common_page_size = 0x80; sysinfo.setLinkerScript(b.path("tools/coreutil_linker.ld")); sysinfo.entry = .disabled; // ---- cpuinfo.elf — one-shot CPU monitor coreutil ---- // Prints the SoC temperature and ARM clock (sys_cpu_temp / sys_cpu_freq // over the VideoCore mailbox) as aligned kv rows, then exits — the // focused sibling of sysinfo's broader summary. Imports flibc + console_ui // only (no pwfile / build_options). Same recipe as ls / sysinfo (flibc // _start shim, flibc_mem, pie=false, ReleaseSmall, strip, shared // coreutil_linker.ld). Staged at /bin/cpuinfo; kept out of the CI // FSH_SCRIPT like sysinfo (live readings would break the baseline count). // Source is Flash (tools/cpuinfo.flash) — flashc transpiles it to Zig at // build time via addFlashSource. const cpuinfo_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/cpuinfo.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); cpuinfo_mod.addImport("flibc", flibc_mod); cpuinfo_mod.addImport("flibc_start", flibc_start_mod); cpuinfo_mod.addImport("flibc_mem", flibc_mem_mod); cpuinfo_mod.addImport("console_ui", console_ui_mod); const cpuinfo = b.addExecutable(.{ .name = "cpuinfo.elf", .root_module = cpuinfo_mod, }); cpuinfo.pie = false; cpuinfo.bundle_compiler_rt = false; cpuinfo.link_z_max_page_size = 0x80; cpuinfo.link_z_common_page_size = 0x80; cpuinfo.setLinkerScript(b.path("tools/coreutil_linker.ld")); cpuinfo.entry = .disabled; // ---- uptime.elf — one-shot uptime monitor coreutil ---- // Prints the humanised seconds-since-boot (sys_uptime, the architectural // counter) as one kv row, then exits — the focused sibling of sysinfo's // uptime row, the way cpuinfo focuses temperature + clock. Imports flibc + // console_ui only (no pwfile / build_options). Same recipe as cpuinfo / // sysinfo (flibc _start shim, flibc_mem, pie=false, ReleaseSmall, strip, // shared coreutil_linker.ld). Staged at /bin/uptime; kept out of the CI // FSH_SCRIPT like sysinfo (a live reading would break the baseline count). // Source is Flash (tools/uptime.flash) — flashc transpiles it to Zig at // build time via addFlashSource. const uptime_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/uptime.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); uptime_mod.addImport("flibc", flibc_mod); uptime_mod.addImport("flibc_start", flibc_start_mod); uptime_mod.addImport("flibc_mem", flibc_mem_mod); uptime_mod.addImport("console_ui", console_ui_mod); const uptime = b.addExecutable(.{ .name = "uptime.elf", .root_module = uptime_mod, }); uptime.pie = false; uptime.bundle_compiler_rt = false; uptime.link_z_max_page_size = 0x80; uptime.link_z_common_page_size = 0x80; uptime.setLinkerScript(b.path("tools/coreutil_linker.ld")); uptime.entry = .disabled; // ---- less.elf — full-screen text pager ---- // First interactive consumer of the navigation scaffold: takes over the // console with console_ui.screen (alt-screen + panelTop title bar), reads // keys through flibc.readKey's VT100 decoder, and scrolls a single named // file with the pure flibc.Pager core. A proof of the full-screen loop the // way sysinfo proved the print-and-exit kv() renderer. Imports flibc + // console_ui only (no pwfile / build_options). Same recipe as ls / sysinfo // (flibc _start shim, flibc_mem, pie=false, ReleaseSmall, strip, shared // coreutil_linker.ld). Staged at /bin/less; kept out of the CI FSH_SCRIPT // like sysinfo (interactive; the free-page baseline must stay deterministic). // Source is Flash (tools/less.flash) — flashc transpiles it to Zig at build // time via addFlashSource. const less_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/less.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); less_mod.addImport("flibc", flibc_mod); less_mod.addImport("flibc_start", flibc_start_mod); less_mod.addImport("flibc_mem", flibc_mem_mod); less_mod.addImport("console_ui", console_ui_mod); const less = b.addExecutable(.{ .name = "less.elf", .root_module = less_mod, }); less.pie = false; less.bundle_compiler_rt = false; less.link_z_max_page_size = 0x80; less.link_z_common_page_size = 0x80; less.setLinkerScript(b.path("tools/coreutil_linker.ld")); less.entry = .disabled; // ---- edit.elf — full-screen text editor ---- // Second interactive consumer of the navigation scaffold and the first // writer: slurps a file into a heap-backed gap buffer (the first real heap // user — brk/sbrk + flibc malloc), edits it via the pure flibc.gapbuf cores, // and writes it back on ctrl-O (unlink + create + write — FAT32 has no // truncate). Reuses grep_match.find for ctrl-W search. Imports flibc + // console_ui (like less) plus the two pure cores. Same recipe as less // (flibc _start shim, flibc_mem, pie=false, ReleaseSmall, strip, shared // coreutil_linker.ld). Staged at /bin/edit; kept out of the CI FSH_SCRIPT // (interactive — no QEMU stdin — so the free-page baseline stays // deterministic). gapbuf is a standalone module like grep_match, not part of // the flibc aggregate, so it adds no footprint to existing boot binaries. // Source is Flash (tools/edit.flash) — flashc transpiles it at build time. const gapbuf_gen = addFlashSource(b, "user_space/lib/flibc/gapbuf.flash"); const gapbuf_mod = b.createModule(.{ .root_source_file = gapbuf_gen, .target = target, .optimize = .ReleaseSmall, }); const edit_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/edit.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); edit_mod.addImport("flibc", flibc_mod); edit_mod.addImport("flibc_start", flibc_start_mod); edit_mod.addImport("flibc_mem", flibc_mem_mod); edit_mod.addImport("console_ui", console_ui_mod); edit_mod.addImport("gapbuf", gapbuf_mod); edit_mod.addImport("grep_match", grep_match_mod); const edit = b.addExecutable(.{ .name = "edit.elf", .root_module = edit_mod, }); edit.pie = false; edit.bundle_compiler_rt = false; edit.link_z_max_page_size = 0x80; edit.link_z_common_page_size = 0x80; edit.setLinkerScript(b.path("tools/coreutil_linker.ld")); edit.entry = .disabled; // ---- clear.elf — terminal-clear coreutil ---- // Smallest console_ui consumer: emits the shared screen-clear sequence // (console_ui.screen.clear) and exits, so the escape bytes stay // single-sourced. Imports flibc + console_ui only, like less. Same recipe // (flibc _start shim, flibc_mem, pie=false, ReleaseSmall, strip, shared // coreutil_linker.ld). Staged at /bin/clear. Source is Flash // (tools/clear.flash) — flashc transpiles it to Zig at build time via // addFlashSource; the cross-import proves a Flash program can consume the // existing Zig flibc + console_ui modules. const clear_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/clear.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); clear_mod.addImport("flibc", flibc_mod); clear_mod.addImport("flibc_start", flibc_start_mod); clear_mod.addImport("flibc_mem", flibc_mem_mod); clear_mod.addImport("console_ui", console_ui_mod); const clear = b.addExecutable(.{ .name = "clear.elf", .root_module = clear_mod, }); clear.pie = false; clear.bundle_compiler_rt = false; clear.link_z_max_page_size = 0x80; clear.link_z_common_page_size = 0x80; clear.setLinkerScript(b.path("tools/coreutil_linker.ld")); clear.entry = .disabled; // ---- login.elf — credential gate + session supervisor ---- // PID-1 execs /bin/login instead of /bin/fsh: it prompts for a username // (echoed) + password (echo suppressed via SYS_SET_CONSOLE_MODE), has the // kernel verify against the active shadow (sys_authenticate), then runs // the session as a child — the child drops privilege (setgid + setuid) // per /etc/passwd and execs the user's shell while login stays root, // waits, reaps, and re-prompts (the logout lifecycle). Same coreutil // recipe as dmesg / ls; imports syscall_defs for the echo mode bit and // pwfile for the /etc/passwd lookup. // Source is Flash (tools/login.flash) — flashc transpiles it to Zig at // build time via addFlashSource. const login_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/login.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); login_mod.addImport("flibc", flibc_mod); login_mod.addImport("flibc_start", flibc_start_mod); login_mod.addImport("flibc_mem", flibc_mem_mod); login_mod.addImport("syscall_defs", syscall_defs_mod); login_mod.addImport("pwfile", pwfile_mod); const login = b.addExecutable(.{ .name = "login.elf", .root_module = login_mod, }); login.pie = false; login.bundle_compiler_rt = false; login.link_z_max_page_size = 0x80; login.link_z_common_page_size = 0x80; login.setLinkerScript(b.path("tools/coreutil_linker.ld")); login.entry = .disabled; // ---- passwd.elf — interactive password change ---- // `passwd [user]` collects the current + new password (kernel echo // off) and calls sys_passwd; the KDF + splice-safe shadow rewrite // live in the kernel. Same coreutil recipe as login; imports pwfile // for the uid -> own-login-name default and syscall_defs for EACCES. // Source is Flash (tools/passwd.flash) — flashc transpiles it to Zig at // build time via addFlashSource. const passwd_bin_mod = b.createModule(.{ .root_source_file = addFlashSource(b, "tools/passwd.flash"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); passwd_bin_mod.addImport("flibc", flibc_mod); passwd_bin_mod.addImport("flibc_start", flibc_start_mod); passwd_bin_mod.addImport("flibc_mem", flibc_mem_mod); passwd_bin_mod.addImport("syscall_defs", syscall_defs_mod); passwd_bin_mod.addImport("pwfile", pwfile_mod); const passwd_bin = b.addExecutable(.{ .name = "passwd.elf", .root_module = passwd_bin_mod, }); passwd_bin.pie = false; passwd_bin.bundle_compiler_rt = false; passwd_bin.link_z_max_page_size = 0x80; passwd_bin.link_z_common_page_size = 0x80; passwd_bin.setLinkerScript(b.path("tools/coreutil_linker.ld")); passwd_bin.entry = .disabled; // ---- pid1.elf — the ELF-loaded PID 1 ---- // Replaces the user_init.o blob. Instead of compiling // user_space/init.zig into the kernel object and wrapping it in // linker.ld's user_start / user_end, PID 1 is now a standalone // aarch64-freestanding ET_EXEC staged into the initramfs at // /sbin/init. kernel_process locates that entry and hands its // bytes to prepare_move_to_user_elf — the same ELF loader the // exec-elf / stackbomb / flibc test payloads travel. // // Recipe mirrors hello.elf (pie=false, strip, ReleaseSmall, tiny // p_align so LLD doesn't page-pad the file). The forked linker // script tools/pid1_linker.ld folds .rodata / .data / .bss into // the single R+X PT_LOAD. Unlike the test payloads pid1.elf is // loaded by kernel_process directly at boot, so there is no // snapshot cap on its size — prepare_move_to_user_elf walks the // PT_LOAD page by page. // init_main (PID 1) is Flash; it imports the in-kernel test harness // (kernel_tests.flash) as a relative sibling. flashc transpiles the // harness at build time, and the generated init_main.zig + the transpiled // kernel_tests.zig are composed into one WriteFiles directory so the // relative @import("kernel_tests.zig") resolves. const pid1_dir = b.addWriteFiles(); _ = pid1_dir.addCopyFile(addFlashSource(b, "user_space/kernel_tests.flash"), "kernel_tests.zig"); const pid1_mod = b.createModule(.{ .root_source_file = pid1_dir.addCopyFile(addFlashSource(b, "user_space/init_main.flash"), "init_main.zig"), .target = target, .optimize = .ReleaseSmall, .strip = true, }); pid1_mod.addImport("syscall_defs", syscall_defs_mod); // pid1 reads build_options for the CI auto-login seed gate (see the // ci-login-seed option above). Off by default → the shipped boot stops // at `login:`; the watchdog builds with the flag for unattended auth. pid1_mod.addOptions("build_options", build_options); const pid1 = b.addExecutable(.{ .name = "pid1.elf", .root_module = pid1_mod, }); pid1.pie = false; pid1.bundle_compiler_rt = false; pid1.link_z_max_page_size = 0x80; pid1.link_z_common_page_size = 0x80; pid1.setLinkerScript(b.path("tools/pid1_linker.ld")); pid1.entry = .disabled; // ---- /etc/shadow generator ---- // Host tool: runs the kernel's PBKDF2 (src/sha256.zig) over fixed test // credentials to emit a deterministic /etc/shadow, staged into the // initramfs below. Reusing the kernel KDF guarantees the baked verifier // matches what sys_authenticate recomputes at login. Output is a pure // function of the in-tool constants, so the kernel image stays byte- // reproducible (Pi hash baseline). const gen_shadow_mod = b.createModule(.{ .root_source_file = b.path("tools/gen_shadow.zig"), .target = b.graph.host, .optimize = .Debug, }); gen_shadow_mod.addImport("sha256", sha256_mod); const gen_shadow = b.addExecutable(.{ .name = "gen_shadow", .root_module = gen_shadow_mod, }); const gen_shadow_cmd = b.addRunArtifact(gen_shadow); const shadow_file = gen_shadow_cmd.addOutputFileArg("shadow"); // Install a copy at zig-out/shadow so the deploy step (a literal-path // shell script, like its kernel8.img reference) can seed the real SD // card with the same bytes the initramfs and the QEMU image carry. const install_shadow = b.addInstallFileWithDir(shadow_file, .prefix, "shadow"); // ---- initramfs.cpio ---- // newc cpio archive embedded into the kernel image via the // .initramfs section (linker.ld on both boards). Stages the // real payloads: pid1.elf at /sbin/init (kernel_process ELF-loads // it as PID 1), and the three test ELFs at /test/*.elf (the // exec-elf / stack-overflow / flibc scenarios open + read + exec // them via the file syscalls instead of the retired .text.user // bridge slots). // // The cpio_stage WriteFiles step collects each ELF under a stable // arc name ("sbin/init", "test/hello.elf", …); the encoder below // walks a fixed, lexicographically-sorted arc list and reads bytes // from the staged directory, so the archive layout never depends // on filesystem walk order. src/initramfs.zig canonicalises the // emitted "./" prefix to "/" so locate("/sbin/init") // matches. // // Step 10 replaced the previous addSystemCommand cpio(1) block // (bsdcpio on macOS, GNU cpio on Linux) with the hand-rolled Zig // encoder at scripts/build_initramfs.zig. The old block stamped // host-clock mtime + non-zero inode at byte 12, so two clean // builds produced different kernel8.img sha256 sums and blocked // Pi-hash baseline refresh. The encoder fixes mtime / uid / gid / // nlink / mode and assigns monotonic ino, making the archive a // pure function of file contents + name list. const cpio_stage = b.addNamedWriteFiles("initramfs_stage"); _ = cpio_stage.addCopyFile(pid1.getEmittedBin(), "sbin/init"); _ = cpio_stage.addCopyFile(hello.getEmittedBin(), "test/hello.elf"); _ = cpio_stage.addCopyFile(stackbomb.getEmittedBin(), "test/stackbomb.elf"); _ = cpio_stage.addCopyFile(flibc_demo.getEmittedBin(), "test/flibc_demo.elf"); _ = cpio_stage.addCopyFile(argv_echo.getEmittedBin(), "test/argv_echo.elf"); _ = cpio_stage.addCopyFile(cat.getEmittedBin(), "bin/cat"); _ = cpio_stage.addCopyFile(clear.getEmittedBin(), "bin/clear"); _ = cpio_stage.addCopyFile(cp.getEmittedBin(), "bin/cp"); _ = cpio_stage.addCopyFile(cpuinfo.getEmittedBin(), "bin/cpuinfo"); _ = cpio_stage.addCopyFile(dmesg.getEmittedBin(), "bin/dmesg"); _ = cpio_stage.addCopyFile(echo.getEmittedBin(), "bin/echo"); _ = cpio_stage.addCopyFile(edit.getEmittedBin(), "bin/edit"); _ = cpio_stage.addCopyFile(forkbomb.getEmittedBin(), "bin/forkbomb"); _ = cpio_stage.addCopyFile(fsh.getEmittedBin(), "bin/fsh"); _ = cpio_stage.addCopyFile(grep.getEmittedBin(), "bin/grep"); _ = cpio_stage.addCopyFile(less.getEmittedBin(), "bin/less"); _ = cpio_stage.addCopyFile(ls.getEmittedBin(), "bin/ls"); _ = cpio_stage.addCopyFile(meminfo.getEmittedBin(), "bin/meminfo"); _ = cpio_stage.addCopyFile(mv.getEmittedBin(), "bin/mv"); _ = cpio_stage.addCopyFile(rm.getEmittedBin(), "bin/rm"); _ = cpio_stage.addCopyFile(sysinfo.getEmittedBin(), "bin/sysinfo"); _ = cpio_stage.addCopyFile(uptime.getEmittedBin(), "bin/uptime"); _ = cpio_stage.addCopyFile(b.path("user_space/fsh/fshrc"), "etc/fshrc"); _ = cpio_stage.addCopyFile(login.getEmittedBin(), "bin/login"); _ = cpio_stage.addCopyFile(passwd_bin.getEmittedBin(), "bin/passwd"); _ = cpio_stage.addCopyFile(b.path("user_space/etc/passwd"), "etc/passwd"); _ = cpio_stage.addCopyFile(shadow_file, "etc/shadow"); const initramfs_encoder = b.addExecutable(.{ .name = "build_initramfs", .root_module = b.createModule(.{ .root_source_file = b.path("scripts/build_initramfs.zig"), .target = b.graph.host, .optimize = .Debug, }), }); // Arc names sorted lexicographically — the encoder writes them in // argv order, so this list is the single source of truth for the // archive's entry order and therefore its sha256. Keep sorted. // // Each arc carries its newc mode: binaries are 0o100755 // (the dropped-privilege shell must still exec them), config files // are 0o100644 (world-readable), and /etc/shadow is 0o100600 so the // VFS permission layer holds the "non-root read → EACCES" line. // This list is the single policy source; the encoder just stamps // what it is told. const initramfs_arcs = [_]struct { arc: []const u8, mode: u32 }{ .{ .arc = "bin/cat", .mode = 0o100755 }, .{ .arc = "bin/clear", .mode = 0o100755 }, .{ .arc = "bin/cp", .mode = 0o100755 }, .{ .arc = "bin/cpuinfo", .mode = 0o100755 }, .{ .arc = "bin/dmesg", .mode = 0o100755 }, .{ .arc = "bin/echo", .mode = 0o100755 }, .{ .arc = "bin/edit", .mode = 0o100755 }, .{ .arc = "bin/forkbomb", .mode = 0o100755 }, .{ .arc = "bin/fsh", .mode = 0o100755 }, .{ .arc = "bin/grep", .mode = 0o100755 }, .{ .arc = "bin/less", .mode = 0o100755 }, .{ .arc = "bin/login", .mode = 0o100755 }, .{ .arc = "bin/ls", .mode = 0o100755 }, .{ .arc = "bin/meminfo", .mode = 0o100755 }, .{ .arc = "bin/mv", .mode = 0o100755 }, .{ .arc = "bin/passwd", .mode = 0o100755 }, .{ .arc = "bin/rm", .mode = 0o100755 }, .{ .arc = "bin/sysinfo", .mode = 0o100755 }, .{ .arc = "bin/uptime", .mode = 0o100755 }, .{ .arc = "etc/fshrc", .mode = 0o100644 }, .{ .arc = "etc/passwd", .mode = 0o100644 }, .{ .arc = "etc/shadow", .mode = 0o100600 }, .{ .arc = "sbin/init", .mode = 0o100755 }, .{ .arc = "test/argv_echo.elf", .mode = 0o100755 }, .{ .arc = "test/flibc_demo.elf", .mode = 0o100755 }, .{ .arc = "test/hello.elf", .mode = 0o100755 }, .{ .arc = "test/stackbomb.elf", .mode = 0o100755 }, }; const cpio_cmd = b.addRunArtifact(initramfs_encoder); const initramfs_bin = cpio_cmd.addOutputFileArg("initramfs.cpio"); cpio_cmd.addDirectoryArg(cpio_stage.getDirectory()); for (initramfs_arcs) |e| cpio_cmd.addArg(b.fmt("{s}:{o}", .{ e.arc, e.mode })); // Stage the cpio next to a directory the assembler can `-I` so // tools/initramfs.S's `.incbin "initramfs.cpio"` resolves // regardless of CWD — same pattern hello_elf.S / stackbomb_elf.S // / flibc_demo_elf.S use above. const initramfs_bin_stage = b.addNamedWriteFiles("initramfs_bin_stage"); _ = initramfs_bin_stage.addCopyFile(initramfs_bin, "initramfs.cpio"); kernel_mod.addAssemblyFile(b.path("tools/initramfs.S")); kernel_mod.addIncludePath(initramfs_bin_stage.getDirectory()); kernel.setLinkerScript(b.path(b.fmt("src/board/{s}/linker.ld", .{@tagName(board)}))); kernel.entry = .disabled; // _start lives in boot.S kernel.link_z_max_page_size = 0x1000; kernel.link_gc_sections = false; const install_kernel_elf = b.addInstallArtifact(kernel, .{}); // ELF → raw binary using the system aarch64-elf-objcopy. const objcopy_kernel = b.addSystemCommand(&.{ "aarch64-elf-objcopy", }); objcopy_kernel.addArtifactArg(kernel); objcopy_kernel.addArg("-O"); objcopy_kernel.addArg("binary"); const kernel_img = objcopy_kernel.addOutputFileArg("kernel8.img"); const install_kernel_img = b.addInstallFileWithDir(kernel_img, .prefix, "kernel8.img"); const kernel_step = b.step("kernel", "Build kernel8.img"); kernel_step.dependOn(&install_kernel_elf.step); kernel_step.dependOn(&install_kernel_img.step); // ---- aggregate / default ---- // The default `all` step bundles per-board artifacts. armstub and // the SD-card deploy are Pi-specific (BCM2711 EL3→EL1 shim, // bcm2711-rpi-4-b.dtb / start4.elf), so they live in the // `if (board == .rpi4b)` arm below. const all_step = b.step("all", "Build everything (default)"); all_step.dependOn(kernel_step); b.default_step.dependOn(all_step); if (board == .rpi4b) { // ---- armstub (EL3→EL1 shim, separate tiny ELF linked at .text=0) ---- const armstub_mod = b.createModule(.{ .root_source_file = b.path("armstub/src/root.zig"), // empty — real code is in armstub8.S .target = target, .optimize = optimize, // Match the kernel's frame-pointer policy under -Dtrace; harmless // otherwise (this module is asm-only). null = leave it alone. .omit_frame_pointer = if (trace) false else null, }); const armstub = b.addExecutable(.{ .name = "armstub8.elf", .root_module = armstub_mod, }); armstub_mod.addAssemblyFile(b.path("armstub/src/armstub8.S")); armstub_mod.addIncludePath(b.path("armstub/src")); armstub.setLinkerScript(b.path("armstub/src/linker.ld")); armstub.entry = .disabled; // _start defined in armstub8.S armstub.link_z_max_page_size = 0x1000; armstub.link_gc_sections = false; armstub.bundle_compiler_rt = false; const objcopy_armstub = b.addSystemCommand(&.{ "aarch64-elf-objcopy", }); objcopy_armstub.addArtifactArg(armstub); objcopy_armstub.addArg("-O"); objcopy_armstub.addArg("binary"); const armstub_bin = objcopy_armstub.addOutputFileArg("armstub8.bin"); const install_armstub_bin = b.addInstallFileWithDir(armstub_bin, .prefix, "armstub8.bin"); const install_armstub_elf = b.addInstallArtifact(armstub, .{}); const armstub_step = b.step("armstub", "Build armstub8.bin"); armstub_step.dependOn(&install_armstub_elf.step); armstub_step.dependOn(&install_armstub_bin.step); all_step.dependOn(armstub_step); } // ---- optional: regenerate symbol_area.S from the linked kernel ELF ---- // Two-pass workflow: build kernel once, run nm | generate_syms.zig to // overwrite src/symbol_area.S, then re-run `zig build` to relink with // the populated table. Exposed as its own step so the default // build stays single-pass. // `grep -v 'compiler_rt\.'` drops the namespaced compiler-rt aliases // (e.g. `compiler_rt.aarch64_outline_atomics.__aarch64_cas16_acq_rel`, // 59+ chars) that overflow generate_syms.zig's fixed-width entry. // The short alias (`__aarch64_cas16_acq_rel`) sits at the same // address and survives the filter, so trace coverage is unchanged — // only the redundant long name is dropped. const populate = b.addSystemCommand(&.{ "sh", "-c", "aarch64-elf-nm -n " ++ "\"$1\" | sort | grep -v '\\$' | grep -v 'compiler_rt\\.' | " ++ "zig run scripts/generate_syms.zig", "--", }); populate.addArtifactArg(kernel); const populate_step = b.step( "populate-syms", "Regenerate src/symbol_area.S from the current kernel ELF (run `zig build` again afterwards)", ); populate_step.dependOn(&populate.step); populate_step.dependOn(kernel_step); // ---- deploy: copy artifacts + RPi firmware to the SD card. ---- // Mirrors the old `make deploy` recipe; tweak the env-var defaults below // for a different mount point or firmware tree. Pi-only — references // armstub8.bin and BCM2711 firmware blobs. if (board == .rpi4b) { const deploy = b.addSystemCommand(&.{ "sh", "-c", \\set -eu \\: "${SD_BOOT:=/Volumes/BOOT}" \\: "${FIRMWARE:=firmware}" \\# Refuse to wipe anything that is not a mounted FAT volume: a typo'd \\# SD_BOOT (e.g. /Volumes or $HOME) must never reach the rm -rf below. \\if ! mount | grep -q " on $SD_BOOT (msdos"; then \\ echo "error: $SD_BOOT is not a mounted FAT32 volume — refusing to wipe it" >&2 \\ exit 1 \\fi \\rm -rf "$SD_BOOT"/* \\cp zig-out/kernel8.img zig-out/armstub8.bin config.txt "$SD_BOOT/" \\cp "$FIRMWARE/bcm2711-rpi-4-b.dtb" "$SD_BOOT/" \\cp "$FIRMWARE/start4.elf" "$SD_BOOT/" \\cp "$FIRMWARE/fixup4.dat" "$SD_BOOT/" \\mkdir -p "$SD_BOOT/overlays" \\cp "$FIRMWARE/overlays/miniuart-bt.dtbo" "$SD_BOOT/overlays/" \\# Re-seed the FAT32 roundtrip test files: the wipe above removed \\# them, and the in-kernel fs-roundtrip scenario needs both present \\# to run its write/verify phases (8.3 names; see scripts/format_sd.sh). \\dd if=/dev/zero of="$SD_BOOT/ROUNDTR.DAT" bs=4096 count=1 2>/dev/null \\dd if=/dev/zero of="$SD_BOOT/ROUNDTR.MAG" bs=1 count=1 2>/dev/null \\rm -f "$SD_BOOT"/._ROUNDTR* 2>/dev/null || true \\# 0-byte EMPTY.TXT for [TEST] fs-empty-write: the first write \\# must allocate this file's first cluster (fat32_backend.write \\# step 0). Stays 0 bytes until that scenario writes it. \\: > "$SD_BOOT/EMPTY.TXT" \\rm -f "$SD_BOOT"/._EMPTY* 2>/dev/null || true \\# Identity seeds: the writable shadow (the boot login \\# reads it first; passwd rewrites it) + the permission overlay \\# that keeps it 0600 root:root. Same bytes as the QEMU image. \\cp zig-out/shadow "$SD_BOOT/SHADOW" \\cp user_space/etc/perms.tab "$SD_BOOT/PERMS.TAB" \\rm -f "$SD_BOOT"/._SHADOW "$SD_BOOT"/._PERMS* 2>/dev/null || true \\sync \\diskutil eject "$SD_BOOT" }); deploy.step.dependOn(all_step); deploy.step.dependOn(&install_shadow.step); const deploy_step = b.step( "deploy", "Copy kernel8.img, armstub8.bin, config.txt and RPi firmware to $SD_BOOT (default /Volumes/BOOT)", ); deploy_step.dependOn(&deploy.step); } // ---- clean: blow away cache + outputs. ---- const clean = b.addSystemCommand(&.{ "sh", "-c", "rm -rf .zig-cache zig-out" }); const clean_step = b.step("clean", "Remove .zig-cache and zig-out"); clean_step.dependOn(&clean.step); // ---- run targets — board-specific QEMU machines ---- // `zig build -Dboard=rpi4b run` boots on `-M raspi4b` (Pi 4 model); // `zig build -Dboard=virt run-virt` boots on `-M virt`. Each step // is only registered for its board; calling `run` with virt or // `run-virt` with rpi4b yields a "step not found" error. if (board == .rpi4b) { // SD-card backing image for QEMU's raspi4b SDHCI peripheral. // scripts/make_test_disk.sh emits a // deterministic 64 MiB zero-filled file at zig-out/test_sd.img; // both raspi4b QEMU steps below depend on it and pass it via // `-drive if=sd,file=...,format=raw`. virt steps do NOT take // the flag — QEMU's `-M virt` rejects `if=sd` because the // machine has no SDHCI peripheral. const make_test_disk_cmd = b.addSystemCommand(&.{ "sh", "scripts/make_test_disk.sh", }); // Identity seeds: the generated shadow (same bytes as the // initramfs /etc/shadow) lands at ::/SHADOW, the permission // overlay at ::/PERMS.TAB — so the rpi4b QEMU target exercises // the writable-shadow + overlay path end to end. LazyPath args // also give this step its dependency on gen_shadow. make_test_disk_cmd.addFileArg(shadow_file); make_test_disk_cmd.addFileArg(b.path("user_space/etc/perms.tab")); const qemu_cmd = b.addSystemCommand(&.{ "qemu-system-aarch64", "-M", "raspi4b", "-display", "none", "-serial", "null", // PL011 (UART4) → discarded "-serial", "stdio", // Mini-UART (UART1) → host stdio "-kernel", "zig-out/kernel8.img", "-drive", "if=sd,file=zig-out/test_sd.img,format=raw", }); // qemu reads zig-out/kernel8.img via a literal path string, so // the install step must finish before qemu launches. Without // this dependency, a clean tree (post `zig build clean`) races // qemu against the install and qemu sees no kernel image. The // same race exists for test_sd.img → depend on make_test_disk_cmd. qemu_cmd.step.dependOn(&install_kernel_img.step); qemu_cmd.step.dependOn(&make_test_disk_cmd.step); const run_step = b.step("run", "Run Flash in QEMU (raspi4b)"); run_step.dependOn(&install_kernel_img.step); // depends on kernel8.img run_step.dependOn(&qemu_cmd.step); // Self-validating QEMU run: the watchdog tails the serial log, // exits 0 on `[ OK ] Reached target Shell.` (with no `[FAIL]` / `ERROR CAUGHT` and // the expected free-page-checkpoint counts), exits 1 on // `ERROR CAUGHT`, any `[FAIL]`, count drift, or timeout. // Same QEMU args as `run`. raspi4b is slow (~5–8 min); the // 720s timeout matches the historical bash-watchdog ceiling. const test_rpi4b_cmd = b.addSystemCommand(&.{ "scripts/run_qemu_test.sh", "720", "qemu-system-aarch64", "-M", "raspi4b", "-display", "none", "-serial", "null", "-serial", "stdio", "-kernel", "zig-out/kernel8.img", "-drive", "if=sd,file=zig-out/test_sd.img,format=raw", }); test_rpi4b_cmd.step.dependOn(&install_kernel_img.step); test_rpi4b_cmd.step.dependOn(&make_test_disk_cmd.step); // The kernel image is passed as a literal path string, not a tracked // file input, so the build graph cannot see it change between runs. // Without this, a second `test-rpi4b` is cache-skipped and reports // success without ever booting QEMU — a false green. A boot test must // always run, so mark it as having side effects. test_rpi4b_cmd.has_side_effects = true; const test_rpi4b_step = b.step("test-rpi4b", "Boot raspi4b in QEMU and assert the boot reaches the fsh prompt"); test_rpi4b_step.dependOn(&test_rpi4b_cmd.step); } if (board == .virt) { const qemu_virt_cmd = b.addSystemCommand(&.{ "qemu-system-aarch64", "-M", "virt,gic-version=3", "-cpu", "cortex-a72", "-m", "1G", "-nographic", // PL011 → host stdio (no separate -serial needed) "-kernel", "zig-out/kernel8.img", }); // Same install-before-launch ordering as the rpi4b branch. qemu_virt_cmd.step.dependOn(&install_kernel_img.step); const run_virt_step = b.step("run-virt", "Run FlashOS in QEMU (virt)"); run_virt_step.dependOn(&install_kernel_img.step); run_virt_step.dependOn(&qemu_virt_cmd.step); // Self-validating QEMU run for virt — same contract as // `test-rpi4b`. virt boots in seconds; 60s is generous. const test_virt_cmd = b.addSystemCommand(&.{ "scripts/run_qemu_test.sh", "60", "qemu-system-aarch64", "-M", "virt,gic-version=3", "-cpu", "cortex-a72", "-m", "1G", "-nographic", "-kernel", "zig-out/kernel8.img", }); test_virt_cmd.step.dependOn(&install_kernel_img.step); // Always boot — same reason as test-rpi4b: the kernel path is a literal // string, so the step would otherwise cache-skip and false-green. test_virt_cmd.has_side_effects = true; const test_virt_step = b.step("test-virt", "Boot virt in QEMU and assert the boot reaches the fsh prompt"); test_virt_step.dependOn(&test_virt_cmd.step); } // ---- iso: GRUB-EFI rescue ISO for VMware Fusion / UEFI hosts ---- // virt-only — Pi has no use for an EFI ISO since the GPU bootloader // chain expects a raw kernel8.img + RPi firmware. Calling // `zig build -Dboard=rpi4b iso` triggers the failure branch with a // clear message, matching the workflow doc's acceptance criterion. const iso_step = b.step("iso", "Build flashos.iso (board=virt only)"); if (board == .virt) { const make_iso = b.addSystemCommand(&.{"scripts/make_iso.sh"}); make_iso.step.dependOn(&install_kernel_img.step); iso_step.dependOn(&make_iso.step); } else { const iso_fail = b.addSystemCommand(&.{ "sh", "-c", "echo 'iso target requires -Dboard=virt' >&2; exit 1", }); iso_step.dependOn(&iso_fail.step); } // Host-side unit tests. One test target per kernel module under test // — the module file IS the test root, so its inline `test "…"` blocks // land in `builtin.test_functions`. The shared `tests/host_stubs.zig` // object satisfies the kernel module's `extern fn` HW-side // dependencies at link time. The natural alternative — a single test // root that imports `src/start.zig` — fails to link because // `start.zig` transitively pulls in assembly-only externs // (`set_pgd`, `ret_from_fork`, `ksyms_init`, …) that no host stub // can satisfy. const host_alloc_obj = b.addObject(.{ .name = "host_alloc", .root_module = b.createModule(.{ .root_source_file = b.path("tests/host_alloc.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const stubs_obj = b.addObject(.{ .name = "host_stubs", .root_module = b.createModule(.{ .root_source_file = b.path("tests/host_stubs.zig"), .target = b.graph.host, .optimize = .Debug, }), }); const test_step = b.step("test", "Run host-side unit tests"); test_step.dependOn(hygiene_step); // Shared task_layout module — see kernel-build comment above for // why the named modules must share a single Module instance. const task_layout_test_mod = b.createModule(.{ .root_source_file = task_layout_src, .target = b.graph.host, .optimize = .Debug, }); const user_layout_test_mod = b.createModule(.{ .root_source_file = user_layout_src, .target = b.graph.host, .optimize = .Debug, }); // Host-target alias of the pure path module so execve.zig's host // build can satisfy its `@import("path")`. The pure // joinResolve helper itself is host-tested via the standalone // src/path.zig target wired below. const path_test_mod = b.createModule(.{ .root_source_file = path_src, .target = b.graph.host, .optimize = .Debug, }); // Host-target alias of the shared ABI file so vfs.zig's host build // can satisfy its `@import("syscall_defs")` for the Dirent type // Pure comptime constants — no externs, no stubs. const syscall_defs_test_mod = b.createModule(.{ .root_source_file = syscall_defs_src, .target = b.graph.host, .optimize = .Debug, }); const fork_stubs_mod = b.createModule(.{ .root_source_file = b.path("tests/fork_stubs.zig"), .target = b.graph.host, .optimize = .Debug, }); fork_stubs_mod.addImport("task_layout", task_layout_test_mod); const host_stubs_fork_mod = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_fork.zig"), .target = b.graph.host, .optimize = .Debug, }); host_stubs_fork_mod.addImport("task_layout", task_layout_test_mod); // execve.zig — argv-block encoder host coverage. Pure // layout function, no externs, so no stubs. The returned Module is // reused as the fork.zig test target's "execve" import — fork.zig // names execve.ArgvBlock in prepare_move_to_user_elf_argv. The // `path` import is satisfied even on host because the file // top-level @imports the module unconditionally; the kernel-only // join site sits behind the comptime is_kernel guard. const execve_test_mod = addHostTest(b, test_step, .{ .src = "src/execve.flash", .src_lazy = execve_src, .imports = &.{.{ .name = "path", .mod = path_test_mod }}, }); // path.zig — cwd-aware path-resolution host coverage. // Pure joinResolve: no externs, no stubs. The freestanding kernel // and the host test exercise the same source through the `path` // module wired above. _ = addHostTest(b, test_step, .{ .src = "src/path.flash", .src_lazy = path_src }); // trace/fp_walk.zig — the -Dtrace sampler's AAPCS64 frame-pointer // chain decoder. Pure `walkChain` over a flat stack-page view (no // kernel externs), so the FP-record math + the bounds/alignment/ // monotonic guards are host-verified deterministically. The live // sampler only fires on real-Pi async timer ticks, so this is the // decode-correctness gate; no stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "src/trace/fp_walk.zig" }); // flibc readline.zig — line-editor state-machine host coverage. // Pure `step` transition + `State` buffer; the SVC // driver sits behind a comptime `has_driver` gate so the host // build never analyses inline asm. No stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/readline.flash", .src_lazy = flibc_srcs.get("readline").? }); // flibc execvp.flash — bare-name → `/bin/` resolver host // coverage. Pure `resolve` path-build; the SVC driver // sits behind the same `has_driver` gate as readline. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/execvp.flash", .src_lazy = flibc_srcs.get("execvp").? }); // fsh tokenize — whitespace splitter + single-`|` split host // coverage. Pure `tokenize`: fills a caller argv array // from a line + scratch buffer; no externs, no stubs, no SVC. Compiles // the flashc-generated module (tokenize.flash is the source of truth). _ = addHostTest(b, test_step, .{ .src = "user_space/fsh/tokenize.flash", .src_lazy = tokenize_gen }); // grep match core — pure windowed substring matcher with ASCII case-fold. // No externs, no stubs, no SVC; the open/read/line-assembly driver sits in // tools/grep.flash. Compiles the flashc-generated module (the .flash is the // source of truth). _ = addHostTest(b, test_step, .{ .src = "tools/grep_match.flash", .src_lazy = grep_match_gen }); // flibc keys.zig — VT100 input Decoder host coverage (arrows / ctrl / tab). // Pure `Decoder.feed`; the SVC readKey driver sits behind the same // has_driver gate as readline. No stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/keys.flash", .src_lazy = flibc_srcs.get("keys").? }); // flibc completion.zig — tab-completion core host coverage (parse, // hasPrefix, commonPrefixLen). Pure; the readdir-driven gathering lives in // readline's driver. No stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/completion.flash", .src_lazy = flibc_srcs.get("completion").? }); // console_ui screen — panel / kv / cursor renderer host coverage. The // test blocks live in the Flash source; compile the generated screen.zig // from the composed console_ui directory so its sibling import resolves. _ = addHostTest(b, test_step, .{ .src = "lib/console_ui/screen.flash", .src_lazy = console_ui_screen_src }); // flibc pager.zig — pure scroll / line-index core host coverage (init line // indexing, line slicing, scroll clamping). The screen.enter + readKey // driver lives in tools/less_elf.zig. No stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/pager.flash", .src_lazy = flibc_srcs.get("pager").? }); // flibc gapbuf.zig — pure editing core host coverage (gap insert/delete/ // moveGap/grow, segment line index, cursor motions, viewport scroll). The // interactive loop the editor builds on it lives in tools/edit.flash and is // Pi-only (no QEMU stdin), so these host tests are the correctness proof. A // standalone module like grep_match — not part of the flibc aggregate, so it // adds no footprint to existing boot binaries. The generated source is shared // with edit.elf's module (gapbuf_gen, declared at the edit wiring above). No // stubs, no imports. _ = addHostTest(b, test_step, .{ .src = "user_space/lib/flibc/gapbuf.flash", .src_lazy = gapbuf_gen }); // virt DTB parser — pure big-endian FDT decode + bounds guards. // The handoff entry (`fromHandoff`) reads the `dtb_pa` extern and the // linear map, so it stays kernel-only; the tests build a `Dtb` over a // hand-written blob and exercise findNode/getProp/findReg/findInterrupt // plus the corrupt-length guard. Imports only std → no stubs. _ = addHostTest(b, test_step, .{ .src = "src/board/virt/dtb.flash", .src_lazy = virt_dtb_src }); // Host-target build of the Flash-sourced elf module for fork.zig's // @import("elf"). Shares the one flashc transpile (elf_src); the kernel // build's elf_mod is aarch64-freestanding, so the test needs its own. const elf_for_fork_mod = b.createModule(.{ .root_source_file = elf_src, .target = b.graph.host, .optimize = .Debug, }); elf_for_fork_mod.addImport("user_layout", user_layout_test_mod); const fork_test_mod = addHostTest(b, test_step, .{ .src = "src/fork.flash", .src_lazy = fork_src, .stubs = b.addObject(.{ .name = "host_stubs_fork", .root_module = host_stubs_fork_mod, }), .imports = &.{ .{ .name = "task_layout", .mod = task_layout_test_mod }, .{ .name = "user_layout", .mod = user_layout_test_mod }, .{ .name = "fdtable", .mod = fork_stubs_mod }, .{ .name = "execve", .mod = execve_test_mod }, .{ .name = "elf", .mod = elf_for_fork_mod }, }, }); // fork.zig top-level @imports build_options for the verbose-fork gate; // the kernel build gets it via kernel_mod, the host test needs it wired // explicitly since this module is built standalone. fork_test_mod.addOptions("build_options", build_options); const mm_user_stubs_mod = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_mm_user.zig"), .target = b.graph.host, .optimize = .Debug, }); mm_user_stubs_mod.addImport("task_layout", task_layout_test_mod); _ = addHostTest(b, test_step, .{ .src = "src/mm_user.flash", .src_lazy = mm_user_src, .stubs = b.addObject(.{ .name = "host_stubs_mm_user", .root_module = mm_user_stubs_mod, }), .imports = &.{ .{ .name = "task_layout", .mod = task_layout_test_mod }, .{ .name = "user_layout", .mod = user_layout_test_mod }, }, }); // vanilla single-module test targets — shared stubs, no named imports. _ = addHostTest(b, test_step, .{ .src = "src/page_alloc.flash", .src_lazy = page_alloc_src, .stubs = stubs_obj }); _ = addHostTest(b, test_step, .{ .src = "src/elf.flash", .src_lazy = elf_src, .stubs = stubs_obj, .imports = &.{.{ .name = "user_layout", .mod = user_layout_test_mod }}, }); // wait_queue is its own test target AND the named module pipe.zig // imports — capture the helper's returned Module so the pipe call // below can plug it back in as the "wait_queue" import. const wq_test_mod = addHostTest(b, test_step, .{ .src = "src/wait_queue.flash", .src_lazy = wait_queue_src, .stubs = stubs_obj, .imports = &.{.{ .name = "task_layout", .mod = task_layout_test_mod }}, }); // pipe.zig pulls in wait_queue + task_layout as named modules + its // own page-allocator stub so it doesn't double-define get_free_page // / free_page against the page_alloc test target. stubs_obj is // already pulled in transitively via wq_test_mod, so omitting it // from `stubs` here keeps the host stubs single-defined. _ = addHostTest(b, test_step, .{ .src = "src/pipe.flash", .src_lazy = pipe_src, .extra_stubs = &.{host_alloc_obj}, .imports = &.{ .{ .name = "wait_queue", .mod = wq_test_mod }, .{ .name = "task_layout", .mod = task_layout_test_mod }, }, }); // console.zig — ring + WaitQueue host coverage. // Same wiring as pipe.zig minus the page allocator (ring is BSS, // shared stubs_obj alone suffices). stubs_obj arrives transitively // via wq_test_mod, so the helper's `stubs` field stays unset. _ = addHostTest(b, test_step, .{ .src = "src/console.flash", .src_lazy = console_src, .imports = &.{ .{ .name = "wait_queue", .mod = wq_test_mod }, .{ .name = "task_layout", .mod = task_layout_test_mod }, }, }); // sched.zig — pure-helper host coverage. sched.zig // itself exports current / preempt_disable / preempt_enable / // schedule, so the shared stubs_obj would double-define those at // link time. Dedicated sched-stub object plugs only the HW-side gap // (core_switch_to, set_pgd, irq_*, free_page*, _schedule) plus a // null get_free_page for the transitively-imported pipe module. const sched_stubs_obj = b.addObject(.{ .name = "host_stubs_sched", .root_module = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_sched.zig"), .target = b.graph.host, .optimize = .Debug, }), }); // Dedicated wait_queue / pipe Modules for the sched test target — // can't reuse the helper-built wq_test_mod (which carries stubs_obj) // or a pipe equivalent (which would carry pipe_stubs_obj) because // either path re-introduces same-symbol collisions against // sched_stubs_obj. Hand-build a stub-free chain instead. const wq_sched_mod = b.createModule(.{ .root_source_file = wait_queue_src, .target = b.graph.host, .optimize = .Debug, }); wq_sched_mod.addImport("task_layout", task_layout_test_mod); const pipe_sched_mod = b.createModule(.{ .root_source_file = pipe_src, .target = b.graph.host, .optimize = .Debug, }); pipe_sched_mod.addImport("wait_queue", wq_sched_mod); pipe_sched_mod.addImport("task_layout", task_layout_test_mod); // file_sched_mod — same stub-free pattern as pipe_sched_mod above. // sched.zig imports `file` for the do_wait_impl reap plumbing; // sched_stubs_obj already provides the // get_free_page / free_page / preempt_* externs file.zig needs. const file_sched_mod = b.createModule(.{ .root_source_file = b.path("src/file.zig"), .target = b.graph.host, .optimize = .Debug, }); file_sched_mod.addImport("task_layout", task_layout_test_mod); const fdtable_sched_mod = b.createModule(.{ .root_source_file = fdtable_src, .target = b.graph.host, .optimize = .Debug, }); fdtable_sched_mod.addImport("task_layout", task_layout_test_mod); fdtable_sched_mod.addImport("pipe", pipe_sched_mod); fdtable_sched_mod.addImport("file", file_sched_mod); _ = addHostTest(b, test_step, .{ .src = "src/sched.flash", .src_lazy = sched_src, .stubs = sched_stubs_obj, .imports = &.{ .{ .name = "task_layout", .mod = task_layout_test_mod }, .{ .name = "fdtable", .mod = fdtable_sched_mod }, }, }); // initramfs.zig — newc cpio parser. Pure data parser with no externs // in host builds — the shared stubs_obj is // linked for parity with the other test targets, not because the // module needs it. _ = addHostTest(b, test_step, .{ .src = "src/initramfs.flash", .src_lazy = initramfs_src, .stubs = stubs_obj, }); // file.zig — File handle helpers. Same shape as pipe.zig: dedicated // per-target stub so the bump // allocator's get_free_page / free_page don't clash with the // page_alloc test target's real allocator. The stub additionally // ships a typed `current: ?*TaskStruct` (instead of the shared // host_stubs.zig's `?*anyopaque`) so future initramfs/file tests // can reach into `current.open_files` directly — see // the post-mortem doc for why this is a new per-target stub // file rather than a widening of // host_stubs.zig. Both this stub's module and the file.zig test // module share `task_layout_test_mod` so the `?*TaskStruct` type // matches at link time. const file_stubs_mod = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_initramfs.zig"), .target = b.graph.host, .optimize = .Debug, }); file_stubs_mod.addImport("task_layout", task_layout_test_mod); const file_stubs_obj = b.addObject(.{ .name = "host_stubs_initramfs", .root_module = file_stubs_mod, }); _ = addHostTest(b, test_step, .{ .src = "src/file.zig", .stubs = file_stubs_obj, .extra_stubs = &.{host_alloc_obj}, .imports = &.{.{ .name = "task_layout", .mod = task_layout_test_mod }}, }); // vfs.zig — VFS dispatch layer. vfs.zig imports the // `file` named module for the `File` type its vtable signatures // reference; a dedicated stub-free file module (same pattern as // file_sched_mod above) shares task_layout_test_mod so the File // type matches at link, and vfs_stubs_obj plugs file.zig's // get_free_page / free_page / preempt_* externs. const file_test_mod = b.createModule(.{ .root_source_file = b.path("src/file.zig"), .target = b.graph.host, .optimize = .Debug, }); file_test_mod.addImport("task_layout", task_layout_test_mod); _ = addHostTest(b, test_step, .{ .src = "src/fdtable.flash", .src_lazy = fdtable_src, .stubs = file_stubs_obj, .extra_stubs = &.{host_alloc_obj}, .imports = &.{ .{ .name = "task_layout", .mod = task_layout_test_mod }, .{ .name = "pipe", .mod = pipe_sched_mod }, .{ .name = "file", .mod = file_test_mod }, }, }); const vfs_stubs_obj = b.addObject(.{ .name = "host_stubs_vfs", .root_module = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_vfs.zig"), .target = b.graph.host, .optimize = .Debug, }), }); _ = addHostTest(b, test_step, .{ .src = "src/vfs.zig", .stubs = vfs_stubs_obj, .extra_stubs = &.{host_alloc_obj}, .imports = &.{ .{ .name = "file", .mod = file_test_mod }, .{ .name = "syscall_defs", .mod = syscall_defs_test_mod }, }, }); // sdhci_cmd.flash — pure-data SDHCI command encoder + CSD parser. // No externs, no fixture state. _ = addHostTest(b, test_step, .{ .src = "src/sdhci_cmd.flash", .src_lazy = sdhci_cmd_src }); // mailbox.flash — pure-data VideoCore property-tag builder + parser. // No externs; the MMIO doorbell lives in // src/board/rpi4b/mailbox.zig. _ = addHostTest(b, test_step, .{ .src = "src/mailbox.flash", .src_lazy = mailbox_src }); // usb_descriptors.flash — byte-exact USB descriptor set + SETUP decode // (DWC2 gadget). No externs; pure data + pure functions. _ = addHostTest(b, test_step, .{ .src = "src/usb_descriptors.flash", .src_lazy = usb_descriptors_src }); // usb_tx_ring.flash — bulk-IN TX byte-ring (DWC2 gadget). // No externs; pure ring arithmetic (push/peek/advance/clear). _ = addHostTest(b, test_step, .{ .src = "src/usb_tx_ring.flash", .src_lazy = usb_tx_ring_src }); // klog_ring.zig — kernel-log byte-ring (overwrite-oldest) host coverage. // Pure ring arithmetic (push / overwrite-oldest / snapshot); // imports syscall_defs only for KLOG_SIZE. The returned Module is reused // as the utilc.zig test target's "klog_ring" import (utilc tees // main_output into the ring), mirroring how wait_queue's test module // doubles as pipe's import. const klog_ring_test_mod = addHostTest(b, test_step, .{ .src = "src/klog_ring.flash", .src_lazy = klog_ring_src, .imports = &.{.{ .name = "syscall_defs", .mod = syscall_defs_test_mod }}, }); // fat32.flash — FAT32 on-disk layout decode. // Pure data module: imports only the host-only block_dev Module // (BlockDev type), uses an in-memory 64 KiB fake disk built by the // inline test fixture. No page-alloc or task-layout externs needed. const block_dev_test_mod = b.createModule(.{ .root_source_file = block_dev_src, .target = b.graph.host, .optimize = .Debug, }); _ = addHostTest(b, test_step, .{ .src = "src/fat32.flash", .src_lazy = fat32_src, .imports = &.{.{ .name = "block_dev", .mod = block_dev_test_mod }}, }); // fat32_backend.zig — FAT32 VFS backend host-test. Asserts the // sub-sector splice contract that write():203-208 fulfills. See // the comment block at the end of // src/fat32_backend.zig for the bug-class link and the // ReleaseSmall reproducibility note. Created modules for fat32 // and vfs because the kernel-side fat32_mod / vfs_mod are wired // for aarch64 freestanding, not host. const fat32_for_backend_mod = b.createModule(.{ .root_source_file = fat32_src, .target = b.graph.host, .optimize = .Debug, }); fat32_for_backend_mod.addImport("block_dev", block_dev_test_mod); const vfs_for_backend_mod = b.createModule(.{ .root_source_file = b.path("src/vfs.zig"), .target = b.graph.host, .optimize = .Debug, }); vfs_for_backend_mod.addImport("file", file_test_mod); vfs_for_backend_mod.addImport("syscall_defs", syscall_defs_test_mod); // overlay.zig — FAT32 permission-overlay parser host coverage. // Pure parse/lookup truth table — the gate for the /mnt overlay: the // fat32_backend wiring (applyOverlay + open lookup) does not ship until // every row passes. The returned Module doubles as fat32_backend's // "overlay" import below (mirroring the klog_ring/utilc pattern). Pins // the format shared with the seed file (user_space/etc/perms.tab) and // the deploy / make_test_disk seeding. const overlay_test_mod = addHostTest(b, test_step, .{ .src = "src/overlay.flash", .src_lazy = overlay_src }); _ = addHostTest(b, test_step, .{ .src = "src/fat32_backend.flash", .src_lazy = fat32_backend_src, .stubs = vfs_stubs_obj, .imports = &.{ .{ .name = "block_dev", .mod = block_dev_test_mod }, .{ .name = "fat32", .mod = fat32_for_backend_mod }, .{ .name = "vfs", .mod = vfs_for_backend_mod }, .{ .name = "file", .mod = file_test_mod }, .{ .name = "overlay", .mod = overlay_test_mod }, }, }); _ = addHostTest(b, test_step, .{ .src = "src/initramfs_backend.flash", .src_lazy = initramfs_backend_src, .stubs = stubs_obj, .imports = &.{ .{ .name = "initramfs", .mod = b.createModule(.{ .root_source_file = initramfs_src, .target = b.graph.host, .optimize = .Debug, }) }, .{ .name = "vfs", .mod = vfs_for_backend_mod }, .{ .name = "file", .mod = file_test_mod }, }, }); // utilc.zig — kernel utility host coverage. // Trivial hex/mem helpers; stubs provided for board-specific UARTs. const utilc_stubs_obj = b.addObject(.{ .name = "host_stubs_utilc", .root_module = b.createModule(.{ .root_source_file = b.path("tests/host_stubs_utilc.zig"), .target = b.graph.host, .optimize = .Debug, }), }); _ = addHostTest(b, test_step, .{ .src = "src/utilc.flash", .src_lazy = utilc_src, .stubs = utilc_stubs_obj, .imports = &.{ .{ .name = "task_layout", .mod = task_layout_test_mod }, // utilc.main_output now tees into the kernel log ring; the host // test build needs the same module the kernel build wires. .{ .name = "klog_ring", .mod = klog_ring_test_mod }, }, }); // sha256.zig — SHA-256 / HMAC-SHA256 / PBKDF2-HMAC-SHA256 host coverage. // Pure compute, no externs, no imports, no allocation. The // vector tests (NIST FIPS 180-2, RFC 4231, the published PBKDF2 set, // plus std.crypto differentials) are the gate for the authentication // work: no kernel consumer of these primitives ships until they pass. _ = addHostTest(b, test_step, .{ .src = "src/sha256.flash", .src_lazy = sha256_src }); // shadow.zig — /etc/shadow line parser + hex decoder. Pure, // no imports; pins the format shared by sys_authenticate + gen_shadow. _ = addHostTest(b, test_step, .{ .src = "src/shadow.flash", .src_lazy = shadow_src }); // perm.zig — VFS permission check host coverage. Pure // checkAccess truth table (owner/group/other × read/write/exec × // root bypass) — the gate for the permission layer: no enforcement // site ships until every row passes. _ = addHostTest(b, test_step, .{ .src = "src/perm.flash", .src_lazy = perm_src }); // pwfile.zig — /etc/passwd parser host coverage. Pure // name/uid lookups shared by sys_passwd (kernel), /bin/login, and // fsh's whoami builtin; pins the 5-field format against // user_space/etc/passwd. _ = addHostTest(b, test_step, .{ .src = "src/pwfile.flash", .src_lazy = pwfile_src }); // build_initramfs.zig — newc encoder host coverage. Pins the // mode/uid/gid byte offsets shared with the kernel parser // (src/initramfs.zig); an encoder/parser drift here would be a silent // permission bypass. _ = addHostTest(b, test_step, .{ .src = "scripts/build_initramfs.zig" }); // hwrng.zig — kernel entropy source host coverage. The pure // SplitMix64 mixer is vector- and differential-tested; the kernel glue // (fill / hwrng_init) runs against host_stubs' ramping get_sys_count, // so the boot self-test + announce path is exercised end-to-end. _ = addHostTest(b, test_step, .{ .src = "src/hwrng.flash", .src_lazy = hwrng_src, .stubs = stubs_obj, .imports = &.{.{ .name = "console_ui", .mod = console_ui_mod }}, }); // Final pass banner. Zig's build runner is silent on a fully-green test // step (counts only surface with `--summary all`), so wire a last system // command that depends on every test run added above — it executes only // after they all pass — and prints one green " tests passed" line. The // file list it counts is host_test_srcs, populated by addHostTest, so the // number tracks the build graph and never drifts. A filtered run prints a // count-free banner instead (only a subset ran). { const argv = b.allocator.alloc([]const u8, 3 + host_test_n) catch @panic("OOM"); argv[0] = "sh"; argv[1] = "scripts/test_tally.sh"; argv[2] = host_test_filter orelse ""; for (host_test_srcs[0..host_test_n], 0..) |src, i| argv[3 + i] = src; const tally = b.addSystemCommand(argv); // Depend on the hygiene checks + every test run already attached to // test_step, so the banner is the last thing printed. for (test_step.dependencies.items) |dep| tally.step.dependOn(dep); test_step.dependOn(&tally.step); } }