serial: make the log sink a build option, off by default
Serial is now a QEMU/dev aid, not a real-hardware necessity: a legacy-free board often has no live COM1, and the boot log is kept in RAM (klog) and flushed to disk. So the serial sink is compiled in only under -Dserial (default false). - kernel.zig gates serialInit + the log sink on build_options.serial - boot/efi.zig gates its EFI: progress breadcrumbs (con_out) via progress(); fatal-error messages stay always-on so a failed boot still explains itself - run-x86-64 boots a serial-enabled image variant (factored addKernel/addBootImage helpers) so a dev boot always captures serial0, without baking serial into the flashable image - test/qemu_test.py builds -Dserial=true (it asserts on serial markers) - the loopback probe stays as a real-HW safety net for -Dserial images
This commit is contained in:
+12
-3
@@ -2,6 +2,7 @@ const std = @import("std");
|
|||||||
const uefi = std.os.uefi;
|
const uefi = std.os.uefi;
|
||||||
const elf = std.elf;
|
const elf = std.elf;
|
||||||
const boot_handoff = @import("boot-handoff");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
const build_options = @import("build_options");
|
||||||
const BootInformation = boot_handoff.BootInformation;
|
const BootInformation = boot_handoff.BootInformation;
|
||||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||||
const EdidActive = uefi.protocol.edid.Active;
|
const EdidActive = uefi.protocol.edid.Active;
|
||||||
@@ -84,7 +85,7 @@ fn boot() !noreturn {
|
|||||||
// the map and exiting would invalidate the map key.
|
// the map and exiting would invalidate the map key.
|
||||||
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
||||||
|
|
||||||
log("EFI: kernel loaded, exiting boot services\r\n");
|
progress("EFI: kernel loaded, exiting boot services\r\n");
|
||||||
boot_information.memory_map = try exitBootServices(bs);
|
boot_information.memory_map = try exitBootServices(bs);
|
||||||
|
|
||||||
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
||||||
@@ -395,7 +396,7 @@ fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !
|
|||||||
const image = try loadFile(bs, init_file_name);
|
const image = try loadFile(bs, init_file_name);
|
||||||
boot_information.init_base = @intFromPtr(image.ptr);
|
boot_information.init_base = @intFromPtr(image.ptr);
|
||||||
boot_information.init_len = image.len;
|
boot_information.init_len = image.len;
|
||||||
log("EFI: /system/services/init loaded\r\n");
|
progress("EFI: /system/services/init loaded\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
||||||
@@ -403,7 +404,7 @@ fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInfo
|
|||||||
const image = try loadFile(bs, initial_ramdisk_file_name);
|
const image = try loadFile(bs, initial_ramdisk_file_name);
|
||||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||||
boot_information.initial_ramdisk_len = image.len;
|
boot_information.initial_ramdisk_len = image.len;
|
||||||
log("EFI: initial_ramdisk loaded\r\n");
|
progress("EFI: initial_ramdisk loaded\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
||||||
@@ -561,6 +562,14 @@ fn log(comptime message: []const u8) void {
|
|||||||
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(message)) catch {};
|
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(message)) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A boot-progress breadcrumb: like `log`, but compiled out unless `-Dserial`
|
||||||
|
/// (off by default), so a real-hardware boot stays silent. Fatal errors use
|
||||||
|
/// `log` directly and always show, so a failed boot still explains itself.
|
||||||
|
fn progress(comptime message: []const u8) void {
|
||||||
|
if (!build_options.serial) return;
|
||||||
|
log(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a runtime ASCII byte string (e.g. an @errorName) by widening to UTF-16.
|
/// Write a runtime ASCII byte string (e.g. an @errorName) by widening to UTF-16.
|
||||||
fn logBytes(bytes: []const u8) void {
|
fn logBytes(bytes: []const u8) void {
|
||||||
const out = uefi.system_table.con_out orelse return;
|
const out = uefi.system_table.con_out orelse return;
|
||||||
|
|||||||
@@ -96,6 +96,105 @@ fn addUserBinary(
|
|||||||
return exe;
|
return exe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding what
|
||||||
|
/// the firmware and loader need off the ESP: the EFI stub, `kernel`, `init`, and
|
||||||
|
/// the initial-ramdisk. Factored so the serial-enabled `run-x86-64` variant can
|
||||||
|
/// bundle its own kernel while sharing the (serial-independent) loader, init, and
|
||||||
|
/// ramdisk. Returns the image's LazyPath.
|
||||||
|
fn addBootImage(
|
||||||
|
b: *std.Build,
|
||||||
|
kernel_bin: std.Build.LazyPath,
|
||||||
|
efi_bin: std.Build.LazyPath,
|
||||||
|
init_bin: std.Build.LazyPath,
|
||||||
|
initial_ramdisk_img: std.Build.LazyPath,
|
||||||
|
) std.Build.LazyPath {
|
||||||
|
const mk_fat = b.addSystemCommand(&.{"python3"});
|
||||||
|
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||||
|
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
||||||
|
mk_fat.addArg("64"); // MiB
|
||||||
|
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
||||||
|
mk_fat.addFileArg(efi_bin);
|
||||||
|
mk_fat.addArg("system/kernel");
|
||||||
|
mk_fat.addFileArg(kernel_bin);
|
||||||
|
mk_fat.addArg("system/services/init");
|
||||||
|
mk_fat.addFileArg(init_bin);
|
||||||
|
mk_fat.addArg("boot/initial-ramdisk.img");
|
||||||
|
mk_fat.addFileArg(initial_ramdisk_img);
|
||||||
|
return fat_image;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
ensureZigVersion();
|
ensureZigVersion();
|
||||||
|
|
||||||
@@ -285,9 +384,12 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
// 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.
|
// 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 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();
|
// The serial-console log sink. Off by default: a real machine often has no
|
||||||
build_options.addOption(?[]const u8, "test_case", test_case);
|
// working legacy COM1, and the boot log is kept in RAM (klog) and flushed to
|
||||||
const build_options_module = build_options.createModule();
|
// 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;
|
||||||
|
|
||||||
// --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader ---
|
// --- 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,
|
// SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff,
|
||||||
@@ -299,41 +401,17 @@ pub fn build(b: *std.Build) void {
|
|||||||
.abi = .none,
|
.abi = .none,
|
||||||
});
|
});
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const kernel_modules = KernelModules{
|
||||||
.name = "kernel",
|
.boot_handoff = boot_handoff_module,
|
||||||
.root_module = b.createModule(.{
|
.abi = abi_module,
|
||||||
.root_source_file = b.path("system/kernel/kernel.zig"),
|
.device_abi = device_abi_module,
|
||||||
.target = kernel_target,
|
.architecture = architecture_module,
|
||||||
.optimize = optimize,
|
.platform = platform_module,
|
||||||
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
.parameters = parameters_module,
|
||||||
.red_zone = false, // interrupts would corrupt the SystemV red zone
|
.initial_ramdisk = initial_ramdisk_module,
|
||||||
.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
|
// The installed/flashable kernel: serial follows -Dserial (off by default).
|
||||||
.stack_check = false, // stack-probe calls have no runtime to land in
|
const exe = addKernel(b, kernel_target, optimize, kernel_modules, test_case, serial);
|
||||||
.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*
|
// 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
|
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
||||||
@@ -498,6 +576,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
// Boot methods live in boot/, one per way of getting the kernel running.
|
// 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
|
// 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.
|
// 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(.{
|
const efiexe = b.addExecutable(.{
|
||||||
.name = "BOOTX64",
|
.name = "BOOTX64",
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
@@ -510,6 +595,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
.imports = &.{
|
.imports = &.{
|
||||||
// The bootloader speaks only the handoff contract — never the user ABI.
|
// The bootloader speaks only the handoff contract — never the user ABI.
|
||||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
||||||
|
.{ .name = "build_options", .module = loader_options_module },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -525,21 +611,17 @@ pub fn build(b: *std.Build) void {
|
|||||||
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
|
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
|
||||||
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
|
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
|
||||||
// harness), and the danos fat driver mounts the same image at /mnt/usb.
|
// harness), and the danos fat driver mounts the same image at /mnt/usb.
|
||||||
const mk_fat = b.addSystemCommand(&.{"python3"});
|
const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||||
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
|
||||||
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
|
||||||
mk_fat.addArg("64"); // MiB
|
|
||||||
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
|
||||||
mk_fat.addFileArg(efiexe.getEmittedBin());
|
|
||||||
mk_fat.addArg("system/kernel");
|
|
||||||
mk_fat.addFileArg(exe.getEmittedBin());
|
|
||||||
mk_fat.addArg("system/services/init");
|
|
||||||
mk_fat.addFileArg(init_exe.getEmittedBin());
|
|
||||||
mk_fat.addArg("boot/initial-ramdisk.img");
|
|
||||||
mk_fat.addFileArg(initial_ramdisk_img);
|
|
||||||
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
||||||
b.getInstallStep().dependOn(&fat_image_install.step);
|
b.getInstallStep().dependOn(&fat_image_install.step);
|
||||||
|
|
||||||
|
// The image `run-x86-64` boots: identical to the flashable one but with the
|
||||||
|
// serial log sink compiled in, so a developer always gets the machine-readable
|
||||||
|
// log captured to serial0 — without baking serial into the image users flash.
|
||||||
|
// Built lazily (only when `run-x86-64` is requested), and never installed.
|
||||||
|
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
||||||
|
const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||||
|
|
||||||
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
||||||
// with the EFI stub present (the builder's own --verify, no external tools).
|
// with the EFI stub present (the builder's own --verify, no external tools).
|
||||||
const check_fat = b.addSystemCommand(&.{"python3"});
|
const check_fat = b.addSystemCommand(&.{"python3"});
|
||||||
@@ -611,8 +693,9 @@ pub fn build(b: *std.Build) void {
|
|||||||
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
||||||
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
|
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
|
||||||
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
|
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
|
||||||
|
// The serial-enabled variant, so serial0 carries the log for this dev boot.
|
||||||
run_efi.addArg("-drive");
|
run_efi.addArg("-drive");
|
||||||
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image);
|
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image_serial);
|
||||||
run_efi.addArgs(&.{
|
run_efi.addArgs(&.{
|
||||||
"-device",
|
"-device",
|
||||||
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
||||||
@@ -634,8 +717,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
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) });
|
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}) });
|
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.
|
// We boot the self-contained `fat_image_serial` (added as a file arg above, so
|
||||||
run_efi.step.dependOn(b.getInstallStep());
|
// it's already a dependency) — not the installed FHS zig-out — so `run-x86-64`
|
||||||
|
// builds only the serial kernel, never the flashable one. Just make the serial
|
||||||
|
// scratch dir first.
|
||||||
run_efi.step.dependOn(&make_log_dir.step);
|
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");
|
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");
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ transcript. Serial is per-architecture (x86 uses port I/O; an ARM board uses a
|
|||||||
memory-mapped UART), so it lives behind the [arch](arch.md) boundary — and adding
|
memory-mapped UART), so it lives behind the [arch](arch.md) boundary — and adding
|
||||||
a new architecture's UART is what makes the same tests run there.
|
a new architecture's UART is what makes the same tests run there.
|
||||||
|
|
||||||
|
The serial log sink is **compiled in only under `-Dserial`** (off by default).
|
||||||
|
A real machine often has no live legacy COM1 — writing to a dead one is slow —
|
||||||
|
and the boot log is kept in a RAM buffer (`klog`) and flushed to disk instead,
|
||||||
|
so serial is now purely a QEMU/dev aid. The harness (`test/qemu_test.py`) builds
|
||||||
|
every case with `-Dserial=true`, and `zig build run-x86-64` boots a serial-enabled
|
||||||
|
image variant, so both get the transcript; a flashable `zig build` image leaves
|
||||||
|
serial out. (Even with `-Dserial`, a loopback probe disables a dead port at boot,
|
||||||
|
so a serial-enabled image is still safe on real hardware.)
|
||||||
|
|
||||||
## In-kernel test cases
|
## In-kernel test cases
|
||||||
|
|
||||||
Building with `-Dtest-case=<name>` makes the kernel, after normal bring-up, run one
|
Building with `-Dtest-case=<name>` makes the kernel, after normal bring-up, run one
|
||||||
|
|||||||
@@ -60,8 +60,16 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// file on a ramdisk/USB/SSD), so a message survives as long as any is present.
|
// file on a ramdisk/USB/SSD), so a message survives as long as any is present.
|
||||||
// A headless, serial-less machine still boots correctly — it just goes quiet,
|
// A headless, serial-less machine still boots correctly — it just goes quiet,
|
||||||
// with port-0x80 checkpoints as the only progress signal.
|
// with port-0x80 checkpoints as the only progress signal.
|
||||||
architecture.serialInit();
|
//
|
||||||
log.addSink(architecture.serialWrite);
|
// Serial is compiled in only under -Dserial (build.zig): a real machine often
|
||||||
|
// has no live legacy COM1, and the log survives in the RAM buffer (below) and
|
||||||
|
// is flushed to disk — so serial is now a QEMU/dev convenience the flashable
|
||||||
|
// image leaves out. When it *is* built in, `serialInit`'s loopback probe still
|
||||||
|
// guards against a dead port (so a -Dserial image is safe on real hardware).
|
||||||
|
if (build_options.serial) {
|
||||||
|
architecture.serialInit();
|
||||||
|
log.addSink(architecture.serialWrite);
|
||||||
|
}
|
||||||
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
|
||||||
// Retain the whole stream in a RAM buffer too, so a user program can later
|
// Retain the whole stream in a RAM buffer too, so a user program can later
|
||||||
// read it back (klog_read) and persist the boot log to disk — the only way to
|
// read it back (klog_read) and persist the boot log to disk — the only way to
|
||||||
@@ -87,7 +95,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n"
|
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n"
|
||||||
else
|
else
|
||||||
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
||||||
log.write(if (architecture.serialPresent())
|
if (build_options.serial) log.write(if (architecture.serialPresent())
|
||||||
"/system/kernel: serial console online (COM1)\n"
|
"/system/kernel: serial console online (COM1)\n"
|
||||||
else
|
else
|
||||||
"/system/kernel: no serial UART (COM1 absent) -> log kept in RAM/debugcon\n");
|
"/system/kernel: no serial UART (COM1 absent) -> log kept in RAM/debugcon\n");
|
||||||
|
|||||||
+4
-1
@@ -504,7 +504,10 @@ TIMEOUT = 30 # seconds per case
|
|||||||
|
|
||||||
|
|
||||||
def build(arch, case):
|
def build(arch, case):
|
||||||
cmd = ["zig", "build", f"-Dtest-case={case}"] + arch["zig_flags"]
|
# -Dserial: the harness asserts on markers the kernel writes to serial0, so the
|
||||||
|
# serial log sink must be compiled in. It is off by default (a flashed real-
|
||||||
|
# hardware image keeps its log in RAM instead; see build.zig / serial.zig).
|
||||||
|
cmd = ["zig", "build", f"-Dtest-case={case}", "-Dserial=true"] + arch["zig_flags"]
|
||||||
r = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True)
|
r = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
return r.stderr.strip() or r.stdout.strip()
|
return r.stderr.strip() or r.stdout.strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user