about things
1# code generation
2
3generating zig source at build time - for help text, unicode tables, bindings, anything derived from data.
4
5## the pattern
6
7build an executable, run it, capture output, import as zig source:
8
9```zig
10// 1. build the generator (always for host, not target)
11const gen_exe = b.addExecutable(.{
12 .name = "helpgen",
13 .root_module = b.createModule(.{
14 .root_source_file = b.path("src/helpgen.zig"),
15 .target = b.graph.host, // runs on build machine
16 }),
17});
18
19// 2. run it and capture stdout
20const gen_run = b.addRunArtifact(gen_exe);
21const gen_output = gen_run.captureStdOut();
22
23// 3. make it available as an import
24step.root_module.addAnonymousImport("help_strings", .{
25 .root_source_file = gen_output,
26});
27```
28
29now your code can `@import("help_strings")` and get the generated content.
30
31## why `.target = b.graph.host`
32
33the generator runs during the build, on your machine. even if you're cross-compiling to arm64-linux, the generator needs to run on your x86-macos (or whatever you're building from).
34
35`b.graph.host` gives you the host target - the machine running the build.
36
37## writing to files instead
38
39if you need the output as a file (not just an import):
40
41```zig
42const wf = b.addWriteFiles();
43const output_path = wf.addCopyFile(
44 gen_run.captureStdOut(),
45 "generated.zig",
46);
47// output_path is a LazyPath you can use elsewhere
48```
49
50## custom build steps for external tools
51
52wrap non-zig tools (metal shader compiler, lipo, etc.) as build steps:
53
54```zig
55pub const MetallibStep = struct {
56 step: std.Build.Step,
57 output: std.Build.LazyPath,
58
59 pub fn create(b: *std.Build, shader_source: []const u8) *MetallibStep {
60 const run = b.addSystemCommand(&.{
61 "/usr/bin/xcrun", "-sdk", "macosx", "metal",
62 "-c", "-o",
63 });
64 const ir_output = run.addOutputFileArg("shader.ir");
65 run.addFileArg(b.path(shader_source));
66
67 // chain another command for metallib...
68 const self = b.allocator.create(MetallibStep) catch @panic("OOM");
69 self.* = .{
70 .step = std.Build.Step.init(.{ ... }),
71 .output = ir_output,
72 };
73 return self;
74 }
75};
76```
77
78key points:
79- `addOutputFileArg()` creates a LazyPath for the output
80- `addFileArg()` adds a dependency on an input file
81- proper dependency tracking means the step reruns when inputs change
82
83## conditional embedding
84
85bun embeds javascript runtime code in release builds but loads from disk in debug:
86
87```zig
88pub fn shouldEmbedCode(opts: *const BuildOptions) bool {
89 return opts.optimize != .Debug or opts.force_embed;
90}
91```
92
93debug builds iterate faster (no recompile to change js). release builds are self-contained.
94
95sources:
96- [ghostty/src/build/HelpStrings.zig](https://github.com/ghostty-org/ghostty/blob/main/src/build/HelpStrings.zig)
97- [ghostty/src/build/MetallibStep.zig](https://github.com/ghostty-org/ghostty/blob/main/src/build/MetallibStep.zig)