Chapter 22Build System Deep Dive

构建系统深度解析

概述

第21章21展示了build.zig.zon如何声明包元数据;本章揭示build.zig如何使用std.Build API编写构建步骤的有向无环图来编排编译过程,构建运行器执行该图以产生产物——可执行文件、库、测试和自定义转换——同时缓存中间结果并并行化独立工作(参见Build.zig)。

与命令式编译单个入口点的zig runzig build-exe不同,build.zig是可执行的Zig代码,它构建声明式构建图:节点表示编译步骤,边表示依赖关系,构建运行器(zig build)以最优方式遍历该图。有关发布详情,请参见v0.15.2

学习目标

  • 区分 zig build(构建图执行)与 zig run / zig build-exe(直接编译)。
  • 使用 b.standardTargetOptions()b.standardOptimizeOption() 暴露用户可配置的目标和优化选项。
  • 使用 b.addModule()b.createModule() 创建模块,理解何时公开模块与私有模块(参见 Module.zig)。
  • 使用 b.addExecutable() 构建可执行文件,使用 b.addLibrary() 构建库,并在构件之间建立依赖关系(参见 Compile.zig)。
  • 使用 b.addTest() 集成测试,使用 b.step() 连接自定义顶层步骤。
  • 使用 zig build -v 调试构建失败,并解释因缺失模块或错误依赖关系导致的图错误。

作为可执行的 Zig 代码

每个 build.zig 导出一个 pub fn build(b: *std.Build) 函数,构建运行器在解析 build.zig.zon 并设置构建图上下文后调用此函数;在此函数内,您使用 *std.Build 指针上的方法以声明方式注册步骤、构件和依赖关系。21

命令式命令 vs. 声明式图

当您运行 zig run main.zig 时,编译器立即编译 main.zig 并执行它——这是一个单次执行的命令式工作流。当您运行 zig build 时,运行器首先执行 build.zig 来构建步骤图,然后分析该图以确定哪些步骤需要运行(基于缓存状态和依赖关系),最后在可能的情况下并行执行这些步骤。

这种声明式方法实现了:

  • 增量构建:未更改的构件不会被重新编译
  • 并行执行:独立的步骤同时运行
  • 可重现性:相同的图产生相同的输出
  • 可扩展性:自定义步骤无缝集成

build.zig 模板

最小化

最简单的 build.zig 创建一个可执行文件并安装它:

Zig
const std = @import("std");

// 最小化的 build.zig:单个可执行文件,无选项
// 演示 Zig 构建系统中最简单的构建脚本。
pub fn build(b: *std.Build) void {
    // 使用最简配置创建一个可执行文件编译步骤。
    // 这代表了生成二进制产物的基本模式。
    const exe = b.addExecutable(.{
        // 输出的二进制文件名(将变为 "hello" 或 "hello.exe")
        .name = "hello",
        // 配置根模块的源文件和编译设置
        .root_module = b.createModule(.{
            // 指定相对于 build.zig 的入口点源文件
            .root_source_file = b.path("main.zig"),
            // 目标为宿主机(运行构建的系统)
            .target = b.graph.host,
            // 使用Debug优化级别(无优化,包含调试符号)
            .optimize = .Debug,
        }),
    });
    
    // 注册可执行文件以安装到输出目录。
    // 运行 `zig build` 时,此产物将被复制到 zig-out/bin/。
    b.installArtifact(exe);
}
Zig
// Entry point for a minimal Zig build system example.
// This demonstrates the simplest possible Zig program structure that can be built
// using the Zig build system, showing the basic main function and standard library import.
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello from minimal build!\n", .{});
}
构建和运行
Shell
$ zig build
$ ./zig-out/bin/hello
输出
Shell
Hello from minimal build!

此示例硬编码 b.graph.host(运行构建的机器)作为目标和 .Debug 优化,因此用户无法自定义它。对于实际项目,请将这些暴露为选项。

build 函数本身不编译任何内容——它只在图中注册步骤。构建运行器在 build() 返回后执行该图。

标准选项助手

大多数项目希望用户能够控制目标架构/操作系统和优化级别;std.Build 提供了两个助手,将这些暴露为 CLI 标志并优雅地处理默认值。

