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/