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:
|
|
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.
|
|
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:
|
|
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.