← 返回 学习

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_module = b.createModule(.{
            .root_source_file = b.path("hello.zig"),
            .target = b.graph.host,
        }),
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install hello success
   +- compile exe hello Debug native success 1s MaxRSS:141M

安装构建产物

Zig 构建系统与其他大多数构建系统一样,将项目视为一系列步骤,这些步骤构成一个有向无环图(DAG),每个步骤都可以独立且并发地执行。

默认情况下,图中的主要步骤是 Install,其作用是将构建产物复制到它们的最终存放位置。Install 步骤开始时没有依赖,因此当执行 zig build 命令时,不会发生任何操作。项目的构建脚本必须添加到要安装的物品清单中,这正是上面提到的 installArtifact 函数调用所执行的操作。

Output

├── build.zig
├── hello.zig
├── .zig-cache
└── zig-out
    └── bin
        └── hello

输出结果中包含了两个生成的目录:.zig-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_module = b.createModule(.{
            .root_source_file = b.path("hello.zig"),
            .target = b.graph.host,
        }),
    });

    b.installArtifact(exe);

    const run_exe = b.addRunArtifact(exe);

    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_exe.step);
}
Shell
$ zig buildrun --summary all
Hello World!
Build Summary: 3/3 steps succeeded
run success
+- run exe hello success 871us
   +- compile exe hello Debug native success 923ms MaxRSS:143M

基础

用户提供的选项

