概述
第20章确定了区分模块、程序、包和库的词汇表;本章展示了zig init如何将该词汇表引导为实际文件,以及build.zig.zon如何将包身份、版本约束和依赖元数据编纂化,以便构建系统和包管理器可靠地解析导入。参见v0.15.2。
在第22章深入构建图编写之前,我们专注于包元数据结构,确保你理解build.zig.zon中每个字段控制什么,以及为什么Zig的指纹机制取代了早期基于UUID的方案。参见22、build.zig.zon和Build.zig。
学习目标
- 使用
zig init和zig init --minimal为模块、可执行文件和测试搭建具有适当样板的新项目。 - 解释
build.zig.zon中的每个字段:名称、版本、指纹、最小Zig版本、依赖和路径。 - 区分远程依赖(URL + 哈希)、本地依赖(路径)和延迟依赖(延迟获取)。
- 解释指纹如何提供全局唯一的包身份,以及它们如何防止恶意分支的混淆。
使用搭建项目
Zig 0.15.2更新了默认的zig init模板,以鼓励将可重用模块与可执行入口点分离,解决了新手的常见困惑,即库代码被不必要地编译为静态归档,而不是作为纯Zig模块公开。参见build.zig。
默认模板:模块 + 可执行文件
在空目录中运行zig init会生成四个文件,展示了对同时需要可重用模块和CLI工具的项目的推荐模式:
$ mkdir myproject && cd myproject
$ zig init
info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
info: see `zig build --help` for a menu of options生成的结构将关注点分离:
src/root.zig:可重用模块,公开公共API(例如bufferedPrint、add)src/main.zig:可执行文件入口点,导入并使用该模块build.zig:构建图,将模块和可执行文件产物连接起来build.zig.zon:包元数据,包括名称、版本和指纹
这种布局使外部包依赖你的模块变得很简单,无需继承不必要的可执行代码,同时仍为本地开发或分发提供便利的CLI。20
如果你只需要模块或只需要可执行文件,删除不需要的文件并相应地简化build.zig——模板是起点,不是强制要求。
最小模板:面向经验用户的存根
对于了解构建系统并希望最少样板文件的用户,zig init --minimal仅生成build.zig.zon和一个存根build.zig:
$ mkdir minimal-project && cd minimal-project
$ zig init --minimal
info: successfully populated 'build.zig.zon' and 'build.zig'生成的build.zig.zon很紧凑:
.{
.name = .minimal_project,
.version = "0.0.1",
.minimum_zig_version = "0.15.2",
.paths = .{""},
.fingerprint = 0x52714d1b5f619765,
}存根build.zig同样简洁:
const std = @Import("std");
pub fn build(b: *std.Build) void {
_ = b; // stub
}此模式适用于你已经清楚构建策略并希望避免删除样板注释和示例代码的情况。
的剖析
Zig对象表示法(ZON)是用于数据字面量的Zig语法的严格子集;build.zig.zon是构建运行器在调用你的build.zig脚本之前解析以解决包元数据的规范文件。参见zon.zig和Zoir.zig。
ZON文件如何被解析
从解析器的角度来看,.zon清单只是Ast.parse()的另一种模式。分词器在.zig和.zon文件之间共享,但.zig被解析为声明容器,而.zon被解析为单个表达式——这正是build.zig.zon包含的内容。
- Zig模式(
.zig文件):将完整的源文件解析为包含声明的容器 - ZON模式(
.zon文件):解析单个表达式(Zig对象表示法)
来源:lib/std/zig/Parse.zig:192-205、lib/std/zig/Parse.zig:208-228
必需字段
每个build.zig.zon必须定义这些核心字段:
.{
.name = .minimal_project,
.version = "0.0.1",
.minimum_zig_version = "0.15.2",
.paths = .{""},
.fingerprint = 0x52714d1b5f619765,
}.name:用作默认依赖键的符号字面量(例如.myproject);按惯例小写,省略冗余的"zig"前缀,因为包已经在Zig命名空间中。.version:语义版本字符串("MAJOR.MINOR.PATCH"),包管理器最终将使用它进行去重。SemanticVersion.zig.minimum_zig_version:此包支持的最早的Zig版本;较旧的编译器将拒绝构建它。.paths:包含在包内容哈希中的文件/目录路径数组(相对于构建根目录);只分发和缓存这些文件。.fingerprint:作为包全局唯一标识符的64位十六进制整数,由工具链生成一次且永不改变(恶意分叉场景除外)。
以下演示展示了这些字段如何映射到运行时内省模式(尽管在实践中构建运行器会自动处理):
const std = @Import("std");
pub fn main() !void {
// 演示解析和内省 build.zig.zon 字段
// 在实践中,构建运行器会自动处理这个
const zon_example =
\\.{
\\ .name = .demo,
\\ .version = "0.1.0",
\\ .minimum_zig_version = "0.15.2",
\\ .fingerprint = 0x1234567890abcdef,
\\ .paths = .{"build.zig", "src"},
\\ .dependencies = .{},
\\}
;
std.debug.print("--- build.zig.zon 字段演示 ---\n", .{});
std.debug.print("示例 ZON 结构:\n{s}\n\n", .{zon_example});
std.debug.print("字段说明:\n", .{});
std.debug.print(" .name: 包标识符(符号字面量)\n", .{});
std.debug.print(" .version: 语义版本字符串\n", .{});
std.debug.print(" .minimum_zig_version: 最低支持的 Zig 版本\n", .{});
std.debug.print(" .fingerprint: 唯一包 ID(十六进制整数)\n", .{});
std.debug.print(" .paths: 包含在包分发中的文件\n", .{});
std.debug.print(" .dependencies: 所需的外部包\n", .{});
std.debug.print("\n注意:Zig 0.15.2 使用 .fingerprint 作为唯一标识\n", .{});
std.debug.print(" (之前使用 UUID 样式的标识符)\n", .{});
}
$ zig run zon_field_demo.zig=== build.zig.zon Field Demo ===
Sample ZON structure:
.{
.name = .demo,
.version = "0.1.0",
.minimum_zig_version = "0.15.2",
.fingerprint = 0x1234567890abcdef,
.paths = .{"build.zig", "src"},
.dependencies = .{},
}
Field explanations:
.name: Package identifier (symbol literal)
.version: Semantic version string
.minimum_zig_version: Minimum supported Zig
.fingerprint: Unique package ID (hex integer)
.paths: Files included in package distribution
.dependencies: External packages required
Note: Zig 0.15.2 uses .fingerprint for unique identity
(Previously used UUID-style identifiers)Zig 0.15.2用更紧凑的.fingerprint字段替换了旧的UUID样式.id字段,简化了生成和比较,同时保持了全局唯一性保证。
指纹:全局身份和分叉检测
.fingerprint字段是包身份的关健:它在你首次运行zig init时生成一次,在包的整个生命周期内不应改变,除非你故意将其分叉为新身份。
改变正在积极维护的上游项目的指纹被认为是恶意分叉——试图劫持包的身份并重定向用户到不同的代码。合法分叉(上游被放弃的情况下)应该重新生成指纹以建立新身份,而维护分叉(反向移植、安全补丁)应保留原始指纹以表示连续性。
const std = @Import("std");
pub fn main() !void {
std.debug.print("--- 包身份验证 ---\n\n", .{});
// 模拟包元数据检查
const pkg_name = "mylib";
const pkg_version = "1.0.0";
const fingerprint: u64 = 0xabcdef1234567890;
std.debug.print("包:{s}\n", .{pkg_name});
std.debug.print("版本:{s}\n", .{pkg_version});
std.debug.print("指纹:0x{x}\n\n", .{fingerprint});
// 验证语义版本格式
const version_valid = validateSemVer(pkg_version);
std.debug.print("版本格式有效:{}\n", .{version_valid});
// 检查指纹唯一性
std.debug.print("\n指纹确保:\n", .{});
std.debug.print(" - 全局唯一包身份\n", .{});
std.debug.print(" - 明确的版本检测\n", .{});
std.debug.print(" - 分叉检测(恶意 vs. 合法)\n", .{});
std.debug.print("\n警告:改变维护项目的指纹\n", .{});
std.debug.print(" 被认为是恶意分叉尝试!\n", .{});
}
fn validateSemVer(version: []const u8) bool {
// 简化验证:检查 X.Y.Z 格式
var parts: u8 = 0;
for (version) |c| {
if (c == '.') parts += 1;
}
return parts == 2; // 必须恰好有 2 个点
}
$ zig run fingerprint_demo.zig=== 包身份验证 ===
Package: mylib
Version: 1.0.0
Fingerprint: 0xabcdef1234567890
Version format valid: true
Fingerprint ensures:
- Globally unique package identity
- Unambiguous version detection
- Fork detection (hostile vs. legitimate)
WARNING: Changing fingerprint of a maintained project
is considered a hostile fork attempt!生成.zon文件中的内联注释// Changing this has security and trust implications.被故意保留,以便在代码审查时如果有人不理解后果就修改指纹时能浮现出来。
依赖:远程、本地和延迟
.dependencies字段是一个结构字面量,将依赖名称映射到获取规范;每个条目要么是远程URL依赖,要么是本地文件系统路径依赖,要么是延迟获取的可选依赖。
带注释的依赖示例
.{
// 包名称:用作依赖表中的键
// 约定:小写,无"zig"前缀(在Zig命名空间中冗余)
.name = .mylib,
// 用于包去重的语义版本
.version = "1.2.3",
// 全局唯一包标识符
// 由工具链生成一次,然后永不改变
// 允许明确检测包更新
.fingerprint = 0xa1b2c3d4e5f60718,
// 最低支持的 Zig 版本
.minimum_zig_version = "0.15.2",
// 外部依赖
.dependencies = .{
// 具有 URL 和哈希的远程依赖
.example_remote = .{
.url = "https://github.com/user/repo/archive/tag.tar.gz",
// 多哈希格式:包身份的真实来源
.hash = "1220abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678",
},
// 本地路径依赖(不需要哈希)
.example_local = .{
.path = "../sibling-package",
},
// 延迟依赖:仅在实际使用时才获取
.example_lazy = .{
.url = "https://example.com/optional.tar.gz",
.hash = "1220fedcba0987654321fedcba0987654321fedcba0987654321fedcba098765",
.lazy = true,
},
},
// 包含在包哈希中的文件
// 仅分发这些文件/目录
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
"README.md",
},
}
- 远程依赖指定
.url(压缩包/zip归档位置)和.hash(多哈希格式的内容哈希)。哈希是真实来源:即使URL更改或添加镜像,包身份仍与哈希绑定。 - 本地依赖指定
.path(相对于构建根目录的目录)。不计算哈希,因为文件系统是权威;这对于单仓库布局或在发布前的开发期间很有用。 - 延迟依赖添加
.lazy = true以延迟获取,直到构建脚本实际导入依赖。这减少了可选功能或平台特定代码路径的带宽。
实践中的依赖类型
const std = @Import("std");
pub fn main() !void {
std.debug.print("--- 依赖类型比较 ---\n\n", .{});
// 演示不同的依赖规范模式
const deps = [_]Dependency{
.{
.name = "remote_package",
.kind = .{ .remote = .{
.url = "https://example.com/pkg.tar.gz",
.hash = "122012345678...",
} },
.lazy = false,
},
.{
.name = "local_package",
.kind = .{ .local = .{
.path = "../local-lib",
} },
.lazy = false,
},
.{
.name = "lazy_optional",
.kind = .{ .remote = .{
.url = "https://example.com/opt.tar.gz",
.hash = "1220abcdef...",
} },
.lazy = true,
},
};
for (deps, 0..) |dep, i| {
std.debug.print("Dependency {d}: {s}\n", .{ i + 1, dep.name });
std.debug.print(" Type: {s}\n", .{@tagName(dep.kind)});
std.debug.print(" Lazy: {}\n", .{dep.lazy});
switch (dep.kind) {
.remote => |r| {
std.debug.print(" URL: {s}\n", .{r.url});
std.debug.print(" Hash: {s}\n", .{r.hash});
std.debug.print(" (Fetched from network, cached locally)\n", .{});
},
.local => |l| {
std.debug.print(" Path: {s}\n", .{l.path});
std.debug.print(" (No hash needed, relative to build root)\n", .{});
},
}
std.debug.print("\n", .{});
}
std.debug.print("关键差异:\n", .{});
std.debug.print(" - 远程:使用哈希作为真实来源\n", .{});
std.debug.print(" - 本地:直接文件系统路径\n", .{});
std.debug.print(" - 延迟:仅在实际导入时获取\n", .{});
}
const Dependency = struct {
name: []const u8,
kind: union(enum) {
remote: struct {
url: []const u8,
hash: []const u8,
},
local: struct {
path: []const u8,
},
},
lazy: bool,
};
$ zig run dependency_types.zig=== 依赖类型比较 ===
Dependency 1: remote_package
Type: remote
Lazy: false
URL: https://example.com/pkg.tar.gz
Hash: 122012345678...
(Fetched from network, cached locally)
Dependency 2: local_package
Type: local
Lazy: false
Path: ../local-lib
(No hash needed, relative to build root)
Dependency 3: lazy_optional
Type: remote
Lazy: true
URL: https://example.com/opt.tar.gz
Hash: 1220abcdef...
(Fetched from network, cached locally)
Key differences:
- Remote: Uses hash as source of truth
- Local: Direct filesystem path
- Lazy: Only fetched when actually imported在同一工作区的多个包之间进行积极开发时使用本地路径,然后在为外部消费者发布时切换到带有哈希的远程URL。24
第24章将通过从头开始build.zig.zon的包解析流程来深入重温这些概念。24
路径:控制包分发
.paths字段指定在计算包哈希和分发包时包含哪些文件和目录;未列出的所有内容都从缓存产物中排除。
典型模式:
.paths = .{
"build.zig", // 构建脚本始终需要
"build.zig.zon", // 元数据文件本身
"src", // 源代码目录(递归)
"LICENSE", // 法律要求
"README.md", // 文档
}列出目录会递归包含其中的所有文件;列出空字符串""包括构建根目录本身(相当于单独列出每个文件,这很少被需要)。
从.paths中排除生成产物(zig-cache/、zig-out/)、编译不需要的大资产和内部开发工具,以保持包下载小且确定性。
深入内部:依赖跟踪中的ZON文件
编译器的增量依赖跟踪器将ZON文件视为与源哈希、嵌入文件和基于声明的依赖不同的被依赖项类别。核心存储是InternPool,它拥有多个映射到一个共享dep_entries数组:
The dependency tracking system uses multiple hash maps to look up dependencies by different dependee types. All maps point into a shared dep_entries array, which stores the actual DepEntry structures forming linked lists of dependencies.
Sources: src/InternPool.zig:34-85
Each category tracks a different kind of dependee:
| Dependee Type | Map Name | Key Type | When Invalidated |
|---|---|---|---|
| Source Hash | src_hash_deps | TrackedInst.Index | ZIR instruction body changes |
| Nav Value | nav_val_deps | Nav.Index | Declaration value changes |
| Nav Type | nav_ty_deps | Nav.Index | Declaration type changes |
| Interned Value | interned_deps | Index | Function IES changes, container type recreated |
| ZON File | zon_file_deps | FileIndex | ZON file imported via @import changes |
| Embedded File | embed_file_deps | EmbedFile.Index | File content accessed via @embedFile changes |
| Full Namespace | namespace_deps | TrackedInst.Index | Any name added/removed in namespace |
| Namespace Name | namespace_name_deps | NamespaceNameKey | Specific name existence changes |
| Memoized State | memoized_state_*_deps | N/A (single entry) | Compiler state fields change |
Sources: src/InternPool.zig:34-71
最低Zig版本:兼容性边界
.minimum_zig_version字段声明包可以构建的最早的Zig版本;较旧的编译器将拒绝继续,防止由于缺失功能或语义更改而导致的静默错误编译。
当语言在1.0.0稳定时,此字段将与语义版本化交互以提供兼容性保证;在1.0.0之前,尽管每次发布都有破坏性更改,它仍作为前瞻性兼容性声明。
版本:用于去重的语义版本化
.version字段目前记录包的语义版本,但尚未强制执行兼容性范围或自动去重;该功能计划在语言稳定后的1.0.0之后实现。
遵循语义版本化约定:
- 主版本:不兼容的API更改时递增
- 次版本:向后兼容的功能添加时递增
- 修订版本:向后兼容的错误修复时递增
一旦包管理器可以在依赖树中自动解析兼容版本,这种规范将得到回报。24
实用工作流:从初始化到首次构建
典型的项目初始化序列如下:
$ mkdir mylib && cd mylib
$ zig init
info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
$ zig build
$ zig build test
All 3 tests passed.
$ zig build run
All your codebase are belong to us.
Run `zig build test` to run the tests.此时,你拥有:
可重用模块(
src/root.zig),公开bufferedPrint和add可执行文件(
src/main.zig),导入并使用该模块模块和可执行文件的测试
准备发布的包元数据(
build.zig.zon)
要与他人共享你的模块,你需要发布带有标签发布的仓库,记录URL和哈希,消费者会将其添加到他们的.dependencies表中。
注意事项和警告
- 指纹是从随机种子生成的;重新生成
build.zig.zon会产生不同的指纹,除非你保留原始指纹。 - 更改
.name不会改变指纹;名称是便利别名,而指纹是身份。 - 本地路径依赖完全绕过基于哈希的内容寻址;它们基于构建时的文件系统状态被信任。
- 包管理器在全球缓存目录中缓存获取的依赖;具有相同哈希的后续构建会跳过重新下载。
练习
- 在新目录中运行
zig init,然后修改build.zig.zon以添加带有占位符哈希的虚假远程依赖;观察运行zig build --fetch时的错误。 - 在兄弟目录中创建两个包,将一个配置为另一个的本地路径依赖,并验证依赖中的更改无需重新获取即可立即可见。
- 使用
zig init --minimal生成build.zig.zon,然后手动添加.dependencies表,并将结果结构与本章中的带注释示例进行比较。 - 通过重新生成指纹(删除该字段并运行
zig build)分叉一个假设包,然后在README中记录为什么这是新身份而不是恶意接管。
警告、替代方案和边缘情况
- 如果你省略
.paths,包管理器可能会在分发中包含意外文件,增加下载大小并暴露内部实现细节。 - 如果主机移动或删除归档,远程依赖URL可能会过时;考虑镜像关键依赖或使用内容寻址存储系统。24
zig fetch --save <url>命令通过下载、哈希和插入正确条目来自动将远程依赖添加到.dependencies——使用它而不是手动输入哈希。- 延迟依赖需要构建脚本配合:如果你的
build.zig无条件引用延迟依赖而不检查可用性,构建将失败并出现"依赖不可用"错误。