:简化跨平台编译

b.standardTargetOptions(.{}) 返回一个 std.Build.ResolvedTarget,它尊重 -Dtarget= 标志,允许用户在不修改 build.zig 的情况下进行跨平台编译:

Shell
$ zig build -Dtarget=x86_64-linux       # Linux x86_64
$ zig build -Dtarget=aarch64-macos      # macOS ARM64
$ zig build -Dtarget=wasm32-wasi        # WebAssembly WASI

空选项结构体 (.{}) 接受默认值;您可以选择性地白名单目标或指定回退:

Zig
const target = b.standardTargetOptions(.{
    .default_target = .{ .cpu_arch = .x86_64, .os_tag = .linux },
});

:用户控制的优化

b.standardOptimizeOption(.{}) 返回一个 std.builtin.OptimizeMode,它尊重 -Doptimize= 标志,值为 .Debug.ReleaseSafe.ReleaseFast.ReleaseSmall

Shell
$ zig build                             # Debug(默认)
$ zig build -Doptimize=ReleaseFast      # 最大速度
$ zig build -Doptimize=ReleaseSmall     # 最小大小

选项结构体接受一个 .preferred_optimize_mode 来在用户未指定时建议默认值。如果您不传递偏好,系统将从 build.zig.zon 中的包 release_mode 设置推断。21

在底层,选择的 OptimizeMode 会输入到编译器配置中,并影响安全检查、调试信息和后端优化级别:

graph TB subgraph "Optimization Mode Effects" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["Runtime Safety Checks"] OptMode --> DebugInfo["Debug Information"] OptMode --> CodegenStrategy["Codegen Strategy"] OptMode --> LLVMOpt["LLVM Optimization Level"] SafetyChecks --> Overflow["Integer overflow checks"] SafetyChecks --> Bounds["Bounds checking"] SafetyChecks --> Null["Null pointer checks"] SafetyChecks --> Unreachable["Unreachable assertions"] DebugInfo --> StackTraces["Stack traces"] DebugInfo --> DWARF["DWARF debug info"] DebugInfo --> LineInfo["Source line information"] CodegenStrategy --> Inlining["Inline heuristics"] CodegenStrategy --> Unrolling["Loop unrolling"] CodegenStrategy --> Vectorization["SIMD vectorization"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + safety"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end

这与 b.standardOptimizeOption() 返回的 OptimizeMode 相同,因此您在 build.zig 中暴露的标志直接决定了哪些安全检查保持启用以及编译器选择哪个优化管道。

完整示例与标准选项

Zig
const std = @import("std");

// 演示标准目标选项和标准优化选项
pub fn build(b: *std.Build) void {
    // 允许用户选择目标:zig build -Dtarget=x86_64-linux
    const target = b.standardTargetOptions(.{});
    
    // 允许用户选择优化:zig build -Doptimize=ReleaseFast
    const optimize = b.standardOptimizeOption(.{});
    
    const exe = b.addExecutable(.{
        .name = "configurable",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    
    b.installArtifact(exe);
    
    // Add run step
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    
    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_cmd.step);
}
Zig
// This program demonstrates how to access and display Zig's built-in compilation
// information through the `builtin` module. It's used in the zigbook to teach
// readers about build system introspection and standard options.

// Import the standard library for debug printing capabilities
const std = @import("std");
// Import builtin module to access compile-time information about the target
// platform, CPU architecture, and optimization mode
const builtin = @import("builtin");

// Main entry point that prints compilation target information
// Returns an error union to handle potential I/O failures from debug.print
pub fn main() !void {
    // Print the target architecture (e.g., x86_64, aarch64) and operating system
    // (e.g., linux, windows) by extracting tag names from the builtin constants
    std.debug.print("Target: {s}-{s}\n", .{
        @tagName(builtin.cpu.arch),
        @tagName(builtin.os.tag),
    });
    // Print the optimization mode (Debug, ReleaseSafe, ReleaseFast, or ReleaseSmall)
    // that was specified during compilation
    std.debug.print("Optimize: {s}\n", .{@tagName(builtin.mode)});
}
使用选项构建和运行
Shell
$ zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast run
输出(示例)
Target: x86_64-linux
Optimize: ReleaseFast

除非您有非常具体的原因需要硬编码值(例如,针对固定嵌入式系统的固件),否则始终使用 standardTargetOptions()standardOptimizeOption()

模块:公共与私有

Zig 0.15.2 区分 公共模块(通过 b.addModule() 暴露给消费者)和 私有模块(当前包内部使用,通过 b.createModule() 创建)。公共模块通过 b.dependency() 出现在下游的 build.zig 文件中,而私有模块仅存在于您的构建图中。

vs.

  • b.addModule(name, options) 创建一个模块并将其注册到包的公共模块表中,使其可供依赖此包的消费者使用。
  • b.createModule(options) 创建一个不暴露的模块;适用于可执行文件特定的代码或内部辅助工具。

两个函数都返回一个 *std.Build.Module,您通过 .imports 字段将其连接到编译步骤中。

示例:公共模块与可执行文件

Zig
const std = @import("std");

// 演示模块创建和导入
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    
    // 创建一个可重用模块(public)
    const math_mod = b.addModule("math", .{
        .root_source_file = b.path("math.zig"),
        .target = target,
    });
    
    // 使用导入的模块创建可执行文件
    const exe = b.addExecutable(.{
        .name = "calculator",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "math", .module = math_mod },
            },
        }),
    });
    
    b.installArtifact(exe);
    
    const run_step = b.step("run", "Run the calculator");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);
}
Zig
// This module provides basic arithmetic operations for the zigbook build system examples.
// It demonstrates how to create a reusable module that can be imported by other Zig files.

