build: phase 2 wave C — test fixtures build as packages

All thirteen /test/system/services fixtures convert on the pci-bus
template (thread-test threaded; crash-test/device-list/pci-cap-test/
iommu-fault-test carrying their protocol and PCI extras). With the last
addUserBinary caller gone, the root build's wrapper functions and its
default-imports plumbing are deleted — every user binary now reaches
the shared recipe only through its own package. Production boot-image
file list unchanged; a -Dtest-case build bundles the fixtures exactly
as before.
This commit is contained in:
Daniel Samson
2026-07-30 04:07:19 +01:00
parent 4701fbd123
commit cb98a9844e
2 changed files with 32 additions and 103 deletions
+19 -103
View File
@@ -51,63 +51,6 @@ fn timestamp(b: *std.Build) []const u8 {
}); });
} }
/// 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,
default_imports: []const std.Build.Module.Import,
name: []const u8,
root: []const u8,
) *std.Build.Step.Compile {
return addUserBinaryImpl(b, target, default_imports, name, root, false);
}
/// As `addUserBinary`, but 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.
fn addThreadedUserBinary(
b: *std.Build,
target: std.Build.ResolvedTarget,
default_imports: []const std.Build.Module.Import,
name: []const u8,
root: []const u8,
) *std.Build.Step.Compile {
return addUserBinaryImpl(b, target, default_imports, name, root, true);
}
fn addUserBinaryImpl(
b: *std.Build,
target: std.Build.ResolvedTarget,
default_imports: []const std.Build.Module.Import,
name: []const u8,
root: []const u8,
threaded: bool,
) *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, .{
.name = name,
.root_source_file = b.path(root),
.shim_source_file = kernel_library.path("root.zig"),
.linker_script = kernel_library.path("user.ld"),
.target = target,
.default_imports = default_imports,
.threaded = threaded,
});
}
/// The `program` module of a binary built by `addUserBinary` — 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.
fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module {
return build_support.programModule(exe);
}
/// The modules the kernel imports, gathered once so both kernel variants (the /// The modules the kernel imports, gathered once so both kernel variants (the
/// installed one and the serial-enabled one `run-x86-64` boots) are built from /// installed one and the serial-enabled one `run-x86-64` boots) are built from
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`, /// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
@@ -250,7 +193,6 @@ pub fn build(b: *std.Build) void {
}); });
const abi_module = kernel_library.module("abi"); const abi_module = kernel_library.module("abi");
const device_abi_module = device_library.module("device-abi"); const device_abi_module = device_library.module("device-abi");
const pci_class_module = device_library.module("pci-class");
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of // Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
// compile-time constants, imported wherever a knob is read; keeps the trade-offs // compile-time constants, imported wherever a knob is read; keeps the trade-offs
@@ -292,14 +234,7 @@ pub fn build(b: *std.Build) void {
}, },
}); });
// The wire protocols some test fixtures name as per-binary extras,
// exported by the protocol package.
const device_manager_protocol_module = protocol_library.module("device-manager-protocol");
// The driver-side PCI library some test fixtures name as a per-binary
// extra. (The rest of the domain modules reach the root only through
// build-support's defaultImports below.)
const pci_module = device_library.module("pci");
// 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
@@ -346,18 +281,7 @@ pub fn build(b: *std.Build) void {
const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "system" } } }); const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
b.getInstallStep().dependOn(&kernel_install.step); b.getInstallStep().dependOn(&kernel_install.step);
// --- init: the first user-space program (a system service) --- // --- the user-space binaries, every one of them a package ---
// The default module set every user binary can import directly, drawn from
// build-support's single authoritative list so the root stanzas and the
// binary packages can never drift apart. Per-binary extras are added with
// programModule(exe).addImport.
const default_imports = build_support.defaultImports(.{
.kernel = kernel_library,
.device = device_library,
.client = client_library,
.xkeyboard_config = xkeyboard_config_library,
});
// Binary packages (docs/build-packages-plan.md, phase 2): each binary // Binary packages (docs/build-packages-plan.md, phase 2): each binary
// builds itself against the domain packages via build-support's shared // builds itself against the domain packages via build-support's shared
// recipe, started in ring 3 by the kernel's user-ELF loader like always; // recipe, started in ring 3 by the kernel's user-ELF loader like always;
@@ -371,7 +295,7 @@ pub fn build(b: *std.Build) void {
// Each is built by the same user-binary recipe and laid out at its FHS path on // Each is built by the same user-binary recipe and laid out at its FHS path on
// the boot volume (see `bundled` below). The EFI loader walks the tree at boot // the boot volume (see `bundled` below). The EFI loader walks the tree at boot
// and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig). // and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig).
const vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "test/system/services/vfs-test/vfs-test.zig"); const vfstest_exe = b.dependency("vfs-test", .{}).artifact("vfs-test");
// The drivers, each directory its own package: the PS/2 bus family (bus + // The drivers, each directory its own package: the PS/2 bus family (bus +
// keyboard + mouse from one package), the xHCI bus driver, the USB HID // keyboard + mouse from one package), the xHCI bus driver, the USB HID
// class drivers, and USB mass storage. Their unit tests ride along. // class drivers, and USB mass storage. Their unit tests ride along.
@@ -394,29 +318,21 @@ pub fn build(b: *std.Build) void {
const display_demo_exe = b.dependency("display-demo", .{}).artifact("display-demo"); const display_demo_exe = b.dependency("display-demo", .{}).artifact("display-demo");
const virtio_gpu_package = b.dependency("virtio-gpu", .{}); const virtio_gpu_package = b.dependency("virtio-gpu", .{});
const virtio_gpu_exe = virtio_gpu_package.artifact("virtio-gpu"); const virtio_gpu_exe = virtio_gpu_package.artifact("virtio-gpu");
const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "test/system/services/shared-memory-server/shared-memory-server.zig"); const shared_memory_server_exe = b.dependency("shared-memory-server", .{}).artifact("shared-memory-server");
const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "test/system/services/shared-memory-client/shared-memory-client.zig"); const shared_memory_client_exe = b.dependency("shared-memory-client", .{}).artifact("shared-memory-client");
const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "test/system/services/fat-test/fat-test.zig"); const fat_test_exe = b.dependency("fat-test", .{}).artifact("fat-test");
// The first binary package (docs/build-packages-plan.md, phase 2): pci-bus // The first binary package (docs/build-packages-plan.md, phase 2): pci-bus
// builds itself against the domain packages; the root build just takes the // builds itself against the domain packages; the root build just takes the
// artifact for the boot image. // artifact for the boot image.
const pci_bus_exe = b.dependency("pci-bus", .{}).artifact("pci-bus"); const pci_bus_exe = b.dependency("pci-bus", .{}).artifact("pci-bus");
// A test fixture, not a real driver: hellos to the device manager, then faults — // crash-test is a fixture, not a real driver: it hellos to the device
// what the driver-restart scenario drives the crash-loop cap with. // manager, then faults — what the driver-restart scenario drives the
const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "test/system/services/crash-test/crash-test.zig"); // crash-loop cap with. pci-cap-test and iommu-fault-test exercise the
programModule(crash_test_exe).addImport("device-manager-protocol", device_manager_protocol_module); // driver-side PCI library surface and the VT-d rogue-DMA negative proof.
const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "test/system/services/device-list/device-list.zig"); const crash_test_exe = b.dependency("crash-test", .{}).artifact("crash-test");
programModule(device_list_exe).addImport("device-manager-protocol", device_manager_protocol_module); const device_list_exe = b.dependency("device-list", .{}).artifact("device-list");
// A test fixture: claims the pci-caps case's extra unclaimed NIC and exercises the const pci_cap_test_exe = b.dependency("pci-cap-test", .{}).artifact("pci-cap-test");
// driver-side PCI library surface (capabilities, MSI, MSI-X, power, FLR) against it. const iommu_fault_test_exe = b.dependency("iommu-fault-test", .{}).artifact("iommu-fault-test");
const pci_cap_test_exe = addUserBinary(b, kernel_target, &default_imports, "pci-cap-test", "test/system/services/pci-cap-test/pci-cap-test.zig");
programModule(pci_cap_test_exe).addImport("pci", pci_module);
programModule(pci_cap_test_exe).addImport("pci-class", pci_class_module);
// The IOMMU-enforcement negative test: claims an unclaimed e1000e and fires a rogue
// DMA that VT-d must fault. Same PCI building blocks as pci-cap-test.
const iommu_fault_test_exe = addUserBinary(b, kernel_target, &default_imports, "iommu-fault-test", "test/system/services/iommu-fault-test/iommu-fault-test.zig");
programModule(iommu_fault_test_exe).addImport("pci", pci_module);
programModule(iommu_fault_test_exe).addImport("pci-class", pci_class_module);
// The discovery service: one swappable process per firmware // The discovery service: one swappable process per firmware
// (docs/discovery.md), bundled under the neutral ramdisk name // (docs/discovery.md), bundled under the neutral ramdisk name
// "discovery" so the device manager never learns which firmware it is on. // "discovery" so the device manager never learns which firmware it is on.
@@ -435,14 +351,14 @@ pub fn build(b: *std.Build) void {
// The input service and its exercisers: the fan-out server, a hardware-free synthetic // The input service and its exercisers: the fan-out server, a hardware-free synthetic
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
const input_exe = b.dependency("input", .{}).artifact("input"); const input_exe = b.dependency("input", .{}).artifact("input");
const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "test/system/services/input-source/input-source.zig"); const input_source_exe = b.dependency("input-source", .{}).artifact("input-source");
const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "test/system/services/input-test/input-test.zig"); const input_test_exe = b.dependency("input-test", .{}).artifact("input-test");
const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "test/system/services/args-echo/args-echo.zig"); const args_echo_exe = b.dependency("args-echo", .{}).artifact("args-echo");
const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "test/system/services/process-test/process-test.zig"); const process_test_exe = b.dependency("process-test", .{}).artifact("process-test");
const logger_exe = b.dependency("logger", .{}).artifact("logger"); const logger_exe = b.dependency("logger", .{}).artifact("logger");
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI // The first multi-threaded binary: exercises runtime.Thread over the thread ABI
// (docs/threading.md). Built threaded so its shared-memory poll is real. // (docs/threading.md). Its package opts into threading (real atomics/TLS).
const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "test/system/services/thread-test/thread-test.zig"); const thread_test_exe = b.dependency("thread-test", .{}).artifact("thread-test");
// Every user binary and its FHS home on the boot volume. There is no packed // Every user binary and its FHS home on the boot volume. There is no packed
// ramdisk artifact any more: make-fat-image.py lays each binary out at this // ramdisk artifact any more: make-fat-image.py lays each binary out at this
+13
View File
@@ -58,6 +58,19 @@
.@"usb-hid" = .{ .path = "system/drivers/usb-hid" }, .@"usb-hid" = .{ .path = "system/drivers/usb-hid" },
.@"usb-storage" = .{ .path = "system/drivers/usb-storage" }, .@"usb-storage" = .{ .path = "system/drivers/usb-storage" },
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" }, .@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
.@"vfs-test" = .{ .path = "test/system/services/vfs-test" },
.@"fat-test" = .{ .path = "test/system/services/fat-test" },
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server" },
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client" },
.@"crash-test" = .{ .path = "test/system/services/crash-test" },
.@"device-list" = .{ .path = "test/system/services/device-list" },
.@"pci-cap-test" = .{ .path = "test/system/services/pci-cap-test" },
.@"iommu-fault-test" = .{ .path = "test/system/services/iommu-fault-test" },
.@"input-source" = .{ .path = "test/system/services/input-source" },
.@"input-test" = .{ .path = "test/system/services/input-test" },
.@"args-echo" = .{ .path = "test/system/services/args-echo" },
.@"process-test" = .{ .path = "test/system/services/process-test" },
.@"thread-test" = .{ .path = "test/system/services/thread-test" },
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{ //.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding // // When updating this field to a new URL, be sure to delete the corresponding