以Zig风格构建你的项目

Lobsters Hottest 工具

摘要

作者详细介绍了构建一个名为bygge-zig的工具,该工具使用Zig构建系统来编译Rust项目,用更少的代码行复制了Cargo的功能,并突出了其中的差异和挑战。

<p><a href="https://lobste.rs/s/a1nops/build_your_project_zig_style">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/17 03:39

# 用 Zig 风格构建你的项目 来源:https://fnordig.de/2026/06/16/build-your-project-zig-style/ 2026年6月16日 · 8分钟阅读 · rust (https://fnordig.de/tagged/rust.html) 过去几个晚上,我一直在捣鼓 Zig (https://ziglang.org/) 的构建系统,因为它最近经历了一次重构 (https://ziglang.org/devlog/2026/?20260213#2026-05-26)。我的计划是:多学点 Zig,深入了解 Zig 的构建系统,并更清楚地理解 Cargo (https://doc.rust-lang.org/cargo/) 是如何构建 Rust 项目的。用非 Rust 的工具来构建 Rust 项目,我已有一些经验 (https://fnordig.de/2020/06/19/build-your-project/)。我之前也接触过一点 Zig,并一直关注它的发展。而 Zig 的构建系统对我而言完全是全新的。于是就有了这个项目:bygge-zig (https://git.fnordig.de/jer/bygge-zig)。它能编译 Rust 代码: ``` src/mozilla/glean$ ls -l Cargo.toml -rw-r--r-- 1 jer staff 616 Jun 15 16:10 Cargo.toml src/mozilla/glean$ zig build --summary line Build Summary: 194/194 steps succeeded src/mozilla/glean$ du -sh .zig-cache 952M .zig-cache src/mozilla/glean$ tree zig-out zig-out ├── bin │ └── uniffi-bindgen └── lib ├── glean ├── glean_core └── uniffi_bindgen ``` 这里构建的是 Glean SDK (https://github.com/mozilla/glean),我在工作中的主要项目。我只用了 374 行 Zig 代码(外加 79 行 Ruby)就实现了 Cargo 用 8 万行 Rust 做的事情。包括编译 Rust 代码、构建 proc-macro,以及构建和运行构建脚本(那些放在某些 crate 顶层目录下的 `build.rs` 文件)。当然,这有些简化了。实际上一个完整的 Rust 构建系统非常复杂。`bygge-zig` 不负责解析和获取依赖,也不负责确定依赖关系或哪些特性应该启用。它只是将构建应有的样子转换成 Zig 构建系统能处理的形式。 Cargo 曾有过“构建计划” (https://doc.rust-lang.org/cargo/reference/unstable.html#build-plan),主要用于收集如何运行构建的信息。后来这个功能被移除了,现在有一种新方法可以获取大部分信息:单元图 (https://doc.rust-lang.org/cargo/reference/unstable.html#unit-graph)(在 Rust Nightly 上可用): ``` $ cargo +nightly build -Z unstable-options --unit-graph | jq . { "version": 1, "units": [ { "pkg_id": "path+file:///home/jer/src/bygge-zig/crates/hello-world#0.1.0", "target": { "kind": [ "bin" ], "crate_types": [ "bin" ], "name": "hello-world", "src_path": "/home/jer/src/bygge-zig/crates/hello-world/src/main.rs", "edition": "2024", "doc": true, "doctest": false, "test": true }, "profile": { "name": "dev", "opt_level": "0", "lto": "false", "codegen_backend": null, "codegen_units": null, "debuginfo": 2, "split_debuginfo": "unpacked", "debug_assertions": true, "overflow_checks": true, "rpath": false, "incremental": true, "panic": "unwind", "strip": { "deferred": "None" } }, "platform": null, "mode": "build", "features": [], "dependencies": [ { "index": 1, "extern_crate_name": "world", "public": false, "noprelude": false, "nounused": false } ] }, ``` 这会输出 Cargo 将在构建中执行的工作单元。但要将其转换成真正可执行的形式,还需要更多工作。例如,示例图中第一个单元生成的 shell 命令是: ``` CARGO_CRATE_NAME=hello_world \ CARGO_PKG_NAME=hello-world \ CARGO_PKG_VERSION=1.0.0 \ rustc /home/jer/src/bygge-zig/crates/hello-world/src/main.rs \ --edition 2024 \ --extern world=target/debug/deps/libworld.rlib \ -L target/debug/deps ``` 这假设 `world` 库已经构建好,并放在 `target/debug/deps/` 目录下。构建时还会设置更多环境变量 (https://doc.rust-lang.org/cargo/reference/environment-variables.html)。Glean SDK(以及扩展的示例项目)在其依赖中都使用了 proc-macro 和构建脚本。它们的构建方式与普通库代码不同:proc-macro 被编译成动态库,在编译器编译时加载;构建脚本 (https://doc.rust-lang.org/cargo/reference/build-scripts.html) 总是针对宿主目标构建,并在 crate 代码编译之前运行。构建脚本会将额外的配置打印到 stdout,由 Cargo 解析并注入到构建的其他部分: ``` $ RUSTC=rustc OUT_DIR=.zig-cache/tmp .zig-cache/o/98128f/rust/build_script_build cargo::rustc-check-cfg=cfg(build_ran) cargo::rustc-cfg=build_ran $ cat .zig-cache/tmp/code.rs pub const NAME: &'static str = "builder"; ``` 这会导致 `--cfg=build_ran` 参数被追加到 `hello-world/src/lib.rs` 的 `rustc` 调用中。并非每个环境变量都会被所有项目读取,也不是每个 `build.rs` 输出行对每个构建都是必须的。构建 Glean SDK 时,我只用了最少的设置。尽管如此,`bygge-zig` 还是能驱动一个相当复杂的构建,并且在 macOS 和 Linux 上都运行良好。 ## 我从 Zig 中学到了什么 在这个项目中,我并没有真正用到 Zig 的特色:没有 `comptime`,没有错误处理,没有 arena 分配器。它绝对比用 C 写类似代码要好,标准库更大,还有一些通用数据类型可用。Zig 的一些高级特性我只触及了皮毛。字符串处理需要分配器,所以必须四处传递,它被嵌入到 `std.Build` 结构中,在 `build.zig` 中可用。内置的反序列化通过 comptime 魔法在后台工作。JSON 解析就利用了这一点(但错误信息很糟糕)。 我真的很习惯 Rust 的所有权系统,不需要考虑谁负责释放数据。Zig 没有这个机制。有时你按值传递,有时按引用传递。但如果是后者,你现在有责任释放它吗?这由文档说明,而不是由类型来保证。在 `bygge-zig` 中,我干脆不释放内存。因为它是一个短时间运行的程序,所有内存在结束时自然释放。 Zig 要求所有代码无警告。我也是这么希望的——当代码完成时。但未使用的变量是开发过程中的一部分,我更希望在这种情况下构建仍然能通过。这是不同的理念。同时,Zig 是懒惰的:未使用的代码片段,比如从未被调用的函数,根本不会被检查,也不会触发编译错误。 最大的优点是:Zig 代码编译速度很快。 ## 我从 Zig 构建系统中学到了什么 它的文档很差,甚至可以说没有。Zig Build System (https://ziglang.org/learn/build-system/) 这篇文章告诉了你如何为 Zig 项目(以及可能的依赖)使用它。`std.Build` 页面 (https://ziglang.org/documentation/master/std/#std.Build) 有一些 API 文档,但没有使用示例。有些部分甚至没有 API 文档,你只能从名字猜测它的用途。由于最近的构建系统变更 (https://ziglang.org/devlog/2026/?20260213#2026-05-26),许多现有用法已经不再适用。在最近的重大变更之后,它还有些不完整 (https://codeberg.org/ziglang/zig/pulls/35428#user-content-followup-issues)。一些尖锐的边缘问题是预料之中的。 我目前在 `bygge-zig` 中对 Zig 构建系统的使用是低效的,肯定是错误的,而且非常 hacky。毕竟,我并没有用它主要设计用来做的事情(构建 Zig 项目)。但我让它工作了。我仍然不知道缓存系统应该如何工作。目前,`bygge-zig` 重建了太多东西。每次运行 `zig build` 都会使 `.zig-cache` 增加大约 200 MB。这使得 `bygge-zig` 比 Cargo 差得多。关于这一点,我还有很多要学的。 ## 我从 Cargo 构建中学到了什么 它极其复杂。我之前在 bygge (https://fnordig.de/2020/06/19/build-your-project/) 项目中学到了一些它的功能,但我在某些地方作弊了,硬编码了很多选项。Cargo 在幕后做了大量工作,想在外部工具中复现这些是很难的。你的依赖树中的每个 crate 都依赖不同的 Cargo 特性。实现一个替代品需要大量的试错才能做对。有很多遗留的做事方式,Cargo 都支持它们。除此之外,`rustc` 本身也做了很多事情。如果你给它顶层文件,`rustc` 就知道如何遍历 crate 的模块树。构建好的 Rust 库通过其元数据来标识,元数据被放在 `.rmeta` 文件中。元数据(以及在 Cargo 情况下的文件名)包含了所有输入的哈希值,包括编译器版本和其他外部信息。如果库及其传递依赖的元数据错误或缺失,`rustc` 会拒绝使用已构建的库。有时错误消息并不会告诉你这一点。 我感觉我现在理解了构建过程的 20%。

相似文章

构建系统重构

Lobsters Hottest

Zig 构建系统已经重构,将配置器和制造器进程分离,支持缓存、发布模式编译,并且'zig build'命令速度提升高达90%。这一变化提高了性能,并允许构建系统在不减速的情况下增加功能。

2026 年的 Zig 与 Rust

Lobsters Hottest

本文在 2026 年的背景下对比了 Zig 和 Rust,认为编程代理通过自动化生成 Rust 代码,削弱了 Zig 在人机交互体验上的优势。

Zig 示例教程

Hacker News Top

通过带注释的示例,对 Zig 编程语言进行实践性介绍,涵盖从基础到高级的主题。灵感来源于 Go by Example。

Zig 构建速度正在提升

Mitchell Hashimoto

Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。

用 Zig 写一个 C 编译器

Hacker News Top

一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。