about things
build organization#
as projects grow, a single build.zig becomes unwieldy. ghostty's solution: treat build logic as a package.
the pattern#
instead of one giant build.zig, create src/build/ as a zig package:
src/build/
├── main.zig # exports everything
├── Config.zig # all -D options in one struct
├── SharedDeps.zig # dependency wiring
├── GhosttyExe.zig # executable-specific logic
├── GhosttyLib.zig # library-specific logic
└── steps/ # custom build steps
the root build.zig becomes a thin shell:
const buildpkg = @import("src/build/main.zig");
pub fn build(b: *std.Build) !void {
const config = buildpkg.Config.fromOptions(b);
const deps = try buildpkg.SharedDeps.init(b, &config);
if (config.emit_exe) {
_ = try buildpkg.GhosttyExe.init(b, &deps);
}
if (config.emit_lib) {
_ = try buildpkg.GhosttyLib.init(b, &deps);
}
}
centralized configuration#
all -D options live in one struct. this makes them discoverable and passable:
// Config.zig
pub const Config = @This();
// features
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
// artifacts to emit
emit_exe: bool = false,
emit_lib: bool = false,
emit_docs: bool = false,
pub fn fromOptions(b: *std.Build) Config {
return .{
.x11 = b.option(bool, "x11", "Enable X11") orelse false,
.wayland = b.option(bool, "wayland", "Enable Wayland") orelse false,
// ...
};
}
// export to comptime for runtime introspection
pub fn addOptions(self: *const Config, step: *std.Build.Step.Compile) void {
const opts = step.root_module.addOptions();
opts.addOption(bool, "x11", self.x11);
opts.addOption(bool, "wayland", self.wayland);
// now @import("build_options").x11 works in source
}
shared dependencies#
avoid duplicating dependency wiring across artifacts:
// SharedDeps.zig
pub const SharedDeps = @This();
config: *const Config,
freetype: *std.Build.Dependency,
harfbuzz: *std.Build.Dependency,
pub fn add(self: *const SharedDeps, step: *std.Build.Step.Compile) void {
step.linkLibrary(self.freetype.artifact("freetype"));
step.linkLibrary(self.harfbuzz.artifact("harfbuzz"));
// add all the deps once, use everywhere
}
now both GhosttyExe and GhosttyLib just call deps.add(step).
when to split#
small projects don't need this. consider splitting when:
- build.zig exceeds ~500 lines
- you have multiple artifacts (exe, lib, tests) sharing deps
- platform-specific logic is getting tangled
- you want to test build logic itself
source: ghostty/src/build/