Zig Build System
tags: zig
Table of Contents
As the popularity of zig cc, more developers may realize Zig is not only a programming language, but also a toolchain to help maintaining C/C++/Zig project.
Today I want to discuss one vital part of this toolchain: Zig's build system, build.zig
the first non-trivial Zig program for newcomers.
Note: This post is based on Zig 0.11.0-dev.2560+602029bb2.
Step
When we initialize project scaffold via zig init-exe
, it will generate build.zig
with detailed comments to help us understand what it does. The most frequent used commands are:
zig build
, default stepinstall
is invokedzig build test
,test
step is invoked
Here we introduce the most important concept in Zig's build system: step
. A step describe one task, such as compile binary, run the test.
Step.zig defines MakeFn
as step's main interface:
pub const MakeFn = *const fn (self: *Step, prog_node: *std.Progress.Node) anyerror!void;
All other concrete steps implements it via composition with @fieldParentPtr
. If readers don't know this idioms, refer this post. The following are most common used steps:
CompileStep
, used to compile binary/static library/static libraryRunStep
, used to execute one programInstallArtifactStep
, used to copy build artifacts tozig-out
directory
std.Build
provides lots of convenient API to define build steps and their relation, such as:
addExecutable
define aCompileStep
for application binaryaddTest
define aCompileStep
for test binaryaddRunArtifact
define aRunStep
for oneCompileStep
Steps' relation is formed by Step.dependOn
function, and their relation construct a directed acyclic graph(DAG), which is used to drive the whole build process.
Figure above show a simple DAG of steps, steps at the top are special, and they can be invoked by zig build ${topLevelStep}
, Build.step
function creates such top level steps.
After explanation above, readers should have a deeper understand what build.zig
does.
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_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
How build systems run
Different with Rust, there is no rustc-like zigc
to compile build.zig
, then how does build.zig
works?
The answer is sub-process. When you build a fresh project, zig build
will first compile build.zig
to a binary, then run this binary to execute specific chosen steps.
The build binary sub-process to other Zig sub-commands to finish their jobs, such as zig build-exe
for CompileStep
.
build_runner.zig contains the main entry for the build binary, build.zig
is imported using const root = @import("@build")
, it requires four arguments when start:
- Zig executable
- directory where
build.zig
locates - Local cache directory(optional)
- Global cache directory(optional)
We can manually invoke the build binary like this:
$ find . -name build
./zig-cache/o/0c7d271c608c27670403b31835132c7d/build
$ ./zig-cache/o/0c7d271c608c27670403b31835132c7d/build $(which zig) $(pwd) .cache .glocal-cache
$ zig-out/bin/demo
All your codebase are belong to us.
Run `zig build test` to run the tests.
So when you have bugs in build.zig
, you first need to find where the build binary is, then run it in gdb/lldb with args above.
Conclusion
A fully featured build system is necessary to keep developers productive, but flexibility come at a cost of misuse, so design a good build system is hard.
Zig's build system is based on the language itself, developers don't need to learn a new language to write their build(CMake, I'm looking at you!). And Zig provides a declarative API to construct steps' DAG, lots of pre-defined steps to finish common task, powerful as will as practical.
Besides build system, package manager in Zig is in rapid development. With those, developer can build old/complex C/C++ projects in just one command: zig build
. Some early adopters:
- pcre2, Perl-Compatible Regular Expressions
- llama.cpp, Port of Facebook's LLaMA model in C/C++
- raylib, A simple and easy-to-use library to enjoy videogames programming
Discussions on Lobsters and Reddit, you can also email me to share your thoughts.