别再把 build.zig 当配置文件:Zig 构建系统从零到实战

别再把 build.zig 当配置文件:Zig 构建系统从零到实战

很多语言把构建配置写成一堆声明:源码目录、编译参数、链接库、安装目录分别放在不同字段里。Zig 走了另一条路:build.zig 本身就是一个 Zig 程序。

这意味着,构建逻辑可以使用普通的变量、条件判断、循环、函数和类型检查。项目从"编译一个文件"发展到"编译多个目标、运行测试、生成代码、接入第三方库"时,仍然只需要扩展这份 Zig 程序。

本文按 Zig 0.16.0 的 API 编写,重点放在能落地的项目结构和 Demo,而不是把 API 名字罗列一遍。

一、zig build 到底做了什么?

运行下面的命令:

bash 复制代码
zig build

Zig 会在当前项目中找到 build.zig,编译并执行其中的 build 函数。这个函数通常长这样:

zig 复制代码
const std = @import("std");

pub fn build(b: *std.Build) void {
    // 在这里描述项目如何构建
}

b 是构建上下文,类型为 *std.Build。可执行文件、库、测试、命令和自定义步骤,都通过这个对象创建。

需要注意,build 函数执行时主要是在"登记任务",并不是看到 addExecutable 就立刻编译。构建系统会把任务组织成一张有向无环图(DAG),再根据依赖关系执行。没有被当前步骤依赖的任务不会被无意义地执行,这也是并发构建和缓存生效的基础。

可以把常见对象先记成下面几类:

text 复制代码
Build          构建上下文
Module         Zig 模块,描述源码和导入关系
Artifact       构建产物,例如可执行文件、静态库、动态库
Step           构建步骤,例如 install、run、test
Target         目标平台,例如 x86_64-linux、aarch64-macos
OptimizeMode   优化模式,例如 Debug、ReleaseSafe

二、最小可运行项目

先准备如下目录:

text 复制代码
hello/
├── build.zig
└── src/
    └── main.zig

src/main.zig

zig 复制代码
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    var stdout_buffer: [256]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("hello from Zig build\n", .{});
    try stdout.flush();
}

对应的 build.zig

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("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(exe);
}

这段代码分成三件事:

  1. standardTargetOptions 读取 -Dtarget
  2. standardOptimizeOption 读取 -Doptimize
  3. addExecutable 创建可执行文件,installArtifact 把它加入默认安装步骤。

执行:

bash 复制代码
zig build
./zig-out/bin/hello

默认产物位于 zig-out/bin/hello.zig-cache 保存中间缓存,zig-out 是安装前缀,两者都不应该提交到 Git。zig-out 的位置可以通过 --prefix 改变:

bash 复制代码
zig build --prefix dist

这里有一个很关键的区别:b.path("src/main.zig") 表示项目内的路径对象;路径不应该写死成 /Users/... 这样的绝对路径,否则缓存、跨平台构建和被其他项目复用都会受到影响。

三、为什么新版推荐 root_module

旧版本常见写法是把 .root_source_file.target.optimize 直接放进 addExecutable。Zig 0.16.0 更推荐先通过 b.createModule 创建模块,再把模块交给 addExecutable

zig 复制代码
const module = b.createModule(.{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});

const exe = b.addExecutable(.{
    .name = "hello",
    .root_module = module,
});

这样做的好处是:模块的导入关系、编译参数和依赖关系都有明确的挂载位置。后面加入本地模块或第三方模块时,直接操作 exe.root_module 即可。

四、添加 run 步骤:让项目真正像项目

zig build 默认只执行安装步骤。开发阶段更常用的是编译后立即运行,因此可以注册一个名为 run 的步骤:

zig 复制代码
const run_cmd = b.addRunArtifact(exe);

if (b.args) |args| {
    run_cmd.addArgs(args);
}

const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_cmd.step);

完整调用链可以读成:

text 复制代码
zig build run
    ↓
run_step
    ↓
addRunArtifact(exe)
    ↓
编译 exe,并执行生成的程序

