build: exact per-binary imports — the pre-wired default set is gone
Every binary's build.zig now names precisely the modules its source
imports (derived by scanning each artifact's sources, transitively
through same-directory files), and its zon carries only the domains
those come from — kernel stays implicit (the root shim + link script
live there). build-support's userBinary resolves each name through one
module-to-domain table (module_homes); Domains/domains()/defaultImports
and the raw recipe entry point are deleted. An undeclared @import is a
compile error (verified: injecting @import("xkeyboard-config") into
logger fails with 'no module named ... available within module
program'), and e.g. xkeyboard-config now appears in exactly two
manifests — the two keyboard drivers. Availability never bloated the
emitted binaries (Zig compiles only what a program imports); this makes
the declared interfaces honest. Production and -Dtest-case manifests
byte-identical; all build variants and standalone package builds
green.
This commit is contained in:
+102
-128
@@ -1,9 +1,16 @@
|
||||
//! 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.
|
||||
//! 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");
|
||||
|
||||
@@ -24,40 +31,79 @@ pub fn freestandingTarget(b: *std.Build) std.Build.ResolvedTarget {
|
||||
});
|
||||
}
|
||||
|
||||
/// What `userBinaryFromImports` needs to know about one user binary.
|
||||
/// 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", .home = "client" },
|
||||
.{ .name = "input", .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,
|
||||
/// 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,
|
||||
/// 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,
|
||||
};
|
||||
|
||||
/// 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
|
||||
@@ -66,38 +112,50 @@ fn findImport(imports: []const std.Build.Module.Import, name: []const u8) *std.B
|
||||
/// 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
|
||||
/// (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 imports.
|
||||
pub fn userBinaryFromImports(b: *std.Build, options: UserBinaryOptions) *std.Build.Step.Compile {
|
||||
/// 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 = options.default_imports,
|
||||
.imports = imports.items,
|
||||
});
|
||||
const exe = b.addExecutable(.{
|
||||
.name = options.name,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = options.shim_source_file,
|
||||
.target = options.target,
|
||||
.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); the program's own file reaches the full default set.
|
||||
// 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 = findImport(options.default_imports, "start") },
|
||||
.{ .name = "logging", .module = findImport(options.default_imports, "logging") },
|
||||
.{ .name = "start", .module = kernel.module("start") },
|
||||
.{ .name = "logging", .module = kernel.module("logging") },
|
||||
.{ .name = "program", .module = program_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
exe.setLinkerScript(options.linker_script);
|
||||
exe.setLinkerScript(kernel.path("user.ld"));
|
||||
exe.entry = .{ .symbol_name = "_start" };
|
||||
exe.image_base = 0x7000_0000_0000;
|
||||
exe.use_llvm = true;
|
||||
@@ -105,95 +163,11 @@ pub fn userBinaryFromImports(b: *std.Build, options: UserBinaryOptions) *std.Bui
|
||||
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
|
||||
/// 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").?;
|
||||
}
|
||||
|
||||
/// 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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user