使用 b.option 来配置构建脚本,便于最终用户和其他依赖此项目的包进行调整。

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_module = b.createModule(.{
            .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-x86_64-linux-0.16.0/zig build [steps] [options]

Steps:
  install (default)            Copy build artifacts to prefix path
  uninstall                    Remove build artifacts from prefix path

Project-Specific Options:
  -Dwindows=[bool]             Target Microsoft Windows

System Integration Options:
  --search-prefix [path]       Add a path to look for binaries, libraries, headers
  --sysroot [path]             Set the system root directory (usually /)
  --libc [file]                Provide a file which specifies libc paths

  --system [pkgdir]            Disable package fetching; enable all integrations
  -fsys=[name]                 Enable a system integration
  -fno-sys=[name]              Disable a system integration

  -fdarling,  -fno-darling     Integration with system-installed Darling to
                               execute macOS programs on Linux hosts
                               (default: no)
  -fqemu,     -fno-qemu        Integration with system-installed QEMU to execute
                               foreign-architecture programs on Linux hosts
                               (default: no)
  --libc-runtimes [path]       Enhances QEMU integration by providing dynamic libc
                               (e.g. glibc or musl) built for multiple foreign
                               architectures, allowing execution of non-native
                               programs that link with libc.
  -frosetta,  -fno-rosetta     Rely on Rosetta to execute x86_64 programs on
                               ARM64 macOS hosts. (default: no)
  -fwasmtime, -fno-wasmtime    Integration with system-installed wasmtime to
                               execute WASI binaries. (default: no)
  -fwine,     -fno-wine        Integration with system-installed Wine to execute
                               Windows programs on Linux hosts. (default: no)

  Available System Integrations:                Enabled:
  (none)                                        -

General Options:
  -h, --help                   Print this help and exit
  -l, --list-steps             Print available steps

  -p, --prefix [path]          Where to install files (default: zig-out)
  --prefix-lib-dir [path]      Where to install libraries
  --prefix-exe-dir [path]      Where to install executables
  --prefix-include-dir [path]  Where to install C header files
  --release[=mode]             Request release mode, optionally specifying a
                               preferred optimization mode: fast, safe, small

  --verbose                    Print commands before executing them
  --color [auto|off|on]        Enable or disable colored error messages
  --error-style [style]        Control how build errors are printed
    verbose                    (Default) Report errors with full context
    minimal                    Report errors after summary, excluding context like command lines
    verbose_clear              Like 'verbose', but clear the terminal at the start of each update
    minimal_clear              Like 'minimal', but clear the terminal at the start of each update
  --multiline-errors [style]   Control how multi-line error messages are printed
    indent                     (Default) Indent non-initial lines to align with initial line
    newline                    Include a leading newline so that the error message is on its own lines
    none                       Print as usual so the first line is misaligned
  --summary [mode]             Control the printing of the build summary
    all                        Print the build summary in its entirety
    new                        Omit cached steps
    failures                   (Default if short-lived) Only print failed steps
    line                       (Default if long-lived) Only print the single-line summary
    none                       Do not print the build summary
  -j<N>                        Limit concurrent jobs (default is to use all CPU cores)
  --maxrss <bytes>             Limit memory usage (default is to use available memory)
  --skip-oom-steps             Instead of failing, skip steps that would exceed --maxrss
  --test-timeout <timeout>     Limit execution time of unit tests, terminating if exceeded.
                               The timeout must include a unit: ns, us, ms, s, m, h
  --watch                      Continuously rebuild when source files are modified
  --debounce <ms>              Delay before rebuilding after changed file detected
  --webui[=ip]                 Enable the web interface on the given IP address
  --fuzz[=limit]               Continuously search for unit test failures with an optional
                               limit to the max number of iterations. The argument supports
                               an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
                               '--webui' when no limit is specified.
  --time-report                Force full rebuild and provide detailed information on
                               compilation time of Zig source code (implies '--webui')
     -fincremental             Enable incremental compilation
  -fno-incremental             Disable incremental compilation

Package Management Options:
  --fetch[=mode]               Fetch dependency tree (optionally choose laziness) and exit
    needed                     (Default) Lazy dependencies are fetched as needed
    all                        Lazy dependencies are always fetched
  --fork=[path]                Override one or more projects from dependency tree

Advanced Options:
  -freference-trace[=num]      How many lines of reference trace should be shown per compile error
  -fno-reference-trace         Disable reference trace
  -fallow-so-scripts           Allows .so files to be GNU ld scripts
  -fno-allow-so-scripts        (default) .so files must be ELF files
  --build-file [file]          Override path to build.zig
  --cache-dir [path]           Override path to local Zig cache directory
  --global-cache-dir [path]    Override path to global Zig cache directory
  --zig-lib-dir [arg]          Override path to Zig lib directory
  --build-runner [file]        Override path to build runner
  --seed [integer]             For shuffling dependency traversal order (default: random)
  --build-id[=style]           At a minor link-time expense, embeds a build ID in binaries
      fast                     8-byte non-cryptographic hash (COFF, ELF, WASM)
      sha1, tree               20-byte cryptographic hash (ELF, WASM)
      md5                      16-byte cryptographic hash (ELF)
      uuid                     16-byte random UUID (ELF, WASM)
      0x[hexstring]            Constant ID, maximum 32 bytes (ELF, WASM)
      none                     (default) No build ID
  --debug-log [scope]          Enable debugging the compiler
  --debug-pkg-config           Fail if unknown pkg-config flags encountered
  --debug-rt                   Debug compiler runtime libraries
  --verbose-link               Enable compiler debug output for linking
  --verbose-air                Enable compiler debug output for Zig AIR
  --verbose-llvm-ir[=file]     Enable compiler debug output for LLVM IR
  --verbose-llvm-bc=[file]     Enable compiler debug output for LLVM BC
  --verbose-cimport            Enable compiler debug output for C imports
  --verbose-cc                 Enable compiler debug output for C compilation
  --verbose-llvm-cpu-features  Enable compiler debug output for LLVM CPU features
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_module = b.createModule(.{
            .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 success
+- install hello success
   +- compile exe hello ReleaseSmall x86_64-windows success 3s MaxRSS:161M

现在,我们的帮助菜单包含了更多的项目:

Project-Specific Options:
  -Dtarget=[string]            The CPU architecture, OS, and ABI to build for
  -Dcpu=[string]               Target CPU features to add or subtract
  -Doptimize=[enum]            Prioritize performance, safety, or binary size (-O flag)
                                 Supported Values:
                                   Debug
                                   ReleaseSafe
                                   ReleaseFast
                                   ReleaseSmall

完全可以直接通过 b.option 创建这些选项,但这个 API 为这些常用设置提供了一种常用的命名约定。

在我们的终端输出中,请注意我们传递了 -Dtarget=x86_64-windows -Doptimize=ReleaseSmall。与第一个示例相比,现在我们在安装路径中看到了不同的文件:

zig-out/
└── bin
    └── hello.exe

用于条件编译的选项

要从构建脚本传递选项到项目的 Zig 代码中,请使用 Options 步骤。

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_module = b.createModule(.{
            .root_source_file = b.path("app.zig"),
            .target = b.graph.host,
        }),
    });

    const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
    const enable_foo = detectWhetherToEnableLibFoo();

    const options = b.addOptions();
    options.addOption([]const u8, "version", version);
    options.addOption(bool, "have_libfoo", enable_foo);

    exe.root_module.addOptions("config", options);

    b.installArtifact(exe);
}

fn detectWhetherToEnableLibFoo() bool {
    return false;
}
Shell
$ zig build-Dversion=1.2.3 --summary all
Build Summary: 4/4 steps succeeded
install success
+- install app success
   +- compile exe app Debug native success 1s MaxRSS:143M
      +- options cached

在这个例子中,由 @import(“config”) 提供的数据是编译时已知的,这阻止了触发 @compileError 语句。如果我们传递了 -Dversion=0.2.3 或者省略了这个选项,那么我们将会看到 app.zig 的编译因为“太旧”的错误而失败。

静态库

这个构建脚本从 Zig 代码创建了一个静态库,并且还创建了一个可执行文件,这个可执行文件依赖于这个静态库。

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");
const Io = std.Io;

extern fn fizzbuzz(n: usize) ?[*:0]const u8;

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    var buf: [1024]u8 = undefined;
    const file_writer = Io.File.stdout().writer(io, &buf);
    const w = &file_writer.interface;
    for (0..100) |n| {
        if (fizzbuzz(n)) |s| {
            try w.print("{s}\n", .{s});
        } else {
            try w.print("{d}\n", .{n});
        }
    }
    try w.flush();
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const libfizzbuzz = b.addLibrary(.{
        .name = "fizzbuzz",
        .linkage = .static,
        .root_module = b.createModule(.{
            .root_source_file = b.path("fizzbuzz.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("demo.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.root_module.linkLibrary(libfizzbuzz);

    b.installArtifact(libfizzbuzz);

    if (b.option(bool, "enable-demo", "install the demo too") orelse false) {
        b.installArtifact(exe);
    }
}
Shell
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install fizzbuzz success
   +- compile lib fizzbuzz Debug native success 186ms MaxRSS:94M

在这种情况下,最终只有静态库被安装:

zig-out/
└── lib
    └── libfizzbuzz.a

然而,如果你仔细观察,构建脚本中包含了一个选项,也可以安装 demo 程序。如果我们额外传递 -Denable-demo 参数,那么我们会在安装路径中看到它:

zig-out/
├── bin
│   └── demo
└── lib
    └── libfizzbuzz.a

请注意,尽管无条件地调用了 addExecutable,但实际上,除非我们明确使用 -Denable-demo 参数,构建系统是不会去构建 demo 程序的。这是因为构建系统是根据一个有向无环图来工作的,这个图记录了不同部分之间的依赖关系。

动态库

在这里,我们使用了之前静态库示例中的所有代码文件,但是对 build.zig 文件进行了修改。

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const libfizzbuzz = b.addLibrary(.{
        .name = "fizzbuzz",
        .linkage = .dynamic,
        .version = .{ .major = 1, .minor = 2, .patch = 3 },
        .root_module = b.createModule(.{
            .root_source_file = b.path("fizzbuzz.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(libfizzbuzz);
}
Shell
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install fizzbuzz success
   +- compile lib fizzbuzz Debug native success 887ms MaxRSS:143M

Output

zig-out
└── lib
    ├── libfizzbuzz.so -> libfizzbuzz.so.1
    ├── libfizzbuzz.so.1 -> libfizzbuzz.so.1.2.3
    └── libfizzbuzz.so.1.2.3

就像在静态库示例中一样,为了让一个可执行文件链接到这个动态库,你需要添加类似这样的代码:

exe.linkLibrary(libfizzbuzz);

测试

单个文件可以直接使用 zig test foo.zig 命令进行测试,然而,更复杂的使用场景可以通过构建脚本来解决。

当使用构建脚本时,单元测试在构建图中被分为两个不同的步骤:Compile 步骤和 Run 步骤。如果没有调用 addRunArtifact 来在这两个步骤之间建立依赖关系,单元测试将不会被执行。

Compile 步骤可以像配置任何可执行文件、库或对象文件一样进行配置,例如通过链接系统库、设置目标选项或添加额外的编译单元。

Run 步骤可以像配置其他 Run 步骤一样进行配置,例如,当主机无法执行二进制文件时,可以跳过执行。

当使用构建系统来运行单元测试时,构建运行器(build runner)和测试运行器(test runner)通过标准输入和标准输出进行通信,以便并发运行多个单元测试套件,并以一种有意义的方式报告测试失败,而不会使它们的输出混淆在一起。这就是为什么在单元测试中向标准输出写数据会是个问题——因为这样会干扰这个通信通道。另一方面,这种机制将启用一个即将推出的功能,即单元测试能够预测 panic

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_module = b.createModule(.{
                .root_source_file = b.path("main.zig"),
                .target = b.resolveTargetQuery(target),
            }),
        });

        const run_unit_tests = b.addRunArtifact(unit_tests);
        test_step.dependOn(&run_unit_tests.step);
    }
}
Shell
$ zig buildtest --summary all
test
+- run test
   +- compile test Debug x86_64-linux 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
    var list = std.ArrayList(i32).init(std.testing.allocator);
               ~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
    return struct {
           ^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -target x86_64-linux -mcpu baseline -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-

test
+- run test
   +- compile test Debug aarch64-macos 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
    var list = std.ArrayList(i32).init(std.testing.allocator);
               ~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
    return struct {
           ^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -target aarch64-macos -mcpu baseline -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-

test
+- run test
   +- compile test Debug native 1 errors
/home/ci/.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig:4:34: error: struct 'array_list.Aligned(i32,null)' has no member named 'init'
    var list = std.ArrayList(i32).init(std.testing.allocator);
               ~~~~~~~~~~~~~~~~~~^~~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/array_list.zig:576:12: note: struct declared here
    return struct {
           ^~~~~~
error: 1 compilation errors
failed command: /home/ci/deps/zig-x86_64-linux-0.16.0/zig test -Mroot=../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing/main.zig --cache-dir /home/ci/cache --global-cache-dir /home/ci/cache --name test --zig-lib-dir /home/ci/deps/zig-x86_64-linux-0.16.0/lib/ --listen=-

Build Summary: 0/7 steps succeeded (3 failed)
test transitive failure
+- run test transitive failure
|  +- compile test Debug native 1 errors
+- run test transitive failure
|  +- compile test Debug x86_64-linux 1 errors
+- run test transitive failure
   +- compile test Debug aarch64-macos 1 errors

error: the following build command failed with exit code 1:
/home/ci/cache/o/a526b193363be5f6ad52dd400fe5f988/build /home/ci/deps/zig-x86_64-linux-0.16.0/zig /home/ci/deps/zig-x86_64-linux-0.16.0/lib ../../../.cache/act/f1c9a46ae85a4f16/hostexecutor/zig-code/build-system/unit-testing /home/ci/cache /home/ci/cache --seed 0xb4c49b02 -Z3dab62a97b183898 test --summary all

在这种情况下,为单元测试开启 skip_foreign_checks 可能是个好主意:

@@ -23,6 +23,7 @@
         });
 
         const run_unit_tests = b.addRunArtifact(unit_tests);
+        run_unit_tests.skip_foreign_checks = true;
         test_step.dependOn(&run_unit_tests.step);
     }
 }

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_module = b.createModule(.{
                .root_source_file = b.path("main.zig"),
                .target = b.resolveTargetQuery(target),
            }),
        });

        const run_unit_tests = b.addRunArtifact(unit_tests);
        run_unit_tests.skip_foreign_checks = true;
        test_step.dependOn(&run_unit_tests.step);
    }
}

