132 lines
6.3 KiB
Zig
132 lines
6.3 KiB
Zig
//! The danos build API (docs/build-packages-plan.md): the one shared recipe
|
|
//! for building a user-space binary. A binary package's build.zig names its
|
|
//! binary and EXACTLY the modules its source imports — the moral equivalent
|
|
//! of a C file's include list — and `userBinary` resolves each name from the
|
|
//! library domain package that exports it. Nothing is pre-wired: an @import
|
|
//! the package did not declare is a compile error, and a domain none of the
|
|
//! imports come from never appears in the package's manifest. The only
|
|
//! implicit dependency is the kernel package, because the shared root shim
|
|
//! (root.zig, user.ld) lives there and itself reaches start + logging.
|
|
//!
|
|
//! Consumers declare this package in their build.zig.zon (as "build-support")
|
|
//! and @import its build.zig from their own build.zig; nothing is compiled
|
|
//! from this package itself — it exports build-time functions only.
|
|
|
|
const std = @import("std");
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
_ = b; // nothing to build: this package exports build-time functions only
|
|
}
|
|
|
|
/// The freestanding x86-64 target every danos binary (kernel and user) is
|
|
/// built for. 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.
|
|
pub fn freestandingTarget(b: *std.Build) std.Build.ResolvedTarget {
|
|
return b.resolveTargetQuery(.{
|
|
.cpu_arch = .x86_64,
|
|
.os_tag = .freestanding,
|
|
.abi = .none,
|
|
});
|
|
}
|
|
|
|
/// Resolve one imported module by searching the packages this binary DECLARED
|
|
/// in its own build.zig.zon — the C include path made literal: an import can
|
|
/// only be satisfied by a domain the binary claims, and each domain's own
|
|
/// build.zig (its addModule exports) is the single statement of who owns
|
|
/// what. There is no name table here to drift.
|
|
fn moduleFromDeclaredDependencies(b: *std.Build, name: []const u8) *std.Build.Module {
|
|
for (b.available_deps) |declared| {
|
|
const dependency = b.dependency(declared[0], .{});
|
|
if (dependency.builder.modules.get(name)) |module| return module;
|
|
}
|
|
@panic(b.fmt(
|
|
"no declared dependency exports a module named '{s}' — declare the domain that owns it in this package's build.zig.zon",
|
|
.{name},
|
|
));
|
|
}
|
|
|
|
/// What `userBinary` needs to know about one user binary.
|
|
pub const UserBinaryOptions = struct {
|
|
name: []const u8,
|
|
/// The program's own source file — it becomes the `program` module the
|
|
/// root shim imports; a program only defines `pub fn main`.
|
|
root_source_file: std.Build.LazyPath,
|
|
/// Exactly the modules the program's source @imports (directly or through
|
|
/// its same-directory files) — no more, no less. Order is free; sorted
|
|
/// reads best. An undeclared @import fails the compile; a name no
|
|
/// declared domain exports fails the build graph, naming the miss.
|
|
imports: []const []const u8,
|
|
/// Built multi-threaded (`single_threaded = false`) so real atomics/TLS
|
|
/// work — required before a binary may call `Thread.spawn`
|
|
/// (docs/threading.md). Threads are a deliberate per-binary opt-in.
|
|
threaded: bool = false,
|
|
};
|
|
|
|
/// Build one user-space binary the same way for every program (init, the
|
|
/// services, the drivers): freestanding, ReleaseSmall, `.large` code model
|
|
/// (the image base is above 4 GiB — smaller models emit 32-bit relocations
|
|
/// that can't reach), linked 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.
|
|
///
|
|
/// The compilation root is not the program's own file but the shared shim
|
|
/// (the kernel package's root.zig), which supplies the root declarations
|
|
/// (`main` re-export, panic handler, `_start` pull) so a program only defines
|
|
/// `pub fn main`. The program's file becomes the `program` module the shim
|
|
/// imports; reach it through `programModule` to add per-binary non-library
|
|
/// modules (compile-time options).
|
|
pub fn userBinary(b: *std.Build, options: UserBinaryOptions) *std.Build.Step.Compile {
|
|
const kernel = b.dependency("kernel", .{});
|
|
var imports: std.ArrayListUnmanaged(std.Build.Module.Import) = .empty;
|
|
for (options.imports) |name| {
|
|
imports.append(b.allocator, .{
|
|
.name = name,
|
|
.module = moduleFromDeclaredDependencies(b, name),
|
|
}) catch @panic("OOM");
|
|
}
|
|
// Settings (target, optimize, code model, ...) live on the root module
|
|
// only; the program module inherits them.
|
|
const program_module = b.createModule(.{
|
|
.root_source_file = options.root_source_file,
|
|
.imports = imports.items,
|
|
});
|
|
const exe = b.addExecutable(.{
|
|
.name = options.name,
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = kernel.path("root.zig"),
|
|
.target = freestandingTarget(b),
|
|
.optimize = .ReleaseSmall,
|
|
.code_model = .large,
|
|
.single_threaded = !options.threaded, // a threaded binary needs real atomics/TLS
|
|
.sanitize_c = .off,
|
|
.stack_check = false,
|
|
.stack_protector = false,
|
|
// The root shim itself imports only start (_start + panic) and
|
|
// logging (std_options) — straight from the kernel package, so a
|
|
// program's own import list stays exactly its own.
|
|
.imports = &.{
|
|
.{ .name = "start", .module = kernel.module("start") },
|
|
.{ .name = "logging", .module = kernel.module("logging") },
|
|
.{ .name = "program", .module = program_module },
|
|
},
|
|
}),
|
|
});
|
|
exe.setLinkerScript(kernel.path("user.ld"));
|
|
exe.entry = .{ .symbol_name = "_start" };
|
|
exe.image_base = 0x7000_0000_0000;
|
|
exe.use_llvm = true;
|
|
exe.use_lld = true;
|
|
return exe;
|
|
}
|
|
|
|
/// The `program` module of a binary built by `userBinary` — the module rooted
|
|
/// at the program's own source file. Per-binary non-library modules (an
|
|
/// addOptions build_options) go here, not on the root shim: module imports
|
|
/// are not transitive, so an import added to the root would be invisible to
|
|
/// the program's code.
|
|
pub fn programModule(exe: *std.Build.Step.Compile) *std.Build.Module {
|
|
return exe.root_module.import_table.get("program").?;
|
|
}
|