// / Adds two 32-bit signed integers and returns their sum.
// / This function is marked pub to be accessible from other modules that import this file.
pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

// / Multiplies two 32-bit signed integers and returns their product.
// / This function is marked pub to be accessible from other modules that import this file.
pub fn multiply(a: i32, b: i32) i32 {
    return a * b;
}
Zig
// 此程序演示如何在Zig的构建系统中使用自定义模块。
// 它导入本地"math"模块并使用其函数执行基本算术运算。

// 导入标准库以获取调试打印功能
const std = @import("std");
// 导入提供算术运算的自定义数学模块
const math = @import("math");

// 主入口点,演示使用基本算术的模块用法
pub fn main() !void {
    // 定义两个常量操作数用于演示
    const a = 10;
    const b = 20;

    // 使用导入的数学模块打印加法结果
    std.debug.print("{d} + {d} = {d}\n", .{ a, b, math.add(a, b) });

    // 使用导入的数学模块打印乘法结果
    std.debug.print("{d} * {d} = {d}\n", .{ a, b, math.multiply(a, b) });
}
构建和运行
Shell
$ zig build run
输出
Shell
10 + 20 = 30
10 * 20 = 200

这里 math 是一个 公共模块(此包的消费者可以 @import("math")),而可执行文件的根模块是 私有的(使用 createModule 创建)。

Module.CreateOptions 中的 .imports 字段是一个 .{ .name = …​, .module = …​ } 对的切片,允许您将任意导入名称映射到模块指针——在消费多个包时避免名称冲突非常有用。

构件:可执行文件、库、对象文件

一个 构件 是产生二进制输出的编译步骤:可执行文件、静态或动态库,或对象文件。std.Build API 提供了 addExecutable()addLibrary()addObject() 函数,它们返回 *Step.Compile 指针。

: 构建程序

b.addExecutable(.{ .name = …​, .root_module = …​ }) 创建一个 Step.Compile,它将 main 函数(或独立环境的 _start)链接到可执行文件中:

Zig
const exe = b.addExecutable(.{
    .name = "myapp",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});
b.installArtifact(exe);
  • .name: 输出文件名(例如,Windows 上的 myapp.exe,Unix 上的 myapp)。
  • .root_module: 包含入口点的模块。
  • 可选:.version.linkage(用于 PIE)、.max_rss.use_llvm.use_lld.zig_lib_dir

: 静态和动态库

