Introduction to build.zig
Zig has a built-in build system.
That means you do not need a separate tool like Make, CMake, Ninja, or a shell script for ordinary Zig projects. You can describe your build directly in Zig code.
The main file is usually named:
build.zigThis file tells Zig how to build your program, how to run it, how to test it, what dependencies it needs, and what options should be passed to the compiler.
A small Zig project often looks like this:
hello/
build.zig
src/
main.zigThe source code lives in src/main.zig. The build instructions live in build.zig.
You build the project with:
zig buildThat command reads build.zig, creates the build graph, compiles the program, and places the output in Zig’s build cache or install directory.
Why Zig Has Its Own Build System
Many languages separate the programming language from the build language.
For example, a C project might use C for the program, but Make, CMake, Meson, or another language for the build.
Zig takes a different approach: the build file itself is Zig code.
That gives you one language for both your program and your build logic. You can use variables, functions, conditionals, loops, structs, and standard library APIs inside the build file.
A build file can stay simple for small projects, but it can also grow into a precise build description for larger systems.
A Minimal build.zig
Here is a simple build.zig file:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
}This looks more complex than a single compiler command, but each part has a clear job.
const std = @import("std");This imports the standard library.
pub fn build(b: *std.Build) void {Every build.zig file defines a public function named build. Zig calls this function when you run:
zig buildThe parameter b is a pointer to a std.Build object. You use it to describe what the project should build.
Target and Optimization Options
These two lines are common in Zig build files:
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});The first line lets the user choose the target platform.
For example, the same project can be built for your current machine, Linux, Windows, macOS, WebAssembly, ARM, or another supported target.
The second line lets the user choose the optimization mode.
Common modes include debug builds and release builds. A debug build is better while developing. A release build is better when you care about speed or small binary size.
This means your build file supports commands such as:
zig build
zig build -Doptimize=ReleaseFast
zig build -Dtarget=x86_64-linuxYou do not need to hard-code everything.
Adding an Executable
This part creates an executable build step:
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});The name of the output program is:
.name = "hello"The main source file is:
.root_source_file = b.path("src/main.zig")The target and optimization settings come from the options we defined earlier.
So this code says: build an executable named hello from src/main.zig, using the selected target and optimization mode.
Installing the Artifact
This line tells Zig to install the executable:
b.installArtifact(exe);An artifact is something produced by the build, such as an executable, static library, dynamic library, or generated file.
When you run:
zig buildZig builds the executable and installs it into the project’s build output directory.
You can usually find installed outputs under:
zig-out/For example:
zig-out/
bin/
helloOn Windows, the file may be:
zig-out/bin/hello.exezig build vs zig build-exe
Earlier, we used:
zig build-exe src/main.zigThat command directly compiles one source file into an executable.
For very small experiments, this is fine.
But for real projects, zig build is usually better.
With zig build, you can describe the whole project in one place: executables, libraries, tests, dependencies, build options, generated files, install rules, and custom commands.
A simple rule is:
Use zig build-exe when learning or testing one file.
Use zig build when making a project.
Build Files Are Programs
A build.zig file is not just configuration. It is actual Zig code.
That means you can write helper functions:
fn addCommonOptions(module: *std.Build.Module) void {
_ = module;
}You can use conditionals:
if (optimize == .Debug) {
// debug-only build setup
}You can create several artifacts:
const server = b.addExecutable(.{
.name = "server",
.root_module = b.createModule(.{
.root_source_file = b.path("src/server.zig"),
.target = target,
.optimize = optimize,
}),
});
const client = b.addExecutable(.{
.name = "client",
.root_module = b.createModule(.{
.root_source_file = b.path("src/client.zig"),
.target = target,
.optimize = optimize,
}),
});This makes the build system flexible without introducing a separate build language.
Build Steps
The Zig build system is based on steps.
A step is an action in the build process. Examples include:
compile this executable
run this executable
run these tests
install this artifact
generate this fileWhen you run zig build, Zig executes the default build steps.
You can also define named steps. For example, many projects define a run step:
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);Then you can run:
zig build runThis builds the executable and runs it.
That is useful during development because you do not need to manually find the compiled binary.
A Build File with a Run Step
Here is a slightly larger example:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the program");
run_step.dependOn(&run_cmd.step);
}Now the project supports:
zig build
zig build runThe first command builds the program.
The second command builds and runs the program.
Adding Tests
A project often needs a test step too.
const exe_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);Now you can run:
zig build testThis gives your project a standard test command.
The build file controls how tests are compiled and executed.
The Important Idea
The purpose of build.zig is to describe the shape of your project.
It answers questions like:
Where is the main source file?
What should the output binary be called?
Which target should this build use?
Which optimization mode should this build use?
What command should run the program?
What command should run the tests?
What files should be installed?
What dependencies should be included?
At first, build.zig may look like extra work. But as a project grows, it becomes one of the most important files in the repository.
It is the place where the project becomes reproducible. A new developer can clone the project and run:
zig buildThe build file tells Zig the rest.