Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

包管理与确定性缓存:build.zig.zon 与缓存布局

Zig 的包管理与增量缓存均基于 内容寻址(Content-Addressed) 机制设计。依赖包的校验与存放、构建中间步骤的缓存命中以及最终产物的复用,均由输入内容的唯一哈希决定,以保证构建结果的确定性与可重复性。


1. 包清单声明:build.zig.zon 与依赖引入方式

Zig 采用 ZON (Zig Object Notation) 语法声明包元数据。ZON 是 Zig 匿名结构体字面量的数据序列化格式,与 Zig 代码语法保持一致。

典型 build.zig.zon 结构

.{
    .name = .my_project,
    .version = "0.1.0",
    .fingerprint = 0xd58b8f2d4e195cc9, // 唯一指纹标识
    .minimum_zig_version = "0.17.0",

    .dependencies = .{
        // 1. 远程归档依赖(URL + 哈希)
        .network = .{
            .url = "https://github.com/MasterQ32/zig-network/archive/refs/tags/v0.1.0.tar.gz",
            .hash = "1220a1b2c3d4e5f67890abcdef...",
        },

        // 2. 本地路径依赖
        .local_utils = .{
            .path = "../shared-utils",
        },

        // 3. Git 仓库依赖
        .zlog = .{
            .url = "git+https://github.com/jiacai2050/zlog.git#v0.2.0",
            .hash = "1220456789abcdef0123...",
        },

        // 4. 惰性依赖(按需拉取)
        .heavy_assets = .{
            .url = "https://example.com/assets.tar.gz",
            .hash = "1220987654321fedcba0...",
            .lazy = true,
        },
    },

    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
        "LICENSE",
        "README.md",
    },
}

四种依赖引入方式对比

引入方式声明字段是否需要 .hash典型应用场景
远程归档.url = "https://..."是生产环境第三方开源库、Release 发布包
本地路径.path = "../path"否Monorepo 多包协同、本地模块解耦开发、离线调试
Git 直连.url = "git+https://...#ref"是直接锁定 GitHub/GitLab 仓库的分支、Tag 或 Commit
惰性依赖追加 .lazy = true是仅在特定平台或特定编译选项下才需要的巨型依赖

1. 远程归档(URL + Hash)

  • 支持 .tar.gz、.tar.xz、.tar、.zip 等格式;
  • 必须提供 .hash。该哈希是包解压后所有源码文件按目录树规则计算得出的 Multihash(通常以 1220 开头,代表 SHA-256 算法与 32 字节哈希值)。构建系统在下载后会校验此哈希,确保内容未被修改。

2. 本地相对路径(Path)

  • 指向相对于当前 build.zig.zon 文件的本地目录;
  • 无需声明 .hash:本地依赖通常处于修改和调试过程中,Zig 会直接追踪该路径下的文件变更,并在相关源码改动时自动触发增量编译。

3. 惰性依赖(.lazy = true)

  • 默认情况下,zig build 会在执行前解析并下载 dependencies 中的所有依赖项;
  • 若设置 .lazy = true,仅当构建脚本中通过 b.dependencyLazy("heavy_assets", .{}) 显式请求时才会触发下载。常用于跨平台条件依赖(例如特定系统的二进制预编译库),避免在其他平台上下载不必要的大文件;
  • 关于运行器如何按需探测并重新触发下载的底层机制,详见 构建自举与双进程:Maker 与 Configurer 架构流转 - 5. 惰性依赖的自动重试机制。

4. .paths 的作用

.paths 声明了当前项目被其他工程作为依赖引入(或计算当前包哈希)时包含的文件与目录列表。未包含在 .paths 中的临时文件、日志或测试产物不会影响最终生成的包哈希。


2. 依赖管理与更新:zig fetch

手动下载压缩包、计算哈希并编写 build.zig.zon 相对繁琐,Zig 提供了 zig fetch 命令行工具来自动化这一流程。

常用命令

