build: single-source the default import set; delegate library tests

Review findings from the pilot: the 17-module default import set was
maintained twice (root's array and build-support's userBinary) — drift
would silently make packaged binaries differ from root-built ones. It
now lives once, as build-support.defaultImports; root and userBinary
both draw from it, and root's user-binary wrapper names the shim +
link script through the kernel package like build-support does.
The root aggregate test step likewise duplicated the library domains'
14 test definitions; it now depends on each domain's own standalone
test step (host-only, so root's -Dtarget/-Doptimize deliberately do
not reach them), and the client domain gains an empty test step for
uniformity. Boot-image file list unchanged.
This commit is contained in:
Daniel Samson
2026-07-26 23:10:10 +01:00
parent 6f4fdc2789
commit 15575960bd
3 changed files with 63 additions and 127 deletions
+24 -15
View File
@@ -148,20 +148,20 @@ pub const DomainUserBinaryOptions = struct {
threaded: bool = false, threaded: bool = false,
}; };
/// Build one user binary against the domain packages' default import set — /// THE default import set every user binary sees — the library/kernel concern
/// the library/kernel concern modules (ipc, memory, process, time, logging, /// modules (ipc, memory, process, time, logging, file-system, ...), the
/// file-system, ...), the device/service clients (driver, block, display, /// device/service clients (driver, block, display, input), mmio, acpi-ids,
/// input), mmio, acpi-ids, and xkeyboard-config. Per-binary extras go through /// and xkeyboard-config — assembled from the domain packages. This is the
/// `programModule(exe).addImport`. This is THE recipe a binary package's /// single authoritative list: the root build's stanzas and every binary
/// build.zig calls; the root shim and user link script come from the kernel /// package both draw from here, so adding a default module is a one-place
/// domain package's directory. /// change (the cross-cutting rule, docs/build-packages-plan.md).
pub fn userBinary(b: *std.Build, options: DomainUserBinaryOptions) *std.Build.Step.Compile { pub fn defaultImports(libraries: Domains) [17]std.Build.Module.Import {
const kernel = options.domains.kernel; const kernel = libraries.kernel;
const device = options.domains.device; const device = libraries.device;
const client = options.domains.client; const client = libraries.client;
const default_imports = [_]std.Build.Module.Import{ return .{
.{ .name = "mmio", .module = device.module("mmio") }, .{ .name = "mmio", .module = device.module("mmio") },
.{ .name = "xkeyboard-config", .module = options.domains.xkeyboard_config.module("xkeyboard-config") }, .{ .name = "xkeyboard-config", .module = libraries.xkeyboard_config.module("xkeyboard-config") },
.{ .name = "acpi-ids", .module = device.module("acpi-ids") }, .{ .name = "acpi-ids", .module = device.module("acpi-ids") },
.{ .name = "system-call", .module = kernel.module("system-call") }, .{ .name = "system-call", .module = kernel.module("system-call") },
.{ .name = "ipc", .module = kernel.module("ipc") }, .{ .name = "ipc", .module = kernel.module("ipc") },
@@ -178,11 +178,20 @@ pub fn userBinary(b: *std.Build, options: DomainUserBinaryOptions) *std.Build.St
.{ .name = "display", .module = client.module("display") }, .{ .name = "display", .module = client.module("display") },
.{ .name = "input", .module = client.module("input") }, .{ .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, .{ return userBinaryFromImports(b, .{
.name = options.name, .name = options.name,
.root_source_file = options.root_source_file, .root_source_file = options.root_source_file,
.shim_source_file = kernel.path("root.zig"), .shim_source_file = options.domains.kernel.path("root.zig"),
.linker_script = kernel.path("user.ld"), .linker_script = options.domains.kernel.path("user.ld"),
.target = freestandingTarget(b), .target = freestandingTarget(b),
.default_imports = &default_imports, .default_imports = &default_imports,
.threaded = options.threaded, .threaded = options.threaded,
+33 -112
View File
@@ -86,11 +86,14 @@ fn addUserBinaryImpl(
root: []const u8, root: []const u8,
threaded: bool, threaded: bool,
) *std.Build.Step.Compile { ) *std.Build.Step.Compile {
// The shim and link script live in the kernel domain package, so name them
// through it — the same spelling build-support's userBinary uses.
const kernel_library = b.dependency("kernel", .{});
return build_support.userBinaryFromImports(b, .{ return build_support.userBinaryFromImports(b, .{
.name = name, .name = name,
.root_source_file = b.path(root), .root_source_file = b.path(root),
.shim_source_file = b.path("library/kernel/root.zig"), .shim_source_file = kernel_library.path("root.zig"),
.linker_script = b.path("library/kernel/user.ld"), .linker_script = kernel_library.path("user.ld"),
.target = target, .target = target,
.default_imports = default_imports, .default_imports = default_imports,
.threaded = threaded, .threaded = threaded,
@@ -251,7 +254,6 @@ pub fn build(b: *std.Build) void {
const csv_module = csv_library.module("csv"); const csv_module = csv_library.module("csv");
const device_registry_module = device_library.module("device-registry"); const device_registry_module = device_library.module("device-registry");
const aml_module = device_library.module("aml"); const aml_module = device_library.module("aml");
const acpi_ids_module = device_library.module("acpi-ids");
const usb_abi_module = device_library.module("usb-abi"); const usb_abi_module = device_library.module("usb-abi");
const usb_ids_module = device_library.module("usb-ids"); const usb_ids_module = device_library.module("usb-ids");
const usb_transfer_protocol_module = protocol_library.module("usb-transfer-protocol"); const usb_transfer_protocol_module = protocol_library.module("usb-transfer-protocol");
@@ -305,27 +307,11 @@ pub fn build(b: *std.Build) void {
const scanout_protocol_module = protocol_library.module("scanout-protocol"); const scanout_protocol_module = protocol_library.module("scanout-protocol");
const power_protocol_module = protocol_library.module("power-protocol"); const power_protocol_module = protocol_library.module("power-protocol");
// The library/kernel concern modules (the userspace private-ABI library), // The driver-side libraries some binaries name as per-binary extras. (The
// the device-domain driver libraries, and the service clients. // rest of the domain modules reach the root only through build-support's
const mmio_module = device_library.module("mmio"); // defaultImports below.)
const system_call_module = kernel_library.module("system-call");
const ipc_module = kernel_library.module("ipc");
const time_module = kernel_library.module("time");
const thread_module = kernel_library.module("thread");
const logging_module = kernel_library.module("logging");
const process_module = kernel_library.module("process");
const file_system_module = kernel_library.module("file-system");
const memory_module = kernel_library.module("memory");
const service_module = kernel_library.module("service");
const start_module = kernel_library.module("start");
const driver_module = device_library.module("driver");
const block_client_module = device_library.module("block");
const display_client_module = client_library.module("display");
const input_client_module = client_library.module("input");
const pci_module = device_library.module("pci"); const pci_module = device_library.module("pci");
const usb_module = device_library.module("usb"); const usb_module = device_library.module("usb");
const xkb_layouts_module = xkeyboard_config_library.module("layouts");
const xkeyboard_config_module = xkeyboard_config_library.module("xkeyboard-config");
// The initial_ramdisk container format, shared by the kernel (unpacks it) and // The initial_ramdisk container format, shared by the kernel (unpacks it) and
// the EFI loader (packs it in RAM from the boot volume's /system tree). No // the EFI loader (packs it in RAM from the boot volume's /system tree). No
@@ -373,28 +359,16 @@ pub fn build(b: *std.Build) void {
b.getInstallStep().dependOn(&kernel_install.step); b.getInstallStep().dependOn(&kernel_install.step);
// --- init: the first user-space program (a system service) --- // --- init: the first user-space program (a system service) ---
// The default module set every user binary can import directly: the library/kernel // The default module set every user binary can import directly, drawn from
// concern modules, the device/service clients, mmio, the keyboard layouts, and the ACPI // build-support's single authoritative list so the root stanzas and the
// id registry. Per-binary extras are added with programModule(exe).addImport. // binary packages can never drift apart. Per-binary extras are added with
const default_imports = [_]std.Build.Module.Import{ // programModule(exe).addImport.
.{ .name = "mmio", .module = mmio_module }, const default_imports = build_support.defaultImports(.{
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module }, .kernel = kernel_library,
.{ .name = "acpi-ids", .module = acpi_ids_module }, .device = device_library,
.{ .name = "system-call", .module = system_call_module }, .client = client_library,
.{ .name = "ipc", .module = ipc_module }, .xkeyboard_config = xkeyboard_config_library,
.{ .name = "memory", .module = memory_module }, });
.{ .name = "process", .module = process_module },
.{ .name = "thread", .module = thread_module },
.{ .name = "time", .module = time_module },
.{ .name = "logging", .module = logging_module },
.{ .name = "file-system", .module = file_system_module },
.{ .name = "service", .module = service_module },
.{ .name = "start", .module = start_module },
.{ .name = "driver", .module = driver_module },
.{ .name = "block", .module = block_client_module },
.{ .name = "display", .module = display_client_module },
.{ .name = "input", .module = input_client_module },
};
// Built by the shared user-binary recipe (see addUserBinary): freestanding, // Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the library/kernel modules, and // linked into the kernel's user region against the library/kernel modules, and
@@ -873,24 +847,14 @@ pub fn build(b: *std.Build) void {
"system/boot-handoff.zig", "system/boot-handoff.zig",
"system/abi.zig", "system/abi.zig",
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic "system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
"library/device/model/device-abi.zig",
"library/device/pci/pci-class.zig", // class/subclass/prog-IF name decoding
"library/device/acpi/acpi-ids.zig", // _HID name decoding
"library/device/acpi/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
"library/device/usb/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
"library/device/usb/usb-ids.zig", // class/subclass/protocol code assignments
"library/csv/csv.zig", // shared /etc/*.csv comment-strip + field-split helpers
"library/device/mmio/mmio.zig", // barriers assemble + registers round-trip
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine "system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly "system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode "system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes "system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian) "system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
"library/protocol/vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection "system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image "system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile "system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
"library/protocol/display/display-protocol.zig", // pack(): native pixel encoding per format
"system/drivers/virtio-gpu/virtio-gpu-protocol.zig", // virtio-gpu command struct sizes "system/drivers/virtio-gpu/virtio-gpu-protocol.zig", // virtio-gpu command struct sizes
"system/drivers/virtio-gpu/virtio-pci.zig", // virtio 1.0 PCI transport struct sizes "system/drivers/virtio-gpu/virtio-pci.zig", // virtio 1.0 PCI transport struct sizes
}) |root| { }) |root| {
@@ -904,33 +868,21 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&b.addRunArtifact(mod_tests).step); test_step.dependOn(&b.addRunArtifact(mod_tests).step);
} }
// The device registry imports the shared `csv` module, so its tests need that // The library domains own their unit tests (each package's standalone
// import wired and don't fit the plain loop above. These prove the /etc/devices.csv // `zig build test` step); the root aggregate delegates to those steps so
// parse + most-specific driver match (incl. virtio 1AF4:1050 beating a class rule). // one command still runs everything and a test added inside a domain
const device_registry_tests = b.addTest(.{ // package can never be silently skipped here. Domain tests are host-only,
.root_module = b.createModule(.{ // so root's -Dtarget/-Doptimize deliberately do not reach them.
.root_source_file = b.path("library/device/registry/device-registry.zig"), for ([_]*std.Build.Dependency{
.target = target, kernel_library,
.optimize = optimize, device_library,
.imports = &.{.{ .name = "csv", .module = csv_module }}, client_library,
}), protocol_library,
}); csv_library,
test_step.dependOn(&b.addRunArtifact(device_registry_tests).step); xkeyboard_config_library,
}) |library| {
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they test_step.dependOn(&library.builder.top_level_steps.get("test").?.step);
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end }
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.
const xkb_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "layouts", .module = xkb_layouts_module },
},
}),
});
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
// The tagged kernel log ring: append/wrap/reclaim/sequence-gap behavior over // The tagged kernel log ring: append/wrap/reclaim/sequence-gap behavior over
// a RAM buffer. Needs the `abi` module (record header layout), so it doesn't // a RAM buffer. Needs the `abi` module (record header layout), so it doesn't
@@ -947,37 +899,6 @@ pub fn build(b: *std.Build) void {
}); });
test_step.dependOn(&b.addRunArtifact(log_ring_tests).step); test_step.dependOn(&b.addRunArtifact(log_ring_tests).step);
// runtime.time's Instant/Duration arithmetic. time.zig pulls in system.zig (the
// syscall wrappers), which needs the `abi` module, so it doesn't fit the plain
// loop above.
const time_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("library/kernel/time.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "abi", .module = abi_module },
},
}),
});
test_step.dependOn(&b.addRunArtifact(time_tests).step);
// runtime.Thread's lock/condvar state machines (Mutex/Condition/RwLock/WaitGroup). Its
// Futex seam falls back to std.Thread.Futex off the danos target, so the tests exercise
// them with real host threads (docs/threading-plan.md M11). Like time.zig it pulls in
// system.zig (syscall wrappers), which needs the `abi` module.
const thread_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("library/kernel/thread.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "abi", .module = abi_module },
},
}),
});
test_step.dependOn(&b.addRunArtifact(thread_tests).step);
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the // Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
// vendored data (offline). `fetch` (the network step) stays a manual script run. // vendored data (offline). `fetch` (the network step) stays a manual script run.
const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" }); const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" });
+6
View File
@@ -26,4 +26,10 @@ pub fn build(b: *std.Build) void {
.{ .name = "input-protocol", .module = protocol.module("input-protocol") }, .{ .name = "input-protocol", .module = protocol.module("input-protocol") },
}, },
}); });
// Standalone `zig build test`, kept for uniformity across the domains (the
// root aggregate depends on every domain's test step). The clients have no
// host-runnable unit tests yet — they are thin IPC conversation wrappers —
// so the step is empty until one grows some.
_ = b.step("test", "Run the client unit tests (none yet)");
} }