// zig-doctest: build-system --collapseable -- test --summary all
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_module = b.createModule(.{
            .root_source_file = b.path("zip.zig"),
            .target = b.graph.host,
            .link_libc = true,
        }),
    });

    exe.root_module.linkSystemLibrary("z", .{});

    b.installArtifact(exe);
}
Shell
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install success
+- install zip success
   +- compile exe zip Debug native success 820ms MaxRSS:144M

使用 zig build 的用户可以使用 --search-prefix 参数来提供额外的目录,这些目录将被视为“系统目录”,用于查找静态库和动态库。

生成文件

运行系统工具

这个版本的 “hello world” 期望在相同的路径下找到一个 word.txt 文件,并且我们希望使用一个系统工具从 JSON 文件生成它。

请注意,系统依赖项会使你的项目对用户来说更难构建。例如,这个构建脚本依赖于 jq,而在大多数 Linux 发行版中默认并不包含它,对于 Windows 用户来说可能也不熟悉这个工具。

下一节将使用源代码中包含的 Zig 工具替换 jq,这种方法是更推荐的。

words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界" 
}

main.zig
const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    const self_exe_dir_path = try std.process.executableDirPathAlloc(io, arena);
    var self_exe_dir = try Io.Dir.cwd().openDir(io, self_exe_dir_path, .{});
    defer self_exe_dir.close(io);

    const word = try self_exe_dir.readFileAlloc(io, "word.txt", arena, .limited(1000));

    var stdout_buffer: [1000]u8 = undefined;
    var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("Hello {s}\n", .{word});
    try stdout.flush();
}
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_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    const install_artifact = b.addInstallArtifact(exe, .{
        .dest_dir = .{ .override = .prefix },
    });
    b.getInstallStep().dependOn(&install_artifact.step);
}
Shell
$ zig build-Dlanguage=ja --summary all
Build Summary: 5/5 steps succeeded
install success
+- install generated to word.txt success
|  +- run jq cached
+- install hello success
   +- compile exe hello Debug native success 693ms MaxRSS:142M