# 1. 仅下载包并打印其内容哈希(不修改 build.zig.zon)
zig fetch https://github.com/MasterQ32/zig-network/archive/refs/tags/v0.1.0.tar.gz

# 2. 下载并将依赖追加到 build.zig.zon(字段名由 URL 或包名推导)
zig fetch --save https://github.com/MasterQ32/zig-network/archive/refs/tags/v0.1.0.tar.gz

# 3. 指定依赖在 build.zig.zon 中的字段名称
zig fetch --save=network https://github.com/MasterQ32/zig-network/archive/refs/tags/v0.1.0.tar.gz

# 4. 将本地路径保存到依赖清单中
zig fetch --save=my_lib ../libs/my_lib

# 5. 精确保存原始 URL(不进行规范化压缩)
zig fetch --save-exact=zlog git+https://github.com/jiacai2050/zlog.git#main

依赖版本更新工作流

当第三方依赖需要升级时,常见有两种操作方式:

  1. 命令行自动更新:

    zig fetch --save=network https://github.com/MasterQ32/zig-network/archive/refs/tags/v0.2.0.tar.gz
    

    zig fetch 会下载新的归档文件,计算新哈希,并更新 build.zig.zon 中对应的 url 与 hash 字段。

  2. 通过编译器报错获取新哈希: 在 build.zig.zon 中将 url 修改为新地址,并将 hash 字段设置为空字符串 ""(或保留旧哈希)。直接运行 zig build,编译器会报错并输出实际计算出的哈希:

    error: url '...' has hash '1220b2...', but expected hash '1220a1...'
    

    将输出中的实际哈希复制回 build.zig.zon 即可。


3. 缓存体系:本地缓存与全局缓存

Zig 采用分层的缓存布局,分为面向单项目的本地缓存和跨项目共享的全局缓存:

graph TD
    subgraph S_Global ["全局缓存 (Global Cache)"]
        G_Pkg["p/ (只读依赖源码包,内容哈希索引)"]
        G_Libc["libc/ (平台头文件与预编译库)"]
        G_Z["z/ (编译器通用中间层缓存)"]
    end

    subgraph S_Local ["项目本地缓存 (Local Cache: .zig-cache/)"]
        L_H["h/ (Manifest 构建步骤元数据清单)"]
        L_O["o/ (Step 产物仓库,独立哈希隔离)"]
        L_Tmp["tmp/ (原子写入与暂存区)"]
    end

    subgraph S_Out ["最终交付目录 (zig-out/)"]
        Out_Bin["bin/ (可执行二进制)"]
        Out_Lib["lib/ (静态库与动态库)"]
        Out_Inc["include/ (导出头文件树)"]
    end

    G_Pkg -- "引入依赖源码" --> L_H
    L_H -- "验证命中/失误" --> L_O
    L_Tmp -- "原子提升" --> L_O
    L_O -- "硬链接/快速复制" --> Out_Bin
    L_O -- "硬链接/快速复制" --> Out_Lib
    L_O -- "硬链接/快速复制" --> Out_Inc

    classDef default stroke:#495057;
    style S_Global stroke:#ff9900,stroke-width:2px;
    style S_Local stroke:#0066cc,stroke-width:2px;
    style S_Out stroke:#009900,stroke-width:2px;
    style G_Pkg stroke:#ff9900,stroke-width:2px;
    style G_Libc stroke:#ff9900,stroke-width:2px;
    style G_Z stroke:#ff9900,stroke-width:2px;
    style L_H stroke:#ffc107,stroke-width:2px;
    style L_O stroke:#0066cc,stroke-width:2px;
    style L_Tmp stroke:#495057,stroke-width:2px;
    style Out_Bin stroke:#009900,stroke-width:2px;
    style Out_Lib stroke:#009900,stroke-width:2px;
    style Out_Inc stroke:#009900,stroke-width:2px;

