kernel/build.zig
2025-05-09 23:16:25 +08:00

501 lines
19 KiB
Zig
Executable File

const std = @import("std");
const builtin = @import("builtin");
pub fn build(b: *std.Build) !void {
const arch =
b.option(std.Target.Cpu.Arch, "arch", "The target kernel architecture")
orelse builtin.cpu.arch;
var query: std.Target.Query = .{
.cpu_arch = arch,
.os_tag = .freestanding,
.abi = .none,
};
var code_model: std.builtin.CodeModel = .default;
switch (arch) {
.aarch64 => {
const Feature = std.Target.aarch64.Feature;
query.cpu_features_sub.addFeature(@intFromEnum(Feature.fp_armv8));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.crypto));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.neon));
},
.loongarch64 => {
const Feature = std.Target.loongarch.Feature;
query.cpu_features_add.addFeature(@intFromEnum(Feature.f));
query.cpu_features_add.addFeature(@intFromEnum(Feature.d));
},
.riscv64 => {
const Feature = std.Target.riscv.Feature;
query.cpu_features_add.addFeature(@intFromEnum(Feature.f));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.d));
},
.x86_64 => {
const Feature = std.Target.x86.Feature;
query.cpu_features_add.addFeature(@intFromEnum(Feature.soft_float));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.mmx));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.sse));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.sse2));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.avx));
query.cpu_features_sub.addFeature(@intFromEnum(Feature.avx2));
code_model = .kernel;
},
else => std.debug.panic("Unsupported architecture: {s}", .{@tagName(arch)}),
}
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.resolveTargetQuery(query);
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const kernel_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.code_model = code_model,
.red_zone = false,
});
const kernel = b.addExecutable(.{
.name = "kernel",
// We will also create a module for our other entry point, 'main.zig'.
.root_module = kernel_mod,
});
// Disable LTO. This prevents Limine requests from being optimized away.
kernel.want_lto = false;
// Add kernel dependencies.
try resolveDependencies(b, kernel_mod);
// Set the linker script.
const linker_script = try generateLinkerScript(b, arch);
kernel.setLinkerScript(linker_script);
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
// step when running `zig build`).
b.installArtifact(kernel);
const img = buildBootableImage(b, kernel.getEmittedBin(), arch);
const run_cmd = generateRunCommand(b, img, arch);
// By making the run step depend on the install step, it will be run from the
// installation directory rather than directly from within the cache directory.
// This is not necessary, however, if the application depends on other installed
// files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build run`
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the kernel");
run_step.dependOn(&run_cmd.step);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
}
fn buildBootableImage(b: *std.Build, kernel: std.Build.LazyPath, arch: std.Target.Cpu.Arch) std.Build.LazyPath {
// Add Limine to the dependency tree.
const limine = b.dependency("limine", .{});
const rootfs = b.addNamedWriteFiles("rootfs");
// Build the bootloader from source.
const exe = b.addExecutable(.{
.name = "limine",
.target = b.resolveTargetQuery(.{}),
.optimize = .ReleaseSafe,
});
exe.addCSourceFile(.{
.file = limine.path("limine.c"),
.flags = &.{"-std=c99"},
});
exe.linkLibC();
_ = rootfs.addCopyFile(b.path("lib/limine/limine.conf"), "boot/limine/limine.conf");
_ = rootfs.addCopyFile(kernel, "boot/kernel");
const run_cmd = b.addSystemCommand(switch (arch) {
.aarch64 => block: {
_ = rootfs.addCopyFile(limine.path("limine-uefi-cd.bin"), "boot/limine/limine-uefi-cd.bin");
_ = rootfs.addCopyFile(limine.path("BOOTAA64.EFI"), "EFI/BOOT/BOOTAA64.EFI");
break :block &.{
"xorriso", "-as", "mkisofs",
"-R", "-r", "-J",
"-hfsplus",
"-apm-block-size", "2048",
"--efi-boot", "boot/limine/limine-uefi-cd.bin",
"-efi-boot-part",
"--efi-boot-image",
"--protective-msdos-label",
};
},
.loongarch64 => block: {
_ = rootfs.addCopyFile(limine.path("limine-uefi-cd.bin"), "boot/limine/limine-uefi-cd.bin");
_ = rootfs.addCopyFile(limine.path("BOOTLOONGARCH64.EFI"), "EFI/BOOT/BOOTLOONGARCH64.EFI");
break :block &.{
"xorriso", "-as", "mkisofs",
"-R", "-r", "-J",
"-hfsplus",
"-apm-block-size", "2048",
"--efi-boot", "boot/limine/limine-uefi-cd.bin",
"-efi-boot-part",
"--efi-boot-image",
"--protective-msdos-label",
};
},
.riscv64 => block: {
_ = rootfs.addCopyFile(limine.path("limine-uefi-cd.bin"), "boot/limine/limine-uefi-cd.bin");
_ = rootfs.addCopyFile(limine.path("BOOTRISCV64.EFI"), "EFI/BOOT/BOOTRISCV64.EFI");
break :block &.{
"xorriso", "-as", "mkisofs",
"-R", "-r", "-J",
"-hfsplus",
"-apm-block-size", "2048",
"--efi-boot", "boot/limine/limine-uefi-cd.bin",
"-efi-boot-part",
"--efi-boot-image",
"--protective-msdos-label",
};
},
.x86_64 => block: {
_ = rootfs.addCopyFile(limine.path("limine-bios.sys"), "boot/limine/limine-bios.sys");
_ = rootfs.addCopyFile(limine.path("limine-bios-cd.bin"), "boot/limine/limine-bios-cd.bin");
_ = rootfs.addCopyFile(limine.path("limine-uefi-cd.bin"), "boot/limine/limine-uefi-cd.bin");
_ = rootfs.addCopyFile(limine.path("BOOTX64.EFI"), "EFI/BOOT/BOOTX64.EFI");
_ = rootfs.addCopyFile(limine.path("BOOTIA32.EFI"), "EFI/BOOT/BOOTIA32.EFI");
break :block &.{
"xorriso", "-as", "mkisofs",
"-R", "-r", "-J",
"-b", "boot/limine/limine-bios-cd.bin",
"-no-emul-boot",
"-boot-load-size", "4",
"-boot-info-table",
"-hfsplus",
"-apm-block-size", "2048",
"--efi-boot", "boot/limine/limine-uefi-cd.bin",
"-efi-boot-part",
"--efi-boot-image",
"--protective-msdos-label",
};
},
else => unreachable,
});
run_cmd.step.dependOn(&rootfs.step);
run_cmd.addDirectoryArg(rootfs.getDirectory());
run_cmd.addArg("-o");
// Retrieve the bootable image from command.
const image = run_cmd.addOutputFileArg("bootable.iso");
// Install Limine stage 1 and 2 for legacy BIOS boot.
// This is only required on x86_64, but we are deploying for all anyway.
const deploy = b.addRunArtifact(exe);
deploy.step.dependOn(&run_cmd.step);
deploy.addArg("bios-install");
deploy.addFileArg(image);
const step = b.step("image", "Generate a bootable ISO image");
step.dependOn(&deploy.step);
return image;
}
fn generateLinkerScript(b: *std.Build, arch: std.Target.Cpu.Arch) !std.Build.LazyPath {
const path = "linker.ld";
const data = try std.mem.join(b.allocator, "\n", &.{
switch (arch) {
.aarch64 => "OUTPUT_FORMAT(elf64-littleaarch64)",
.loongarch64 => "OUTPUT_FORMAT(elf64-loongarch)",
.riscv64 => "OUTPUT_FORMAT(elf64-littleriscv)",
.x86_64 => "OUTPUT_FORMAT(elf64-x86-64)",
else => unreachable,
},
\\ENTRY(_start)
\\
\\/* Define the program headers we want so the bootloader gives us the right */
\\/* MMU permissions; this also allows us to exert more control over the linking */
\\/* process. */
\\PHDRS
\\{
\\ text PT_LOAD;
\\ rodata PT_LOAD;
\\ data PT_LOAD;
\\}
\\
\\SECTIONS
\\{
\\ /* We want to be placed in the topmost 2GiB of the address space, for optimisations */
\\ /* and because that is what the Limine spec mandates. */
\\ /* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
\\ /* that is the beginning of the region. */
\\ . = 0xffffffff80000000;
\\
\\ .text : {
\\ *(.text .text.*)
\\ } :text
\\
\\ /* Move to the next memory page for .rodata */
\\ . = ALIGN(CONSTANT(MAXPAGESIZE));
\\
\\ .rodata : {
\\ *(.rodata .rodata.*)
\\ } :rodata
\\
\\ /* Move to the next memory page for .data */
\\ . = ALIGN(CONSTANT(MAXPAGESIZE));
\\
\\ .data : {
\\ *(.data .data.*)
\\
\\ /* Place the sections that contain the Limine requests as part of the .data */
\\ /* output section. */
\\ KEEP(*(.requests_start_marker))
\\ KEEP(*(.requests))
\\ KEEP(*(.requests_end_marker))
\\ } :data
\\
\\ /* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
\\ /* unnecessary zeros will be written to the binary. */
\\ /* If you need, for example, .init_array and .fini_array, those should be placed */
\\ /* above this. */
\\ .bss : {
\\ *(.bss .bss.*)
\\ *(COMMON)
\\ } :data
\\
\\ /* Discard .note.* and .eh_frame* since they may cause issues on some hosts. */
\\ /DISCARD/ : {
\\ *(.eh_frame*)
\\ *(.note .note.*)
\\ }
\\}
});
const script = b.addWriteFile(path, data);
b.getInstallStep().dependOn(&script.step);
return script.getDirectory().path(b, path);
}
fn generateRunCommand(b: *std.Build, image: std.Build.LazyPath, arch: std.Target.Cpu.Arch) *std.Build.Step.Run {
// Add edk2_ovmf to the dependency tree
const edk2 = b.dependency("edk2", .{});
const fs = b.addWriteFiles();
var code: std.Build.LazyPath = undefined;
var vars: std.Build.LazyPath = undefined;
// This *creates* a Run step in the build graph, to be executed when another
// step is evaluated that depends on it. The next line below will establish
// such a dependency.
const run_cmd: *std.Build.Step.Run = switch (arch) {
.aarch64 => block: {
code = fs.addCopyFile(edk2.path("bin/RELEASEAARCH64_QEMU_EFI.fd"), "code.fd");
vars = fs.addCopyFile(edk2.path("bin/RELEASEAARCH64_QEMU_VARS.fd"), "vars.fd");
const dd1_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=67108864"});
dd1_cmd.addPrefixedFileArg("of=", code);
dd1_cmd.step.dependOn(&fs.step);
const dd2_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=67108864"});
dd2_cmd.addPrefixedFileArg("of=", vars);
dd2_cmd.step.dependOn(&fs.step);
const run_cmd = b.addSystemCommand(&.{
"qemu-system-aarch64",
"-M", "virt",
"-cpu", "cortex-a72",
"-device", "ramfb",
"-device", "qemu-xhci",
"-device", "usb-kbd",
"-device", "usb-mouse",
"-serial", "stdio",
});
run_cmd.step.dependOn(&dd1_cmd.step);
run_cmd.step.dependOn(&dd2_cmd.step);
break :block run_cmd;
},
.loongarch64 => block: {
code = fs.addCopyFile(edk2.path("bin/RELEASELOONGARCH64_QEMU_EFI.fd"), "code.fd");
vars = fs.addCopyFile(edk2.path("bin/RELEASELOONGARCH64_QEMU_VARS.fd"), "vars.fd");
const dd1_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=5242880"});
dd1_cmd.addPrefixedFileArg("of=", code);
dd1_cmd.step.dependOn(&fs.step);
const dd2_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=5242880"});
dd2_cmd.addPrefixedFileArg("of=", vars);
dd2_cmd.step.dependOn(&fs.step);
const run_cmd = b.addSystemCommand(&.{
"qemu-system-loongarch64",
"-M", "virt",
"-cpu", "la464",
"-device", "ramfb",
"-device", "qemu-xhci",
"-device", "usb-kbd",
"-device", "usb-mouse",
"-serial", "stdio",
});
run_cmd.step.dependOn(&dd1_cmd.step);
run_cmd.step.dependOn(&dd2_cmd.step);
break :block run_cmd;
},
.riscv64 => block: {
code = fs.addCopyFile(edk2.path("bin/RELEASERISCV64_VIRT_CODE.fd"), "code.fd");
vars = fs.addCopyFile(edk2.path("bin/RELEASERISCV64_VIRT_VARS.fd"), "vars.fd");
const dd1_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=33554432"});
dd1_cmd.addPrefixedFileArg("of=", code);
dd1_cmd.step.dependOn(&fs.step);
const dd2_cmd = b.addSystemCommand(&.{"dd", "if=/dev/zero", "bs=1", "count=0", "seek=33554432"});
dd2_cmd.addPrefixedFileArg("of=", vars);
dd2_cmd.step.dependOn(&fs.step);
const run_cmd = b.addSystemCommand(&.{
"qemu-system-riscv64",
"-M", "virt",
"-cpu", "rv64",
"-device", "ramfb",
"-device", "qemu-xhci",
"-device", "usb-kbd",
"-device", "usb-mouse",
"-serial", "stdio",
});
run_cmd.step.dependOn(&dd1_cmd.step);
run_cmd.step.dependOn(&dd2_cmd.step);
break :block run_cmd;
},
.x86_64 => block: {
code = edk2.path("bin/RELEASEX64_OVMF_CODE.fd");
vars = edk2.path("bin/RELEASEX64_OVMF_VARS.fd");
break :block b.addSystemCommand(&.{
"qemu-system-x86_64",
"-M", "q35",
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
"-serial", "stdio",
"-rtc", "clock=vm",
});
},
else => unreachable,
};
run_cmd.addArg("-drive");
run_cmd.addPrefixedFileArg("if=pflash,unit=0,format=raw,readonly=on,file=", code);
run_cmd.addArg("-drive");
run_cmd.addPrefixedFileArg("if=pflash,unit=1,format=raw,file=", vars);
run_cmd.addArg("-cdrom");
run_cmd.addFileArg(image);
return run_cmd;
}
fn resolveDependencies(b: *std.Build, kernel: *std.Build.Module) !void {
const libk = block: {
const file = b.path("lib/kernel/kernel.zig");
const mod = b.addModule("kernel", .{
.root_source_file = file,
.optimize = kernel.optimize,
.target = kernel.resolved_target
});
break :block mod;
};
const limine = block: {
const file = b.path("lib/limine/limine.zig");
const mod = b.createModule(.{ .root_source_file = file });
break :block mod;
};
const uacpi = block: {
const dependency = b.dependency("uacpi", .{});
const file = b.path("lib/uacpi/uacpi.zig");
const mod = b.createModule(.{ .root_source_file = file });
var flags: std.ArrayList([]const u8) = .init(b.allocator);
for (b.debug_log_scopes) |scope| {
if (std.mem.eql(u8, scope, "uacpi")) {
try flags.append("-DUACPI_DEFAULT_LOG_LEVEL=UACPI_LOG_TRACE");
break;
}
}
mod.addIncludePath(dependency.path("include"));
mod.addCSourceFiles(.{
.root = dependency.path("source"),
.files = &.{
"default_handlers.c",
"event.c",
"interpreter.c",
"io.c",
"mutex.c",
"namespace.c",
"notify.c",
"opcodes.c",
"opregion.c",
"osi.c",
"registers.c",
"resources.c",
"shareable.c",
"sleep.c",
"stdlib.c",
"tables.c",
"types.c",
"uacpi.c",
"utilities.c",
},
.flags = try flags.toOwnedSlice(),
});
break :block mod;
};
kernel.addImport("libk", libk);
kernel.addImport("uacpi", uacpi);
kernel.addImport("limine", limine);
}