b.addLibrary(.{ .name = …​, .root_module = …​, . linkage = …​ }) 创建一个库:

Zig
const lib = b.addLibrary(.{
    .name = "utils",
    .root_module = b.createModule(.{
        .root_source_file = b.path("utils.zig"),
        .target = target,
        .optimize = optimize,
    }),
    . linkage = .static, // or .dynamic
    .version = .{ .major = 1, .minor = 0, .patch = 0 },
});
b.installArtifact(lib);
  • .linkage = .static 产生一个 .a(Unix)或 .lib(Windows)归档文件。
  • .linkage = .dynamic 产生一个 .so(Unix)、.dylib(macOS)或 .dll(Windows)共享库。
  • .version: 嵌入在库元数据中的语义版本(仅 Unix)。

将库链接到可执行文件

要将库链接到可执行文件中,在创建两个构件后调用 exe.linkLibrary(lib)

Zig
const std = @import("std");

// 演示库创建
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // 创建一个静态库
    const lib = b.addLibrary(.{
        .name = "utils",
        .root_module = b.createModule(.{
            .root_source_file = b.path("utils.zig"),
            .target = target,
            .optimize = optimize,
        }),
        .linkage = .static,
        .version = .{ .major = 1, .minor = 0, .patch = 0 },
    });

    b.installArtifact(lib);

    // 创建链接库的可执行文件
    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.linkLibrary(lib);
    b.installArtifact(exe);

    const run_step = b.step("run", "Run the demo");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);
}
Zig
// ! 实用工具模块,演示导出函数和格式化输出。
// ! 此模块是构建系统深入研究章节的一部分,展示如何创建
// ! 可以导出并在不同构建工件中使用的库函数。

const std = @import("std");

/// 将输入整数值翻倍。
/// 此函数被导出,可以从C或其他语言调用。
/// 使用`export`关键字使其在编译的库中可用。
export fn util_double(x: i32) i32 {
    return x * 2;
}

/// 将输入整数值平方。
/// 此函数被导出,可以从C或其他语言调用。
/// 使用`export`关键字使其在编译的库中可用。
export fn util_square(x: i32) i32 {
    return x * x;
}

/// 使用整数值将消息格式化到提供的缓冲区中。
/// 这是一个公共Zig函数(未导出),演示基于缓冲区的格式化。
///
/// 返回包含格式化消息的缓冲区切片,或如果缓冲区太小而无法容纳格式化输出,则返回错误。
pub fn formatMessage(buf: []u8, value: i32) ![]const u8 {
    return std.fmt.bufPrint(buf, "Value: {d}", .{value});
}
Zig
// Import the standard library for printing capabilities
const std = @import("std");

// 外部函数声明:将输入整数翻倍
// 此函数在单独的库/对象文件中定义
extern fn util_double(x: i32) i32;

// 外部函数声明:将输入整数平方
// 此函数在单独的库/对象文件中定义
extern fn util_square(x: i32) i32;

// 演示库链接的主入口点
// 调用外部工具函数以展示构建系统集成
pub fn main() !void {
    // 用于演示外部函数的测试值
    const x: i32 = 7;

    // 使用外部函数打印 x 翻倍的结果
    std.debug.print("double({d}) = {d}\n", .{ x, util_double(x) });

    // 使用外部函数打印 x 平方 Results
    std.debug.print("square({d}) = {d}\n", .{ x, util_square(x) });
}
构建和运行
Shell
$ zig build run
输出
Shell
double(7) = 14
square(7) = 49

当链接 Zig 库时,符号必须被 export(用于 C ABI)或者您必须使用模块导入——Zig 没有与模块导出不同的链接器级"公共 Zig API"概念。

安装构件:

b.installArtifact(exe) 添加对默认安装步骤(不带参数的 zig build)的依赖,该步骤将构件复制到 zig-out/bin/(可执行文件)或 zig-out/lib/(库)。您可以自定义安装目录,或者如果构件仅是中间产物,则完全跳过安装。

测试与测试步骤

Zig 的测试块直接集成到构建系统中:b.addTest(.{ .root_module = …​ }) 创建一个特殊的可执行文件,运行给定模块中的所有 test 块,并向构建运行器报告通过/失败。13

