Cover image for How to Release your Zig Applications
Loris Cro
Loris Cro

Posted on

How to Release your Zig Applications

So you just wrote an app using Zig and you would like for others to use it. One way to make the life of your users easier is to provide them with pre-built executables of your applications. In this article I'm going to explain the two main things that you need to get right in order to make a good release.

Why offer pre-built executables?

Given the way C/C++ dependency systems work (or don't work, rather), for certain C/C++ projects offering pre-built executables is almost mandatory, as normal people will otherwise be stuck in a tar pit of build systems and config systems, times the number of dependencies of the project. With Zig this should never be the case as the Zig build system (plus the upcoming Zig package manager) will be able to handle everything, meaning that most well-written applications should build successfully by simply running zig build.

That said, the more your application is popular, the less your users will care about which language it's written in. Your users don't want to install Zig and run a build process to be able to use your app (99% of the time, more on the remaining 1% later), so it's in your best interest to just pre-build your app.

zig build vs zig build-exe

In this article we're going to see how to make release builds for a Zig project so it's worth taking a moment to fully understand the relationship between the Zig build system and the command line.

If you have a very simple Zig application (eg, single file, no dependencies) the simplest way to build your project is to use zig build-exe myapp.zig which will immediately produce an executable in the current directory.

As a project grows, especially if it starts having dependencies, you might want to add a build.zig file and start using the Zig build system. Once you do that, you will be in full control of which command-line arguments are available and how they end up influencing the build.

You can use zig init-exe to see what the baseline build.zig file looks like. Note that everything is explicit so each line in the file will go towards defining the subcommands available under zig build.

One last thing to note is that, while the command line arguments will differ when using zig build vs zig build-exe, the two are equivalent when it comes to building Zig code. More specifically, while Zig build can invoke arbitrary commands and do other things that might not even have to do with Zig code at all, when it comes to building Zig code, all that zig build does is prepare command-line arguments for build-exe. This means that, when it comes to compiling Zig code, there's a complete bidirectional mapping between zig build (given the right code in build.zig) and zig build-exe. The only difference is convenience.

Build modes

When building a Zig project with zig build or zig build-exe myapp.zig, you will normally obtain a debug build of the executable. Debug builds are designed for development and so are generally to be considered unfit for releases. Debug builds are designed to be fast to produce (faster to compile) at the expense of runtime performance (slower to run) and soon the Zig compiler will start making this tradeoff even more clear with the introduction of incremental compilation with in-place binary patching.

Zig has three main build modes for releases: ReleaseSafe, ReleaseFast and ReleaseSmall.

ReleaseSafe should be considered the main mode to be used for releases: it applies optimizations but still maintains certain safety checks (eg overflow and array out of bound) that are absolutely worth the overhead when releasing software that deals with tricky sources of input (eg, the internet).

ReleaseFast is meant to be used for software where performance is the main concern, like video games for example. This build mode not only disables the aforementioned safety checks but, to perform even more aggressive optimizations, it also assumes that those kinds of programming errors are not present in the code.

ReleaseSmall is like ReleaseFast (ie, no safety checks), but instead of prioritizing performance, it tries to minimize executable size. This is a build mode that for example makes a lot of sense for Webassembly, since you want the smallest possible executable size and where the sandboxed runtime environment will already provide a lot of safety out of the box.

How to set the build mode

With zig build-exe you can add -O ReleaseSafe (or ReleaseFast, or ReleaseSmall) to obtain the corresponding build mode.

With zig build it depends on how the build script is configured. Default build scripts will feature these lines:

// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();

// ...
Enter fullscreen mode Exit fullscreen mode

This is how you would specify a release mode in the command line: zig build -Drelease-safe (or -Drelease-fast, or -Drelease-small) .

Selecting the right target

Now that we selected the correct release mode, it's time to think about the target. Obviously you will need to specify a target when building from a system different than yours, but you must be careful even if you plan to only make a release for your same platform.

For the purpose of this example let's assume that you're on Windows 10 and are trying to make a build of your program to give a friend who is also using Windows 10. The naive way of doing so would be to just call zig build or zig build-exe (see above differences & similarities between the two commands), and send the resulting executable to your friend.

If you do so, sometimes it will work but some other times it will crash with illegal instruction (or similar errors). What's going on?

CPU Features

When making a build that doesn't specify a target, Zig will produce a build optimized for the current machine, which means making use of all the instruction sets that your CPU supports. If your CPU has AVX, then Zig will use it to perform SIMD operations. Unfortunately this also means that if your friend's CPU doesn't have AVX extensions, then the application will crash because indeed it contains illegal instructions.

The most simple solution to this problem is the following: always specify a target when doing releases. That's right, if you specify that you want to build for x86-64-linux, Zig will assume a baseline CPU that is expected to be fully compatible with all CPUs of the family.

If you want to finetune the selection of instruction sets you can take a look at -Dcpu when using zig build and -mcpu when using zig build-exe. I won't go more into these details in this post.

In practice here's how you would want to make a release for Arm macOS:

$ zig build -Dtarget=aarch64-macos
$ zig build-exe myapp.zig -target aarch64-macos
Enter fullscreen mode Exit fullscreen mode

Note that at the moment the = is mandatory when using zig build, while it won't work when using build-exe (ie you have to put a space between -target and its value). Hopefully these quirks will be cleaned up in the near future.

A few other relevant targets:

x86-64-linux // uses musl libc
x86-64-linux-gnu // uses glibc
x86-64-windows // uses MingW headers
x86-64-windows-msvc // uses MSVC headers but they need to be present in your system
wasm32-freestanding // you will have to use build-obj since wasm modules are not full exes
Enter fullscreen mode Exit fullscreen mode

You can see a full list of the target CPUs and OSs (and libcs, and instruction sets) that Zig support by calling zig targets. Fair warning: it's a big list.

Finally, don't forget that everything inside build.zig has to be explicitly defined, so target options work this way thanks to the following lines:

// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});

// ...
Enter fullscreen mode Exit fullscreen mode

This also means that if you want to add other restrictions or somehow change the way a target should be specified when building, you can do so by adding your own code.


You have now seen what you need to make sure to get right when making a release build: choose a release optimization mode and select the correct target, including when releasing for the same system that you're building from.

One interesting implication of this last point is that for some of your users (1% in normal cases, optimistically), building the program from scratch will be indeed preferable to ensure they make full use of what their CPU can do.

Discussion (4)

prajwalch profile image
Prajwal Chapagain

One thing I desperately want to learn is "how to use/apply semantic versioning on my zig library?" Hope someone would help me.

laserbeam3 profile image

x86-64-windows-msvc // uses MSVC headers but they need to be present in your system

your system (the developer's system) or the user's system?

kristoff profile image
Loris Cro Author • Edited on

The developer's. In other words, Zig doesn't bundle MSVC headers (because the license doesn't allow us), so you will need to install Visual Studio or whichever othre devtool bundles them on Windows.

All other libcs mentioned in that section come bundled with Zig.

pjz profile image
Paul Jimenez

IWBN if there were, say, .rpm and .deb build options :) Maybe someday.