1. 全局缓存(Global Cache)

  • 默认路径:
    • Linux: ~/.cache/zig(遵循 XDG 规范)
    • macOS: ~/Library/Caches/zig
    • Windows: %LOCALAPPDATA%\zig
  • 主要内容:
    • p/(Package 缓存):通过网络获取的依赖包均解压在此处,目录名即为其 Multihash(如 p/1220a1b2c3...)。多工程若引用相同版本依赖,全局只保留一份解压内容,避免重复下载与占用空间;
    • libc/:Zig 为目标系统提取的 libc 符号集与头文件;
    • z/:编译器的中间层共享数据。

2. 项目本地缓存(.zig-cache/)

位于项目根目录下,通常加入 .gitignore:

  • c/(Configuration 构建图配置缓存): 持久化存放已序列化的二进制构建图配置(.zig-cache/c/{digest})。若输入参数与构建文件未变动且未受污染,Maker 直接读取此配置,跳过派生 configurer 进程;
  • h/(Manifest 清单记录): 持久化记录每个 Step 的输入依赖元数据,采用连续紧凑的二进制格式存储,支持直接内存映射高效比对;
  • o/(Object 编译产物): 每个 Step 独立生成的二进制文件、编译对象(.o)及代码生成结果。每个构建配置与源码状态对应一个唯一的 32 位 Hex 子目录(如 o/a4f3b890.../),不同 Target、不同 Optimize 模式互不干扰;
  • tmp/(临时工作区): 编译和写盘过程中的临时文件暂存区。

4. 增量构建的缓存判定机制

Zig 构建引擎通过 std.Build.Cache 维护两级哈希比对与 Manifest 校验,以此判定 Step 是否命中缓存。

graph TD
    subgraph S_Inputs ["1. 输入与配置计算"]
        Cfg["构建参数与选项<br/>(Target, Optimize, Flags)"]
        Src["输入源码与依赖文件<br/>(src/*.zig, headers)"]
        Hash1["配置哈希计算<br/>(bin_digest)"]

        Cfg --> Hash1
    end

    subgraph S_Cache ["2. 缓存匹配与 Manifest 校验"]
        M_Check["读取 .zig-cache/h/<digest><br/>检查 Manifest 记录"]
        Fast_Stat["Inode / Size / Mtime 预检"]
        Sha_Check["SHA-256 内容哈希比对"]

        Hash1 --> M_Check
        Src --> Fast_Stat
        M_Check --> Fast_Stat
        Fast_Stat -- "时间戳变化" --> Sha_Check
    end

    subgraph S_Exec ["3. 执行与原子更新"]
        Hit["缓存命中 (Cache Hit)<br/>跳过编译直接复用"]
        Miss["缓存未命中 (Cache Miss)<br/>执行 Step 编译"]
        Tmp["写入临时目录<br/>.zig-cache/tmp/..."]
        Atomic["原子重命名 (Atomic Rename)<br/>移动至 .zig-cache/o/<digest>/"]
        M_Write["写入新 Manifest<br/>.zig-cache/h/<digest>"]

        Sha_Check -- "内容一致" --> Hit
        Fast_Stat -- "未被修改" --> Hit
        Sha_Check -- "内容变更" --> Miss
        M_Check -- "无记录" --> Miss
        Miss --> Tmp
        Tmp --> Atomic
        Atomic --> M_Write
    end

    subgraph S_Install ["4. 产物交付"]
        Out["投影至 zig-out/<br/>(硬链接 / 快速拷贝)"]

        Hit --> Out
        Atomic --> Out
    end

    classDef default stroke:#495057;
    style S_Inputs stroke:#0066cc,stroke-width:2px;
    style S_Cache stroke:#ff9900,stroke-width:2px;
    style S_Exec stroke:#495057,stroke-width:2px;
    style S_Install stroke:#009900,stroke-width:2px;

    style Cfg stroke:#495057,stroke-width:2px;
    style Src stroke:#495057,stroke-width:2px;
    style Hash1 stroke:#0066cc,stroke-width:2px;
    style M_Check stroke:#ff9900,stroke-width:2px;
    style Fast_Stat stroke:#ffc107,stroke-width:2px;
    style Sha_Check stroke:#ffc107,stroke-width:2px;
    style Hit stroke:#198754,stroke-width:2px;
    style Miss stroke:#dc3545,stroke-width:2px;
    style Tmp stroke:#495057,stroke-width:2px;
    style Atomic stroke:#0066cc,stroke-width:2px;
    style M_Write stroke:#009900,stroke-width:2px;
    style Out stroke:#198754,stroke-width:2px;