Output

zig-out
├── hello
└── word.txt

注意 captureStdOut 如何创建一个临时文件来存储 jq 命令的输出。

运行项目工具

这个版本的 “hello world” 期望在相同的路径下找到一个 word.txt 文件。我们计划在构建过程中,通过运行一个 Zig 程序来处理 JSON 文件并生成目标文件。

tools/words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界" 
}

main.zig
const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    const self_exe_dir_path = try std.process.executableDirPathAlloc(io, arena);
    var self_exe_dir = try Io.Dir.cwd().openDir(io, self_exe_dir_path, .{});
    defer self_exe_dir.close(io);

    const word = try self_exe_dir.readFileAlloc(io, "word.txt", arena, .limited(1000));

    var stdout_buffer: [1000]u8 = undefined;
    var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("Hello {s}\n", .{word});
    try stdout.flush();
}

word_select.zig
const std = @import("std");
const Io = std.Io;

const usage =
    \\Usage: ./word_select [options]
    \\
    \\Options:
    \\  --input-file INPUT_JSON_FILE
    \\  --output-file OUTPUT_TXT_FILE
    \\  --lang LANG
    \\
;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    const args = try init.minimal.args.toSlice(arena);

    var opt_input_file_path: ?[]const u8 = null;
    var opt_output_file_path: ?[]const u8 = null;
    var opt_lang: ?[]const u8 = null;

    {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const arg = args[i];
            if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
                try Io.File.stdout().writeStreamingAll(io, usage);
                return std.process.cleanExit(io);
            } else if (std.mem.eql(u8, "--input-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_input_file_path = args[i];
            } else if (std.mem.eql(u8, "--output-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_output_file_path = args[i];
            } else if (std.mem.eql(u8, "--lang", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
                opt_lang = args[i];
            } else {
                fatal("unrecognized arg: '{s}'", .{arg});
            }
        }
    }

    const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
    const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
    const lang = opt_lang orelse fatal("missing --lang", .{});

    var input_file = Io.Dir.cwd().openFile(io, input_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
    };
    defer input_file.close(io);
    var input_file_buffer: [1000]u8 = undefined;
    var input_file_reader = input_file.reader(io, &input_file_buffer);

    var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close(io);

    var json_reader: std.json.Reader = .init(arena, &input_file_reader.interface);
    var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
        .allocate = .alloc_if_needed,
        .max_value_len = 1000,
    });

    const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});

    try output_file.writeStreamingAll(io, w);
    return std.process.cleanExit(io);
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}

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_module = b.createModule(.{
            .root_source_file = b.path("tools/word_select.zig"),
            .target = b.graph.host,
        }),
    });

    const tool_step = b.addRunArtifact(tool);
    tool_step.addArg("--input-file");
    tool_step.addFileArg(b.path("tools/words.json"));
    tool_step.addArg("--output-file");
    const output = tool_step.addOutputFileArg("word.txt");
    tool_step.addArgs(&.{ "--lang", lang });

    b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    const install_artifact = b.addInstallArtifact(exe, .{
        .dest_dir = .{ .override = .prefix },
    });
    b.getInstallStep().dependOn(&install_artifact.step);
}
Shell
$ zig build--summary all
Build Summary: 6/6 steps succeeded
install success
+- install generated to word.txt success
|  +- run exe word_select (word.txt) success 10ms
|     +- compile exe word_select Debug native success 1s MaxRSS:150M
+- install hello success
   +- compile exe hello Debug native success 1s MaxRSS:142M

