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 stepinstallis invokedzig build test,teststep 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:
1pub 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-outdirectory
std.Build provides lots of convenient API to define build steps and their relation, such as:
addExecutabledefine aCompileStepfor application binaryaddTestdefine aCompileStepfor test binaryaddRunArtifactdefine aRunStepfor 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.
1const std = @import("std");
2
3pub fn build(b: *std.Build) void {
4 const target = b.standardTargetOptions(.{});
5 const optimize = b.standardOptimizeOption(.{});
6 const exe = b.addExecutable(.{
7 .name = "demo",
8 .root_source_file = .{ .path = "src/main.zig" },
9 .target = target,
10 .optimize = optimize,
11 });
12 b.installArtifact(exe);
13 const run_cmd = b.addRunArtifact(exe);
14 run_cmd.step.dependOn(b.getInstallStep());
15 if (b.args) |args| {
16 run_cmd.addArgs(args);
17 }
18 const run_step = b.step("run", "Run the app");
19 run_step.dependOn(&run_cmd.step);
20 const unit_tests = b.addTest(.{
21 .root_source_file = .{ .path = "src/main.zig" },
22 .target = target,
23 .optimize = optimize,
24 });
25 const run_unit_tests = b.addRunArtifact(unit_tests);
26 const test_step = b.step("test", "Run unit tests");
27 test_step.dependOn(&run_unit_tests.step);
28}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.ziglocates - Local cache directory(optional)
- Global cache directory(optional)
We can manually invoke the build binary like this:
1$ find . -name build
2./zig-cache/o/0c7d271c608c27670403b31835132c7d/build
3$ ./zig-cache/o/0c7d271c608c27670403b31835132c7d/build $(which zig) $(pwd) .cache .glocal-cache
4$ zig-out/bin/demo
5All your codebase are belong to us.
6Run `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.