Chapter 22Build System Deep Dive

构建系统深入探讨

概述

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

// Minimal build.zig: single executable, no options
// 演示Zig构建系统最简单的构建脚本
// Demonstrates the simplest possible build script for the Zig build system.
pub fn build(b: *std.Build) void {
    // Create an executable compilation step with minimal configuration.
    // 创建最小配置的可执行文件编译步骤
    // This represents the fundamental pattern for producing a binary artifact.
    const exe = b.addExecutable(.{
        // The output binary name (becomes "hello" or "hello.exe")
        // 输出二进制文件名(成为"hello"或"hello.exe")
        .name = "hello",
        // Configure the root module with source file and compilation settings
        // 使用源文件和编译设置配置根模块
        .root_module = b.createModule(.{
            // Specify the entry point source file relative to build.zig
            // 指定相对于build.zig的入口源文件
            .root_source_file = b.path("main.zig"),
            // Target the host machine (the system running the build)
            // 目标主机(运行构建的系统)
            .target = b.graph.host,
            // Use Debug optimization level (no optimizations, debug symbols included)
            // 使用Debug优化级别(无优化,包含调试符号)
            .optimize = .Debug,
        }),
    });

    // Register the executable to be installed to the output directory.
    // 注册要安装到输出目录的可执行文件
    // When `zig build` runs, this artifact will be copied to zig-out/bin/
    b.installArtifact(exe);
}
Zig
// Entry point for a minimal Zig build system example.
// 最小Zig构建系统示例的入口点
// This demonstrates the simplest possible Zig program structure that can be built
// 展示了可以使用Zig构建系统构建的最简单的Zig程序结构
// 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(.{})返回一个遵循-Dtarget=标志的std.Build.ResolvedTarget,允许用户在不修改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(.{})返回一个遵循-Doptimize=标志的std.builtin.OptimizeMode,其值为.Debug.ReleaseSafe.ReleaseFast.ReleaseSmall

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

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

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

graph TB subgraph "优化模式影响" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["运行时安全检查"] OptMode --> DebugInfo["调试信息"] OptMode --> CodegenStrategy["代码生成策略"] OptMode --> LLVMOpt["LLVM优化级别"] SafetyChecks --> Overflow["整数溢出检查"] SafetyChecks --> Bounds["边界检查"] SafetyChecks --> Null["空指针检查"] SafetyChecks --> Unreachable["不可达断言"] DebugInfo --> StackTraces["堆栈跟踪"] DebugInfo --> DWARF["DWARF调试信息"] DebugInfo --> LineInfo["源行信息"] CodegenStrategy --> Inlining["内联启发式"] CodegenStrategy --> Unrolling["循环展开"] CodegenStrategy --> Vectorization["SIMD向量化"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + 安全"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end

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

带标准选项的完整示例

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

// Demonstrating standardTargetOptions and standardOptimizeOption
// 演示standardTargetOptions和standardOptimizeOption
pub fn build(b: *std.Build) void {
    // Allows user to choose target: zig build -Dtarget=x86_64-linux
    // 允许用户选择目标:zig build -Dtarget=x86_64-linux
    const target = b.standardTargetOptions(.{});

    // Allows user to choose optimization: zig build -Doptimize=ReleaseFast
    // 允许用户选择优化: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
// 该程序演示如何通过`builtin`模块访问和显示Zig的内置编译信息
// information through the `builtin` module. It's used in the zigbook to teach
// 它在zigbook中用于教读者了解构建系统内省和标准选项
// 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
// 导入builtin模块以访问关于目标平台的编译时信息
// 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
    // 打印目标架构(例如x86_64, aarch64)和操作系统(例如linux, windows)
    // (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)
    // 打印在编译期间指定的优化模式(Debug, ReleaseSafe, ReleaseFast, 或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");

// Demonstrating module creation and imports
// 演示模块创建和导入
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Create a reusable module (public)
    // 创建一个可重用的模块(公开)
    const math_mod = b.addModule("math", .{
        .root_source_file = b.path("math.zig"),
        .target = target,
    });

    // Create the executable with import of the module
    // 创建包含模块导入的可执行文件
    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
// \1\n    // 该模块为zigbook构建系统示例提供基本算术运算

/// 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
// This program demonstrates how to use custom modules in Zig's build system.
// It imports a local "math" module and uses its functions to perform basic arithmetic operations.

// Import the standard library for debug printing capabilities
const std = @import("std");
// Import the custom math module which provides arithmetic operations
const math = @import("math");

// Main entry point demonstrating module usage with basic arithmetic
pub fn main() !void {
    // Define two constant operands for demonstration
    const a = 10;
    const b = 20;
    
    // Print the result of addition using the imported math module
    std.debug.print("{d} + {d} = {d}\n", .{ a, b, math.add(a, b) });
    
    // Print the result of multiplication using the imported math module
    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(.{});

    // Create a static library
    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);

    // Create an executable that links the library
    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
//! Utility module demonstrating exported functions and formatted output.
//! This module is part of the build system deep dive chapter, showing how to create
//! library functions that can be exported and used across different build artifacts.

const std = @import("std");

/// Doubles the input integer value.
/// This function is exported and can be called from C or other languages.
/// Uses the `export` keyword to make it available in the compiled library.
export fn util_double(x: i32) i32 {
    return x * 2;
}

/// Squares the input integer value.
/// This function is exported and can be called from C or other languages.
/// Uses the `export` keyword to make it available in the compiled library.
export fn util_square(x: i32) i32 {
    return x * x;
}

/// Formats a message with an integer value into the provided buffer.
/// This is a public Zig function (not exported) that demonstrates buffer-based formatting.
/// 
/// Returns a slice of the buffer containing the formatted message, or an error if
/// the buffer is too small to hold the formatted output.
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");

// External function declaration: doubles the input integer
// This function is defined in a separate library/object file
extern fn util_double(x: i32) i32;

// External function declaration: squares the input integer
// This function is defined in a separate library/object file
extern fn util_square(x: i32) i32;

// Main entry point demonstrating library linking
// Calls external utility functions to show build system integration
pub fn main() !void {
    // Test value for demonstrating the external functions
    const x: i32 = 7;
    
    // Print the result of doubling x using the external function
    std.debug.print("double({d}) = {d}\n", .{ x, util_double(x) });
    
    // Print the result of squaring x using the external function
    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()addExecutable()一样返回一个*Step.Compile,但它以测试模式编译模块,链接测试运行器并启用仅测试的代码路径。

完整的测试集成示例

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,
    });
    
    // Create tests for the library module
    const lib_tests = b.addTest(.{
        .root_module = lib_mod,
    });
    
    const run_lib_tests = b.addRunArtifact(lib_tests);
    
    // Create a test step
    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&run_lib_tests.step);
    
    // Also create an executable that uses the library
    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.
