概述
第20章建立了区分模块、程序、包和库的词汇表;本章展示zig init如何将这些词汇引导到实际文件中,以及build.zig.zon如何将包身份、版本约束和依赖元数据编码化,以便构建系统和包管理器能够可靠地解析导入。参见20和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对象表示法)
Sources: lib/std/zig/Parse.zig:192-205, lib/std/zig/Parse.zig:208-228
Required Fields
Every build.zig.zon must define these core fields:
.{
.name = .myproject,
.version = "0.1.0",
.minimum_zig_version = "0.15.2",
.paths = .{""},
.fingerprint = 0xa1b2c3d4e5f60718,
}
.name: A symbol literal (e.g.,.myproject) used as the default dependency key; conventionally lowercase, omitting redundant "zig" prefixes since the package already lives in the Zig namespace..version: A semantic version string ("MAJOR.MINOR.PATCH") that the package manager will eventually use for deduplication. SemanticVersion.zig.minimum_zig_version: The earliest Zig release that this package supports; older compilers will refuse to build it..paths: An array of file/directory paths (relative to the build root) included in the package’s content hash; only these files are distributed and cached..fingerprint: A 64-bit hexadecimal integer serving as the package’s globally unique identifier, generated once by the toolchain and never changed (except in hostile fork scenarios).
The following demo shows how these fields map to runtime introspection patterns (though in practice the build runner handles this automatically):
const std = @import("std");
pub fn main() !void {
// Demonstrate parsing and introspecting build.zig.zon fields
// In practice, the build runner handles this automatically
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 Field Demo ---\n", .{});
std.debug.print("Sample ZON structure:\n{s}\n\n", .{zon_example});
std.debug.print("Field explanations:\n", .{});
std.debug.print(" .name: Package identifier (symbol literal)\n", .{});
std.debug.print(" .version: Semantic version string\n", .{});
std.debug.print(" .minimum_zig_version: Minimum supported Zig\n", .{});
std.debug.print(" .fingerprint: Unique package ID (hex integer)\n", .{});
std.debug.print(" .paths: Files included in package distribution\n", .{});
std.debug.print(" .dependencies: External packages required\n", .{});
std.debug.print("\nNote: Zig 0.15.2 uses .fingerprint for unique identity\n", .{});
std.debug.print(" (Previously used UUID-style identifiers)\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 replaced the old UUID-style .id field with the more compact .fingerprint field, simplifying generation and comparison while maintaining global uniqueness guarantees.
Fingerprint: Global Identity and Fork Detection
The .fingerprint field is the linchpin of package identity: it is generated once when you first run zig init, and should never change for the lifetime of the package unless you are deliberately forking it into a new identity.
Changing the fingerprint of an actively maintained upstream project is considered a hostile fork—an attempt to hijack the package’s identity and redirect users to different code. Legitimate forks (where the upstream is abandoned) should regenerate the fingerprint to establish a new identity, while maintaining forks (backports, security patches) preserve the original fingerprint to signal continuity.
const std = @import("std");
pub fn main() !void {
std.debug.print("--- Package Identity Validation ---\n\n", .{});
// Simulate package metadata inspection
const pkg_name = "mylib";
const pkg_version = "1.0.0";
const fingerprint: u64 = 0xabcdef1234567890;
std.debug.print("Package: {s}\n", .{pkg_name});
std.debug.print("Version: {s}\n", .{pkg_version});
std.debug.print("Fingerprint: 0x{x}\n\n", .{fingerprint});
// Validate semantic version format
const version_valid = validateSemVer(pkg_version);
std.debug.print("Version format valid: {}\n", .{version_valid});
// Check fingerprint uniqueness
std.debug.print("\nFingerprint ensures:\n", .{});
std.debug.print(" - Globally unique package identity\n", .{});
std.debug.print(" - Unambiguous version detection\n", .{});
std.debug.print(" - Fork detection (hostile vs. legitimate)\n", .{});
std.debug.print("\nWARNING: Changing fingerprint of a maintained project\n", .{});
std.debug.print(" is considered a hostile fork attempt!\n", .{});
}
fn validateSemVer(version: []const u8) bool {
// Simplified validation: check for X.Y.Z format
var parts: u8 = 0;
for (version) |c| {
if (c == '.') parts += 1;
}
return parts == 2; // Must have exactly 2 dots
}
$ zig run fingerprint_demo.zig=== Package Identity Validation ===
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!The inline comment // Changing this has security and trust implications. in the generated .zon file is deliberately preserved to surface during code review if someone modifies the fingerprint without understanding the consequences.
Dependencies: Remote, Local, and Lazy
The .dependencies field is a struct literal mapping dependency names to fetch specifications; each entry is either a remote URL dependency, a local filesystem path dependency, or a lazily-fetched optional dependency.
Annotated Dependency Examples
.{
// Package name: used as key in dependency tables
// Convention: lowercase, no "zig" prefix (redundant in Zig namespace)
.name = .mylib,
// Semantic version for package deduplication
.version = "1.2.3",
// Globally unique package identifier
// Generated once by toolchain, then never changes
// Allows unambiguous detection of package updates
.fingerprint = 0xa1b2c3d4e5f60718,
// Minimum supported Zig version
.minimum_zig_version = "0.15.2",
// External dependencies
.dependencies = .{
// Remote dependency with URL and hash
.example_remote = .{
.url = "https://github.com/user/repo/archive/tag.tar.gz",
// Multihash format: source of truth for package identity
.hash = "1220abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678",
},
// Local path dependency (no hash needed)
.example_local = .{
.path = "../sibling-package",
},
// Lazy dependency: only fetched if actually used
.example_lazy = .{
.url = "https://example.com/optional.tar.gz",
.hash = "1220fedcba0987654321fedcba0987654321fedcba0987654321fedcba098765",
.lazy = true,
},
},
// Files included in package hash
// Only these files/directories are distributed
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
"README.md",
},
}
- Remote dependencies specify
.url(a tarball/zip archive location) and.hash(a multihash-format content hash). The hash is the source of truth: even if the URL changes or mirrors are added, the package identity remains tied to the hash. - Local dependencies specify
.path(a relative directory from the build root). No hash is computed because the filesystem is the authority; this is useful for monorepo layouts or during development before publishing. - Lazy dependencies add
.lazy = trueto defer fetching until the dependency is actually imported by a build script. This reduces bandwidth for optional features or platform-specific code paths.
Dependency Types in Practice
const std = @import("std");
pub fn main() !void {
std.debug.print("--- Dependency Types Comparison ---\n\n", .{});
// Demonstrate different dependency specification patterns
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("Key differences:\n", .{});
std.debug.print(" - Remote: Uses hash as source of truth\n", .{});
std.debug.print(" - Local: Direct filesystem path\n", .{});
std.debug.print(" - Lazy: Only fetched when actually imported\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 Types Comparison ===
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 importedUse local paths during active development across multiple packages in the same workspace, then switch to remote URLs with hashes when publishing for external consumers. 24
Chapter 24 revisits these concepts in depth by walking through a package resolution pipeline that starts from build.zig.zon. 24
Paths: Controlling Package Distribution
The .paths field specifies which files and directories are included when computing the package hash and distributing the package; everything not listed is excluded from the cached artifact.
Typical patterns:
.paths = .{
"build.zig", // Build script is always needed
"build.zig.zon", // Metadata file itself
"src", // Source code directory (recursive)
"LICENSE", // Legal requirement
"README.md", // Documentation
}Listing a directory includes all files within it recursively; listing the empty string "" includes the build root itself (equivalent to listing every file individually, which is rarely desired).
Exclude generated artifacts (zig-cache/, zig-out/), large assets not needed for compilation, and internal development tools from .paths to keep package downloads small and deterministic.
Under the hood: ZON files in dependency tracking
The compiler’s incremental dependency tracker treats ZON files as a distinct dependee category alongside source hashes, embedded files, and declaration-based dependencies. The core storage is an InternPool that owns multiple maps into a shared dep_entries array:
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
Minimum Zig Version: Compatibility Bounds
The .minimum_zig_version field declares the earliest Zig release that the package can build with; older compilers will refuse to proceed, preventing silent miscompilations due to missing features or changed semantics.
When the language stabilizes at 1.0.0, this field will interact with semantic versioning to provide compatibility guarantees; before 1.0.0, it serves as a forward-looking compatibility declaration even though breaking changes happen every release.
Version: Semantic Versioning for Deduplication
The .version field currently documents the package’s semantic version but does not yet enforce compatibility ranges or automatic deduplication; that functionality is planned for post-1.0.0 when the language stabilizes.
Follow semantic versioning conventions:
- MAJOR: Increment for incompatible API changes
- MINOR: Increment for backward-compatible feature additions
- PATCH: Increment for backward-compatible bug fixes
This discipline will pay off once the package manager can auto-resolve compatible versions within dependency trees. 24
Practical Workflow: From Init to First Build
A typical project initialization sequence looks like this:
$ 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.At this point, you have:
A reusable module (
src/root.zig) exposingbufferedPrintandaddAn executable (
src/main.zig) importing and using the moduleTests for both the module and executable
Package metadata (
build.zig.zon) ready for publishing
To share your module with other packages, you would publish the repository with a tagged release, document the URL and hash, and consumers would add it to their .dependencies table.
Notes & Caveats
- The fingerprint is generated from a random seed; regenerating
build.zig.zonwill produce a different fingerprint unless you preserve the original. - Changing
.namedoes not change the fingerprint; the name is a convenience alias while the fingerprint is the identity. - Local path dependencies bypass the hash-based content addressing entirely; they are trusted based on filesystem state at build time.
- The package manager caches fetched dependencies in a global cache directory; subsequent builds with the same hash skip re-downloading.
Exercises
- Run
zig initin a new directory, then modifybuild.zig.zonto add a fake remote dependency with a placeholder hash; observe the error when runningzig build --fetch. - Create two packages in sibling directories, configure one as a local path dependency of the other, and verify that changes in the dependency are immediately visible without re-fetching.
- Generate a
build.zig.zonwithzig init --minimal, then manually add a.dependenciestable and compare the resulting structure with the annotated example in this chapter. - Fork a hypothetical package by regenerating the fingerprint (delete the field and run
zig build), then document in a README why this is a new identity rather than a hostile takeover.
Caveats, alternatives, edge cases
- If you omit
.paths, the package manager may include unintended files in the distribution, inflating download size and exposing internal implementation details. - Remote dependency URLs can become stale if the host moves or removes the archive; consider mirroring critical dependencies or using content-addressed storage systems. 24
- The
zig fetch --save <url>command automates adding a remote dependency to.dependenciesby downloading, hashing, and inserting the correct entry—use it instead of hand-typing hashes. - Lazy dependencies require build script cooperation: if your
build.zigunconditionally references a lazy dependency without checking availability, the build will fail with a "dependency not available" error.