Output

zig-out
├── hello
└── word.txt

@embedFile 生成文件

这个版本的 “hello world” 希望将构建时生成的文件通过 @embedFile 进行嵌入,我们将使用一个用 Zig 编写的工具来生成这个文件。

tools/words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界" 
}

main.zig
const std = @import("std");
const word = @embedFile("word");

pub fn main() !void {
    std.log.info("Hello {s}\n", .{word});
}

word_select.zig
const std = @import("std");
const Io = std.Io;

const usage =
    \\Usage: ./word_select [options]
    \\
    \\Options:
    \\  --input-file INPUT_JSON_FILE
    \\  --output-file OUTPUT_TXT_FILE
    \\  --lang LANG
    \\
;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    const args = try init.minimal.args.toSlice(arena);

    var opt_input_file_path: ?[]const u8 = null;
    var opt_output_file_path: ?[]const u8 = null;
    var opt_lang: ?[]const u8 = null;

    {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const arg = args[i];
            if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
                try Io.File.stdout().writeStreamingAll(io, usage);
                return std.process.cleanExit(io);
            } else if (std.mem.eql(u8, "--input-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_input_file_path = args[i];
            } else if (std.mem.eql(u8, "--output-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_output_file_path = args[i];
            } else if (std.mem.eql(u8, "--lang", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
                opt_lang = args[i];
            } else {
                fatal("unrecognized arg: '{s}'", .{arg});
            }
        }
    }

    const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
    const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
    const lang = opt_lang orelse fatal("missing --lang", .{});

    var input_file = Io.Dir.cwd().openFile(io, input_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
    };
    defer input_file.close(io);
    var input_file_buffer: [1000]u8 = undefined;
    var input_file_reader = input_file.reader(io, &input_file_buffer);

    var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close(io);

    var json_reader: std.json.Reader = .init(arena, &input_file_reader.interface);
    var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
        .allocate = .alloc_if_needed,
        .max_value_len = 1000,
    });

    const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});

    try output_file.writeStreamingAll(io, w);
    return std.process.cleanExit(io);
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}

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_module = b.createModule(.{
            .root_source_file = b.path("tools/word_select.zig"),
            .target = b.graph.host,
        }),
    });

    const tool_step = b.addRunArtifact(tool);
    tool_step.addArg("--input-file");
    tool_step.addFileArg(b.path("tools/words.json"));
    tool_step.addArg("--output-file");
    const output = tool_step.addOutputFileArg("word.txt");
    tool_step.addArgs(&.{ "--lang", lang });

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.root_module.addAnonymousImport("word", .{
        .root_source_file = output,
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install hello success
   +- compile exe hello Debug native success 652ms MaxRSS:145M
      +- run exe word_select (word.txt) success 10ms
         +- compile exe word_select Debug native success 1s MaxRSS:149M

Output

zig-out/
└── bin
    └── hello

生成 Zig 源代码

这个构建脚本使用一个 Zig 程序来生成一个 Zig 源文件,并将其作为模块提供给主程序。

main.zig
const std = @import("std");
const Person = @import("person").Person;

pub fn main() !void {
    const p: Person = .{};
    std.log.info("Hello {any}\n", .{p});
}
generate_struct.zig
const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    const args = try init.minimal.args.toSlice(arena);

    if (args.len != 2) fatal("wrong number of arguments", .{});

    const output_file_path = args[1];

    var output_file = Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close(io);

    try output_file.writeStreamingAll(io,
        \\pub const Person = struct {
        \\   age: usize = 18,
        \\   name: []const u8 = "foo"
        \\};
    );
    return std.process.cleanExit(io);
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const tool = b.addExecutable(.{
        .name = "generate_struct",
        .root_module = b.createModule(.{
            .root_source_file = b.path("tools/generate_struct.zig"),
            .target = b.graph.host,
        }),
    });

    const tool_step = b.addRunArtifact(tool);
    const output = tool_step.addOutputFileArg("person.zig");

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.root_module.addAnonymousImport("person", .{
        .root_source_file = output,
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install hello success
   +- compile exe hello Debug native success 791ms MaxRSS:141M
      +- run exe generate_struct (person.zig) success 8ms
         +- compile exe generate_struct Debug native success 715ms MaxRSS:148M

Output

zig-out/
└── bin
    └── hello

处理一个或多个生成的文件

WriteFiles 步骤提供了一种方法,用于生成同一父目录下一个或多个文件。生成的目录位于项目的 .zig-cache 里,每个生成的文件以及父目录本身都可以独立地作为 std.Build.LazyPath 使用。

这个 API 支持将任意字符串写入生成的目录,以及将文件复制到其中。

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_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = b.graph.host,
        }),
    });

    const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";

    const wf = b.addWriteFiles();
    const app_exe_name = b.fmt("project/{s}", .{exe.out_filename});
    _ = wf.addCopyFile(exe.getEmittedBin(), app_exe_name);
    _ = wf.add("project/version.txt", version);

    const tar = b.addSystemCommand(&.{ "tar", "czf" });
    tar.setCwd(wf.getDirectory());
    const out_file = tar.addOutputFileArg("project.tar.gz");
    tar.addArgs(&.{"project/"});

    const install_tar = b.addInstallFileWithDir(out_file, .prefix, "project.tar.gz");
    b.getInstallStep().dependOn(&install_tar.step);
}
Shell
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install success
+- install generated to project.tar.gz success
   +- run tar (project.tar.gz) success 615ms
      +- WriteFile project/app success
         +- compile exe app Debug native success 1s MaxRSS:144M

