Skip to content

Build Steps

A build file describes steps.

A build file describes steps.

A step is one action in the build graph. It may compile a program, install a file, run a command, generate source, or group other steps under a name.

The default build has an install step. This is why a simple project can be built with:

zig build

The command runs the default step, and that step depends on the artifacts that must be installed.

You can create your own step:

const check_step = b.step("check", "Check that the project builds");

The first string is the command name. The second string is the help text shown by:

zig build --help

A named step does nothing by itself. It becomes useful when other steps are attached to it.

check_step.dependOn(&exe.step);

Now this command builds the executable, but does not install it:

zig build check

The important operation is dependOn.

a.dependOn(b);

This means: before step a is complete, step b must be complete.

Build steps form a graph. The graph may branch and join.

For example, a project may have one step that runs tests and another that builds examples:

const test_step = b.step("test", "Run tests");
test_step.dependOn(&test_cmd.step);

const examples_step = b.step("examples", "Build examples");
examples_step.dependOn(&example_exe.step);

A third step may depend on both:

const ci_step = b.step("ci", "Run checks used by CI");
ci_step.dependOn(test_step);
ci_step.dependOn(examples_step);

Now:

zig build ci

runs the test step and the examples step.

A run step executes a compiled artifact:

const run_cmd = b.addRunArtifact(exe);

const run_step = b.step("run", "Run the program");
run_step.dependOn(&run_cmd.step);

Arguments can be passed to the program:

if (b.args) |args| {
    run_cmd.addArgs(args);
}

Then everything after -- is passed to the program:

zig build run -- input.txt output.txt

A system command can also be a step:

const fmt_cmd = b.addSystemCommand(&.{
    "zig",
    "fmt",
    "src",
});

Attach it to a named step:

const fmt_step = b.step("fmt", "Format source files");
fmt_step.dependOn(&fmt_cmd.step);

Now:

zig build fmt

runs the formatter.

Use system commands carefully. They depend on programs being present on the host machine. Prefer Zig build operations when the build system provides them.

A generated file is also represented by steps. A command produces a file. Another step uses that file.

const gen = b.addSystemCommand(&.{
    "python3",
    "tools/gen.py",
});

const generated = gen.addOutputFileArg("table.zig");

The generated path can be used as an input to a module:

const mod = b.createModule(.{
    .root_source_file = generated,
    .target = target,
    .optimize = optimize,
});

The dependency is now recorded. If the module needs the generated file, the generator runs first.

Build steps should be small. Each step should have one clear job.

Good steps are named after actions:

run
test
check
fmt
docs
examples
bench
ci

Avoid hiding unrelated work behind one vague step. A user should be able to ask the build system for exactly the action needed.

A build graph also documents the project. It says which artifacts exist, how they are made, and which commands are part of normal development.

Exercise 15-9. Add a check step that compiles the executable without installing it.

Exercise 15-10. Add a fmt step that formats the source directory.

Exercise 15-11. Add a ci step that depends on check, fmt, and test.

Exercise 15-12. Add argument forwarding to a run step.