← 返回
学习
Zig 构建系统
何时使用 Zig 构建系统? 基本的命令 zig build-exe
、zig build-lib
、zig build-obj
和 zig test
通常已经足够。然而,有时项目需要另一层抽象来管理从源代码构建的复杂性。
例如,可能存在以下情况之一:
命令行变得过长且难以操作,你希望有一个地方可以将其写下来。 你想要构建许多东西,或者构建过程包含许多步骤。 你想要利用并发和缓存来减少构建时间。 你想要为项目提供配置选项。 构建过程根据目标系统和其他选项的不同而有所区别。 你的项目依赖于其他项目。 你想要避免对 cmake、make、shell、msvc、python 等不必要的依赖,使项目对更多贡献者开放。 你想要提供一个包供第三方使用。 你想要为工具(如 IDE)提供一个标准化的方式,以便它们能够从语义上理解如何构建项目。 如果符合以上任何一种情况,采用 Zig 构建系统将大有裨益。
开始使用 简单的可执行文件 这个构建脚本负责将一个包含对外可见 main 函数的 Zig 文件编译成可执行文件。
hello.zig const std = @import ("std" );
pub fn main () !void {
std.debug.print("Hello World!\n" , .{});
}
build.zig 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);
}
Shell $ 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
步骤,以便可以直接从构建命令中运行主应用程序。
hello.zig const std = @import ("std" );
pub fn main () !void {
std.debug.print("Hello World!\n" , .{});
}
build.zig 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);
}
Shell $ 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
来配置构建脚本,便于最终用户和其他依赖此项目的包进行调整。
build.zig 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);
}
Shell $ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 34ms MaxRSS:36M
example.zig 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
之间进行选择的机会。在默认情况下,构建脚本不会自动选择任何发布模式作为首选设置,因此用户需要明确选择一个优化级别,以便生成一个发布版本的构建。
hello.zig const std = @import ("std" );
pub fn main () !void {
std.debug.print("Hello World!\n" , .{});
}
build.zig 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);
}
Shell $ 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
步骤。
app.zig 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();
}
}
build.zig 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 ;
}
Shell $ 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 代码创建了一个静态库,并且还创建了一个可执行文件,这个可执行文件依赖于这个静态库。
fizzbuzz.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 ;
}
demo.zig 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();
}
build.zig 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);
}
}
Shell $ 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
文件进行了修改。
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);
}
Shell $ 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 。
main.zig 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());
}
build.zig const std = @import ("std" );
const test_targets = [_]std.Target.Query{
.{},
.{
.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);
}
}
Shell $ 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);
}
}
build.zig const std = @import ("std" );
const test_targets = [_]std.Target.Query{
.{},
.{
.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);
}
}
Shell $ zig build--summary all
Build Summary: 1/1 steps succeeded
install cached
链接系统库 为了满足库依赖,有两个选择:
通过 Zig 构建系统提供这些库(参见包管理 和静态库 )。 使用宿主系统提供的文件。 对于上游项目维护者的使用场景,通过 Zig 构建系统获取这些库可以减少很多麻烦,并将配置能力掌握在维护者手中。像这样构建,每个人都会得到可复现、一致的结果,并且它可以在每个操作系统上工作,甚至支持交叉编译。此外,它允许项目精确决定其整个依赖树中希望构建的每个库的确切版本。这被认为是依赖外部库的普遍首选方式。
然而,对于将软件打包到诸如 Debian、Homebrew 或 Nix 等仓库的使用场景,必须链接到系统库。因此,构建脚本必须能检测模式 并相应地配置。
build.zig 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);
}
Shell $ 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" : "世界"
}
main.zig 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});
}
build.zig 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" ,
});
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);
}
Shell $ 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" : "世界"
}
main.zig 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});
}
word_select.zig 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 );
}
build.zig 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);
}
Shell $ 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" : "世界"
}
main.zig const std = @import ("std" );
const word = @embedFile ("word" );
pub fn main () !void {
try std.io.getStdOut().writer().print("Hello {s}\n" , .{word});
}
word_select.zig 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 );
}
build.zig 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);
}
Shell $ 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 源文件,并将其作为模块提供给主程序。
main.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});
}
generate_struct.zig 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 );
}
build.zig 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);
}
Shell $ 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 支持将任意字符串写入生成的目录,以及将文件复制到其中。
main.zig const std = @import ("std" );
pub fn main () !void {
std.debug.print("hello world\n" , .{});
}
build.zig 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);
}
Shell $ 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 中提取出来,成为一个独立的构建步骤 。
使用这个功能时要小心;它不应该在正常的构建过程中使用,而应该由有意更新源文件的开发者作为工具运行,然后这些文件将被提交到版本控制中。如果在正常的构建过程中使用,它将导致缓存和并发错误。
proto_gen.zig 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 );
}
main.zig 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});
}
protocol.zig pub const Header = extern struct {
magic: u64 ,
width: u32 ,
height: u32 ,
};
build.zig 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
步骤时更改一些默认设置,以便将每个目标的构建产物放入安装路径内的单独子目录中。
build.zig 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);
}
}
Shell $ 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
hello.zig 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