Skip to Content

Managing external dependencies in Zig

A fresh look on how things can be done.
September 20, 2026 by
Managing external dependencies in Zig
Jim Fitzpatrick

As mentioned in Zig Multi Builds Zig can also manage the external dependencies from the Zig build system without needing other scripts. Or at least for the most part. We will be making one script as a helper, but when we get there it will be understandable why it is there. 

I should also point out that when when I say external dependencies in this context it is tooling required to build parts of the project, and not some third party package required within the project. 

As this is a Zig blog, and Zig has not reached v1 as of yet it is worth mention in this blog Zig 0.16.0 was used. Everything is subject to change in newer versions.

Use Case 

For our project we are going to keep a changelog and we are going to follow the rules in keepachangelog.com. While we could write our own tools to manage the changelog, we are going to use a tool called changie

Changie will allow use to make fragments on of release notes while we build towards a release. Allowing these fragments to be curated at release time. 

We want to make the creation of these fragments as simple as possible for those working in the project. They should not have to worry about installing the tool, is the version correct, so on and so fort. So we are going to use the zig build system to add a command called zig build changelog:add, and a few other commands for managing the changelog. 

Also we are expecting the developers working on this project to be work on Linux of Mac, using x86 or arm. This means the correct version of the tool needs to be installed for the correct platform.

Dive into the example

Setting up a basic project

We are going to need a project to manage the changelog in so lets create a empty zig project. Really we don't need any code, or real builds to work though the example. We will create a sample project called project. If you have done the walkthrough from Zig Multi Builds that source code could also be used.

First lets start by setting the project.

$ mkdir project
$ cd project
$ zig init -m
$ tree
.
├── build.zig
└── build.zig.zon

If we look at the contents of the files we see that they are stubed out. I use bat as my cat replacement.

$ bat *
───────┬──────────────────────────────────────────
│ File: build.zig
───────┼──────────────────────────────────────────
1 │ const std = @import("std");
2 │
3 │ pub fn build(b: *std.Build) void {
4 │ _ = b; // stub
5 │ }
───────┴──────────────────────────────────────────
───────┬──────────────────────────────────────────
│ File: build.zig.zon
───────┼──────────────────────────────────────────
1 │ .{
2 │ .name = .project,
3 │ .version = "0.0.1",
4 │ .minimum_zig_version = "0.16.0",
5 │ .paths = .{""},
6 │ .fingerprint = 0x2fb3d0eee2624064,
7 │ }
───────┴──────────────────────────────────────────

And running zig build --help will complete. 

Initial Changie setup

The first thing we are going to add to the project is a script for setting the version of changie to be used. This is a script that we don't not need to create, but make the mangement of the updating the version of changie used in the future easier. Lets create a file scripts/changie.sh.

#!/bin/env bash

VERSION=1.26.0

DOWNLOAD=https://github.com/miniscruff/changie/releases/download

zig fetch --save=changie_linux_amd64 "$DOWNLOAD"/v"$VERSION"/changie_"$VERSION"_linux_amd64.tar.gz
zig fetch --save=changie_linux_arm64 "$DOWNLOAD"/v"$VERSION"/changie_"$VERSION"_linux_arm64.tar.gz
zig fetch --save=changie_darwin_amd64 "$DOWNLOAD"/v"$VERSION"/changie_"$VERSION"_darwin_amd64.tar.gz
zig fetch --save=changie_darwin_arm64 "$DOWNLOAD"/v"$VERSION"/changie_"$VERSION"_darwin_arm64.tar.gz

What are we doing it this script. Firstly VERSION is setting the version of changie that we want to install, The DOWNLOAD is shorting the path to where the downloads can be found. The Zig commands are the commands that do the heavy lifting. The zig fetchdownloads and adds the dependencies to the project. This information is also saved in the build.zig.zon file, and the --save is telling what that reference should be saved as.

Run the script and check the contains of the zon file.

$ bash scripts/changie.sh
$ cat build.zig.zon
.{
.name = .project,
.version = "0.0.1",
.dependencies = .{
.changie_linux_amd64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_linux_amd64.tar.gz",
.hash = "N-V-__8AAF1dnADW_F-vM1ZIjbag7HxVyu6HxqU9AdL30CR2",
},
.changie_linux_arm64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_linux_arm64.tar.gz",
.hash = "N-V-__8AAF1NkQDXxnSKddLy6-Kn73PGLsCEQ7wqWmGh7vuF",
},
.changie_darwin_amd64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_darwin_amd64.tar.gz",
.hash = "N-V-__8AAFtxnwDlZmlvV80nlROFn8nGwcmEYngYT5EXsTGM",
},
.changie_darwin_arm64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_darwin_arm64.tar.gz",
.hash = "N-V-__8AAP2XlABMulfHeBmajiu1klj8gaWrSjVD8pZylVuD",
},
},
.minimum_zig_version = "0.16.0",
.paths = .{""},
.fingerprint = 0x2fb3d0eee2624064,
}