4.1 缓存判定流程与原子提交流程

  1. 第一级:配置哈希计算(bin_digest) 当 Step 准备执行时,构建引擎先对其非文件输入进行哈希:

    • 目标架构三元组(Target Triple,如 aarch64-macos);
    • 优化模式(Debug、ReleaseFast、ReleaseSafe、ReleaseSmall);
    • 编译器参数列表(宏定义、头文件搜索路径、链接选项等);
    • 依赖模块的拓扑指纹。 任何参数变更都会产生新的配置哈希,指向不同的缓存条目。
  2. 第二级:输入文件清单校验(Manifest & Fast Check) 配置哈希对应 .zig-cache/h/<digest> 中的 Manifest 记录,里面存储了该步骤上一次执行时的所有输入文件元数据:

    • 元数据快速检查(Fast Check):引擎首先比对文件的 inode、size 和 mtime(最后修改时间)。若属性一致,直接判定未修改;
    • 内容哈希复验(Content Check):当文件修改时间发生变化(例如 git checkout 或编辑器保存触碰了 mtime)但实际内容未变时,Zig 会进一步计算文件的 SHA-256 并与 Manifest 比对。若哈希一致,依然判定为缓存命中(Cache Hit),避免了仅因时间戳变动导致的重复编译。
  3. 第三级:执行与原子提交(Atomic Update)

    • 命中(Hit):Step 标记为 result_cached = true,直接跳过实际编译工作;
    • 未命中(Miss):执行该 Step 的编译或运行任务。产物先写入 .zig-cache/tmp/<uuid> 临时目录。只有当子进程成功退出且退出码为 0 时,引擎才通过操作系统的**原子重命名(Atomic Rename)**将其移动至 .zig-cache/o/<digest>/,并写入新的 Manifest 文件;
    • 异常安全:若构建过程被手动中断(如 Ctrl+C)或编译失败,临时文件仅停留在 tmp/ 中,不会在产物目录 o/ 留下残缺文件。

4.2 二进制 Manifest 与诊断工具 zig cache-cat

缓存记录文件采用紧凑二进制格式存储:

  • 紧凑二进制格式:缓存记录采用连续二进制存储,加载到内存后可直接按结构体指针比对,减少反序列化开销;
  • 支持元数据模式:对静态巨型资源,可配置仅比对 inode/mtime/size 而不进行昂贵的内容哈希比对;
  • 官方反序列化查看工具 zig cache-cat: 由于二进制格式不可直接用文本编辑器阅读,Zig 提供了官方命令将其反序列化为可读的 ZON 数据结构:
    $ zig cache-cat .zig-cache/h/ac2f2b6c157f36769986aff06a3c3b06
    
    输出示例:
    .{
        .input_hash = "ac2f2b6c157f36769986aff06a3c3b06",
        .files = .{
            .{
                .size = 1420,
                .inode = 86241092,
                .mtime = 1728045612,
                .digest = "d5a8b3...",
                .prefix = .cwd,
                .path = "src/main.zig",
            },
        },
        .discovered_hash = "f12c8b74a...",
    }
    
    当遇到意外重编或缓存未命中时,可通过该命令迅速排查导致哈希失效的具体文件。

5. 产物安装:.zig-cache 与 zig-out 的关系

