Zig 构建系统
何时使用 Zig 构建系统?
基本的命令 zig build-exe、zig build-lib、zig build-obj 和 zig test 通常已经足够。然而,有时项目需要另一层抽象来管理从源代码构建的复杂性。
例如,可能存在以下情况之一:
- 命令行变得过长且难以操作,你希望有一个地方可以将其写下来。
- 你想要构建许多东西,或者构建过程包含许多步骤。
- 你想要利用并发和缓存来减少构建时间。
- 你想要为项目提供配置选项。
- 构建过程根据目标系统和其他选项的不同而有所区别。
- 你的项目依赖于其他项目。
- 你想要避免对 cmake、make、shell、msvc、python 等不必要的依赖,使项目对更多贡献者开放。
- 你想要提供一个包供第三方使用。
- 你想要为工具(如 IDE)提供一个标准化的方式,以便它们能够从语义上理解如何构建项目。
如果符合以上任何一种情况,采用 Zig 构建系统将大有裨益。
开始使用
简单的可执行文件
这个构建脚本负责将一个包含对外可见 main 函数的 Zig 文件编译成可执行文件。
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("hello.zig"),
.target = b.graph.host,
}),
});
b.installArtifact(exe);
}$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install hello success
+- compile exe hello Debug native success 1s MaxRSS:141M
安装构建产物
Zig 构建系统与其他大多数构建系统一样,将项目视为一系列步骤,这些步骤构成一个有向无环图(DAG),每个步骤都可以独立且并发地执行。
默认情况下,图中的主要步骤是 Install,其作用是将构建产物复制到它们的最终存放位置。Install 步骤开始时没有依赖,因此当执行 zig build 命令时,不会发生任何操作。项目的构建脚本必须添加到要安装的物品清单中,这正是上面提到的 installArtifact 函数调用所执行的操作。
Output
├── build.zig
├── hello.zig
├── .zig-cache
└── zig-out
└── bin
└── hello
输出结果中包含了两个生成的目录:.zig-cache 和 zig-out。第一个目录包含的文件可以加快后续构建的速度,但这些文件不应该被提交到源代码控制系统中,并且这个目录可以在任何时候完全删除,而不会产生任何影响。
第二个目录,zig-out,用作“安装路径”(installation prefix)。它对应于标准的文件系统层次结构概念。这个目录不是由项目选择的,而是由使用 zig build 的用户通过 --prefix 参数(简写为 -p)来指定的。
作为项目维护者,你决定将哪些内容放入这个目录,但用户选择在他们的系统中安装到哪个位置。构建脚本不应将输出路径直接编写在代码中,因为这样做会破坏缓存、并发性和组合性,同时也会让最终用户感到不便。
为运行应用程序添加一个便捷步骤
通常,我们会添加一个 Run 步骤,以便可以直接从构建命令中运行主应用程序。
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("hello.zig"),
.target = b.graph.host,
}),
});
b.installArtifact(exe);
const run_exe = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_exe.step);
}$ zig buildrun --summary all
Hello World!
Build Summary: 3/3 steps succeeded
run success
+- run exe hello success 871us
+- compile exe hello Debug native success 923ms MaxRSS:143M
基础
用户提供的选项
使用 b.option 来配置构建脚本,便于最终用户和其他依赖此项目的包进行调整。
const std = @import("std");
pub fn build(b: *std.Build) void {
const windows = b.option(bool, "windows", "Target Microsoft Windows") orelse false;
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("example.zig"),
.target = b.resolveTargetQuery(.{
.os_tag = if (windows) .windows else null,
}),
}),
});
b.installArtifact(exe);
}$ zig build--help
Usage: /home/ci/deps/zig-x86_64-linux-0.16.0/zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
System Integration Options:
--search-prefix [path] Add a path to look for binaries, libraries, headers
--sysroot [path] Set the system root directory (usually /)
--libc [file] Provide a file which specifies libc paths
--system [pkgdir] Disable package fetching; enable all integrations
-fsys=[name] Enable a system integration
-fno-sys=[name] Disable a system integration
-fdarling, -fno-darling Integration with system-installed Darling to
execute macOS programs on Linux hosts
(default: no)
-fqemu, -fno-qemu Integration with system-installed QEMU to execute
foreign-architecture programs on Linux hosts
(default: no)
--libc-runtimes [path] Enhances QEMU integration by providing dynamic libc
(e.g. glibc or musl) built for multiple foreign
architectures, allowing execution of non-native
programs that link with libc.
-frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
ARM64 macOS hosts. (default: no)
-fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
execute WASI binaries. (default: no)
-fwine, -fno-wine Integration with system-installed Wine to execute
Windows programs on Linux hosts. (default: no)
Available System Integrations: Enabled:
(none) -
General Options:
-h, --help Print this help and exit
-l, --list-steps Print available steps
-p, --prefix [path] Where to install files (default: zig-out)
--prefix-lib-dir [path] Where to install libraries
--prefix-exe-dir [path] Where to install executables
--prefix-include-dir [path] Where to install C header files
--release[=mode] Request release mode, optionally specifying a
preferred optimization mode: fast, safe, small
--verbose Print commands before executing them
--color [auto|off|on] Enable or disable colored error messages
--error-style [style] Control how build errors are printed
verbose (Default) Report errors with full context
minimal Report errors after summary, excluding context like command lines
verbose_clear Like 'verbose', but clear the terminal at the start of each update
minimal_clear Like 'minimal', but clear the terminal at the start of each update
--multiline-errors [style] Control how multi-line error messages are printed
indent (Default) Indent non-initial lines to align with initial line
newline Include a leading newline so that the error message is on its own lines
none Print as usual so the first line is misaligned
--summary [mode] Control the printing of the build summary
all Print the build summary in its entirety
new Omit cached steps
failures (Default if short-lived) Only print failed steps
line (Default if long-lived) Only print the single-line summary
none Do not print the build summary
-j<N> Limit concurrent jobs (default is to use all CPU cores)
--maxrss <bytes> Limit memory usage (default is to use available memory)
--skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
--test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
The timeout must include a unit: ns, us, ms, s, m, h
--watch Continuously rebuild when source files are modified
--debounce <ms> Delay before rebuilding after changed file detected
--webui[=ip] Enable the web interface on the given IP address
--fuzz[=limit] Continuously search for unit test failures with an optional
limit to the max number of iterations. The argument supports
an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
'--webui' when no limit is specified.
--time-report Force full rebuild and provide detailed information on
compilation time of Zig source code (implies '--webui')
-fincremental Enable incremental compilation
-fno-incremental Disable incremental compilation
Package Management Options:
--fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit
needed (Default) Lazy dependencies are fetched as needed
all Lazy dependencies are always fetched
--fork=[path] Override one or more projects from dependency tree
Advanced Options:
-freference-trace[=num] How many lines of reference trace should be shown per compile error
-fno-reference-trace Disable reference trace
-fallow-so-scripts Allows .so files to be GNU ld scripts
-fno-allow-so-scripts (default) .so files must be ELF files
--build-file [file] Override path to build.zig
--cache-dir [path] Override path to local Zig cache directory
--global-cache-dir [path] Override path to global Zig cache directory
--zig-lib-dir [arg] Override path to Zig lib directory
--build-runner [file] Override path to build runner
--seed [integer] For shuffling dependency traversal order (default: random)
--build-id[=style] At a minor link-time expense, embeds a build ID in binaries
fast 8-byte non-cryptographic hash (COFF, ELF, WASM)
sha1, tree 20-byte cryptographic hash (ELF, WASM)
md5 16-byte cryptographic hash (ELF)
uuid 16-byte random UUID (ELF, WASM)
0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM)
none (default) No build ID
--debug-log [scope] Enable debugging the compiler
--debug-pkg-config Fail if unknown pkg-config flags encountered
--debug-rt Debug compiler runtime libraries
--verbose-link Enable compiler debug output for linking
--verbose-air Enable compiler debug output for Zig AIR
--verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
--verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
--verbose-cimport Enable compiler debug output for C imports
--verbose-cc Enable compiler debug output for C compilation
--verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
请关注这几行:
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
帮助菜单的这一部分是在运行 build.zig 逻辑时自动生成的。用户可以通过这种方式发现构建脚本的配置选项。
标准配置选项
之前我们使用一个布尔选项来表示为 Windows 构建。然而,我们可以做得更好。
大多数项目都希望提供更改目标和优化设置的能力。为了鼓励这些选项的标准命名约定,Zig 提供了辅助函数 standardTargetOptions 和 standardOptimizeOption。
standardTargetOptions 使得执行 zig build 命令的用户能够选择他们想要构建的目标平台。默认情况下,脚本将允许构建任何目标平台,如果不做选择,则默认为目标宿主系统。此外,还提供了选项来限制支持的目标平台集合。
standardOptimizeOption 为执行 zig build 命令的用户提供了在 Debug、ReleaseSafe、ReleaseFast 和 ReleaseSmall 之间进行选择的机会。在默认情况下,构建脚本不会自动选择任何发布模式作为首选设置,因此用户需要明确选择一个优化级别,以便生成一个发布版本的构建。
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}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("hello.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
}$ zig build-Dtarget=x86_64-windows -Doptimize=ReleaseSmall --summary all
Build Summary: 3/3 steps succeeded
install success
+- install hello success
+- compile exe hello ReleaseSmall x86_64-windows success 3s MaxRSS:161M
现在,我们的帮助菜单包含了更多的项目:
Project-Specific Options:
-Dtarget=[string] The CPU architecture, OS, and ABI to build for
-Dcpu=[string] Target CPU features to add or subtract
-Doptimize=[enum] Prioritize performance, safety, or binary size (-O flag)
Supported Values:
Debug
ReleaseSafe
ReleaseFast
ReleaseSmall
完全可以直接通过 b.option 创建这些选项,但这个 API 为这些常用设置提供了一种常用的命名约定。
在我们的终端输出中,请注意我们传递了 -Dtarget=x86_64-windows -Doptimize=ReleaseSmall。与第一个示例相比,现在我们在安装路径中看到了不同的文件:
zig-out/
└── bin
└── hello.exe
用于条件编译的选项
要从构建脚本传递选项到项目的 Zig 代码中,请使用 Options 步骤。
const std = @import("std");
const config = @import("config");
const semver = std.SemanticVersion.parse(config.version) catch unreachable;
extern fn foo_bar() void;
pub fn main() !void {
if (semver.major < 1) {
@compileError("too old");
}
std.debug.print("version: {s}\n", .{config.version});
if (config.have_libfoo) {
foo_bar();
}
}const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("app.zig"),
.target = b.graph.host,
}),
});
const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
const enable_foo = detectWhetherToEnableLibFoo();
const options = b.addOptions();
options.addOption([]const u8, "version", version);
options.addOption(bool, "have_libfoo", enable_foo);
exe.root_module.addOptions("config", options);
b.installArtifact(exe);
}
fn detectWhetherToEnableLibFoo() bool {
return false;
}$ zig build-Dversion=1.2.3 --summary all
Build Summary: 4/4 steps succeeded
install success
+- install app success
+- compile exe app Debug native success 1s MaxRSS:143M
+- options cached
在这个例子中,由 @import(“config”) 提供的数据是编译时已知的,这阻止了触发 @compileError 语句。如果我们传递了 -Dversion=0.2.3 或者省略了这个选项,那么我们将会看到 app.zig 的编译因为“太旧”的错误而失败。
静态库
这个构建脚本从 Zig 代码创建了一个静态库,并且还创建了一个可执行文件,这个可执行文件依赖于这个静态库。
export fn fizzbuzz(n: usize) ?[*:0]const u8 {
if (n % 5 == 0) {
if (n % 3 == 0) {
return "fizzbuzz";
} else {
return "fizz";
}
} else if (n % 3 == 0) {
return "buzz";
}
return null;
}const std = @import("std");
const Io = std.Io;
extern fn fizzbuzz(n: usize) ?[*:0]const u8;
pub fn main(init: std.process.Init) !void {
const io = init.io;
var buf: [1024]u8 = undefined;
const file_writer = Io.File.stdout().writer(io, &buf);
const w = &file_writer.interface;
for (0..100) |n| {
if (fizzbuzz(n)) |s| {
try w.print("{s}\n", .{s});
} else {
try w.print("{d}\n", .{n});
}
}
try w.flush();
}const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const libfizzbuzz = b.addLibrary(.{
.name = "fizzbuzz",
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
}),
});
const exe = b.addExecutable(.{
.name = "demo",
.root_module = b.createModule(.{
.root_source_file = b.path("demo.zig"),
.target = target,
.optimize = optimize,
}),
});
exe.root_module.linkLibrary(libfizzbuzz);
b.installArtifact(libfizzbuzz);
if (b.option(bool, "enable-demo", "install the demo too") orelse false) {
b.installArtifact(exe);
}
}$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install fizzbuzz success
+- compile lib fizzbuzz Debug native success 186ms MaxRSS:94M
在这种情况下,最终只有静态库被安装:
zig-out/
└── lib
└── libfizzbuzz.a
然而,如果你仔细观察,构建脚本中包含了一个选项,也可以安装 demo 程序。如果我们额外传递 -Denable-demo 参数,那么我们会在安装路径中看到它:
zig-out/
├── bin
│ └── demo
└── lib
└── libfizzbuzz.a
请注意,尽管无条件地调用了 addExecutable,但实际上,除非我们明确使用 -Denable-demo 参数,构建系统是不会去构建 demo 程序的。这是因为构建系统是根据一个有向无环图来工作的,这个图记录了不同部分之间的依赖关系。
动态库
在这里,我们使用了之前静态库示例中的所有代码文件,但是对 build.zig 文件进行了修改。
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const libfizzbuzz = b.addLibrary(.{
.name = "fizzbuzz",
.linkage = .dynamic,
.version = .{ .major = 1, .minor = 2, .patch = 3 },
.root_module = b.createModule(.{
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(libfizzbuzz);
}$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install fizzbuzz success
+- compile lib fizzbuzz Debug native success 887ms MaxRSS:143M
Output
zig-out
└── lib
├── libfizzbuzz.so -> libfizzbuzz.so.1
├── libfizzbuzz.so.1 -> libfizzbuzz.so.1.2.3
└── libfizzbuzz.so.1.2.3
就像在静态库示例中一样,为了让一个可执行文件链接到这个动态库,你需要添加类似这样的代码:
exe.linkLibrary(libfizzbuzz);
测试
单个文件可以直接使用 zig test foo.zig 命令进行测试,然而,更复杂的使用场景可以通过构建脚本来解决。
当使用构建脚本时,单元测试在构建图中被分为两个不同的步骤:Compile 步骤和 Run 步骤。如果没有调用 addRunArtifact 来在这两个步骤之间建立依赖关系,单元测试将不会被执行。
Compile 步骤可以像配置任何可执行文件、库或对象文件一样进行配置,例如通过链接系统库、设置目标选项或添加额外的编译单元。
Run 步骤可以像配置其他 Run 步骤一样进行配置,例如,当主机无法执行二进制文件时,可以跳过执行。
当使用构建系统来运行单元测试时,构建运行器(build runner)和测试运行器(test runner)通过标准输入和标准输出进行通信,以便并发运行多个单元测试套件,并以一种有意义的方式报告测试失败,而不会使它们的输出混淆在一起。这就是为什么在单元测试中向标准输出写数据会是个问题——因为这样会干扰这个通信通道。另一方面,这种机制将启用一个即将推出的功能,即单元测试能够预测 panic。
const std = @import("std");
test "simple test" {
var list = std.ArrayList(i32).init(std.testing.allocator);
defer list.deinit();
try list.append(42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}const std = @import("std");
const test_targets = [_]std.Target.Query{
.{}, // native
.{
.cpu_arch = .x86_64,
.os_tag = .linux,
},
.{
.cpu_arch = .aarch64,
.os_tag = .macos,
},
};
pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
for (test_targets) |target| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("main.zig"),
.target = b.resolveTargetQuery(target),
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
test_step.dependOn(&run_unit_tests.step);
}
}$ zig buildtest --summary all
test
+- run test
+- compile test Debug x86_64-linux 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
var list = std.ArrayList(i32).init(std.testing.allocator);
~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
return struct {
^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -target x86_64-linux -mcpu baseline -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-
test
+- run test
+- compile test Debug aarch64-macos 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
var list = std.ArrayList(i32).init(std.testing.allocator);
~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
return struct {
^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -target aarch64-macos -mcpu baseline -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-
test
+- run test
+- compile test Debug native 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
var list = std.ArrayList(i32).init(std.testing.allocator);
~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
return struct {
^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-
Build Summary: 0/7 steps succeeded (3 failed)
test transitive failure
+- run test transitive failure
| +- compile test Debug native 1 errors
+- run test transitive failure
| +- compile test Debug x86_64-linux 1 errors
+- run test transitive failure
+- compile test Debug aarch64-macos 1 errors
error: the following build command failed with exit code 1:
/home/ci/cache/o/a526b193363be5f6ad52dd400fe5f988/build /home/ci/deps/zig-x86_64-linux-0.16.0/zig /home/ci/deps/zig-x86_64-linux-0.16.0/lib ../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing /home/ci/cache /home/ci/cache --seed 0xb4c49b02 -Z3dab62a97b183898 test --summary all
在这种情况下,为单元测试开启 skip_foreign_checks 可能是个好主意:
@@ -23,6 +23,7 @@
});
const run_unit_tests = b.addRunArtifact(unit_tests);
+ run_unit_tests.skip_foreign_checks = true;
test_step.dependOn(&run_unit_tests.step);
}
}
const std = @import("std");
const test_targets = [_]std.Target.Query{
.{}, // native
.{
.cpu_arch = .x86_64,
.os_tag = .linux,
},
.{
.cpu_arch = .aarch64,
.os_tag = .macos,
},
};
pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
for (test_targets) |target| {
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("main.zig"),
.target = b.resolveTargetQuery(target),
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
run_unit_tests.skip_foreign_checks = true;
test_step.dependOn(&run_unit_tests.step);
}
}
// zig-doctest: build-system --collapseable -- test --summary all$ zig build--summary all
Build Summary: 1/1 steps succeeded
install cached
链接系统库
为了满足库依赖,有两个选择:
- 通过 Zig 构建系统提供这些库(参见包管理和静态库)。
- 使用宿主系统提供的文件。
对于上游项目维护者的使用场景,通过 Zig 构建系统获取这些库可以减少很多麻烦,并将配置能力掌握在维护者手中。像这样构建,每个人都会得到可复现、一致的结果,并且它可以在每个操作系统上工作,甚至支持交叉编译。此外,它允许项目精确决定其整个依赖树中希望构建的每个库的确切版本。这被认为是依赖外部库的普遍首选方式。
然而,对于将软件打包到诸如 Debian、Homebrew 或 Nix 等仓库的使用场景,必须链接到系统库。因此,构建脚本必须能检测模式并相应地配置。
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "zip",
.root_module = b.createModule(.{
.root_source_file = b.path("zip.zig"),
.target = b.graph.host,
.link_libc = true,
}),
});
exe.root_module.linkSystemLibrary("z", .{});
b.installArtifact(exe);
}$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install zip success
+- compile exe zip Debug native success 820ms MaxRSS:144M
使用 zig build 的用户可以使用 --search-prefix 参数来提供额外的目录,这些目录将被视为“系统目录”,用于查找静态库和动态库。
生成文件
运行系统工具
这个版本的 “hello world” 期望在相同的路径下找到一个 word.txt 文件,并且我们希望使用一个系统工具从 JSON 文件生成它。
请注意,系统依赖项会使你的项目对用户来说更难构建。例如,这个构建脚本依赖于 jq,而在大多数 Linux 发行版中默认并不包含它,对于 Windows 用户来说可能也不熟悉这个工具。
下一节将使用源代码中包含的 Zig 工具替换 jq,这种方法是更推荐的。
words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
const Io = std.Io;
pub fn main(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const self_exe_dir_path = try std.process.executableDirPathAlloc(io, arena);
var self_exe_dir = try Io.Dir.cwd().openDir(io, self_exe_dir_path, .{});
defer self_exe_dir.close(io);
const word = try self_exe_dir.readFileAlloc(io, "word.txt", arena, .limited(1000));
var stdout_buffer: [1000]u8 = undefined;
var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
const stdout = &stdout_writer.interface;
try stdout.print("Hello {s}\n", .{word});
try stdout.flush();
}const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool_run = b.addSystemCommand(&.{"jq"});
tool_run.addArgs(&.{
b.fmt(
\\.["{s}"]
, .{lang}),
"-r", // raw output to omit quotes around the selected string
});
tool_run.addFileArg(b.path("words.json"));
const output = tool_run.captureStdOut(.{});
b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);
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,
}),
});
const install_artifact = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = .prefix },
});
b.getInstallStep().dependOn(&install_artifact.step);
}$ zig build-Dlanguage=ja --summary all
Build Summary: 5/5 steps succeeded
install success
+- install generated to word.txt success
| +- run jq cached
+- install hello success
+- compile exe hello Debug native success 693ms MaxRSS:142M
Output
zig-out
├── hello
└── word.txt
注意 captureStdOut 如何创建一个临时文件来存储 jq 命令的输出。
运行项目工具
这个版本的 “hello world” 期望在相同的路径下找到一个 word.txt 文件。我们计划在构建过程中,通过运行一个 Zig 程序来处理 JSON 文件并生成目标文件。
tools/words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
const Io = std.Io;
pub fn main(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const self_exe_dir_path = try std.process.executableDirPathAlloc(io, arena);
var self_exe_dir = try Io.Dir.cwd().openDir(io, self_exe_dir_path, .{});
defer self_exe_dir.close(io);
const word = try self_exe_dir.readFileAlloc(io, "word.txt", arena, .limited(1000));
var stdout_buffer: [1000]u8 = undefined;
var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
const stdout = &stdout_writer.interface;
try stdout.print("Hello {s}\n", .{word});
try stdout.flush();
}
const std = @import("std");
const Io = std.Io;
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
pub fn main(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);
var opt_input_file_path: ?[]const u8 = null;
var opt_output_file_path: ?[]const u8 = null;
var opt_lang: ?[]const u8 = null;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try Io.File.stdout().writeStreamingAll(io, usage);
return std.process.cleanExit(io);
} else if (std.mem.eql(u8, "--input-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_input_file_path = args[i];
} else if (std.mem.eql(u8, "--output-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_output_file_path = args[i];
} else if (std.mem.eql(u8, "--lang", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
opt_lang = args[i];
} else {
fatal("unrecognized arg: '{s}'", .{arg});
}
}
}
const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
const lang = opt_lang orelse fatal("missing --lang", .{});
var input_file = Io.Dir.cwd().openFile(io, input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close(io);
var input_file_buffer: [1000]u8 = undefined;
var input_file_reader = input_file.reader(io, &input_file_buffer);
var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close(io);
var json_reader: std.json.Reader = .init(arena, &input_file_reader.interface);
var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
.allocate = .alloc_if_needed,
.max_value_len = 1000,
});
const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});
try output_file.writeStreamingAll(io, w);
return std.process.cleanExit(io);
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool = b.addExecutable(.{
.name = "word_select",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/word_select.zig"),
.target = b.graph.host,
}),
});
const tool_step = b.addRunArtifact(tool);
tool_step.addArg("--input-file");
tool_step.addFileArg(b.path("tools/words.json"));
tool_step.addArg("--output-file");
const output = tool_step.addOutputFileArg("word.txt");
tool_step.addArgs(&.{ "--lang", lang });
b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);
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,
}),
});
const install_artifact = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = .prefix },
});
b.getInstallStep().dependOn(&install_artifact.step);
}$ zig build--summary all
Build Summary: 6/6 steps succeeded
install success
+- install generated to word.txt success
| +- run exe word_select (word.txt) success 10ms
| +- compile exe word_select Debug native success 1s MaxRSS:150M
+- install hello success
+- compile exe hello Debug native success 1s MaxRSS:142M
Output
zig-out
├── hello
└── word.txt
为 @embedFile 生成文件
这个版本的 “hello world” 希望将构建时生成的文件通过 @embedFile 进行嵌入,我们将使用一个用 Zig 编写的工具来生成这个文件。
tools/words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
const word = @embedFile("word");
pub fn main() !void {
std.log.info("Hello {s}\n", .{word});
}
const std = @import("std");
const Io = std.Io;
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
pub fn main(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);
var opt_input_file_path: ?[]const u8 = null;
var opt_output_file_path: ?[]const u8 = null;
var opt_lang: ?[]const u8 = null;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try Io.File.stdout().writeStreamingAll(io, usage);
return std.process.cleanExit(io);
} else if (std.mem.eql(u8, "--input-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_input_file_path = args[i];
} else if (std.mem.eql(u8, "--output-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_output_file_path = args[i];
} else if (std.mem.eql(u8, "--lang", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
opt_lang = args[i];
} else {
fatal("unrecognized arg: '{s}'", .{arg});
}
}
}
const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
const lang = opt_lang orelse fatal("missing --lang", .{});
var input_file = Io.Dir.cwd().openFile(io, input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close(io);
var input_file_buffer: [1000]u8 = undefined;
var input_file_reader = input_file.reader(io, &input_file_buffer);
var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close(io);
var json_reader: std.json.Reader = .init(arena, &input_file_reader.interface);
var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
.allocate = .alloc_if_needed,
.max_value_len = 1000,
});
const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});
try output_file.writeStreamingAll(io, w);
return std.process.cleanExit(io);
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool = b.addExecutable(.{
.name = "word_select",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/word_select.zig"),
.target = b.graph.host,
}),
});
const tool_step = b.addRunArtifact(tool);
tool_step.addArg("--input-file");
tool_step.addFileArg(b.path("tools/words.json"));
tool_step.addArg("--output-file");
const output = tool_step.addOutputFileArg("word.txt");
tool_step.addArgs(&.{ "--lang", lang });
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,
}),
});
exe.root_module.addAnonymousImport("word", .{
.root_source_file = output,
});
b.installArtifact(exe);
}$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install hello success
+- compile exe hello Debug native success 652ms MaxRSS:145M
+- run exe word_select (word.txt) success 10ms
+- compile exe word_select Debug native success 1s MaxRSS:149M
Output
zig-out/
└── bin
└── hello
生成 Zig 源代码
这个构建脚本使用一个 Zig 程序来生成一个 Zig 源文件,并将其作为模块提供给主程序。
const std = @import("std");
const Person = @import("person").Person;
pub fn main() !void {
const p: Person = .{};
std.log.info("Hello {any}\n", .{p});
}const std = @import("std");
const Io = std.Io;
pub fn main(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);
if (args.len != 2) fatal("wrong number of arguments", .{});
const output_file_path = args[1];
var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close(io);
try output_file.writeStreamingAll(io,
\\pub const Person = struct {
\\ age: usize = 18,
\\ name: []const u8 = "foo"
\\};
);
return std.process.cleanExit(io);
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}const std = @import("std");
pub fn build(b: *std.Build) void {
const tool = b.addExecutable(.{
.name = "generate_struct",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/generate_struct.zig"),
.target = b.graph.host,
}),
});
const tool_step = b.addRunArtifact(tool);
const output = tool_step.addOutputFileArg("person.zig");
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,
}),
});
exe.root_module.addAnonymousImport("person", .{
.root_source_file = output,
});
b.installArtifact(exe);
}$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install hello success
+- compile exe hello Debug native success 791ms MaxRSS:141M
+- run exe generate_struct (person.zig) success 8ms
+- compile exe generate_struct Debug native success 715ms MaxRSS:148M
Output
zig-out/
└── bin
└── hello
处理一个或多个生成的文件
WriteFiles 步骤提供了一种方法,用于生成同一父目录下一个或多个文件。生成的目录位于项目的 .zig-cache 里,每个生成的文件以及父目录本身都可以独立地作为 std.Build.LazyPath 使用。
这个 API 支持将任意字符串写入生成的目录,以及将文件复制到其中。
const std = @import("std");
pub fn main() !void {
std.debug.print("hello world\n", .{});
}const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = b.graph.host,
}),
});
const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
const wf = b.addWriteFiles();
const app_exe_name = b.fmt("project/{s}", .{exe.out_filename});
_ = wf.addCopyFile(exe.getEmittedBin(), app_exe_name);
_ = wf.add("project/version.txt", version);
const tar = b.addSystemCommand(&.{ "tar", "czf" });
tar.setCwd(wf.getDirectory());
const out_file = tar.addOutputFileArg("project.tar.gz");
tar.addArgs(&.{"project/"});
const install_tar = b.addInstallFileWithDir(out_file, .prefix, "project.tar.gz");
b.getInstallStep().dependOn(&install_tar.step);
}$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install generated to project.tar.gz success
+- run tar (project.tar.gz) success 615ms
+- WriteFile project/app success
+- compile exe app Debug native success 1s MaxRSS:144M
Output
zig-out/
└── project.tar.gz
直接修改源文件
虽然不常见,但有时项目会将生成的文件直接提交到版本控制中。当生成的文件很少更新,且更新过程依赖于繁琐的系统依赖时,这样做可能是有用的,但仅限于更新过程中。
为此,WriteFiles 提供了一种方法来完成这个任务。这个功能将在未来的 Zig 版本中从 WriteFiles 中提取出来,成为一个独立的构建步骤。
使用这个功能时要小心;它不应该在正常的构建过程中使用,而应该由有意更新源文件的开发者作为工具运行,然后这些文件将被提交到版本控制中。如果在正常的构建过程中使用,它将导致缓存和并发错误。
const std = @import("std");
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
if (args.len != 2) fatal("wrong number of arguments", .{});
const output_file_path = args[1];
var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close();
try output_file.writeAll(
\\pub const Header = extern struct {
\\ magic: u64,
\\ width: u32,
\\ height: u32,
\\};
);
return std.process.cleanExit();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}const std = @import("std");
const Protocol = @import("protocol.zig");
pub fn main() !void {
const header = try std.io.getStdIn().reader().readStruct(Protocol.Header);
std.debug.print("header: {any}\n", .{header});
}pub const Header = extern struct {
magic: u64,
width: u32,
height: u32,
};const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("src/main.zig"),
.target = target,
});
b.installArtifact(exe);
const proto_gen = b.addExecutable(.{
.name = "proto_gen",
.root_source_file = b.path("tools/proto_gen.zig"),
.target = target,
});
const run = b.addRunArtifact(proto_gen);
const generated_protocol_file = run.addOutputFileArg("protocol.zig");
const wf = b.addUpdateSourceFiles();
wf.addCopyFileToSource(generated_protocol_file, "src/protocol.zig");
const update_protocol_step = b.step("update-protocol", "update src/protocol.zig to latest");
update_protocol_step.dependOn(&wf.step);
}
fn detectWhetherToEnableLibFoo() bool {
return false;
}
$ zig build update-protocol --summary all
Build Summary: 4/4 steps succeeded
update-protocol success
└─ WriteFile success
└─ run proto_gen (protocol.zig) success 401us MaxRSS:1M
└─ zig build-exe proto_gen Debug native success 1s MaxRSS:183M
运行这个命令后,src/protocol.zig 文件会被原地更新。
实用示例
为多个目标构建以发布版本
在这个例子中,我们在创建 InstallArtifact 步骤时更改一些默认设置,以便将每个目标的构建产物放入安装路径内的单独子目录中。
const std = @import("std");
const targets: []const std.Target.Query = &.{
.{ .cpu_arch = .aarch64, .os_tag = .macos },
.{ .cpu_arch = .aarch64, .os_tag = .linux },
.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu },
.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl },
.{ .cpu_arch = .x86_64, .os_tag = .windows },
};
pub fn build(b: *std.Build) !void {
for (targets) |t| {
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("hello.zig"),
.target = b.resolveTargetQuery(t),
.optimize = .ReleaseSafe,
}),
});
const target_output = b.addInstallArtifact(exe, .{
.dest_dir = .{
.override = .{
.custom = try t.zigTriple(b.allocator),
},
},
});
b.getInstallStep().dependOn(&target_output.step);
}
}$ zig build--summary all
Build Summary: 11/11 steps succeeded
install success
+- install hello success
| +- compile exe hello ReleaseSafe aarch64-macos success 19s MaxRSS:399M
+- install hello success
| +- compile exe hello ReleaseSafe aarch64-linux success 23s MaxRSS:489M
+- install hello success
| +- compile exe hello ReleaseSafe x86_64-linux-gnu success 21s MaxRSS:441M
+- install hello success
| +- compile exe hello ReleaseSafe x86_64-linux-musl success 22s MaxRSS:428M
+- install hello success
+- compile exe hello ReleaseSafe x86_64-windows success 17s MaxRSS:371M
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
Output
zig-out
├── aarch64-linux
│ └── hello
├── aarch64-macos
│ └── hello
├── x86_64-linux-gnu
│ └── hello
├── x86_64-linux-musl
│ └── hello
└── x86_64-windows
├── hello.exe
└── hello.pdb