build: phase 0 — extract the build-support package

The shared user-binary recipe (freestanding target, root-shim wiring,
link-script and image-base settings) moves out of the root build into
build-support/, the package that is the single home for cross-cutting
build changes (docs/build-packages-plan.md). The root build's
addUserBinary/addThreadedUserBinary keep their signatures and delegate;
nothing else moves. Boot-image file list unchanged.
This commit is contained in:
Daniel Samson
2026-07-26 22:49:05 +01:00
parent fc0b934b7f
commit 4194bb6e32
4 changed files with 144 additions and 71 deletions
+17 -63
View File
@@ -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,