about things
1# build organization
2
3as projects grow, a single `build.zig` becomes unwieldy. ghostty's solution: treat build logic as a package.
4
5## the pattern
6
7instead of one giant build.zig, create `src/build/` as a zig package:
8
9```
10src/build/
11├── main.zig # exports everything
12├── Config.zig # all -D options in one struct
13├── SharedDeps.zig # dependency wiring
14├── GhosttyExe.zig # executable-specific logic
15├── GhosttyLib.zig # library-specific logic
16└── steps/ # custom build steps
17```
18
19the root `build.zig` becomes a thin shell:
20
21```zig
22const buildpkg = @import("src/build/main.zig");
23
24pub fn build(b: *std.Build) !void {
25 const config = buildpkg.Config.fromOptions(b);
26 const deps = try buildpkg.SharedDeps.init(b, &config);
27
28 if (config.emit_exe) {
29 _ = try buildpkg.GhosttyExe.init(b, &deps);
30 }
31 if (config.emit_lib) {
32 _ = try buildpkg.GhosttyLib.init(b, &deps);
33 }
34}
35```
36
37## centralized configuration
38
39all `-D` options live in one struct. this makes them discoverable and passable:
40
41```zig
42// Config.zig
43pub const Config = @This();
44
45// features
46x11: bool = false,
47wayland: bool = false,
48sentry: bool = true,
49
50// artifacts to emit
51emit_exe: bool = false,
52emit_lib: bool = false,
53emit_docs: bool = false,
54
55pub fn fromOptions(b: *std.Build) Config {
56 return .{
57 .x11 = b.option(bool, "x11", "Enable X11") orelse false,
58 .wayland = b.option(bool, "wayland", "Enable Wayland") orelse false,
59 // ...
60 };
61}
62
63// export to comptime for runtime introspection
64pub fn addOptions(self: *const Config, step: *std.Build.Step.Compile) void {
65 const opts = step.root_module.addOptions();
66 opts.addOption(bool, "x11", self.x11);
67 opts.addOption(bool, "wayland", self.wayland);
68 // now @import("build_options").x11 works in source
69}
70```
71
72## shared dependencies
73
74avoid duplicating dependency wiring across artifacts:
75
76```zig
77// SharedDeps.zig
78pub const SharedDeps = @This();
79
80config: *const Config,
81freetype: *std.Build.Dependency,
82harfbuzz: *std.Build.Dependency,
83
84pub fn add(self: *const SharedDeps, step: *std.Build.Step.Compile) void {
85 step.linkLibrary(self.freetype.artifact("freetype"));
86 step.linkLibrary(self.harfbuzz.artifact("harfbuzz"));
87 // add all the deps once, use everywhere
88}
89```
90
91now both `GhosttyExe` and `GhosttyLib` just call `deps.add(step)`.
92
93## when to split
94
95small projects don't need this. consider splitting when:
96- build.zig exceeds ~500 lines
97- you have multiple artifacts (exe, lib, tests) sharing deps
98- platform-specific logic is getting tangled
99- you want to test build logic itself
100
101source: [ghostty/src/build/](https://github.com/ghostty-org/ghostty/tree/main/src/build)