The AML module becomes a build module compiled into both the kernel (for the \_S5 sleep state it still needs) and the new acpi service — one source, two builds, no fork. The kernel publishes a single acpi-tables node: the DSDT/SSDT blobs as memory resources, a broad io_port grant (the honest trust boundary — firmware AML names whatever ports it chose, known only after parsing), and the SCI for the M21 event track. The acpi service claims the node, maps each blob through the ordinary mmio grant (which preserves the sub-page offset onto the bytecode), and runs the same parser the kernel does. It self-verifies its namespace Device count against the kernel's — 34 = 34 — deterministically via an argv the acpi-parse test passes, so no racing the shared serial buffer. Parse-only touches no hardware; OperationRegion evaluation waits for _CRS/_STA in M20.2. The manager spawns 'discovery' (the neutral ramdisk name) at startup. Suite 56/56.
612 lines
34 KiB
Zig
612 lines
34 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
/// 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);
|
|
}
|
|
}
|
|
|
|
/// Return the first path in `candidates` that exists on the build host, else the
|
|
/// first candidate as a fallback so a missing-firmware error still names a
|
|
/// concrete (and, by convention, the primary) path. Used to locate OVMF firmware
|
|
/// across distro/OS layouts without configuration.
|
|
fn firstExisting(io: std.Io, candidates: []const []const u8) []const u8 {
|
|
for (candidates) |path| {
|
|
std.Io.Dir.accessAbsolute(io, path, .{}) catch continue;
|
|
return path;
|
|
}
|
|
return candidates[0];
|
|
}
|
|
|
|
/// A UTC timestamp like "20260708-153045", for naming a per-run artifact so
|
|
/// repeated runs don't clobber each other's logs. Resolved when `build.zig` runs
|
|
/// (i.e. at `zig build` invocation), which is moments before QEMU launches.
|
|
fn timestamp(b: *std.Build) []const u8 {
|
|
const ns = std.Io.Clock.now(.real, b.graph.io).nanoseconds;
|
|
const secs: u64 = @intCast(@divFloor(ns, std.time.ns_per_s));
|
|
const es = std.time.epoch.EpochSeconds{ .secs = secs };
|
|
const yd = es.getEpochDay().calculateYearDay();
|
|
const md = yd.calculateMonthDay();
|
|
const ds = es.getDaySeconds();
|
|
return b.fmt("{d:0>4}{d:0>2}{d:0>2}-{d:0>2}{d:0>2}{d:0>2}", .{
|
|
yd.year,
|
|
md.month.numeric(),
|
|
@as(u32, md.day_index) + 1,
|
|
ds.getHoursIntoDay(),
|
|
ds.getMinutesIntoHour(),
|
|
ds.getSecondsIntoMinute(),
|
|
});
|
|
}
|
|
|
|
/// 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.
|
|
fn addUserBinary(
|
|
b: *std.Build,
|
|
target: std.Build.ResolvedTarget,
|
|
runtime_module: *std.Build.Module,
|
|
posix_module: *std.Build.Module,
|
|
mmio_module: *std.Build.Module,
|
|
xkeyboard_config_module: *std.Build.Module,
|
|
acpi_ids_module: *std.Build.Module,
|
|
name: []const u8,
|
|
root: []const u8,
|
|
) *std.Build.Step.Compile {
|
|
const exe = b.addExecutable(.{
|
|
.name = name,
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path(root),
|
|
.target = target,
|
|
.optimize = .ReleaseSmall,
|
|
.code_model = .large,
|
|
.single_threaded = true,
|
|
.sanitize_c = .off,
|
|
.stack_check = false,
|
|
.stack_protector = false,
|
|
.imports = &.{
|
|
.{ .name = "runtime", .module = runtime_module },
|
|
// POSIX/C compatibility layer, available to any program that wants it
|
|
// (danos-native code uses `runtime` directly). See library/posix/.
|
|
.{ .name = "posix", .module = posix_module },
|
|
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
|
.{ .name = "mmio", .module = mmio_module },
|
|
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
|
// to any program that wants it. See library/xkeyboard-config/.
|
|
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module },
|
|
// ACPI/PnP hardware-ID registry, so drivers name devices
|
|
// (HardwareId.ps2_keyboard) instead of magic "_HID" strings.
|
|
.{ .name = "acpi-ids", .module = acpi_ids_module },
|
|
},
|
|
}),
|
|
});
|
|
exe.setLinkerScript(b.path("library/runtime/user.ld"));
|
|
exe.entry = .{ .symbol_name = "_start" };
|
|
exe.image_base = 0x7000_0000_0000;
|
|
exe.use_llvm = true;
|
|
exe.use_lld = true;
|
|
return exe;
|
|
}
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
ensureZigVersion();
|
|
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
// 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, ...)
|
|
const boot_handoff_module = b.addModule("boot-handoff", .{
|
|
.root_source_file = b.path("system/boot-handoff.zig"),
|
|
});
|
|
const abi_module = b.addModule("abi", .{
|
|
.root_source_file = b.path("system/abi.zig"),
|
|
});
|
|
// The devices sub-project's public interface (the flat wire types), exposed as
|
|
// its own module like vfs-protocol — importable by user space, unlike the
|
|
// kernel-internal device model it also feeds (system/devices/device-model.zig).
|
|
const device_abi_module = b.addModule("device-abi", .{
|
|
.root_source_file = b.path("system/devices/device-abi.zig"),
|
|
});
|
|
// PCI class-code decoding (class/subclass/prog-IF -> names). Pure reference data,
|
|
// shared by kernel discovery (the device-tree dump) and any user-space PCI tool.
|
|
const pci_class_module = b.addModule("pci-class", .{
|
|
.root_source_file = b.path("system/devices/pci-class.zig"),
|
|
});
|
|
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
|
// nodes. Also shared reference data.
|
|
// The AML interpreter, a build module so the ring-3 acpi service can run the
|
|
// same parser the kernel does (docs/m19-m20-plan.md decision 1). Pure Zig,
|
|
// no kernel imports — one source, two builds.
|
|
const aml_module = b.addModule("aml", .{
|
|
.root_source_file = b.path("system/devices/aml/aml.zig"),
|
|
});
|
|
|
|
const acpi_ids_module = b.addModule("acpi-ids", .{
|
|
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
|
});
|
|
|
|
// 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/devices/platform.zig).
|
|
const platform_module = b.addModule("platform", .{
|
|
.root_source_file = b.path("system/devices/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 = "pci-class", .module = pci_class_module }, // decode PCI class codes in the device dump
|
|
.{ .name = "acpi-ids", .module = acpi_ids_module }, // decode ACPI _HID names in the device dump
|
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
|
|
},
|
|
});
|
|
|
|
// The VFS wire protocol: the vfs sub-project's public interface, exposed as its
|
|
// own module. Both the vfs server and the runtime's file layer (unistd/stdio)
|
|
// depend on this contract by name — neither reaches into the other's files. This
|
|
// is the first "protocol module" (see docs/driver-model.md); usb/block will
|
|
// expose theirs the same way.
|
|
const vfs_protocol_module = b.addModule("vfs-protocol", .{
|
|
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
|
});
|
|
|
|
// The input wire protocol: the input service's public interface, exposed as its own
|
|
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
|
// `input` helper (subscribe/publish), and every source and subscriber.
|
|
const input_protocol_module = b.addModule("input-protocol", .{
|
|
.root_source_file = b.path("system/services/input/protocol.zig"),
|
|
});
|
|
|
|
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
|
// heap, IPC helpers, the process start shim, device access. This is the stable
|
|
// application ABI; POSIX compatibility is a separate library on top (see below).
|
|
// Compiled into every user binary (see addUserBinary), so it inherits each exe's
|
|
// `.large` code model — do NOT set a target/code_model here. It imports `abi`
|
|
// for the shared SystemCall numbers / mmap flags, `device-abi` for the device
|
|
// types its `device` helper wraps, and re-exports `vfs-protocol` for the VFS
|
|
// server. It never touches `boot-handoff` — user space has no business with the
|
|
// loader↔kernel handoff.
|
|
const runtime_module = b.addModule("runtime", .{
|
|
.root_source_file = b.path("library/runtime/runtime.zig"),
|
|
.imports = &.{
|
|
.{ .name = "abi", .module = abi_module },
|
|
.{ .name = "device-abi", .module = device_abi_module },
|
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
.{ .name = "input-protocol", .module = input_protocol_module },
|
|
},
|
|
});
|
|
|
|
// The device-manager protocol: hello + (M18.2) tree reports, exposed as its
|
|
// own module like the other protocol modules. Imported through the runtime.
|
|
const device_manager_protocol_module = b.addModule("device-manager-protocol", .{
|
|
.root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"),
|
|
});
|
|
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
|
|
|
|
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
|
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
|
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
|
const mmio_module = b.addModule("mmio", .{
|
|
.root_source_file = b.path("library/mmio/mmio.zig"),
|
|
});
|
|
|
|
// Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig
|
|
// (keycode + modifiers -> keysym/character). The `layouts` tables are generated by
|
|
// tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them.
|
|
// No target set, so each inherits its importer's. See library/xkeyboard-config/.
|
|
const xkb_layouts_module = b.addModule("layouts", .{
|
|
.root_source_file = b.path("library/xkeyboard-config/generated/layouts.zig"),
|
|
});
|
|
const xkeyboard_config_module = b.addModule("xkeyboard-config", .{
|
|
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
|
.imports = &.{
|
|
.{ .name = "layouts", .module = xkb_layouts_module },
|
|
},
|
|
});
|
|
|
|
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
|
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
|
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
|
// and library/posix/posix.zig.
|
|
const posix_module = b.addModule("posix", .{
|
|
.root_source_file = b.path("library/posix/posix.zig"),
|
|
.imports = &.{
|
|
.{ .name = "runtime", .module = runtime_module },
|
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
|
},
|
|
});
|
|
|
|
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
|
// build-time packer tools/make-initial-ramdisk.py (produces it). 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)");
|
|
const build_options = b.addOptions();
|
|
build_options.addOption(?[]const u8, "test_case", test_case);
|
|
const build_options_module = build_options.createModule();
|
|
|
|
// --- 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,
|
|
});
|
|
|
|
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,
|
|
.imports = &.{
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
|
.{ .name = "abi", .module = abi_module },
|
|
.{ .name = "device-abi", .module = device_abi_module },
|
|
.{ .name = "architecture", .module = architecture_module },
|
|
.{ .name = "platform", .module = platform_module },
|
|
.{ .name = "parameters", .module = parameters_module },
|
|
.{ .name = "build_options", .module = build_options_module },
|
|
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
|
},
|
|
}),
|
|
});
|
|
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;
|
|
|
|
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
|
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
|
// kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
|
|
// zig-out/system/services/init, and so on (see docs/README.md). The bootloader
|
|
// then loads these FHS paths off the volume.
|
|
const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
|
|
b.getInstallStep().dependOn(&kernel_install.step);
|
|
|
|
// --- init: the first user-space program (a system service) ---
|
|
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
|
// linked into the kernel's user region against the `runtime` runtime library, and
|
|
// started in ring 3 by the kernel's user-ELF loader.
|
|
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
|
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
|
b.getInstallStep().dependOn(&init_install.step);
|
|
|
|
// --- initial_ramdisk: a bundle of extra user binaries (VFS server + drivers) ---
|
|
// Each is built by the same user-binary recipe, then packed into one image by
|
|
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
|
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
|
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
|
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
|
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "hpet", "system/drivers/hpet/hpet.zig");
|
|
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "bus", "system/drivers/bus/bus.zig");
|
|
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
|
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
|
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
|
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
|
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
|
// what the driver-restart scenario drives the crash-loop cap with.
|
|
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
|
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
|
// The discovery service: one swappable process per firmware
|
|
// (docs/m19-m20-plan.md decision 7), 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). Both are placeholders until M20.1 (acpi) and the
|
|
// ARM bring-up (fdt).
|
|
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_source: []const u8 = switch (discovery) {
|
|
.acpi => "system/services/acpi/acpi.zig",
|
|
.fdt => "system/services/fdt/fdt.zig",
|
|
};
|
|
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
|
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
|
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
|
// 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.
|
|
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
|
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
|
|
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
|
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
|
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
|
|
|
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
|
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
|
// make-initial-ramdisk.py <out> [<name> <file>]... — one name/file pair per binary.
|
|
const mk_run = b.addSystemCommand(&.{"python3"});
|
|
mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py"));
|
|
const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img");
|
|
mk_run.addArg("vfs");
|
|
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
|
mk_run.addArg("vfs-test");
|
|
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
|
mk_run.addArg("hpet");
|
|
mk_run.addFileArg(hpet_exe.getEmittedBin());
|
|
mk_run.addArg("bus");
|
|
mk_run.addFileArg(bus_exe.getEmittedBin());
|
|
mk_run.addArg("ps2-bus");
|
|
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
|
mk_run.addArg("ps2-keyboard");
|
|
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
|
mk_run.addArg("ps2-mouse");
|
|
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
|
mk_run.addArg("usb-xhci-bus");
|
|
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
|
mk_run.addArg("pci-bus");
|
|
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
|
mk_run.addArg("crash-test");
|
|
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
|
mk_run.addArg("device-list");
|
|
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
|
mk_run.addArg("discovery");
|
|
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
|
mk_run.addArg("device-manager");
|
|
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
|
mk_run.addArg("input");
|
|
mk_run.addFileArg(input_exe.getEmittedBin());
|
|
mk_run.addArg("input-source");
|
|
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
|
mk_run.addArg("input-test");
|
|
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
|
mk_run.addArg("args-echo");
|
|
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
|
mk_run.addArg("process-test");
|
|
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
|
|
|
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
|
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
|
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
|
.{ vfs_exe, "system/services" },
|
|
.{ device_manager_exe, "system/services" },
|
|
.{ input_exe, "system/services" },
|
|
.{ hpet_exe, "system/drivers" },
|
|
.{ bus_exe, "system/drivers" },
|
|
.{ ps2_bus_exe, "system/drivers" },
|
|
.{ ps2_keyboard_exe, "system/drivers" },
|
|
.{ ps2_mouse_exe, "system/drivers" },
|
|
.{ usb_xhci_bus_exe, "system/drivers" },
|
|
}) |entry| {
|
|
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
|
b.getInstallStep().dependOn(&step.step);
|
|
}
|
|
|
|
// The initial-ramdisk itself installs to /boot (with the loaders).
|
|
const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "boot/initial-ramdisk.img");
|
|
b.getInstallStep().dependOn(&initial_ramdisk_install.step);
|
|
|
|
// 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.
|
|
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 only the handoff contract — never the user ABI.
|
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
|
},
|
|
}),
|
|
});
|
|
|
|
// UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
|
|
// so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
|
|
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
|
b.getInstallStep().dependOn(&efi_install.step);
|
|
|
|
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
|
// Firmware lives in different places per OS/distro, so probe the known
|
|
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
|
|
// that exists. Override with -Dovmf-code / -Dovmf-vars if yours is elsewhere.
|
|
const ovmf_code = b.option(
|
|
[]const u8,
|
|
"ovmf-code",
|
|
"Path to the OVMF_CODE firmware image",
|
|
) orelse firstExisting(b.graph.io, &.{
|
|
"/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Architecture
|
|
"/usr/share/OVMF/OVMF_CODE_4M.fd", // Debian/Ubuntu
|
|
"/usr/share/OVMF/OVMF_CODE.fd", // older Debian/Ubuntu
|
|
"/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", // Fedora
|
|
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Apple Silicon)
|
|
"/usr/local/share/qemu/edk2-x86_64-code.fd", // macOS Homebrew (Intel)
|
|
});
|
|
const ovmf_vars = b.option(
|
|
[]const u8,
|
|
"ovmf-vars",
|
|
"Path to the OVMF_VARS firmware image (a writable copy is made)",
|
|
) orelse firstExisting(b.graph.io, &.{
|
|
"/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Architecture
|
|
"/usr/share/OVMF/OVMF_VARS_4M.fd", // Debian/Ubuntu
|
|
"/usr/share/OVMF/OVMF_VARS.fd", // older Debian/Ubuntu
|
|
"/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", // Fedora
|
|
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Apple Silicon)
|
|
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
|
|
});
|
|
|
|
// The FHS zig-out (installed above) *is* the boot volume — no separate ESP to
|
|
// assemble. QEMU presents it to the guest as a FAT drive below.
|
|
|
|
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
|
|
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
|
const vars_out = vars_copy.addOutputFileArg("OVMF_VARS.4m.fd");
|
|
|
|
const run_efi = b.addSystemCommand(&.{
|
|
"qemu-system-x86_64",
|
|
"-device",
|
|
"qemu-xhci,id=xhci",
|
|
"-device",
|
|
"usb-mouse,bus=xhci.0",
|
|
"-device",
|
|
"usb-kbd,bus=xhci.0",
|
|
// "-usb",
|
|
// "-device",
|
|
// "usb-ehci,id=ehci",
|
|
// "-device",
|
|
// "usb-tablet,bus=usb-bus.0",
|
|
// "-device",
|
|
// "usb-mouse,bus=ehci.0",
|
|
"-machine",
|
|
"q35",
|
|
"-m",
|
|
"128M",
|
|
"-drive",
|
|
b.fmt("if=pflash,format=raw,readonly=on,file={s}", .{ovmf_code}),
|
|
});
|
|
run_efi.addArg("-drive");
|
|
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
|
// Present the FHS zig-out to the guest as a FAT drive — it is the boot volume.
|
|
run_efi.addArgs(&.{
|
|
"-drive",
|
|
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}),
|
|
"-net",
|
|
"none",
|
|
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
|
// resolution, so the kernel's native-resolution switch has something to
|
|
// find. `-vga none` avoids a second, default adapter.
|
|
"-vga",
|
|
"none",
|
|
"-device",
|
|
"VGA,edid=on,xres=1280,yres=720",
|
|
});
|
|
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
|
|
// scratch area — a dev/host artifact, kept out of the FHS boot volume we mount.
|
|
// (/var/log/system is reserved for the kernel's own logging system later.) One
|
|
// timestamped file per run.
|
|
const log_dir = b.fmt("{s}/qemu-test", .{b.install_path});
|
|
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
|
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
|
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
|
// The whole FHS zig-out must be installed (and the scratch dir created) before we mount it.
|
|
run_efi.step.dependOn(b.getInstallStep());
|
|
run_efi.step.dependOn(&make_log_dir.step);
|
|
|
|
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
|
run_efi_step.dependOn(&run_efi.step);
|
|
|
|
// const run_cmd = b.addRunArtifact(exe);
|
|
// const run_step = b.step("run", "Run the app");
|
|
// run_step.dependOn(&run_cmd.step);
|
|
// run_cmd.step.dependOn(b.getInstallStep());
|
|
//
|
|
// if (b.args) |args| {
|
|
// run_cmd.addArgs(args);
|
|
// }
|
|
|
|
// 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/devices/device-abi.zig",
|
|
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
|
"system/devices/acpi-ids.zig", // _HID name decoding
|
|
"system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
|
"system/devices/usb-ids.zig", // class/subclass/protocol code assignments
|
|
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
|
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
|
|
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
|
|
}) |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 xkeyboard-config keymap tests need its generated `layouts` import wired, so they
|
|
// 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);
|
|
|
|
// 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);
|
|
}
|