code generation#
generating zig source at build time - for help text, unicode tables, bindings, anything derived from data.
the pattern#
build an executable, run it, capture output, import as zig source:
// 1. build the generator (always for host, not target)
const gen_exe = b.addExecutable(.{
.name = "helpgen",
.root_module = b.createModule(.{
.root_source_file = b.path("src/helpgen.zig"),
.target = b.graph.host, // runs on build machine
}),
});
// 2. run it and capture stdout
const gen_run = b.addRunArtifact(gen_exe);
const gen_output = gen_run.captureStdOut();
// 3. make it available as an import
step.root_module.addAnonymousImport("help_strings", .{
.root_source_file = gen_output,
});
now your code can @import("help_strings") and get the generated content.
why .target = b.graph.host#
the 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).
b.graph.host gives you the host target - the machine running the build.
writing to files instead#
if you need the output as a file (not just an import):
const wf = b.addWriteFiles();
const output_path = wf.addCopyFile(
gen_run.captureStdOut(),
"generated.zig",
);
// output_path is a LazyPath you can use elsewhere
custom build steps for external tools#
wrap non-zig tools (metal shader compiler, lipo, etc.) as build steps:
pub const MetallibStep = struct {
step: std.Build.Step,
output: std.Build.LazyPath,
pub fn create(b: *std.Build, shader_source: []const u8) *MetallibStep {
const run = b.addSystemCommand(&.{
"/usr/bin/xcrun", "-sdk", "macosx", "metal",
"-c", "-o",
});
const ir_output = run.addOutputFileArg("shader.ir");
run.addFileArg(b.path(shader_source));
// chain another command for metallib...
const self = b.allocator.create(MetallibStep) catch @panic("OOM");
self.* = .{
.step = std.Build.Step.init(.{ ... }),
.output = ir_output,
};
return self;
}
};
key points:
addOutputFileArg()creates a LazyPath for the outputaddFileArg()adds a dependency on an input file- proper dependency tracking means the step reruns when inputs change
conditional embedding#
bun embeds javascript runtime code in release builds but loads from disk in debug:
pub fn shouldEmbedCode(opts: *const BuildOptions) bool {
return opts.optimize != .Debug or opts.force_embed;
}
debug builds iterate faster (no recompile to change js). release builds are self-contained.
sources: