Build your project Zig-style

caleb_thompson1 pts0 comments

Build your project Zig-style

Build your project Zig-style

Jun 16, 2026 · 8 minute read

· rust

Over the past few nights I've been tinkering with the Zig build system,<br>after it got a bit of a rework lately.<br>I had a plan:<br>Learn a bit more about Zig,<br>learn a bit more about the Zig build system<br>and get a deeper understanding of what Cargo does to build a Rust project.

Building Rust projects with something that is not Rust is something I have experience with.<br>With Zig I tinkered around a bit before and I'm keeping an eye on its development.<br>The Zig build system was completely new to me.

I wrote an implementation of Uxn in Zig<br>(it's not published).

So just like that I present: bygge-zig.

It builds Rust code:

src/mozilla/glean$ ls -l Cargo.toml<br>-rw-r--r-- 1 jer staff 616 Jun 15 16:10 Cargo.toml<br>src/mozilla/glean$ zig build --summary line<br>Build Summary: 194/194 steps succeeded<br>src/mozilla/glean$ du -sh .zig-cache<br>952M .zig-cache<br>src/mozilla/glean$ tree zig-out<br>zig-out<br>├── bin<br>│   └── uniffi-bindgen<br>└── lib<br>├── glean<br>├── glean_core<br>└── uniffi_bindgen

This is a build of the Glean SDK, my main project at work.

In 374 lines of Zig (and 79 lines of Ruby) I replicated what Cargo does in 80.000 lines of Rust.<br>That includes building Rust code, building proc-macros and building and running build scripts (those build.rs files in the top-level directory of some crates).

Of course that is an oversimplification.<br>Turns out a complete Rust build system is really tricky.

bygge-zig does nothing about resolving and fetching dependencies.<br>It does not figure out what depends on what.<br>It does not decide which features should be active.<br>It translates what the build should look like into something the Zig build system can handle.

At one point Cargo had build plans,<br>which allowed to gather information about how to run the build, mostly.<br>That was removed and now there is a new way to get most of that information: the unit graph (on Rust Nightly):

$ cargo +nightly build -Z unstable-options --unit-graph | jq .<br>"version": 1,<br>"units": [<br>"pkg_id": "path+file:///home/jer/src/bygge-zig/crates/hello-world#0.1.0",<br>"target": {<br>"kind": [<br>"bin"<br>],<br>"crate_types": [<br>"bin"<br>],<br>"name": "hello-world",<br>"src_path": "/home/jer/src/bygge-zig/crates/hello-world/src/main.rs",<br>"edition": "2024",<br>"doc": true,<br>"doctest": false,<br>"test": true<br>},<br>"profile": {<br>"name": "dev",<br>"opt_level": "0",<br>"lto": "false",<br>"codegen_backend": null,<br>"codegen_units": null,<br>"debuginfo": 2,<br>"split_debuginfo": "unpacked",<br>"debug_assertions": true,<br>"overflow_checks": true,<br>"rpath": false,<br>"incremental": true,<br>"panic": "unwind",<br>"strip": {<br>"deferred": "None"<br>},<br>"platform": null,<br>"mode": "build",<br>"features": [],<br>"dependencies": [<br>"index": 1,<br>"extern_crate_name": "world",<br>"public": false,<br>"noprelude": false,<br>"nounused": false<br>},

This emits the units of work that Cargo will execute for the build.<br>It still requires a lot more effort to turn that into something actually executable.<br>For example the generated shell command for the first unit in the example graph is this:

CARGO_CRATE_NAME=hello_world \<br>CARGO_PKG_NAME=hello-world \<br>CARGO_PKG_VERSION=1.0.0 \

rustc /home/jer/src/bygge-zig/crates/hello-world/src/main.rs \<br>--edition 2024 \<br>--extern world=target/debug/deps/libworld.rlib \<br>-L target/debug/deps

This assumes the world library is already built and placed in the target/debug/deps/ directory.<br>There's many more more environment variables that are set for a build.

The Glean SDK (and the expanded sample project) does make use of both proc-macros and build scripts across its dependencies.<br>The way they need to be built differs from library code.<br>proc-macros are built into dynamic libraries and loaded by the Rust compiler at compile time.<br>Build scripts are always built for the host target and run before the crate's code gets compiled.<br>Build scripts print out additional configuration to stdout,<br>which is parsed by Cargo and injected into the rest of the build.

$ RUSTC=rustc OUT_DIR=.zig-cache/tmp .zig-cache/o/98128f/rust/build_script_build<br>cargo::rustc-check-cfg=cfg(build_ran)<br>cargo::rustc-cfg=build_ran<br>$ cat .zig-cache/tmp/code.rs<br>pub const NAME: &'static str = "builder";

This will result in the parameter --cfg=build_ran being appended to the rustc invocation for hello-world/src/lib.rs.

Not every environment variable is read by every project.<br>Not every build.rs output line is needed for every build.<br>To build the Glean SDK I'm getting away with the bare minimum.<br>Nonetheless bygge-zig is able to drive quite a complicated build.<br>And it does that on both macOS and Linux just fine.

What I learned about Zig

In this project I didn't really use much of what makes Zig Zig.<br>No comptime, no error handling, no arena allocators.<br>It is definitely an improvement over writing similar code in C, with a larger standard library and some generic data types to use.<br>Some of the more Zig things I barely scratched.<br>String handling requires an allocator, so it needs to be passed around, it's embedded...

build rust cargo world project glean

Related Articles