Output

zig-out/
└── project.tar.gz

直接修改源文件

虽然不常见,但有时项目会将生成的文件直接提交到版本控制中。当生成的文件很少更新,且更新过程依赖于繁琐的系统依赖时,这样做可能是有用的,但仅限于更新过程中。

为此,WriteFiles 提供了一种方法来完成这个任务。这个功能将在未来的 Zig 版本中从 WriteFiles 中提取出来,成为一个独立的构建步骤

使用这个功能时要小心;它不应该在正常的构建过程中使用,而应该由有意更新源文件的开发者作为工具运行,然后这些文件将被提交到版本控制中。如果在正常的构建过程中使用,它将导致缓存和并发错误。

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 target = b.standardTargetOptions(.{});
    const exe = b.addExecutable(.{
        .name = "demo",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
    });
    b.installArtifact(exe);

    const proto_gen = b.addExecutable(.{
        .name = "proto_gen",
        .root_source_file = b.path("tools/proto_gen.zig"),
        .target = target,
    });

    const run = b.addRunArtifact(proto_gen);
    const generated_protocol_file = run.addOutputFileArg("protocol.zig");

    const wf = b.addUpdateSourceFiles();
    wf.addCopyFileToSource(generated_protocol_file, "src/protocol.zig");

    const update_protocol_step = b.step("update-protocol", "update src/protocol.zig to latest");
    update_protocol_step.dependOn(&wf.step);
}

