//! The danos build API (docs/build-packages-plan.md): the one shared recipe //! for building a user-space binary. A binary package's build.zig names its //! binary and EXACTLY the modules its source imports — the moral equivalent //! of a C file's include list — and `userBinary` resolves each name from the //! library domain package that exports it. Nothing is pre-wired: an @import //! the package did not declare is a compile error, and a domain none of the //! imports come from never appears in the package's manifest. The only //! implicit dependency is the kernel package, because the shared root shim //! (root.zig, user.ld) lives there and itself reaches start + logging. //! //! Consumers declare this package in their build.zig.zon (as "build-support") //! and @import its build.zig from their own build.zig; nothing is compiled //! from this package itself — it exports build-time functions only. const std = @import("std"); pub fn build(b: *std.Build) void { _ = b; // nothing to build: this package exports build-time functions only } /// The freestanding x86-64 target every danos binary (kernel and user) is /// built for. SSE2 is part of the x86_64 baseline and UEFI leaves it enabled /// at handoff, so we keep it: disabling it forces soft-float and makes the /// compiler unable to encode the vector ops that std's formatting/runtime /// still emit. pub fn freestandingTarget(b: *std.Build) std.Build.ResolvedTarget { return b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .freestanding, .abi = .none, }); } /// Which library domain package exports each importable module — the one /// name -> home table. When a domain grows a module, it gets a row here; a /// binary naming a module whose home is missing from its own build.zig.zon /// fails loudly at dependency resolution. const ModuleHome = struct { name: []const u8, home: []const u8 }; const module_homes = [_]ModuleHome{ // library/kernel — the userspace private-ABI library, split by concern. .{ .name = "abi", .home = "kernel" }, .{ .name = "system-call", .home = "kernel" }, .{ .name = "ipc", .home = "kernel" }, .{ .name = "time", .home = "kernel" }, .{ .name = "thread", .home = "kernel" }, .{ .name = "logging", .home = "kernel" }, .{ .name = "process", .home = "kernel" }, .{ .name = "file-system", .home = "kernel" }, .{ .name = "memory", .home = "kernel" }, .{ .name = "service", .home = "kernel" }, .{ .name = "start", .home = "kernel" }, // library/device — driver-side libraries + the flat reference data. .{ .name = "mmio", .home = "device" }, .{ .name = "acpi-ids", .home = "device" }, .{ .name = "device-abi", .home = "device" }, .{ .name = "aml", .home = "device" }, .{ .name = "usb-abi", .home = "device" }, .{ .name = "usb-ids", .home = "device" }, .{ .name = "usb", .home = "device" }, .{ .name = "driver", .home = "device" }, .{ .name = "block", .home = "device" }, .{ .name = "pci", .home = "device" }, .{ .name = "pci-class", .home = "device" }, .{ .name = "device-registry", .home = "device" }, // library/client — userspace service clients. .{ .name = "display-client", .home = "client" }, .{ .name = "input-client", .home = "client" }, // library/protocol — the wire protocols. .{ .name = "vfs-protocol", .home = "protocol" }, .{ .name = "input-protocol", .home = "protocol" }, .{ .name = "block-protocol", .home = "protocol" }, .{ .name = "usb-transfer-protocol", .home = "protocol" }, .{ .name = "device-manager-protocol", .home = "protocol" }, .{ .name = "display-protocol", .home = "protocol" }, .{ .name = "scanout-protocol", .home = "protocol" }, .{ .name = "power-protocol", .home = "protocol" }, // library/csv — the /etc/*.csv helpers. .{ .name = "csv", .home = "csv" }, // library/xkeyboard-config — keycode -> keysym/character tables. .{ .name = "xkeyboard-config", .home = "xkeyboard-config" }, }; fn moduleHome(name: []const u8) ?[]const u8 { for (module_homes) |entry| { if (std.mem.eql(u8, entry.name, name)) return entry.home; } return null; } /// What `userBinary` needs to know about one user binary. pub const UserBinaryOptions = struct { name: []const u8, /// The program's own source file — it becomes the `program` module the /// root shim imports; a program only defines `pub fn main`. root_source_file: std.Build.LazyPath, /// Exactly the modules the program's source @imports (directly or through /// its same-directory files) — no more, no less. Order is free; sorted /// reads best. An undeclared @import fails the compile; a declared name no /// domain exports fails the build graph with a pointer to module_homes. imports: []const []const u8, /// Built multi-threaded (`single_threaded = false`) so real atomics/TLS /// work — required before a binary may call `Thread.spawn` /// (docs/threading.md). Threads are a deliberate per-binary opt-in. threaded: bool = false, }; /// Build one user-space binary the same way for every program (init, the /// services, the drivers): freestanding, ReleaseSmall, `.large` code model /// (the image base is above 4 GiB — smaller models emit 32-bit relocations /// that can't reach), linked with the shared user link script. Pinned to /// LLVM + LLD so the script's PHDRS (segment permissions) are authoritative — /// the kernel's W^X user-ELF loader requires exact perms. /// /// The compilation root is not the program's own file but the shared shim /// (the kernel package's root.zig), which supplies the root declarations /// (`main` re-export, panic handler, `_start` pull) so a program only defines /// `pub fn main`. The program's file becomes the `program` module the shim /// imports; reach it through `programModule` to add per-binary non-library /// modules (compile-time options). pub fn userBinary(b: *std.Build, options: UserBinaryOptions) *std.Build.Step.Compile { const kernel = b.dependency("kernel", .{}); var imports: std.ArrayListUnmanaged(std.Build.Module.Import) = .empty; for (options.imports) |name| { const home = moduleHome(name) orelse @panic(b.fmt( "no library domain exports a module named '{s}' — if a domain grew it, add its row to module_homes in build-support/build.zig", .{name}, )); const dependency = if (std.mem.eql(u8, home, "kernel")) kernel else b.dependency(home, .{}); imports.append(b.allocator, .{ .name = name, .module = dependency.module(name) }) catch @panic("OOM"); } // Settings (target, optimize, code model, ...) live on the root module // only; the program module inherits them. const program_module = b.createModule(.{ .root_source_file = options.root_source_file, .imports = imports.items, }); const exe = b.addExecutable(.{ .name = options.name, .root_module = b.createModule(.{ .root_source_file = kernel.path("root.zig"), .target = freestandingTarget(b), .optimize = .ReleaseSmall, .code_model = .large, .single_threaded = !options.threaded, // a threaded binary needs real atomics/TLS .sanitize_c = .off, .stack_check = false, .stack_protector = false, // The root shim itself imports only start (_start + panic) and // logging (std_options) — straight from the kernel package, so a // program's own import list stays exactly its own. .imports = &.{ .{ .name = "start", .module = kernel.module("start") }, .{ .name = "logging", .module = kernel.module("logging") }, .{ .name = "program", .module = program_module }, }, }), }); exe.setLinkerScript(kernel.path("user.ld")); exe.entry = .{ .symbol_name = "_start" }; exe.image_base = 0x7000_0000_0000; exe.use_llvm = true; exe.use_lld = true; return exe; } /// The `program` module of a binary built by `userBinary` — the module rooted /// at the program's own source file. Per-binary non-library modules (an /// addOptions build_options) go here, not on the root shim: module imports /// are not transitive, so an import added to the root would be invisible to /// the program's code. pub fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module { return exe.root_module.import_table.get("program").?; }