构建项目时,.zig-cache/ 与 zig-out/ 承担着不同的角色:

  • .zig-cache/ 是内部构建缓存: 构建过程中生成的所有中间产物、目标文件与二进制都保存在 .zig-cache/o/<hash>/ 中。这些目录是由哈希构成的扁平化存储,供构建引擎内部索引使用,不建议外部脚本直接依赖该目录结构;
  • zig-out/ 是最终交付目录(Installation Prefix): 当在 build.zig 中调用安装步骤时:
    b.installArtifact(exe);
    lib.installHeadersDirectory(...);
    
    安装步骤会将对应的最终二进制或头文件以硬链接(Hardlink)或复制的形式从 .zig-cache/o/<hash>/ 输出到 zig-out/bin/ 或 zig-out/include/ 中。

因此:

  • 即使删除 zig-out/ 目录,只要 .zig-cache/ 保持完整,重新执行 zig build 时仅需重新建立硬链接或轻量复制即可完成产物输出;
  • 即使清理了项目内的 .zig-cache/,只要全局缓存 ~/.cache/zig/p/ 存在,Zig 仍可以直接使用本地已解压的依赖包,无需重复联网拉取。

6. 本地依赖调试与分叉:--fork 命令行替换

6.1 传统本地调试的局限

在调试第三方依赖(尤其是深层传递依赖)时,传统的做法是在 build.zig.zon 中临时修改依赖声明:

.dependencies = .{
    .zlog = .{
        // 传统方式:手动修改 build.zig.zon 为本地相对路径
        .path = "../zlog",
    },
},

这种方式存在以下维护问题:

  1. 侵入版本库:修改 build.zig.zon 容易因疏忽提交到版本库,导致 CI 或下游消费时因缺少本地路径报错;
  2. 难以修补深层传递依赖:若项目依赖库 A,而库 A 间接依赖库 B。在传统模式下,修补库 B 需要在本地同时克隆库 B 与库 A,修改库 A 的 build.zig.zon,并在主工程中级联替换库 A,维护流程繁琐。

6.2 --fork 的设计与工作原理

Zig 在命令行中提供了原生的包分叉替换机制:

zig build --fork [path]
# 或
zig build --fork=[path]

--fork 允许开发者在命令行中将依赖树中的任意项目临时重定向到本地开发目录,无需修改任何 build.zig.zon:

底层匹配与拦截机制(源码解析)

查看编译器源码 lib/compiler/Maker/Fetch.zig:L595-L608:

  1. 提取项目标识(Project ID): 执行 zig build --fork ../my-patched-lib 时,Maker 进程读取目标本地目录下的 build.zig.zon,提取包名(name)与指纹(fingerprint),生成对应的 Package.ProjectId(内部由 padded_name 和 fingerprint_id 构成,见 lib/compiler/Maker/Package.zig)并存入全局 fork_set 表;
  2. 依赖图遍历与重定向: 当 Maker 遍历解析依赖树时,对每个依赖声明通过 expected_hash.projectId() 查询 fork_set:
    // lib/compiler/Maker/Fetch.zig:L595-L608
    if (remote.hash) |expected_hash| {
        const expected_project_id: Package.ProjectId = expected_hash.projectId();
        if (job_queue.fork_set.getKeyPtrAdapted(expected_project_id, ...)) |fork| {
            log.debug("using fork {f} for {s}", .{ fork.path, fork.manifest.name });
            f.package_root = fork.path; // 重定向为本地目录
            // ...
        }
    }
    
  3. 支持多个分叉选项:
    • 命令行传入 --fork 时,全局缓存与网络下载被跳过,直接使用本地源码执行配置与编译;
    • 支持同时指定多个 --fork 选项进行联合调试:
      zig build --fork ../zlog --fork ../zig-network
      
    • 调试结束后,只需移除命令行中的 --fork 参数,项目即恢复为远程固定版本依赖,版本库中不会残留临时修改。

💡 提示: 本地分叉项目的 build.zig.zon 必须同时与原依赖的包名(name)和指纹(fingerprint)一致。Zig 依靠这两者构成的 Package.ProjectId 定位目标依赖;若任一项不一致,Maker 会报错提示该分叉未被匹配。