:编译测试可执行文件

Zig
const lib_tests = b.addTest(.{
    .root_module = lib_mod,
});

const run_lib_tests = b.addRunArtifact(lib_tests);

const test_step = b.step("test", "Run library tests");
test_step.dependOn(&run_lib_tests.step);

b.addTest() 返回一个 *Step.Compile,就像 addExecutable() 一样,但它以测试模式编译模块,链接测试运行器并启用仅测试代码路径。

完整测试集成示例

Zig
const std = @import("std");

// 演示测试集成
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    
    const lib_mod = b.addModule("mylib", .{
        .root_source_file = b.path("lib.zig"),
        .target = target,
    });
    
    // 为库模块创建测试
    const lib_tests = b.addTest(.{
        .root_module = lib_mod,
    });
    
    const run_lib_tests = b.addRunArtifact(lib_tests);
    
    // 创建测试步骤
    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&run_lib_tests.step);
    
    // 同时创建一个使用该库的可执行文件
    const exe = b.addExecutable(.{
        .name = "app",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "mylib", .module = lib_mod },
            },
        }),
    });
    
    b.installArtifact(exe);
}
Zig
//  Computes the factorial of a non-negative integer using recursion.
//  使用递归计算非负整数的阶乘。
//  The factorial of n (denoted as n!) is the product of all positive integers less than or equal to n.
//  n 的阶乘(表示为 n!)是所有小于或等于 n 的正整数的乘积。
//  Base case: factorial(0) = factorial(1) = 1
//  基本情况:阶乘(0) = 阶乘(1) = 1
//  Recursive case: factorial(n) = n * factorial(n-1)
//  递归情况:阶乘(n) = n * 阶乘(n-1)
pub fn factorial(n: u32) u32 {
    // Base case: 0! and 1! both equal 1
    // 基本情况:0! 和 1! 都等于 1
    if (n <= 1) return 1;
    // Recursive case: multiply n by factorial of (n-1)
    // 递归情况:将 n 乘以 (n-1) 的阶乘
    return n * factorial(n - 1);
}

// Test: Verify that the factorial of 0 returns 1 (base case)
// 测试:验证 0 的阶乘返回 1(基本情况)
test "factorial of 0 is 1" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 1), factorial(0));
}

// Test: Verify that the factorial of 5 returns 120 (5! = 5*4*3*2*1 = 120)
// 测试:验证 5 的阶乘返回 120 (5! = 5*4*3*2*1 = 120)
test "factorial of 5 is 120" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 120), factorial(5));
}

// Test: Verify that the factorial of 1 returns 1 (base case)
// 测试:验证 1 的阶乘返回 1(基本情况)
test "factorial of 1 is 1" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 1), factorial(1));
}
Zig
// Main entry point demonstrating the factorial function from mylib.
// This example shows how to:
// - Import and use custom library modules
// - Call library functions with different input values
// - Display computed results using debug printing
const std = @import("std");
const mylib = @import("mylib");

pub fn main() !void {
    std.debug.print("5! = {d}\n", .{mylib.factorial(5)});
    std.debug.print("10! = {d}\n", .{mylib.factorial(10)});
}
运行测试
Shell
$ zig build test
输出(成功)
All 3 tests passed.

为每个模块创建单独的测试步骤,以隔离故障并启用并行测试执行。

要了解这在大型代码库中的扩展方式,Zig 编译器自身的 将许多专门的测试步骤连接到一个统一的 步骤中:

