//! The danos build API (docs/build-packages-plan.md, phase 0): the one shared //! recipe for building a user-space binary, extracted from the root build so //! cross-cutting build changes have a single home. 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, }); } /// What `userBinaryFromImports` 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, /// The shared root shim supplying the compilation-root declarations /// (`main` re-export, panic handler, `_start` pull): library/kernel/root.zig. shim_source_file: std.Build.LazyPath, /// The shared user link script. Its PHDRS (segment permissions) are /// authoritative — the kernel's W^X user-ELF loader requires exact perms. linker_script: std.Build.LazyPath, target: std.Build.ResolvedTarget, /// The default set of importable modules every user binary sees — the /// library/kernel concern modules (ipc, memory, process, time, logging, /// file-system, ...), the device/service clients (driver, block, display, /// input), mmio, acpi-ids, and xkeyboard-config. Must include `start` and /// `logging` (the root shim reaches those two directly). default_imports: []const std.Build.Module.Import, /// 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, }; /// The module registered under `name` in `imports` — the root shim reaches the /// couple of concern modules it needs (start, logging) out of the default set. fn findImport(imports: []const std.Build.Module.Import, name: []const u8) *std.Build.Module { for (imports) |import| { if (std.mem.eql(u8, import.name, name)) return import.module; } @panic("default_imports is missing a module the root shim needs"); } /// 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 /// (options.shim_source_file), 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 imports. pub fn userBinaryFromImports(b: *std.Build, options: UserBinaryOptions) *std.Build.Step.Compile { // 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 = options.default_imports, }); const exe = b.addExecutable(.{ .name = options.name, .root_module = b.createModule(.{ .root_source_file = options.shim_source_file, .target = options.target, .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); the program's own file reaches the full default set. .imports = &.{ .{ .name = "start", .module = findImport(options.default_imports, "start") }, .{ .name = "logging", .module = findImport(options.default_imports, "logging") }, .{ .name = "program", .module = program_module }, }, }), }); exe.setLinkerScript(options.linker_script); 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 `userBinaryFromImports` — the /// module rooted at the program's own source file. Per-binary imports /// (protocol modules, bus ABIs) 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").?; } /// The library domain packages a user binary's default import set draws from. /// A binary package declares these in its build.zig.zon under exactly these /// dependency names (kernel, device, client, xkeyboard-config) and resolves /// them with `domains`. Path dependencies deduplicate by resolved location, /// so every binary and the root build share one instance of each module. pub const Domains = struct { kernel: *std.Build.Dependency, device: *std.Build.Dependency, client: *std.Build.Dependency, xkeyboard_config: *std.Build.Dependency, }; /// Resolve the four default-set domain packages by their conventional /// dependency names — the one-liner for a binary package's build fn. pub fn domains(b: *std.Build) Domains { return .{ .kernel = b.dependency("kernel", .{}), .device = b.dependency("device", .{}), .client = b.dependency("client", .{}), .xkeyboard_config = b.dependency("xkeyboard-config", .{}), }; } /// What `userBinary` needs to know about one user binary. The domain wiring /// (default imports, root shim, link script, target) is derived. pub const DomainUserBinaryOptions = struct { name: []const u8, /// The program's own source file — it becomes the `program` module. root_source_file: std.Build.LazyPath, domains: Domains, /// See UserBinaryOptions.threaded. threaded: bool = false, }; /// THE default import set every user binary sees — the library/kernel concern /// modules (ipc, memory, process, time, logging, file-system, ...), the /// device/service clients (driver, block, display, input), mmio, acpi-ids, /// and xkeyboard-config — assembled from the domain packages. This is the /// single authoritative list: the root build's stanzas and every binary /// package both draw from here, so adding a default module is a one-place /// change (the cross-cutting rule, docs/build-packages-plan.md). pub fn defaultImports(libraries: Domains) [17]std.Build.Module.Import { const kernel = libraries.kernel; const device = libraries.device; const client = libraries.client; return .{ .{ .name = "mmio", .module = device.module("mmio") }, .{ .name = "xkeyboard-config", .module = libraries.xkeyboard_config.module("xkeyboard-config") }, .{ .name = "acpi-ids", .module = device.module("acpi-ids") }, .{ .name = "system-call", .module = kernel.module("system-call") }, .{ .name = "ipc", .module = kernel.module("ipc") }, .{ .name = "memory", .module = kernel.module("memory") }, .{ .name = "process", .module = kernel.module("process") }, .{ .name = "thread", .module = kernel.module("thread") }, .{ .name = "time", .module = kernel.module("time") }, .{ .name = "logging", .module = kernel.module("logging") }, .{ .name = "file-system", .module = kernel.module("file-system") }, .{ .name = "service", .module = kernel.module("service") }, .{ .name = "start", .module = kernel.module("start") }, .{ .name = "driver", .module = device.module("driver") }, .{ .name = "block", .module = device.module("block") }, .{ .name = "display", .module = client.module("display") }, .{ .name = "input", .module = client.module("input") }, }; } /// Build one user binary against the domain packages' default import set /// (`defaultImports`). Per-binary extras go through /// `programModule(exe).addImport`. This is THE recipe a binary package's /// build.zig calls; the root shim and user link script come from the kernel /// domain package's directory. pub fn userBinary(b: *std.Build, options: DomainUserBinaryOptions) *std.Build.Step.Compile { const default_imports = defaultImports(options.domains); return userBinaryFromImports(b, .{ .name = options.name, .root_source_file = options.root_source_file, .shim_source_file = options.domains.kernel.path("root.zig"), .linker_script = options.domains.kernel.path("user.ld"), .target = freestandingTarget(b), .default_imports = &default_imports, .threaded = options.threaded, }); }