A protocol is reached by name now, not by a compile-time integer. Init is PID 1 and already knows which binary it started, so init serves /protocol as a vfs backend: bind claims a contract with the provider's endpoint attached, open answers with that endpoint as the reply's capability, and readdir lists what is bound with the task and binary behind it. The kernel reserves the prefix — nothing may mount over it, under it, or unmount it — and ServiceId, ipc_register and ipc_lookup are gone, their syscall numbers left vacant. A bind is authorized by who the caller *is*: the kernel-stamped binary together with the supervising task's identity, matched against /system/configuration/protocol.csv. Identity, not spelling — spawn is ungated, so an attacker can run any bundled binary, and a name-only rule would have let it launder grants through an init of its own making. A name a live process holds is refused to everyone else; a dead one's is released. Three review rounds against a hostile ring-3 process found what 108 green tests could not, because the suite contains no attacker. Publishing init's supervision endpoint as the registry put PID 1's mailbox in every process's hands, where two forged bytes reached the shutdown path: privileged traffic is now believed only from the task that holds the contract it speaks for. A capability arriving on a request outlived every path that ignored it, one handle per call until the table was full — in init, and in the harness ten services share — so the arriving capability is owned by the turn and released unless a handler says otherwise. And the kernel let anyone holding an endpoint handle aim signals, timers, exit notices and interrupts at it: binding now requires having created it. Suite 108/108. The new protocol-registry case asserts eleven properties, each one an attack that must fail.
456 lines
24 KiB
Zig
456 lines
24 KiB
Zig
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 —
|
|
// what ships (the bundled list), the kernel + loader, and the test aggregate.
|
|
// Image assembly and the QEMU run steps live beside it in build/.
|
|
const build_support = @import("build-support");
|
|
const images = @import("build/images.zig");
|
|
const qemu = @import("build/qemu.zig");
|
|
|
|
/// 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
|
|
/// floor — so reject anything off the 0.16 line to keep the build reproducible.
|
|
fn ensureZigVersion() void {
|
|
const v = builtin.zig_version;
|
|
if (v.major != 0 or v.minor != 16) {
|
|
std.debug.print(
|
|
"danos requires Zig 0.16.x, but this is {d}.{d}.{d}. " ++
|
|
"Zig makes breaking changes between minor releases pre-1.0.\n",
|
|
.{ v.major, v.minor, v.patch },
|
|
);
|
|
std.process.exit(1);
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
/// the same set. `build_options` is *not* here — it carries `serial`/`test_case`,
|
|
/// which differ per variant, so `addKernel` builds it fresh each time.
|
|
const KernelModules = struct {
|
|
boot_handoff: *std.Build.Module,
|
|
abi: *std.Build.Module,
|
|
device_abi: *std.Build.Module,
|
|
architecture: *std.Build.Module,
|
|
platform: *std.Build.Module,
|
|
parameters: *std.Build.Module,
|
|
initial_ramdisk: *std.Build.Module,
|
|
};
|
|
|
|
/// Build the freestanding x86_64 kernel ELF. Factored so we can build it twice
|
|
/// from one recipe: the installed/flashable image (serial off by default) and the
|
|
/// serial-enabled variant `run-x86-64` boots — they differ only in the `serial`
|
|
/// build option baked into `build_options`.
|
|
fn addKernel(
|
|
b: *std.Build,
|
|
kernel_target: std.Build.ResolvedTarget,
|
|
optimize: std.builtin.OptimizeMode,
|
|
modules: KernelModules,
|
|
test_case: ?[]const u8,
|
|
serial: bool,
|
|
) *std.Build.Step.Compile {
|
|
// Compile-time configuration the kernel reads as `@import("build_options")`:
|
|
// the QEMU harness's -Dtest-case, and whether the serial log sink is compiled
|
|
// in (see the -Dserial option). Built per variant since `serial` differs.
|
|
const build_options = b.addOptions();
|
|
build_options.addOption(?[]const u8, "test_case", test_case);
|
|
build_options.addOption(bool, "serial", serial);
|
|
const build_options_module = build_options.createModule();
|
|
|
|
const exe = b.addExecutable(.{
|
|
.name = "kernel",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("system/kernel/kernel.zig"),
|
|
.target = kernel_target,
|
|
.optimize = optimize,
|
|
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
|
.red_zone = false, // interrupts would corrupt the SystemV red zone
|
|
.single_threaded = false, // SMP: the big kernel lock's atomics must be real across cores
|
|
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
|
|
.stack_check = false, // stack-probe calls have no runtime to land in
|
|
.stack_protector = false,
|
|
.strip = optimize != .Debug, // DWARF info doubles the flashable image; keep it only for debug builds
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = modules.boot_handoff },
|
|
.{ .name = "abi", .module = modules.abi },
|
|
.{ .name = "device-abi", .module = modules.device_abi },
|
|
.{ .name = "architecture", .module = modules.architecture },
|
|
.{ .name = "platform", .module = modules.platform },
|
|
.{ .name = "parameters", .module = modules.parameters },
|
|
.{ .name = "build_options", .module = build_options_module },
|
|
.{ .name = "initial-ramdisk", .module = modules.initial_ramdisk },
|
|
},
|
|
}),
|
|
});
|
|
exe.setLinkerScript(b.path("system/kernel/architecture/x86_64/linker.ld"));
|
|
exe.entry = .{ .symbol_name = "_start" };
|
|
// The self-hosted linker ignores parts of the linker script (PHDRS,
|
|
// /DISCARD/, AT(), section order); the higher-half layout depends on the
|
|
// script being authoritative, so pin the kernel to LLVM + LLD.
|
|
exe.use_llvm = true;
|
|
exe.use_lld = true;
|
|
// Higher-half virtual base (matches KERNEL_VIRT_BASE in linker.ld); the
|
|
// linker's AT() clauses give each segment a low physical load address
|
|
// (.text at 1 MiB), which the loader allocates and copies into.
|
|
exe.image_base = 0xFFFFFFFF80100000;
|
|
return exe;
|
|
}
|
|
|
|
/// One row of the production ship table: which package, which of its
|
|
/// artifacts, and the FHS boot path. For most binaries all three share one
|
|
/// name; the helpers below make a row from just that name.
|
|
const ShipRow = struct { path: []const u8, package: []const u8, artifact: []const u8 };
|
|
|
|
fn service(comptime name: []const u8) ShipRow {
|
|
return .{ .path = "system/services/" ++ name, .package = name, .artifact = name };
|
|
}
|
|
fn driver(comptime name: []const u8) ShipRow {
|
|
return .{ .path = "system/drivers/" ++ name, .package = name, .artifact = name };
|
|
}
|
|
/// An extra artifact of a multi-binary driver package (ps2-bus, usb-hid),
|
|
/// bundled at its own flattened /system/drivers path.
|
|
fn driverArtifact(comptime package: []const u8, comptime artifact: []const u8) ShipRow {
|
|
return .{ .path = "system/drivers/" ++ artifact, .package = package, .artifact = artifact };
|
|
}
|
|
|
|
/// The production ship table — what a plain `zig build` image contains,
|
|
/// beyond the specials the build fn adds around it (init, discovery, the
|
|
/// /system/configuration data files; the /test fixtures join only under
|
|
/// -Dtest-case).
|
|
/// Selecting what goes into a build = selecting rows: a package in no row is
|
|
/// not just unshipped, its build file is never even loaded
|
|
/// (docs/build-packages-plan.md).
|
|
const production_ship = [_]ShipRow{
|
|
service("fat"),
|
|
service("display"),
|
|
service("display-demo"),
|
|
service("device-manager"),
|
|
service("input"),
|
|
service("logger"),
|
|
driver("pci-bus"),
|
|
driver("ps2-bus"),
|
|
driverArtifact("ps2-bus", "ps2-keyboard"),
|
|
driverArtifact("ps2-bus", "ps2-mouse"),
|
|
driver("usb-xhci-bus"),
|
|
driverArtifact("usb-hid", "usb-hid-keyboard"),
|
|
driverArtifact("usb-hid", "usb-hid-mouse"),
|
|
driver("usb-storage"),
|
|
driver("virtio-gpu"),
|
|
};
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
ensureZigVersion();
|
|
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
// The library domain packages (docs/build-packages-plan.md, phase 1): each
|
|
// domain owns a build.zig/zon that wires and exports its modules, and this
|
|
// root build is a consumer — a library interface change now happens in the
|
|
// domain's own build file, not here. The per-module commentary lives with
|
|
// each domain's build.zig.
|
|
const kernel_library = b.dependency("kernel", .{});
|
|
const device_library = b.dependency("device", .{});
|
|
const client_library = b.dependency("client", .{});
|
|
const protocol_library = b.dependency("protocol", .{});
|
|
const csv_library = b.dependency("csv", .{});
|
|
const xkeyboard_config_library = b.dependency("xkeyboard-config", .{});
|
|
|
|
// The three shared contracts, each with its own audience so every import
|
|
// declares which one it speaks (no target is set, so each inherits the target of
|
|
// whichever binary imports it). See docs/coding-standards.md.
|
|
// boot-handoff : loader <-> kernel (BootInformation, framebuffer, VM layout)
|
|
// abi : kernel <-> runtime, core (SystemCall, mmap prot flags, page_size)
|
|
// device-abi : kernel <-> user, devices (DeviceDescriptor, DeviceClass, ...)
|
|
// boot-handoff stays a root module (a system/ source the loader <-> kernel
|
|
// pair speaks); abi is exported by the kernel library package (its source
|
|
// also lives in system/), device-abi by the device package.
|
|
const boot_handoff_module = b.addModule("boot-handoff", .{
|
|
.root_source_file = b.path("system/boot-handoff.zig"),
|
|
});
|
|
const abi_module = kernel_library.module("abi");
|
|
const device_abi_module = device_library.module("device-abi");
|
|
|
|
// 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
|
|
// in one place instead of scattered across the tree. See system/parameters.zig.
|
|
const parameters_module = b.addModule("parameters", .{
|
|
.root_source_file = b.path("system/parameters.zig"),
|
|
});
|
|
|
|
// Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging).
|
|
// The generic kernel imports this as "architecture" and never names x86_64, so a new
|
|
// architecture is a matter of pointing this module at a different directory.
|
|
const architecture_module = b.addModule("architecture", .{
|
|
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module }, // paging uses BootInformation/memory-map + physicalToVirtual
|
|
.{ .name = "abi", .module = abi_module }, // paging works in page_size units
|
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
|
|
},
|
|
});
|
|
// CPU-exception stubs — real assembly, since they need cross-symbol
|
|
// jumps/calls that Zig inline asm can't express (see the file's header).
|
|
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/isr.s"));
|
|
// The AP bring-up trampoline: 16-/32-/64-bit mode-switch code that can't be
|
|
// inline asm (it runs relocated to a low page, not at its link address).
|
|
architecture_module.addAssemblyFile(b.path("system/kernel/architecture/x86_64/trampoline.s"));
|
|
|
|
// Firmware-agnostic device discovery. The generic kernel imports this as
|
|
// "platform" and asks it to enumerate hardware into a backend-neutral device
|
|
// tree, never naming ACPI (or, later, device-tree) — the same discipline the
|
|
// architecture module applies to CPU code. The backend is selected at runtime from
|
|
// the boot handoff (see system/kernel/platform.zig).
|
|
const platform_module = b.addModule("platform", .{
|
|
.root_source_file = b.path("system/kernel/platform.zig"),
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module }, // BootInformation (carries the ACPI RSDP), physicalToVirtual
|
|
.{ .name = "abi", .module = abi_module }, // acpi.zig works in page_size units
|
|
.{ .name = "device-abi", .module = device_abi_module }, // device-model's DeviceClass/ResourceKind live here
|
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
|
|
},
|
|
});
|
|
|
|
|
|
|
|
// 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
|
|
// dependencies.
|
|
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
|
.root_source_file = b.path("system/initial-ramdisk.zig"),
|
|
});
|
|
|
|
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
|
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
|
|
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see system/kernel/tests.zig)");
|
|
// The serial-console log sink. Off by default: a real machine often has no
|
|
// working legacy COM1, and the boot log is kept in RAM (klog) and flushed to
|
|
// disk instead — serial is now only a QEMU convenience. `run-x86-64` and the
|
|
// QEMU test harness (test/qemu_test.py, which asserts on serial markers) turn
|
|
// it on; a flashable `zig build` image leaves it out. See serial.zig.
|
|
const serial = b.option(bool, "serial", "Compile the serial-console log sink into the kernel (default: off; run-x86-64 and the test harness enable it)") orelse false;
|
|
// The diagnose boot: init skips the display service (and demo), so the
|
|
// on-screen boot transcript is never suppressed — the full timestamped
|
|
// timeline stays on the screen for real-hardware debugging by eye.
|
|
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 ---
|
|
// (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,
|
|
.abi = abi_module,
|
|
.device_abi = device_abi_module,
|
|
.architecture = architecture_module,
|
|
.platform = platform_module,
|
|
.parameters = parameters_module,
|
|
.initial_ramdisk = initial_ramdisk_module,
|
|
};
|
|
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
|
// The serial-enabled twin is what `run-x86-64` boots — built lazily (only
|
|
// when its image is requested), never installed.
|
|
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
|
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
|
|
|
// --- what ships: the boot tree ---
|
|
// 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 its
|
|
// path on the image, and the EFI loader walks /system and /test at boot and
|
|
// builds the in-RAM initial_ramdisk table from the trees — the volume's file
|
|
// structure is the single source of truth. Entry names (and hence argv[0] and
|
|
// task names) are these paths with a leading slash.
|
|
//
|
|
// The uniform rows live in `production_ship` (the table above `build`);
|
|
// spelled out here are only the genuinely non-uniform entries: init
|
|
// (receives the root's -Dserial as a dependency option — its liveness
|
|
// heartbeat is a serial/test-build diagnostic the QEMU harness asserts
|
|
// on; a flashable image leaves it out), discovery (the -Ddiscovery pick),
|
|
// and the /system/configuration data files. Each binary builds itself
|
|
// against the domain packages via build-support's shared recipe; the root
|
|
// just takes artifacts (docs/build-packages-plan.md).
|
|
var bundled_list: std.ArrayListUnmanaged(images.BundledBinary) = .empty;
|
|
bundled_list.append(b.allocator, .{
|
|
.path = "system/services/init",
|
|
.binary = b.dependency("init", .{ .serial = serial }).artifact("init").getEmittedBin(),
|
|
}) catch @panic("OOM");
|
|
// The discovery service: one swappable process per firmware
|
|
// (docs/discovery.md), bundled under the neutral ramdisk name
|
|
// "discovery" so the device manager never learns which firmware it is on.
|
|
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
|
// flattened device tree — the aarch64 target flips the default when it
|
|
// lands (docs/arm.md). Each firmware's service is its own LAZY package;
|
|
// both export an artifact named "discovery", and this option picks which
|
|
// one ships — the unselected package's build file is never even loaded.
|
|
// (lazyDependency returns null only for an unfetched remote package; these
|
|
// are in-repo path dependencies, so a null means the directory is gone.)
|
|
const Discovery = enum { acpi, fdt };
|
|
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
|
const discovery_exe = switch (discovery) {
|
|
.acpi => (b.lazyDependency("acpi", .{}) orelse @panic("system/services/acpi is missing")).artifact("discovery"),
|
|
.fdt => (b.lazyDependency("fdt", .{}) orelse @panic("system/services/fdt is missing")).artifact("discovery"),
|
|
};
|
|
bundled_list.append(b.allocator, .{
|
|
.path = "system/services/discovery",
|
|
.binary = discovery_exe.getEmittedBin(),
|
|
}) catch @panic("OOM");
|
|
// The ship table: every uniform row, one line each.
|
|
for (production_ship) |row| {
|
|
bundled_list.append(b.allocator, .{
|
|
.path = row.path,
|
|
.binary = b.dependency(row.package, .{}).artifact(row.artifact).getEmittedBin(),
|
|
}) catch @panic("OOM");
|
|
}
|
|
// Data files, not binaries: packing them under /system/configuration rides
|
|
// the kernel's read-only initrd mount of /system (system/kernel/vfs.zig
|
|
// setInitialRamdisk) — the device manager reads its registry and init its
|
|
// service list with no filesystem service running. -Ddiagnose selects the
|
|
// init.csv variant that omits the display stack (so the kernel's boot
|
|
// transcript stays on screen); both bundle at the same
|
|
// /system/configuration/init.csv path.
|
|
const init_csv_source = if (diagnose) "system/configuration/init-diagnose.csv" else "system/configuration/init.csv";
|
|
bundled_list.append(b.allocator, .{ .path = "system/configuration/devices.csv", .binary = b.path("system/configuration/devices.csv") }) catch @panic("OOM");
|
|
bundled_list.append(b.allocator, .{ .path = "system/configuration/init.csv", .binary = b.path(init_csv_source) }) catch @panic("OOM");
|
|
// The protocol grants: who may claim which name under /protocol
|
|
// (docs/os-development/protocol-namespace.md). init reads it beside init.csv,
|
|
// out of the same read-only initrd, before it spawns anything — the registrar
|
|
// has to know its policy before the first provider asks.
|
|
bundled_list.append(b.allocator, .{ .path = "system/configuration/protocol.csv", .binary = b.path("system/configuration/protocol.csv") }) catch @panic("OOM");
|
|
// A no-option build assumes neither -Dtest-case nor -Ddiagnose: it ships the
|
|
// production set only. The userspace test fixtures under /test join in only
|
|
// for a test build — which the QEMU harness signals by passing
|
|
// -Dtest-case=<name> for every scenario, exactly when they must be on the
|
|
// boot volume. They are LAZY dependencies too. Fixture packages are
|
|
// uniform — the dependency name, the artifact name, and the boot path's
|
|
// leaf all match the directory — so a name is a whole entry.
|
|
if (test_case != null) for ([_][]const u8{
|
|
"vfs-test", // the user-space VFS round-trip client
|
|
"fat-test",
|
|
"shared-memory-server",
|
|
"shared-memory-client",
|
|
"crash-test", // hellos to the device manager, then faults — drives the crash-loop cap
|
|
"device-list",
|
|
"pci-cap-test", // exercises the driver-side PCI library against the pci-caps NIC
|
|
"iommu-fault-test", // fires the rogue DMA that VT-d must fault
|
|
"input-source",
|
|
"input-test",
|
|
"args-echo",
|
|
"process-test",
|
|
"thread-test", // the multi-threaded fixture (its package sets .threaded)
|
|
"user-memory-test", // aims deliberately bad user pointers at the checked copy layer
|
|
"protocol-registry-test", // drives the registrar: ungranted bind, collision, restart
|
|
}) |fixture| {
|
|
const package = b.lazyDependency(fixture, .{}) orelse
|
|
@panic("a test fixture package is missing under test/system/services");
|
|
bundled_list.append(b.allocator, .{
|
|
.path = b.fmt("test/system/services/{s}", .{fixture}),
|
|
.binary = package.artifact(fixture).getEmittedBin(),
|
|
}) catch @panic("OOM");
|
|
};
|
|
const bundled = bundled_list.items;
|
|
|
|
// Boot methods live in boot/, one per way of getting the kernel running.
|
|
// Each is its own binary/entry (a loader is built for its own target); today
|
|
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
|
// The loader reads -Dserial too, so its boot-progress breadcrumbs (con_out,
|
|
// which firmware may mirror to a serial console) are silenced by default — a
|
|
// real-hardware boot stays quiet. Fatal-error messages ignore this and always
|
|
// show, so a failed boot still explains itself on screen. See boot/efi.zig.
|
|
const loader_options = b.addOptions();
|
|
loader_options.addOption(bool, "serial", serial);
|
|
const loader_options_module = loader_options.createModule();
|
|
const efiexe = b.addExecutable(.{
|
|
.name = "BOOTX64",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("boot/efi.zig"),
|
|
.target = b.resolveTargetQuery(.{
|
|
.cpu_arch = .x86_64,
|
|
.os_tag = .uefi,
|
|
}),
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
// The bootloader speaks the handoff contract and the ramdisk
|
|
// container it packs the /system tree into — never the user ABI.
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
|
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
|
.{ .name = "build_options", .module = loader_options_module },
|
|
},
|
|
}),
|
|
});
|
|
|
|
// Image assembly (the FHS install tree, boot manifest + capsule, both FAT32
|
|
// images, the release ISO, the check steps) and the QEMU run steps live in
|
|
// build/ — the root decides what ships, those files own how it runs.
|
|
const fat_image_serial = images.addImageSteps(b, .{
|
|
.kernel = exe,
|
|
.kernel_serial = exe_serial,
|
|
.efi = efiexe,
|
|
.bundled = bundled,
|
|
});
|
|
qemu.addRunSteps(b, fat_image_serial);
|
|
|
|
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
|
// and can't be executed natively, so only the shared contracts are unit-tested
|
|
// here (compiled for the host rather than inheriting a freestanding target) —
|
|
// which also compile-checks that the three-way split stays self-consistent.
|
|
const test_step = b.step("test", "Run tests");
|
|
for ([_][]const u8{
|
|
"system/boot-handoff.zig",
|
|
"system/abi.zig",
|
|
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
|
|
}) |root| {
|
|
const mod_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path(root),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
|
}
|
|
|
|
// The library domains and the binary packages own their unit tests (each
|
|
// package's standalone `zig build test` step); the root aggregate
|
|
// delegates to those steps so one command still runs everything and a
|
|
// test added inside a package can never be silently skipped here. Package
|
|
// tests are host-only, so root's -Dtarget/-Doptimize deliberately do not
|
|
// reach them.
|
|
for ([_]*std.Build.Dependency{
|
|
kernel_library,
|
|
device_library,
|
|
client_library,
|
|
protocol_library,
|
|
csv_library,
|
|
xkeyboard_config_library,
|
|
b.dependency("fat", .{}),
|
|
b.dependency("display", .{}),
|
|
b.dependency("ps2-bus", .{}),
|
|
b.dependency("usb-hid", .{}),
|
|
b.dependency("usb-storage", .{}),
|
|
b.dependency("virtio-gpu", .{}),
|
|
}) |package| {
|
|
test_step.dependOn(&package.builder.top_level_steps.get("test").?.step);
|
|
}
|
|
|
|
// 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
|
|
// fit the plain loop above.
|
|
const log_ring_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("system/kernel/log-ring.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "abi", .module = abi_module },
|
|
},
|
|
}),
|
|
});
|
|
test_step.dependOn(&b.addRunArtifact(log_ring_tests).step);
|
|
|
|
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
|
// 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_step = b.step("gen-xkeyboard-config", "Regenerate library/xkeyboard-config/generated from the vendored data");
|
|
gen_xkb_step.dependOn(&gen_xkb.step);
|
|
}
|