fn detectWhetherToEnableLibFoo() bool {
    return false;
}

$ zig build update-protocol --summary all
Build Summary: 4/4 steps succeeded
update-protocol success
└─ WriteFile success
   └─ run proto_gen (protocol.zig) success 401us MaxRSS:1M
      └─ zig build-exe proto_gen Debug native success 1s MaxRSS:183M

运行这个命令后,src/protocol.zig 文件会被原地更新。

实用示例

为多个目标构建以发布版本

在这个例子中,我们在创建 InstallArtifact 步骤时更改一些默认设置,以便将每个目标的构建产物放入安装路径内的单独子目录中。

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_module = b.createModule(.{
                .root_source_file = b.path("hello.zig"),
                .target = b.resolveTargetQuery(t),
                .optimize = .ReleaseSafe,
            }),
        });

        const target_output = b.addInstallArtifact(exe, .{
            .dest_dir = .{
                .override = .{
                    .custom = try t.zigTriple(b.allocator),
                },
            },
        });

        b.getInstallStep().dependOn(&target_output.step);
    }
}
Shell
$ zig build--summary all
Build Summary: 11/11 steps succeeded
install success
+- install hello success
|  +- compile exe hello ReleaseSafe aarch64-macos success 19s MaxRSS:399M
+- install hello success
|  +- compile exe hello ReleaseSafe aarch64-linux success 23s MaxRSS:489M
+- install hello success
|  +- compile exe hello ReleaseSafe x86_64-linux-gnu success 21s MaxRSS:441M
+- install hello success
|  +- compile exe hello ReleaseSafe x86_64-linux-musl success 22s MaxRSS:428M
+- install hello success
   +- compile exe hello ReleaseSafe x86_64-windows success 17s MaxRSS:371M
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