graph TB subgraph "测试步骤" TEST_STEP["test 步骤<br/>(总括步骤)"] FMT["test-fmt<br/>格式检查"] CASES["test-cases<br/>编译器测试用例"] MODULES["test-modules<br/>按目标模块测试"] UNIT["test-unit<br/>编译器单元测试"] STANDALONE["独立测试"] CLI["CLI 测试"] STACK_TRACE["堆栈跟踪测试"] ERROR_TRACE["错误跟踪测试"] LINK["链接测试"] C_ABI["C ABI 测试"] INCREMENTAL["test-incremental<br/>增量编译"] end subgraph "模块测试" BEHAVIOR["行为测试<br/>test/behavior.zig"] COMPILER_RT["compiler_rt 测试<br/>lib/compiler_rt.zig"] ZIGC["zigc 测试<br/>lib/c.zig"] STD["std 测试<br/>lib/std/std.zig"] LIBC_TESTS["libc 测试"] end subgraph "测试配置" TARGET_MATRIX["test_targets 数组<br/>不同架构<br/>不同操作系统<br/>不同ABI"] OPT_MODES["优化模式:<br/>Debug, ReleaseFast<br/>ReleaseSafe, ReleaseSmall"] FILTERS["test-filter<br/>test-target-filter"] end TEST_STEP --> FMT TEST_STEP --> CASES TEST_STEP --> MODULES TEST_STEP --> UNIT TEST_STEP --> STANDALONE TEST_STEP --> CLI TEST_STEP --> STACK_TRACE TEST_STEP --> ERROR_TRACE TEST_STEP --> LINK TEST_STEP --> C_ABI TEST_STEP --> INCREMENTAL MODULES --> BEHAVIOR MODULES --> COMPILER_RT MODULES --> ZIGC MODULES --> STD TARGET_MATRIX --> MODULES OPT_MODES --> MODULES FILTERS --> MODULES

您自己的项目可以借鉴这种模式:一个高级别的 test 步骤,分散到格式检查、单元测试、集成测试和跨目标测试矩阵,全部使用相同的 b.stepb.addTest 原语连接在一起。

顶层步骤:自定义构建命令

一个 顶层步骤 是一个命名的入口点,用户使用 zig build <step-name> 调用它。您使用 b.step(name, description) 创建它们,并使用 step.dependOn(other_step) 连接依赖关系。

创建 步骤

Zig
const run_step = b.step("run", "Run the application");
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_cmd.step);
  • b.step("run", …​) 创建用户调用的顶层步骤。
  • b.addRunArtifact(exe) 创建一个执行已编译二进制文件的步骤。
  • run_cmd.step.dependOn(b.getInstallStep()) 确保在运行二进制文件之前已安装它。
  • run_step.dependOn(&run_cmd.step) 将顶层步骤连接到运行命令。

这种模式出现在几乎每个 zig init 生成的 build.zig 中。

在 Zig 编译器自身的 中,默认安装和测试步骤形成了一个更大的依赖图:

graph TB subgraph "安装步骤(默认)" INSTALL["b.getInstallStep()"] end subgraph "编译器构件" EXE_STEP["exe.step<br/>(编译编译器)"] INSTALL_EXE["install_exe.step<br/>(安装二进制文件)"] end subgraph "文档" LANGREF["generateLangRef()"] INSTALL_LANGREF["install_langref.step"] STD_DOCS_GEN["autodoc_test"] INSTALL_STD_DOCS["install_std_docs.step"] end subgraph "库文件" LIB_FILES["installDirectory(lib/)"] end subgraph "测试步骤" TEST["test 步骤"] FMT["test-fmt 步骤"] CASES["test-cases 步骤"] MODULES["test-modules 步骤"] end INSTALL --> INSTALL_EXE INSTALL --> INSTALL_LANGREF INSTALL --> LIB_FILES INSTALL_EXE --> EXE_STEP INSTALL_LANGREF --> LANGREF INSTALL --> INSTALL_STD_DOCS INSTALL_STD_DOCS --> STD_DOCS_GEN TEST --> EXE_STEP TEST --> FMT TEST --> CASES TEST --> MODULES CASES --> EXE_STEP MODULES --> EXE_STEP

运行 zig build(没有显式步骤)通常执行像这样的默认安装步骤,而 zig build test 执行一个依赖于相同核心编译操作的专用测试步骤。

为了将本章置于更广泛的 Zig 工具链中,编译器自身的引导过程使用 CMake 生成一个中间的 可执行文件,然后在其本地的 脚本上调用 :

