diff --git a/build-support/build.zig b/build-support/build.zig new file mode 100644 index 0000000..449f27b --- /dev/null +++ b/build-support/build.zig @@ -0,0 +1,115 @@ +//! 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").?; +} diff --git a/build-support/build.zig.zon b/build-support/build.zig.zon new file mode 100644 index 0000000..a38ba8a --- /dev/null +++ b/build-support/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .build_support, + .version = "0.0.0", + .fingerprint = 0xad91962994f4be41, // Changing this has security and trust implications. + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{""}, +} diff --git a/build.zig b/build.zig index 422281e..c844d3e 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,8 @@ const std = @import("std"); const builtin = @import("builtin"); +// The danos build API (docs/build-packages-plan.md): the shared user-binary +// recipe lives in the build-support package; this root build orchestrates. +const build_support = @import("build-support"); /// danos is developed against Zig 0.16.x. Pre-1.0 Zig makes breaking API changes /// between minor releases, and the .zon's `minimum_zig_version` only enforces a @@ -48,18 +51,10 @@ fn timestamp(b: *std.Build) []const u8 { }); } -/// Build one user-space binary the same way for every program (init, and later -/// the VFS server + drivers): freestanding, ReleaseSmall, `.large` code model -/// (the image base is above 4 GiB — smaller models emit 32-bit relocations that -/// can't reach), linked against the `runtime` runtime library 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 -/// library/kernel/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 imports. +/// One user-space binary via the shared build-support recipe (freestanding, +/// ReleaseSmall, `.large` code model, root shim + user link script — see +/// build-support/build.zig for the full story). The program's file becomes the +/// `program` module; reach it through `programModule` to add per-binary imports. fn addUserBinary( b: *std.Build, target: std.Build.ResolvedTarget, @@ -83,15 +78,6 @@ fn addThreadedUserBinary( return addUserBinaryImpl(b, target, default_imports, name, root, true); } -/// 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"); -} - fn addUserBinaryImpl( b: *std.Build, target: std.Build.ResolvedTarget, @@ -100,41 +86,15 @@ fn addUserBinaryImpl( root: []const u8, threaded: bool, ) *std.Build.Step.Compile { - // Every user binary gets the same default set of importable modules — 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. - // Per-binary extras go through programModule(exe).addImport. Settings (target, optimize, - // code model, ...) live on the root module only; the program module inherits them. - const program_module = b.createModule(.{ - .root_source_file = b.path(root), - .imports = default_imports, - }); - const exe = b.addExecutable(.{ + return build_support.userBinaryFromImports(b, .{ .name = name, - .root_module = b.createModule(.{ - .root_source_file = b.path("library/kernel/root.zig"), - .target = target, - .optimize = .ReleaseSmall, - .code_model = .large, - .single_threaded = !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(default_imports, "start") }, - .{ .name = "logging", .module = findImport(default_imports, "logging") }, - .{ .name = "program", .module = program_module }, - }, - }), + .root_source_file = b.path(root), + .shim_source_file = b.path("library/kernel/root.zig"), + .linker_script = b.path("library/kernel/user.ld"), + .target = target, + .default_imports = default_imports, + .threaded = threaded, }); - exe.setLinkerScript(b.path("library/kernel/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 `addUserBinary` — the module rooted @@ -142,7 +102,7 @@ fn addUserBinaryImpl( /// 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. fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module { - return exe.root_module.import_table.get("program").?; + return build_support.programModule(exe); } /// The modules the kernel imports, gathered once so both kernel variants (the @@ -597,14 +557,8 @@ pub fn build(b: *std.Build) void { const diagnose = b.option(bool, "diagnose", "Boot without the display service so the timestamped boot transcript stays on screen (real-hardware debugging)") orelse false; // --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader --- - // 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. - const kernel_target = b.resolveTargetQuery(.{ - .cpu_arch = .x86_64, - .os_tag = .freestanding, - .abi = .none, - }); + // (See build-support/build.zig for why SSE2 stays enabled.) + const kernel_target = build_support.freestandingTarget(b); const kernel_modules = KernelModules{ .boot_handoff = boot_handoff_module, diff --git a/build.zig.zon b/build.zig.zon index 2d0d4f8..c397039 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,6 +32,9 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ + // The danos build API — the shared user-binary recipe every build file + // (root and per-binary packages) consumes (docs/build-packages-plan.md). + .@"build-support" = .{ .path = "build-support" }, // See `zig fetch --save ` for a command-line interface for adding dependencies. //.example = .{ // // When updating this field to a new URL, be sure to delete the corresponding @@ -70,12 +73,5 @@ // Paths are relative to the build root. Use the empty string (`""`) to refer to // the build root itself. // A directory listed here means that all files within, recursively, are included. - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - // For example... - //"LICENSE", - //"README.md", - }, + .paths = .{""}, }