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();
// ...
exe.setBuildMode(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
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
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(.{});
// ...
exe.setTarget(target);
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.
Conclusion
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.
Top comments (6)
One thing I desperately want to learn is "how to use/apply semantic versioning on my zig library?" Hope someone would help me.
your system (the developer's system) or the user's system?
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.
IWBN if there were, say, .rpm and .deb build options :) Maybe someday.
It should be fairly simple to create a zig library you can use in your
build.zig
that handles the creation of such packages. AFAIK most big distributions have scripts and tools to automate creation of packages anyway, so you could just shell out to them.However I don't think it's a sane idea to create packages this way for projects which dynamically link libraries, because [un]fortunately (let's not get into that debate) the same package formats are often used across different distributions, which may call the same library different names in their repos.
How can you target a specific platform and also allow CPU extensions?