zig build is the integrated build system that allows you to define projects using Zig code. If you have worked with C or C++, you are probably already familiar with Make and CMake.
You write your code, but then you have to write a Makefile with a cryptic syntax from the 70s. Or worse, you have to write a CMakeLists.txt with its own scripting language that seems designed to confuse. And if you want it to work on both Windows and Linux… good luck.
Zig proposes describing the build graph with its own API.
In Zig, there is no separate configuration language. The “script” that defines how your project is compiled is a program written in Zig.
This means you have:
- Autocompletion and static typing in your build script.
- Access to the entire standard library (manipulating files, running processes, etc.).
- A portable base; external libraries and tools still depend on the environment required by the project.
The build.zig file
When you create a new project with zig init, you will see a build.zig file generated in the root. This file is the entry point of the build system.
Its basic structure is a public build function that receives a std.Build object.
const std = @import("std");
pub fn build(b: *std.Build) void {
// Here we define HOW our project is built
}This b object is our conductor. Through it, we will create a graph of steps (compile, install, run tests, generate docs).
Anatomy of a standard build
Let’s break down a typical build.zig line by line.
const std = @import("std");
pub fn build(b: *std.Build) void {
// 1. Standard options (Target and Optimize)
// Allows the user to pass -Dtarget=... and -Doptimize=... via command line
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// 2. Define the executable
// We say: "I want to compile an exe named 'my-app' from 'src/main.zig'"
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
// 3. Install Step
// This says: "Copy the result to the zig-out/bin folder"
b.installArtifact(exe);
}User options (-D)
The standardTargetOptions and standardOptimizeOption lines save us a lot of work. Just by adding them, our script accepts powerful arguments from the terminal:
- Compile for Release:
zig build -Doptimize=ReleaseFast - Cross-compile for Windows from Linux:
zig build -Dtarget=x86_64-windows
Without configuring toolchains or strange environment variables! Zig handles everything.
Steps and dependencies
Zig’s build system is based on Steps. A step can depend on another.
For example, the zig build run command (which compiles and runs) does not appear from nowhere. It is a step that we define in the build.zig.
// ... (continuation of the previous code)
// 4. Create the run command
const run_cmd = b.addRunArtifact(exe);
// Propagate arguments written after -- to the program
if (b.args) |args| {
run_cmd.addArgs(args);
}
// 5. Expose the step to the user
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_cmd.step);Thanks to this, when we type zig build --help, we will see our run step documented.
Compiling C and C++ with zig build
zig build not only organizes Zig code; it can also compile C and C++ sources using the bundled toolchain.
If you have a mixed project (or purely C), you can use build.zig to manage it.
const exe_c = b.addExecutable(.{
.name = "app-in-c",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
// Add C source files
exe_c.root_module.addCSourceFile(.{
.file = b.path("src/main.c"),
.flags = &[_][]const u8{ "-Wall", "-Wextra" }, // GCC/Clang flags
});
b.installArtifact(exe_c);This will compile your C code using the integrated zig cc compiler, with all the caching and cross-compilation advantages of Zig.
Dependency management
Since version 0.11, Zig includes an official package manager. zig build integrates with a file called build.zig.zon (Zig Object Notation).
To use an external library:
- Declare it in
build.zig.zon. - In
build.zig, “invoke” it:
// Get the 'zap' dependency (a web server, for example)
const zap_dep = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
// Add the zap module to our executable
exe.root_module.addImport("zap", zap_dep.module("zap"));Zig fetches the declared dependency, verifies its hash, and incorporates it into the build graph. Already available dependencies remain in the local cache.