Skip to content

`build.zig`

The file build.zig is a Zig program.

build.zig

The file build.zig is a Zig program.

It is not a configuration file. It is not a list of compiler flags. It is code that constructs a build graph.

A minimal file has one public function:

const std = @import("std");

pub fn build(b: *std.Build) void {
    _ = b;
}

The build system looks for this function:

pub fn build(b: *std.Build) void

The name must be build. The parameter is a pointer to std.Build.

The build context b owns the objects used during the build. It allocates build steps, stores options, resolves paths, and connects dependencies between steps.

A useful build.zig usually starts like this:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(exe);
}

The two standard options are important.

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

This adds the standard -Dtarget option to the build.

const optimize = b.standardOptimizeOption(.{});

This adds the standard -Doptimize option.

Together they allow ordinary command lines such as:

zig build
zig build -Doptimize=ReleaseSafe
zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos -Doptimize=ReleaseFast

The call to b.addExecutable creates a compile step. It does not immediately compile the program. It adds a node to the build graph.

The call to b.installArtifact(exe) adds another step. This step depends on the executable step. If the executable has not been built, it is built first.

The default zig build command runs the install step.

A build file can define named steps. A named step is a command the user can run.

const run_cmd = b.addRunArtifact(exe);

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

Now this command exists:

zig build run

The build graph is explicit. The run step depends on the run command. The run command depends on the executable. The executable depends on its source files and options.

Tests are added in the same style:

const tests = b.addTest(.{
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

const test_cmd = b.addRunArtifact(tests);

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

Now:

zig build test

builds and runs the tests.

The build file can also read project options.

const enable_logs = b.option(bool, "logs", "Enable log output") orelse false;

The user passes the option like this:

zig build -Dlogs=true

The value can be passed into the program as a compile-time option.

const options = b.addOptions();
options.addOption(bool, "enable_logs", enable_logs);

exe.root_module.addOptions("config", options);

Then source code can import it:

const config = @import("config");

pub fn main() void {
    if (config.enable_logs) {
        // logging code
    }
}

This keeps build-time decisions explicit. The program does not inspect hidden environment state. The build script decides what to expose.

Paths should normally be written through the build context:

b.path("src/main.zig")

This makes the path relative to the package root. It avoids depending on the current working directory.

A build.zig file should stay boring. It should describe how to build the project, not become a second application. Use helper functions when the project grows, but keep the graph visible.

A good build file answers these questions plainly:

What is being built?

Where is the root source file?

What target is used?

What optimization mode is used?

What steps can the user run?

What options can the user pass?

Exercise 15-5. Write a build.zig file for an executable named count.

Exercise 15-6. Add a run step.

Exercise 15-7. Add a boolean option named trace.

Exercise 15-8. Import the option from source code and print a message only when it is enabled.