/// Base case: factorial(0) = factorial(1) = 1
/// Recursive case: factorial(n) = n * factorial(n-1)
pub fn factorial(n: u32) u32 {
    // Base case: 0! and 1! both equal 1
    if (n <= 1) return 1;
    // Recursive case: multiply n by factorial of (n-1)
    return n * factorial(n - 1);
}

// Test: Verify that the factorial of 0 returns 1 (base case)
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)
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)
test "factorial of 1 is 1" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 1), factorial(1));
}
Zig
// \1\n    // 演示模块使用和基本算术的主入口点
// 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 step<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 step"] FMT["test-fmt step"] CASES["test-cases step"] MODULES["test-modules step"] 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 "输出" 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");

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

    // Custom boolean option
    const enable_logging = b.option(
        bool,
        "enable-logging",
        "Enable debug logging",
    ) orelse false;

    // Custom string option
    const app_name = b.option(
        []const u8,
        "app-name",
        "Application name",
    ) orelse "MyApp";

    // Create options module to pass config to code
    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");
// \1\n    // 导入在build.zig中定义的构建时配置选项
const config = @import("config");

/// Entry point of the application demonstrating the use of build options.
/// This function showcases how to access and use configuration values that
/// are set during the build process through the Zig build system.
pub fn main() !void {
    // Display the application name from build configuration
    std.debug.print("Application: {s}\n", .{config.app_name});
    // Display the logging toggle status from build configuration
    std.debug.print("Logging enabled: {}\n", .{config.enable_logging});

    // Conditionally execute debug logging based on build-time configuration
    // This demonstrates compile-time branching using build options
    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 "命令行" CLI["-Ddebug-allocator<br/>-Denable-llvm<br/>-Dversion-string<br/>等。"] end subgraph "build.zig" PARSE["b.option()<br/>解析选项"] OPTIONS["exe_options =<br/>b.addOptions()"] ADD["exe_options.addOption()"] PARSE --> OPTIONS OPTIONS --> ADD end subgraph "生成的模块" BUILD_OPTIONS["build_options<br/>(自动生成)"] CONSTANTS["pub const mem_leak_frames = 4;<br/>pub const have_llvm = true;<br/>pub const version = '0.16.0';<br/>等。"] BUILD_OPTIONS --> CONSTANTS end subgraph "编译器源" 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字段;直接在ExecutableOptions上使用root_source_file的旧代码在Zig 0.15.2上会失败。
  • 一些项目仍然手动使用--pkg-begin/--pkg-end标志,而不是模块系统——这些已被弃用,应迁移到Module.addImport()20
  • 构建运行器不支持build.zig本身的增量编译——更改build.zig会触发完整的图重新评估。
  • 如果你在文档中看到“userland”,这意味着构建系统完全在Zig标准库代码中实现,而不是编译器魔法——你可以阅读std.Build源代码来理解任何行为。

Help make this chapter better.

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