← 返回 学习

Zig 构建系统

何时使用 Zig 构建系统?

基本的命令 zig build-exezig build-libzig build-objzig test 通常已经足够。然而,有时项目需要另一层抽象来管理从源代码构建的复杂性。

例如,可能存在以下情况之一:

如果符合以上任何一种情况,采用 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-cachezig-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--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
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 提供了辅助函数 standardTargetOptionsstandardOptimizeOption

standardTargetOptions 使得执行 zig build 命令的用户能够选择他们想要构建的目标平台。默认情况下,脚本将允许构建任何目标平台,如果不做选择,则默认为目标宿主系统。此外,还提供了选项来限制支持的目标平台集合。

standardOptimizeOption 为执行 zig build 命令的用户提供了在 DebugReleaseSafeReleaseFastReleaseSmall 之间进行选择的机会。在默认情况下,构建脚本不会自动选择任何发布模式作为首选设置,因此用户需要明确选择一个优化级别,以便生成一个发布版本的构建。

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{
    .{}, // 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);
    }
}
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{
    .{}, // 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
Shell
$ zig build--summary all

Build Summary: 1/1 steps succeeded
install cached

链接系统库

为了满足库依赖,有两个选择:

  1. 通过 Zig 构建系统提供这些库(参见包管理静态库)。
  2. 使用宿主系统提供的文件。

对于上游项目维护者的使用场景,通过 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", // 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);
}
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