In the zon file you will a `.depenedencies` section added, and with it we the is the four different tagarts. You may also notice that there is now a zig-pkg directory in the root of the project. Checking the contains of that directory shows something interesting. It shows all the data that has being download when we ran the script. There is four entries for changie. This is not something that we want, and we will be addressing it a bit later.

$ tree zig-pkg/
zig-pkg/
├── N-V-__8AAF1dnADW_F-vM1ZIjbag7HxVyu6HxqU9AdL30CR2
│   ├── CHANGELOG.md
│   ├── changie
│   ├── LICENSE
│   └── README.md
├── N-V-__8AAF1NkQDXxnSKddLy6-Kn73PGLsCEQ7wqWmGh7vuF
│   ├── CHANGELOG.md
│   ├── changie
│   ├── LICENSE
│   └── README.md
├── N-V-__8AAFtxnwDlZmlvV80nlROFn8nGwcmEYngYT5EXsTGM
│   ├── CHANGELOG.md
│   ├── changie
│   ├── LICENSE
│   └── README.md
└── N-V-__8AAP2XlABMulfHeBmajiu1klj8gaWrSjVD8pZylVuD
├── CHANGELOG.md
├── changie
├── LICENSE
└── README.md

Now we have zig installing the tool that we want it would be time to set the tool up.

Setting up Changie

This step would only be required when the project is being bootstrapped, so it does not make sense to add these steps into build system. Lets initialize the changie skeleton. A little bit of exploring is required here as we want to call the changie tool but it is not in our path. However, we have it downloaded in zig-pkg, but there it is in some directory named by a hash. To find out what hash directory you need to be use the binary from have a look in the zon file for your systems architecture hash. I promise this is the only time that this will need to be done. For me I am on Linux x86, so I will use the hash under .changie_linux_amd64.

We will run the help command first, just to make sure we have the correct binary. Then we will run the init function.

$ ./zig-pkg/N-V-__8AAF1dnADW_F-vM1ZIjbag7HxVyu6HxqU9AdL30CR2/changie help
Changie keeps your changes organized and attached to your code.

...

$ ./zig-pkg/N-V-__8AAF1dnADW_F-vM1ZIjbag7HxVyu6HxqU9AdL30CR2/changie init

Once that runs there will be new files, and directories created within the project. These files, and directories are related to changie and how it is configured. How to configure changie is out of scope of this walkthrough, but more files will be added to these locations as we progress is it is good to know they are there.

$ tree -a . 
.
├── CHANGELOG.md
├── .changes
│   ├── header.tpl.md
│   └── unreleased
│   └── .gitkeep
├── .changie.yaml

Setting the first zig build command

Now we are at the stage where we can set up our first Zig build command. This is going to zig build changelog:add. This command will be used to create the fragments, and the caller does not need to know what tool is being used allowing use to changes tools later if we decide to create our own.

The first step will be to get the correct binary for the current system. In the build.zig file we are going to update the build function, and add a new function.

/// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
const changie_bin = get_changie_bin(b) orelse return;
_ = changie_bin;
}

fn get_changie_bin(b: *std.Build) ?std.Build.LazyPath {
const host = b.graph.host.result;
const name = switch (host.os.tag) {
.linux => switch (host.cpu.arch) {
.x86_64 => "changie_linux_amd64",
.aarch64 => "changie_linux_arm64",
else => @panic("unsupported cpu arch"),
},
.macos => switch (host.cpu.arch) {
.x86_64 => "changie_darwin_amd64",
.aarch64 => "changie_darwin_arm64",
else => @panic("unsupported cpu arch"),
},
else => @panic("unsupported os"),
};

if (b.lazyDependency(name, .{})) |dep| {
return dep.path("changie");
} else {
return null;
}
}

The adding of the _ = changie_bin; allows the build to happen without errors. That line will be removed once we start to use the changie_bin. So what is going on. The const changie_bin = get_changie_bin(b) orelse return; is getting the path to the changie binary inside of zig-pkg. In the function we switch on the host OS and architecture, and calling panic if no matching results were found. Then we if we return the path to the binary with return dep.path("changie");. If the binary was in some sub path of the archive that was download it would be here that the path would be updated.

Lets now create the actual command. In the build function we are going to add the following.

/// build.zig

pub fn build(b: *std.Build) void {
const changie_bin = get_changie_bin(b) orelse unreachable;

// changie add
const changie_add_cmd = b.step("changelog:add", "Add changelog fragment");
const changie_add = std.Build.Step.Run.create(b, "run changie");
changie_add.addFileArg(changie_bin);
changie_add.addArg("new");
changie_add_cmd.dependOn(&changie_add.step);
}

The first thing we are doing is creating a entry for the command in the build system with b.step(). When zig build --help is ran the information passed to this command will be used for the name of the step and the description.

Next we set up a blank run step with std.Build.Step.Run.create(). Then we add the path to what we want to run with .addFileArg(), followed by the arguments we what to use addArg(). Finally we say that the changie_add_cmd step depends on the changie_add step by using .dependOn(). And that is kinda it. We now can run zig build --help and see the command entry in the steps section. But more importantly we can now run zig build changelog:add to create a new change fragment.

