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_source_file = b.path("hello.zig"),
.target = b.host,
});
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 66ms MaxRSS:36M
安装构建产物
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_source_file = b.path("hello.zig"),
.target = b.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 hello success 14ms MaxRSS:1M
└─ zig build-exe hello Debug native cached 44ms MaxRSS:36M
基础
用户提供的选项
使用 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_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-linux-x86_64-0.13.0/zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
General Options:
-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
-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)
--glibc-runtimes [path] Enhances QEMU integration by providing glibc built
for multiple foreign architectures, allowing
execution of non-native programs that link with glibc.
-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)
-h, --help Print this help and exit
-l, --list-steps Print available steps
--verbose Print commands before executing them
--color [auto|off|on] Enable or disable colored error messages
--prominent-compile-errors Buffer compile errors and display at end
--summary [mode] Control the printing of the build summary
all Print the build summary in its entirety
new Omit cached steps
failures (Default) Only print failed steps
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
--fetch Exit after fetching dependency tree
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
Available System Integrations: Enabled:
(none) -
Advanced Options:
-freference-trace[=num] How many lines of reference trace should be shown per compile error
-fno-reference-trace Disable reference trace
--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)
--debug-log [scope] Enable debugging the compiler
--debug-pkg-config Fail if unknown pkg-config flags encountered
--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_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 cached
└─ install hello cached
└─ zig build-exe hello ReleaseSmall x86_64-windows cached 44ms MaxRSS:36M
现在,我们的帮助菜单包含了更多的项目:
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_source_file = b.path("app.zig"),
.target = b.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 cached
└─ install app cached
└─ zig build-exe app Debug native cached 33ms MaxRSS:36M
└─ 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");
extern fn fizzbuzz(n: usize) ?[*:0]const u8;
pub fn main() !void {
const stdout = std.io.getStdOut();
var bw = std.io.bufferedWriter(stdout.writer());
const w = bw.writer();
for (0..100) |n| {
if (fizzbuzz(n)) |s| {
try w.print("{s}\n", .{s});
} else {
try w.print("{d}\n", .{n});
}
}
try bw.flush();
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const libfizzbuzz = b.addStaticLibrary(.{
.name = "fizzbuzz",
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("demo.zig"),
.target = target,
.optimize = optimize,
});
exe.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 cached
└─ install fizzbuzz cached
└─ zig build-lib fizzbuzz Debug native cached 56ms MaxRSS:36M
在这种情况下,最终只有静态库被安装:
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.addSharedLibrary(.{
.name = "fizzbuzz",
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
.version = .{ .major = 1, .minor = 2, .patch = 3 },
});
b.installArtifact(libfizzbuzz);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install fizzbuzz cached
└─ zig build-lib fizzbuzz Debug native cached 33ms MaxRSS:37M
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_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 failure
error: the host system (x86_64-linux.5.10...5.10-gnu.2.31) is unable to execute binaries from the target (aarch64-macos.11.7.1...14.1-none)
Build Summary: 5/7 steps succeeded; 1 failed
test transitive failure
├─ run test cached
│ └─ zig test Debug native cached 49ms MaxRSS:36M
├─ run test cached
│ └─ zig test Debug x86_64-linux cached 61ms MaxRSS:36M
└─ run test failure
└─ zig test Debug aarch64-macos cached 38ms MaxRSS:36M
error: the following build command failed with exit code 1:
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache/o/25475ace5ef269b60e98416da65a5cde/build /home/ci/deps/zig-linux-x86_64-0.13.0/zig /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache /home/ci/.cache/zig --seed 0x10a7accf -Zd5915bdf16628db4 --color on 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_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 构建系统获取这些库可以减少很多麻烦,并将配置能力掌握在维护者手中。像这样构建,每个人都会得到可复现、一致的结果,并且它可以在每个操作系统上工作,甚至支持交叉编译。此外,它允许项目精确决定其整个依赖树中希望构建的每个库的确切版本。这被认为是依赖外部库的普遍首选方式。
然而,对于将软件打包到诸如 Debian、Homebrew 或 Nix 等仓库的使用场景,必须链接到系统库。因此,构建脚本必须能检测模式并相应地配置。
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "zip",
.root_source_file = b.path("zip.zig"),
.target = b.host,
});
exe.linkSystemLibrary("z");
exe.linkLibC();
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install zip cached
└─ zig build-exe zip Debug native cached 95ms MaxRSS:37M
使用 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");
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 self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
defer self_exe_dir.close();
const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
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_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 cached
├─ install generated to word.txt cached
│ └─ run jq cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 48ms MaxRSS:36M
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");
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 self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
defer self_exe_dir.close();
const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
const std = @import("std");
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
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);
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 std.io.getStdOut().writeAll(usage);
return std.process.cleanExit();
} 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 = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close();
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();
var json_reader = std.json.reader(arena, input_file.reader());
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.writeAll(w);
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");
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_source_file = b.path("tools/word_select.zig"),
.target = b.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_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 cached
├─ install generated to word.txt cached
│ └─ run word_select (word.txt) cached
│ └─ zig build-exe word_select Debug native cached 43ms MaxRSS:36M
└─ install hello cached
└─ zig build-exe hello Debug native cached 83ms MaxRSS:36M
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 {
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
const std = @import("std");
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
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);
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 std.io.getStdOut().writeAll(usage);
return std.process.cleanExit();
} 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 = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close();
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();
var json_reader = std.json.reader(arena, input_file.reader());
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.writeAll(w);
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");
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_source_file = b.path("tools/word_select.zig"),
.target = b.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_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 cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 51ms MaxRSS:36M
└─ run word_select (word.txt) cached
└─ zig build-exe word_select Debug native cached 88ms MaxRSS:36M
Output
zig-out/
└── bin
└── hello
生成 Zig 源代码
这个构建脚本使用一个 Zig 程序来生成一个 Zig 源文件,并将其作为模块提供给主程序。
const std = @import("std");
const Person = @import("person").Person;
pub fn main() !void {
const p: Person = .{};
try std.io.getStdOut().writer().print("Hello {any}\n", .{p});
}
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 Person = struct {
\\ age: usize = 18,
\\ name: []const u8 = "foo"
\\};
);
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");
pub fn build(b: *std.Build) void {
const tool = b.addExecutable(.{
.name = "generate_struct",
.root_source_file = b.path("tools/generate_struct.zig"),
.target = b.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_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 cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 39ms MaxRSS:36M
└─ run generate_struct (person.zig) cached
└─ zig build-exe generate_struct Debug native cached 53ms MaxRSS:36M
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_source_file = b.path("src/main.zig"),
.target = b.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 cached
└─ install generated to project.tar.gz cached
└─ run tar (project.tar.gz) cached
└─ WriteFile project/app cached
└─ zig build-exe app Debug native cached 68ms MaxRSS:36M
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 exe = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("src/main.zig"),
});
b.installArtifact(exe);
const proto_gen = b.addExecutable(.{
.name = "proto_gen",
.root_source_file = b.path("tools/proto_gen.zig"),
});
const run = b.addRunArtifact(proto_gen);
const generated_protocol_file = run.addOutputFileArg("protocol.zig");
const wf = b.addWriteFiles();
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_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 cached
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe aarch64-macos cached 33ms MaxRSS:36M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe aarch64-linux cached 98ms MaxRSS:36M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe x86_64-linux-gnu cached 37ms MaxRSS:33M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe x86_64-linux-musl cached 43ms MaxRSS:36M
└─ install hello cached
└─ zig build-exe hello ReleaseSafe x86_64-windows cached 45ms MaxRSS:36M
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