Skip to content

Zig Build#


Zig build scripts (usually named build.zig) are ordinary Zig programs with a special exported function (pub fn build(b: * void) utilizing
The build runner is invoked by zig build which in turn invokes said build.zig:build()

  • create DAG of nodes where each Step
  • executes a part of our build process
  • has a set of dependencies that need to be made before the step itself is made
  • user can invoke named steps by calling zig build step-name or predefined steps (e.g. install)
  • create with Builder.step:
    pub fn build(b: * void {
        const named_step = b.step("step-name", "This is what is shown in help");

Compiling Executable#

Source Compilation#

Builder exposes Builder.addExecutable which will create us a new LibExeObjStep

  • a convenient wrapper around zig build-exe, zig build-lib, zig build-obj or zig test depending on how it is initialized

  • example:

pub fn build(b: * void {
    const exe = b.addExecutable("fresh", "src/main.zig");

    const target = b.standardTargetOptions(.{});

    const mode = b.standardReleaseOptions();

    const compile_step = b.step("compile", "Compiles src/main.zig");
  • create with Builder.addExecutable that will compile main.zig into fresh/fresh.exe

  • add dependency graph with compile_step.dependOn(&exe.step);. This is how we build our dependency graph and declare that when compile_step is made, exe also needs to be made.

Cross Compilation#

  • cross compilation is enabled by setting the target and build mode of our program:
  • exe.setBuildMode(.ReleaseSafe); will pass -O ReleaseSafe to the build invocation.
  • exe.setTarget(...); will set what -target ... will see.
  • Builder.standardReleaseOptions/Builder.standardTargetOptions: convenience functions to make both the build mode and the target available as a command line option
  • invoke zig build --help to see command line options added by standardTargetOptions (first two) and standardReleaseOptions (rest)

    Project-Specific Options:
      -Dtarget=[string]           The CPU architecture, OS, and ABI to build for
      -Dcpu=[string]              Target CPU features to add or subtract
      -Drelease-safe=[bool]       Optimizations on and safety on
      -Drelease-fast=[bool]       Optimizations on and safety off
      -Drelease-small=[bool]      Size optimizations on and safety off

  • example command line:

    zig build -Dtarget=x86_64-windows-gnu -Dcpu=athlon_fx
    zig build -Drelease-safe=true
    zig build -Drelease-small

Installing Artifacts#

Installation involves making a step on the install step of the Builder

  • install step always created and accessed via Builder.getInstallStep()

  • InstallArtifactStep is build step responsible for copying exe artifact to install directory

pub fn build(b: * void {
    const exe = b.addExecutable("fresh", "src/main.zig");

    const install_exe = b.addInstallArtifact(exe);

This will now do several things:

  • b.addInstallArtifact creates a new InstallArtifactStep that copies the compilation result of exe to $prefix/bin (usually zig-out)
  • InstallArtifactStep (implicitly) depends on exe so will build exe as well
  • invoke by zig build install (or just zig build for short)
  • uninstall the artifact by invoking zig build uninstall
  • the InstallArtifactStep registers the output file for exe in a list that allows uninstalling it again
  • NOTE: deletes all files created by zig build install, but not directories!
  • Other helper functions
  • b.installArtifact(exe)/exe.install(): convenience functions to wrap above steps. Ex:

    pub fn build(b: * void {
        const exe = b.addExecutable("fresh", "src/main.zig");
        b.installArtifact(exe);  //    Helper 1
        exe.install();           // OR Helper 2

  • Builder.installFile/installDirectory/etc: install other types of artifacts

Running Applications#

Can run programs from build script for convenience

  • usually exposed via a run step that can be invoked via zig build run
pub fn build(b: * void {
    const exe = b.addExecutable("fresh", "src/main.zig");

    const run_step =, "run fresh");

    const step = b.step("run", "Runs the executable");
  • runs any executable on the system

  • RunStep.addArg will add a single string argument to argv.

  • RunStep.addArgs will add several strings at the same time

  • RunStep.addArtifactArg will add the result file of a LibExeObjStep to argv

  • RunStep.addFileSourceArg will add any file generated by other steps to the argv

  • NOTE: first argument must be the path to the executable we want to run. In this case, we want to run the compiled output of exe

  • NOTE: RunStep runs executable in the compile cache directory, not install directory (e.g. ./zig-cache/o/b0f56fa4ce81bb82c61d98fb6f77b809/fresh vs zig-out/bin/fresh)

  • helper convenience function for above

    pub fn build(b: * void {
        const exe = b.addExecutable("fresh", "src/main.zig");
        const run_step =;
        const step = b.step("run", "Runs the executable");
  • Builder.args contains command line args that can be passed to process. Ex:

    pub fn build(b: * void {
        const exe = b.addExecutable("fresh", "src/main.zig");
        const run_step =;
        if (b.args) |args| { run_step.addArgs(args); }
        const step = b.step("run", "Runs the executable");
    zig build run -- -o foo.bin foo.asm


  • Use LibExeObjStep.addPackage/addPackagePath with a Pkg{ .name = "library", .path = "/path/to/the/library"}
  • use const library = @import("library"); in your root source file
  • Set output directory: foo_lib.setOutputDir(output_path);
  • note: this should be done before foo_lib.setTarget(..) as that will recompute the full output path
  • Get lib output lib path source: exe.linkSystemLibrary(foo_lib.getOutputLibSource());

Use a native (C) library#

  • Use LibExeObjStep.linkSystemLibrary() with your library's name
  • @cInclude() in your source code

Use a native (C++) library#

  • use LibExeObjStep.linkSystemLibrary("c++");

Use build-time custom command line flags (-Dsomething)#

  • Use LibExeObjStep.addBuildOption() to add a value to the build_options package
  • To get this value from the building user, use Builder.option()
  • Supported types for option are Strings and Enums (-Dname=value style), Booleans (-Dname, -Dname=true, -Dname=false style) and list of strings (-Dname=value -Dname=value2 style)
  • Use from your source code like const should_do_thing = @import("build_options").do_thing;


provide compile-time configuration to your code

  • the build system can create a package called build_options to communicate values from build.zig to your project's source code
  • how to use:
  • create OptionStep in build.zig with declarations to populate:

    const build_options = b.addOptions();
    build_options.addOption(bool, "enable_tracy", false);
    build_options.addOption(bool, "enable_tracy_callstack", false);
    build_options.addOption(bool, "enable_tracy_allocation", false);

  • add the options package to exe artifact: exe.addOptions("build_options", build_options);

  • NOTE: if a package requires build options, must manually add it to its dependencies

    const fooPkg = Pkg{
      .name = "foo",
      .path = FileSource{ .path = "foo.zig" },
      .dependencies = &[_]Pkg{

  • can provide user input for these in form of -Dname=value flags.

  • can get the value a user provided (or null if they didn't, so use orelse on anything you get from this) using Builder.option(type, name, description)

Run commands as build steps#

  • Use Builder.addSystemCommand() to get a step that runs your command
  • create a top level step using b.step()
  • make the top level step depend on your run step using top.dependOn(&run.step)
  • zig build --verbose: emits the actual command passed to zig build-exe/lib/obj with the compilation flags
  • zig build --verbose-link: will emit the linker flags passed to llvm

Generate documentation#

  • Use Builder.addTest() to get a step that will test your program that we will call test_doc
  • make it emit documentation using test_doc.emit_docs = true;
  • make it stop emitting binary files using test_doc.emit_bin = false
  • finally set the output directory to some folder using for example test_doc.output_dir = "docs"
  • create a top level step using b.step()
  • make that newly created step depends on documentation step using doc_step.dependOn(&test_doc.step)


Incrementally Porting C App Series#



  • representing a pending build and a DAG of all of its associated steps and their respective settings
  • build.zig:pub fn build(b: *Builder) void is responsible for adding the custom build logic for module to said Builder
  • invoking zig build does under the hood is building and running lib/std/special/build_runner.zig,
  • just a normal Zig application with pub fn main() and all the things you might already know from your actual project
  • build_runner imports your project's build.zig (it does this with a magic @import("@build"))
  • somewhere in its belly invokes your pub fn build(b: *Builder) on a Builder it created earlier
  • The very last thing it does is hand over to this Builder you got to modify using make()
  • the main workhorse is LibExeObjStep.make which spawns the actual zig compiler (e.g. zig build-exe/zig build-lib/zig cc) with the builder/step settings converted as command line args
  • code at src/main.zig

Compiler Stages#

Zig uses multiple compiler stages for bootstrapping the compiler:

  • zig0: is just the c++ compiler as a static library
  • only implements the backend for build-exe/obj etc
  • stage1: is the current compiler, written in C++, compiled with Clang
  • uses zig0 library to build pieces of stage2 in (subcommands like translate-c etc)
  • stage2: is the current project, written in Zig, compiled with stage1
  • stage3: is the fully self-hosted, stage2 code compiled with stage2
  • stage1 doesn't implement full optimizations so stage2 binary is not optimized
  • stage3 binary is optimized b/c stage2 implements optimizations/much better codegen

The core build graph coordinator. Main purpose:

  • Coordinate and execute Steps that describe different stages of a build
  • Provide default target and release mode for Steps
  • Provide build_options

the base node in the build DAG

  • two noteworthy properties:
  • makeFn: does the actual work which implementing this step entails
  • dependencies: an ArrayList of different Steps that must be executed before this one (though that isn't handled by Step itself)
  • you'll mostly use structs that wrap a bare Step
  • BuildExeObjStep: this is the big one that actually does all of the compiling work
  • LogStep: very simple step that writes something to stderr
  • RunStep: which runs a system command
  • these are usually constructed with one of many convenience methods on Builder like builder.addTranslateC(
  • to get a quick overview of them, grep for pub fn add while in the source file

main step capable of invoking the zig compiler on your sources and turning them into executables or shared objects/DLLs

  • usually constructed with one of Builders addX methods and then its myriad settings modified
  • finally call install() to create a build artifact in ./zig-cache/bin (this path is also adjustable using setOutputDir)
  • can also use a LibExeObjStep to run your tests as done in the default build.zig for libraries


defines project Build Targets

  • build.zig template exposes the full power of Zig's cross-compiling to the building user
  • use LibExeObjStep.setTarget(std.CrossTarget) to set targets
  • easiest way is calling it with std.CrossTarget.parse(std.CrossTarget.ParseOptions) to get a interface reminiscent of the -target CLI option
  • The ParseOptions struct is fairly well documented in the source.
  • Builder.standardTargetOptions() is convenience wrapper around std.CrossTarget.parse()

Compiler Internals#