$ zig build --help
Usage: /home/boomatang/.zvm/0.16.0/zig build [steps] [options]

Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
changelog:add Add changelog fragment

...

$ zig build changelog:add
// opens interactive session.
// I created a "add" fragment

So I made a new "add" fragment for the changelog. We can see this in the .changes/unreleaseddirectory.

$ tree .changes
.changes
├── header.tpl.md
└── unreleased
└── Added-20260828-163227.yaml

This is the basics of adding commands to manage a third party tool. We will jump forward and add two more commands inforce the workflow.

Manage the changelog fragments.

In changie when you are getting ready to create a release you need to compile the fragments into the CHANGELOG.md so lets make some commands for doing that.

The first command is going to be changelog:batch, and in this command we are going to be setting the version from the build.zig.zon file as the release version. changie has a way to determine the version, but I like to have some control over the this. 

/// build.zig
const std = @import("std");
const zon = @import("./build.zig.zon");

pub fn build(b: *std.Build) void {
...

// changie batch
const changie_batch_cmd = b.step("changelog:batch", "Batch changelog fragment");
const changie_batch = std.Build.Step.Run.create(b, "run chnagie");
changie_batch.addFileArg(changie_bin);
changie_batch.addArg("batch");
changie_batch.addArg(zon.version);
changie_batch_cmd.dependOn(&changie_batch.step);

What you many notice from the added changes it is almost the same as the "add" command. The only real difference is the inclusion of zon.version to pass the version to the batch command. This means that to create a new version we only need to update the version in the zon file. 

$ zig build --help
Usage: /home/boomatang/.zvm/0.16.0/zig build [steps] [options]

Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
changelog:add Add changelog fragment
changelog:batch Batch changelog fragment

...

$ zig build changelog:batch

$ tree .changes
.changes
├── 0.0.1.md <-- notice the creation of the new version
├── header.tpl.md
└── unreleased

The last command we will add is the merge command that merges the changes into the changelog. Again this follows the patterns of the other two commands.

    // changie merge
const changie_merge_cmd = b.step("changelog:merge", "Merge changelog batches into the CHANGELOG.md");
const changie_merge = std.Build.Step.Run.create(b, "run chnagie");
changie_merge.addFileArg(changie_bin);
changie_merge.addArg("merge");
changie_merge_cmd.dependOn(&changie_merge.step);

This command can then be ran to merge the release batches into the CHANGELOG.md.

The one final thing

 This was mentioned earlier. Right now when you build the Zig project is includes all version of the changie. This is something that we don't want to be doing. So lets address it. What we want to do is be lazy, and only install the version that is used in the build system. 

In the build.zig.zon we add to each dependency entry .lazy = true. This will tell Zig to only download that dependency when it is actually needed. 

$ cat build.zig.zon
.{
.name = .project,
.version = "0.0.1",
.dependencies = .{
.changie_linux_amd64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_linux_amd64.tar.gz",
.hash = "N-V-__8AAF1dnADW_F-vM1ZIjbag7HxVyu6HxqU9AdL30CR2",
.lazy = true,
},
.changie_linux_arm64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_linux_arm64.tar.gz",
.hash = "N-V-__8AAF1NkQDXxnSKddLy6-Kn73PGLsCEQ7wqWmGh7vuF",
.lazy = true,
},
.changie_darwin_amd64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_darwin_amd64.tar.gz",
.hash = "N-V-__8AAFtxnwDlZmlvV80nlROFn8nGwcmEYngYT5EXsTGM",
.lazy = true,
},
.changie_darwin_arm64 = .{
.url = "https://github.com/miniscruff/changie/releases/download/v1.26.0/changie_1.26.0_darwin_arm64.tar.gz",
.hash = "N-V-__8AAP2XlABMulfHeBmajiu1klj8gaWrSjVD8pZylVuD",
.lazy = true,
},
},
.minimum_zig_version = "0.16.0",
.paths = .{""},
.fingerprint = 0x2fb3d0eee2624064,
}

To see this take affect on our system you will need to remove an number of cache directories, ./zig-pkg, ./.zig-cache and ~/.cache/zig. The last one will affect you build times in other times, but after doing the Zig build only pull the required version of changie.

To conclude

This was a simple explore of how the Zig build system can be used to manage more than just the build of projects, but can also help with managing the tools required to build a project. It shows how not all projects need to use Makefiles to have a build system. Over all the amount of code required to manage these third party binaries was very simple. 

I do image there would be some pain points on projects that have large dependency chains, but on these projects you also have some pain when managing dependencies. So this method may not actual be any worse. Where I think it may come into its own is when tool A requires tool B to run first, and if there is any issue the errors are reported quickly. 

Some other niceness that comes from this form of tool management is the error and time reporting that the Zig build system can gives us out of the box, see --summary all in the zig build --help, and that is passed to our custom build functions. 


Managing external dependencies in Zig
Jim Fitzpatrick September 20, 2026
Share this post
Our blogs
Archive
Zig Multi Builds
Lets cross compile our releases