graph TB subgraph "CMake 阶段 (stage2)" CMAKE["CMake"] ZIG2_C["zig2.c<br/>(生成的 C 代码)"] ZIGCPP["zigcpp<br/>(C++ LLVM/Clang 包装器)"] ZIG2["zig2 可执行文件"] CMAKE --> ZIG2_C CMAKE --> ZIGCPP ZIG2_C --> ZIG2 ZIGCPP --> ZIG2 end subgraph "原生构建系统 (stage3)" BUILD_ZIG["build.zig<br/>原生构建脚本"] BUILD_FN["build() 函数"] COMPILER_STEP["addCompilerStep()"] EXE["std.Build.Step.Compile<br/>(编译器可执行文件)"] INSTALL["安装步骤"] BUILD_ZIG --> BUILD_FN BUILD_FN --> COMPILER_STEP COMPILER_STEP --> EXE EXE --> INSTALL end subgraph "构建参数" ZIG_BUILD_ARGS["ZIG_BUILD_ARGS<br/>--zig-lib-dir<br/>-Dversion-string<br/>-Dtarget<br/>-Denable-llvm<br/>-Doptimize"] end ZIG2 -->|"zig2 build"| BUILD_ZIG ZIG_BUILD_ARGS --> BUILD_FN subgraph "Output" STAGE3_BIN["stage3/bin/zig"] STD_LIB["stage3/lib/zig/std/"] LANGREF["stage3/doc/langref.html"] end INSTALL --> STAGE3_BIN INSTALL --> STD_LIB INSTALL --> LANGREF

换句话说,您用于应用程序项目的相同 API 也驱动着自托管的 Zig 编译器构建。

自定义构建选项

除了 standardTargetOptions()standardOptimizeOption() 之外,您可以使用 b.option() 定义任意面向用户的标志,并通过 b.addOptions() 将它们暴露给 Zig 源代码(参见 Options.zig)。

: CLI 标志

b.option(T, name, description) 注册一个面向用户的标志并返回 ?T(如果用户未提供则为 null):

Zig
const enable_logging = b.option(bool, "enable-logging", "Enable debug logging") orelse false;
const app_name = b.option([]const u8, "app-name", "Application name") orelse "MyApp";

用户通过 -Dname=value 传递值:

Shell
$ zig build -Denable-logging -Dapp-name=CustomName run

: 将配置传递给代码

b.addOptions() 创建一个步骤,从键值对生成 Zig 源文件,然后您将其作为模块导入:

Zig
const std = @import("std");

// 演示自定义构建选项
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // 自定义布尔选项
    const enable_logging = b.option(
        bool,
        "enable-logging",
        "Enable debug logging",
    ) orelse false;

    // 自定义字符串选项
    const app_name = b.option(
        []const u8,
        "app-name",
        "Application name",
    ) orelse "MyApp";

    // 创建选项模块以将配置传递给代码
    const config = b.addOptions();
    config.addOption(bool, "enable_logging", enable_logging);
    config.addOption([]const u8, "app_name", app_name);

    const config_module = config.createModule();

    const exe = b.addExecutable(.{
        .name = "configapp",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "config", .module = config_module },
            },
        }),
    });

    b.installArtifact(exe);

    const run_step = b.step("run", "Run the app");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);
}
Zig

// Import standard library for debug printing functionality
// 导入标准库用于调试打印功能
const std = @import("std");
// Import build-time configuration options defined in build.zig
// 从 build.zig 导入构建时配置选项
const config = @import("config");

// / 应用程序的入口点,演示构建选项的使用。
// / 此函数展示了如何通过 Zig 构建系统访问和使用在构建过程中设置的配置值。
pub fn main() !void {
    // 显示构建配置中的应用程序名称
    std.debug.print("Application: {s}\n", .{config.app_name});
    // 显示构建配置中的日志开关状态
    std.debug.print("Logging enabled: {}\n", .{config.enable_logging});

    // 根据构建时配置有条件地执行调试日志记录
    // 这演示了使用构建选项的编译时分支
    if (config.enable_logging) {
        std.debug.print("[DEBUG] This is a debug message\n", .{});
    }
}
使用自定义选项构建和运行
Shell
$ zig build run -Denable-logging -Dapp-name=TestApp
输出
Shell
Application: TestApp
Logging enabled: true
[DEBUG] This is a debug message