程序参数放在 -- 后面:

bash 复制代码
zig build run -- config.json --verbose

-- 前面的内容属于 Zig 构建命令,-- 后面的内容才会交给应用程序。

五、本地模块:把源码拆开仍然能清晰导入

假设项目需要一个问候模块:

text 复制代码
src/
├── main.zig
└── greeting.zig

src/greeting.zig

zig 复制代码
pub fn text() []const u8 {
    return "hello from a local module";
}

build.zig 中声明模块并挂到可执行文件:

zig 复制代码
const greeting = b.createModule(.{
    .root_source_file = b.path("src/greeting.zig"),
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("greeting", greeting);

src/main.zig 里就可以这样使用:

zig 复制代码
const std = @import("std");
const greeting = @import("greeting");

pub fn main() void {
    std.debug.print("{s}\n", .{greeting.text()});
}

这里的 "greeting" 不是文件名,而是构建脚本注册的导入名。模块名和文件名可以相同,也可以不同。

六、测试步骤不能只编译,必须连接运行步骤

b.addTest 创建的是测试产物。要真正执行测试,还需要用 b.addRunArtifact 创建运行步骤,再把运行步骤接到自定义的 test 步骤上:

zig 复制代码
const tests = b.addTest(.{
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);

src/main.zig 里增加测试:

zig 复制代码
test "greeting text" {
    try std.testing.expectEqualStrings(
        "hello from a local module",
        greeting.text(),
    );
}

运行:

bash 复制代码
zig build test

只写 b.addTest 而不连接 addRunArtifact,结果只是生成测试二进制,测试代码并不会被执行。这是构建脚本里非常常见的遗漏。

七、一个完整 Demo:程序、模块、运行和测试

下面这份 build.zig 可以直接作为项目骨架:

zig 复制代码
const std = @import("std");

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

    const greeting = b.createModule(.{
        .root_source_file = b.path("src/greeting.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    exe.root_module.addImport("greeting", greeting);
    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    if (b.args) |args| run_cmd.addArgs(args);
    const run_step = b.step("run", "Run the demo");
    run_step.dependOn(&run_cmd.step);

    const tests = b.addTest(.{
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    tests.root_module.addImport("greeting", greeting);

    const run_tests = b.addRunArtifact(tests);
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_tests.step);
}

对应命令:

bash 复制代码
zig build                 # 构建并安装到 zig-out/bin/demo
zig build run             # 编译后运行
zig build run -- Alice    # 把 Alice 传给程序
zig build test            # 执行单元测试
zig build -Doptimize=ReleaseSafe
zig build -Dtarget=x86_64-windows
zig build --summary all   # 显示构建步骤和缓存情况

-Dtarget-Doptimize 能正常生效,前提就是脚本使用了两个 standard... 函数。硬编码目标平台会直接破坏交叉编译能力。

八、静态库和动态库

可执行文件之外,构建系统也能生成库。Zig 0.16.0 可以统一使用 addLibrary,通过 linkage 选择静态或动态:

zig 复制代码
const lib = b.addLibrary(.{
    .name = "math",
    .linkage = .static,
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/math.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

b.installArtifact(lib);

生成动态库时把 .static 改成 .dynamic。如果同一个项目里的可执行文件要链接这个库,可以使用:

zig 复制代码
exe.linkLibrary(lib);

库是否安装、Demo 是否安装,都可以通过 b.option 控制。没有被当前步骤依赖的产物不会被额外编译,这正是构建图带来的好处。

九、第三方依赖:build.zig.zonb.dependency

build.zig 描述"怎么构建",build.zig.zon 描述"项目依赖哪些包"。依赖声明示意如下:

zon 复制代码
.{
    .name = "demo",
    .version = "0.1.0",
    .dependencies = .{
        .some_lib = .{
            .url = "https://example.com/some_lib.tar.gz",
            .hash = "1220...",
        },
    },
}

build.zig 中加载依赖:

zig 复制代码
const dep = b.dependency("some_lib", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("some_lib", dep.module("some_lib"));

依赖的模块名由依赖包提供,不能只根据压缩包文件名猜测。实际接入时应查看依赖包的 build.zig,确认 dep.module("...") 使用的名称。

十、链接系统库和编译 C 代码

确实依赖系统库时,可以让模块链接 libc,再连接具体库:

zig 复制代码
const exe = b.addExecutable(.{
    .name = "zip-tool",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    }),
});

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

项目自带的 Zig 依赖通常更容易复现,也更容易交叉编译;发行版打包时则可能必须链接系统库。两种方式没有绝对替代关系,关键在于明确库由谁提供、版本由谁控制。

十一、自定义步骤和构建参数

b.step 只负责注册名字和描述,必须通过 dependOn 连接实际任务:

zig 复制代码
const check_step = b.step("check", "Run project checks");
const check_cmd = b.addSystemCommand(&.{ "echo", "checks passed" });
check_step.dependOn(&check_cmd.step);

之后执行:

bash 复制代码
zig build check

自定义布尔参数可以这样定义:

zig 复制代码
const enable_demo = b.option(
    bool,
    "enable-demo",
    "Install the demo executable",
) orelse false;

if (enable_demo) {
    b.installArtifact(exe);
}

调用时写成:

bash 复制代码
zig build -Denable-demo

这种配置适合控制示例程序、基准测试、可选后端和额外工具。与环境变量相比,-D 参数会出现在 zig build --help 中,也能进入构建图和缓存计算,更适合项目级配置。

十二、常见问题

忘记 installArtifact

只创建 exe 不代表默认的 zig build 会安装它。需要明确写出:

zig 复制代码
b.installArtifact(exe);

自定义步骤注册了却没有动作

b.step("run", "...") 只是一个空节点。必须让它依赖 run_cmd.steptest_run.step 或其他实际步骤。

zig-out.zig-cache 提交进仓库

这两个目录都是生成物。项目通常只提交源码、build.zigbuild.zig.zon,并在 .gitignore 中排除:

gitignore 复制代码
.zig-cache/
zig-out/

直接复制旧版本 API

Zig 仍处于快速演进阶段,构建 API 也会变化。尤其是 root_module、I/O API、测试运行方式和 C 代码导入方式,不能把 0.12 或 0.13 的片段直接拼进 0.16 项目。实际排查时先确认:

bash 复制代码
zig version
zig build --help

再以当前版本的官方 Build System 文档和 zig init-exe 生成的模板为准。

总结

build.zig 可以理解成一份用 Zig 写成的项目构建程序:

text 复制代码
standardTargetOptions  读取目标平台
standardOptimizeOption 读取优化模式
createModule            描述源码模块
addExecutable           创建可执行文件
addLibrary              创建库
addTest                 创建测试产物
addRunArtifact          执行程序或测试
step + dependOn         组织自定义命令
installArtifact         加入默认安装步骤

真正值得掌握的不是某个 API 的参数顺序,而是"产物 + 步骤 + 依赖关系"这套模型。掌握这三层之后,单文件程序、库、测试、代码生成、跨平台发布和第三方依赖,都可以沿着同一张构建图逐步扩展。

相关推荐
Naylor16 分钟前
只借不占:Rust 的引用与借用
后端·rust
大勇前进24 分钟前
Python装饰器、生成器、上下文管理器:3个必会的"魔法"机制
后端
Go_error31 分钟前
Fyne:让 Go 开发者也能玩转 GUI
后端·go
用户07956750431435 分钟前
Python + OpenCV 识别 GIF 中旋转最快的图形:基于时间周期的动态识别
后端
长大198837 分钟前
Python+Flask 1小时搭建个人博客:比WordPress轻量10倍
后端
闪学it44 分钟前
Python测试开发进阶线上班28期
后端
Go_error44 分钟前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error1 小时前
Go-redis:执行 Lua 脚本
后端·go
IT毕设实战小研1 小时前
电影数据可视化推荐系统
大数据·后端·爬虫·python·算法·信息可视化·课程设计