Chapter 11: Dependency Management
(
build.zig.zon
) — Mach Engine 0.4
CAUTION: THIS TUTORIAL WAS AI-GENERATED AND MAY CONTAIN ERRORS. IT IS NOT AFFILIATED WITH OR ENDORSED BY HEXOPS/MACH.
In Chapter 10: Build System, we saw how the build.zig
file acts as the construction manager for our project, telling the Zig compiler how to assemble our application. We learned that it uses a helper function like mach.addExecutable
and refers to dependencies like Mach itself using b.dependency("mach", ...)
.
But how does build.zig
know where to find the Mach engine code or other external libraries? How does it ensure we get the correct version and that the code hasn’t been tampered with? This is where dependency management and the build.zig.zon
file come into play.
Your Project’s Shopping List
Imagine you’re baking a cake (your application). You have your recipe (build.zig
), but you need ingredients that you don’t have in your kitchen (like flour, sugar, eggs – or in our case, the Mach engine code, maybe an image loading library, etc.). You need a shopping list!
The build.zig.zon
file is exactly that: your project’s shopping list. It tells the Zig build system (our “shopping assistant”):
- What ingredients (dependencies) you need: e.g., “mach”, “zigimg”.
- Where to get them: A specific download URL (the “store address”).
- How to verify them: A unique content hash (like a “quality seal” or barcode) to make sure you got exactly what you expected.
This system ensures your project can reliably find and use external code libraries.
Key Concepts
Let’s unpack the core ideas:
- Dependency: A piece of external code (another Zig project or library) that your project needs to function. Mach itself is the primary dependency for a Mach application.
- Zig Package Manager: The built-in system within Zig’s build tools that reads
build.zig.zon
, downloads dependencies, verifies them, and makes them available to yourbuild.zig
script. build.zig.zon
: The manifest file (the “shopping list”). It’s written in a simple.zon
(Zig Object Notation) format. It lists all top-level dependencies.- URL: The web address where the package manager can download the dependency’s source code (usually a compressed archive like
.tar.gz
). - Hash: A cryptographic checksum (typically SHA-256) of the downloaded file’s content. The package manager calculates the hash of the downloaded file and compares it to the hash listed in
build.zig.zon
. If they don’t match, the build fails, preventing accidental use of incorrect or malicious code. lazy = true
: An optional flag for a dependency inbuild.zig.zon
. If set, the package manager will only download and verify this dependency if thebuild.zig
script actually requests it (usually viab.lazyDependency(...)
or conditionally callingb.dependency(...)
). This saves time and bandwidth if certain dependencies are only needed for specific build configurations or features.
Putting build.zig.zon
to Work
Let’s look at the build.zig.zon
file from Mach itself (which your project’s build.zig.zon
would typically reference).
1. The build.zig.zon
File Structure
// build.zig.zon (Simplified Example from Mach)
.{
// Name of this package
.name = "mach",
// Version of this package
.version = "0.4.0", // Or current version
// List of files/directories included in this package itself
.paths = .{
"src",
"build.zig",
"build.zig.zon",
// ... other files like LICENSE, README ...
},
// --- The Dependencies List ---
.dependencies = .{
// --- Example: A Core Dependency (Maybe for Fonts) ---
.mach_freetype = .{
// Where to download it from
.url = "https://pkg.machengine.org/mach-freetype/...",
// The expected content hash
.hash = "1220adfccce3dbc4e4fa...",
// Download only if requested by build.zig
.lazy = true,
},
// --- Example: A Platform-Specific Dependency (macOS) ---
.mach_objc = .{
.url = "https://pkg.machengine.org/mach-objc/...",
.hash = "12203675829014e69b...",
.lazy = true,
},
// --- Example: A Dependency for Examples Only ---
.zigimg = .{
.url = "https://github.com/zigimg/zigimg/archive/....tar.gz",
.hash = "12201b874ac217853e...",
.lazy = true,
},
// ... many other dependencies for different features/platforms ...
},
}
.name
,.version
,.paths
: Describe the package defined by thisbuild.zig.zon
file..dependencies
: This is the crucial part – an anonymous struct where each field represents a dependency.- The field name (
.mach_freetype
,.mach_objc
,.zigimg
) is the name you use to refer to this dependency in yourbuild.zig
script (e.g.,b.dependency("mach_freetype", ...)
). .url
: Specifies the download location..hash
: Provides the SHA-256 hash for verification..lazy = true
: Indicates this dependency might not always be needed.
- The field name (
2. How build.zig
Uses It
In your project’s build.zig
, when you call b.dependency()
, the Zig package manager springs into action.
// build.zig (Snippet from Chapter 10)
// Get the Mach dependency (defined in build.zig.zon)
const mach_dep = b.dependency("mach", .{ // <-- Asks for "mach"
.target = target,
.optimize = optimize,
// Optional: Request specific sub-features if Mach's build.zig supports it
// .core = true,
// .sysaudio = true,
});
b.dependency("mach", ...)
tells the package manager: “I need the dependency named ‘mach’ as defined inbuild.zig.zon
.”- The package manager finds the
.mach = .{ ... }
entry inbuild.zig.zon
, checks its cache, downloads if necessary, verifies the hash, and then makes the dependency’s code (and its ownbuild.zig
file!) available to your script via themach_dep
object.
3. Lazy Dependencies
Mach’s own build.zig
often uses b.lazyDependency
to avoid fetching things unless absolutely required. For example, it might only fetch the mach_objc
dependency if the target platform is macOS.
// Mach's build.zig (Conceptual Example)
// Check if the target is macOS
if (target.result.isDarwin()) {
// Only try to get the 'mach_objc' dependency if we are on macOS
if (b.lazyDependency("mach_objc", .{ // <-- Asks lazily
.target = target,
.optimize = optimize,
})) |dep| {
// If successful (downloaded & verified), make it available
module.addImport("objc", dep.module("mach-objc"));
}
}
b.lazyDependency("mach_objc", ...)
checks if the dependency is needed and requested. If both are true, it behaves likeb.dependency()
. If the dependency is not needed or not requested by the user (lazy = true
in.zon
), it might returnnull
or skip the download/verification step.
4. Adding/Updating Dependencies (zig fetch
)
How do entries get into build.zig.zon
in the first place? While you can edit it manually, Zig provides a command:
# Example: Add mach as a dependency to your project
zig fetch --save https://pkg.machengine.org/mach/LATEST_COMMIT_HASH.tar.gz
# Example: Add zigimg as a dependency named 'zigimg'
zig fetch --save --name zigimg https://github.com/zigimg/zigimg/archive/COMMIT_HASH.tar.gz
zig fetch --save <URL>
: Downloads the code from the URL, calculates its hash, and automatically adds or updates the corresponding entry in yourbuild.zig.zon
file. The dependency name will be inferred from the URL or you can specify it with--name
. This is the recommended way to manage dependencies.
Under the Hood: The Package Manager’s Workflow
What happens when zig build
encounters a b.dependency("mach", ...)
call?
High-Level Walkthrough:
- Request: The
build.zig
script requests the dependency named “mach”. - Manifest Lookup: The Zig package manager reads the current project’s
build.zig.zon
file and finds the entry for.mach
. It gets the URL and expected hash. - Cache Check: It checks a global cache directory on your computer (often
~/.cache/zig/
) to see if a package matching that URL and hash has already been downloaded and verified. - Download (if needed): If not found in the cache, it downloads the
.tar.gz
file from the specified URL. - Verification: It calculates the SHA-256 hash of the downloaded file.
- Hash Comparison: It compares the calculated hash with the
.hash
value listed inbuild.zig.zon
.- Match: Success! The downloaded code is correct. It unpacks the code into the cache.
- Mismatch: Error! The build fails immediately with a hash mismatch error. This protects you from corrupted downloads or unexpected changes.
- Provide Access: The package manager makes the path to the verified, unpacked code in the cache available to the
build.zig
script (inside themach_dep
object in our example). The script can then access the dependency’s source code (mach_dep.module("mach")
) and its ownbuild.zig
file (mach_dep.builder
).
(For lazy = true
dependencies requested via b.lazyDependency
, steps 4-7 might be skipped if the dependency isn’t actually required for the current build configuration).
Sequence Diagram (b.dependency("mach", ...)
Flow):
sequenceDiagram
participant BuildScript as build.zig
participant ZigPM as Zig Package Manager
participant Cache as ~/.cache/zig/
participant WebServer as Dependency URL
participant DepObject as Returned Dependency Handle
BuildScript->>ZigPM: Request dependency "mach" (via b.dependency)
ZigPM->>ZigPM: Read build.zig.zon (get URL, Hash)
ZigPM->>Cache: Check if URL+Hash exists?
alt Already Cached
Cache-->>ZigPM: Yes, path is /path/to/cached/mach
else Not Cached
Cache-->>ZigPM: No
ZigPM->>WebServer: Download from URL
WebServer-->>ZigPM: Return file data (.tar.gz)
ZigPM->>ZigPM: Calculate Hash of downloaded data
alt Hash Matches
ZigPM->>ZigPM: Hashes match!
ZigPM->>Cache: Unpack data to /path/to/cached/mach
Cache-->>ZigPM: Stored successfully
else Hash Mismatches
ZigPM-->>BuildScript: Error: Hash Mismatch! (Build Fails)
end
end
ZigPM->>DepObject: Create handle with path to cached code
DepObject-->>BuildScript: Return dependency handle ('mach_dep')
This careful process of downloading, hashing, and caching ensures that your builds are reproducible and secure. Once a dependency version is cached, subsequent builds using the same version are much faster as they don’t need to download it again.
Code Glance:
build.zig.zon
: (Already shown above) Defines the list of dependencies, their URLs, and hashes. The structure is key.-
build.zig
: Uses theb.dependency
orb.lazyDependency
functions provided by thestd.Build
API.// build.zig (Relevant functions) const mach_dep = b.dependency("mach", .{...}); // Standard request const freetype_dep = b.lazyDependency("mach_freetype", .{...}); // Lazy request // Accessing the dependency's code module const mach_module = mach_dep.module("mach"); app_mod.addImport("mach", mach_module); // Accessing the dependency's own build.zig (if needed, e.g., for helpers) const mach_builder = mach_dep.builder; // @import("mach") within build.zig refers to the dependency's build.zig const exe = @import("mach").addExecutable(mach_builder, .{...});
Conclusion
Congratulations on reaching the end of this tutorial series! In this final chapter, you’ve learned about dependency management in Zig and Mach using the build.zig.zon
file. This file acts as your project’s “shopping list,” specifying external dependencies, their download URLs, and content hashes for verification. The Zig package manager uses this file, along with calls like b.dependency
in your build.zig
script, to automatically download, verify, and cache these dependencies, ensuring your builds are reliable and reproducible. The lazy = true
option helps optimize builds by only fetching dependencies when they are actively needed.
Understanding build.zig.zon
completes the picture of how a Mach project is structured, built, and connected to its required libraries. You now have a foundational understanding of Mach’s core concepts, from windowing and object management to graphics, audio, math, and the build process.
We encourage you to explore the examples in the Mach repository, experiment with the concepts you’ve learned, and join the Mach Discord community if you have questions or want to share your projects. Happy coding!
Generated by AI Codebase Knowledge Builder