这种模式避免了在构建时常量足够时对环境变量或运行时配置文件的需求。

Zig 编译器本身使用相同的方法:命令行 -D 选项通过 b.option() 解析,通过 b.addOptions() 收集到选项步骤中,然后作为 build_options 模块导入,普通 Zig 代码可以读取它。

graph LR subgraph "Command Line" CLI["-Ddebug-allocator<br/>-Denable-llvm<br/>-Dversion-string<br/>etc."] end subgraph "build.zig" PARSE["b.option()<br/>Parse options"] OPTIONS["exe_options =<br/>b.addOptions()"] ADD["exe_options.addOption()"] PARSE --> OPTIONS OPTIONS --> ADD end subgraph "Generated Module" BUILD_OPTIONS["build_options<br/>(auto-generated)"] CONSTANTS["pub const mem_leak_frames = 4;<br/>pub const have_llvm = true;<br/>pub const version = '0.16.0';<br/>etc."] BUILD_OPTIONS --> CONSTANTS end subgraph "Compiler Source" IMPORT["@import('build_options')"] USE["if (build_options.have_llvm) { ... }"] IMPORT --> USE end CLI --> PARSE ADD --> BUILD_OPTIONS BUILD_OPTIONS --> IMPORT

b.addOptions() 视为从您的 zig build 命令行到普通 Zig 模块的结构化、类型检查的配置通道,就像编译器为其自身的 build_options 模块所做的那样。

调试构建失败

zig build 失败时,错误消息通常指向缺失的模块、错误的依赖关系或配置错误的步骤。-v 标志启用详细输出,显示所有编译器调用。

: 检查编译器调用

Shell
$ zig build -v
zig build-exe /path/to/main.zig -target x86_64-linux -O Debug -femit-bin=zig-cache/...
zig build-lib /path/to/lib.zig -target x86_64-linux -O Debug -femit-bin=zig-cache/...
...

这揭示了构建运行器执行的确切 zig 子命令,有助于诊断标志问题或缺失文件。

常见图错误

  • "module 'foo' not found": .imports 表不包含名为 foo 的模块,或者依赖关系未正确连接。
  • "circular dependency detected": 两个步骤传递性地相互依赖——构建图必须是无环的。
  • "file not found: src/main.zig": 传递给 b.path() 的路径相对于构建根目录不存在。
  • "no member named 'root_source_file' in ExecutableOptions": 您正在使用 Zig 0.15.2 语法与较旧的编译器,反之亦然。

注意事项与警告

  • 构建运行器在 zig-cache/ 中缓存构件哈希;删除此目录会强制完全重新构建。
  • zig build run 后传递 -- 会将参数转发给执行的二进制文件:zig build run — --help
  • b.installArtifact() 是暴露输出的规范方式;除非有特定需求,否则避免手动文件复制。
  • 默认安装步骤(不带参数的 zig build)安装所有使用 installArtifact() 注册的构件——如果您想要无操作的默认值,请不要安装任何内容。

练习

  • 修改最小示例以硬编码跨编译目标(例如,wasm32-wasi),并使用 file zig-out/bin/hello 验证输出格式。43
  • 扩展模块示例以创建第二个模块 utils,让 math 导入它,演示传递性依赖关系。
  • 向选项示例添加自定义选项 -Dmax-threads=N,并使用它来初始化编译时常量线程池大小。
  • 创建一个具有静态和动态链接模式的库,安装两者,并检查输出文件以查看大小差异。

警告、替代方案、边缘情况

  • Zig 0.14.0 引入了 root_module 字段;在 Zig 0.15.2 上,直接在 ExecutableOptions 上使用 root_source_file 的旧代码将失败。
  • 一些项目仍然手动使用 --pkg-begin/--pkg-end 标志而不是模块系统——这些已弃用,应迁移到 Module.addImport()20
  • 构建运行器不支持 build.zig 本身的增量编译——更改 build.zig 会触发完整的图重新评估。
  • If you see "userland" mentioned in documentation, it means the build system is implemented entirely in Zig standard library code, not compiler magic—you can read std.Build source to understand any behavior.

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.