Understanding Zig Build Options
Build options in Zig are extremely powerful and very versatile, but getting
started with them can be a bit of a struggle: the
standard library docs don't
explain much, and the fact that there are four very different build-related
methods with option
in their names also doesn't help.1
After spending some time figuring it out myself, I'm writing this post to give a quick guide on how to set up simple build options for your Zig project! This post was made for Zig 0.13.0 and 0.14.0.
The Goal
We want to be able to:
- Specify certain options to be parsed in the
zig build
command - Use those options in the
build.zig
script - Access those options in source files using
@import
The following sections will show how to do each of those things in the order listed above!
Option Parsing
Let's say you want to add a build flag, simd
, that enables
SIMD
algorithms for hardware that can use it. First, in our build.zig
, we need to
specify that the flag exists so that it can be parsed:
pub fn build(b: *std.Build) !void {
// ...
const simd = b.option(
bool,
"simd",
"Enables/disables support for SIMD algorithms",
) orelse false;
// ...
}
The three arguments are pretty simple here: type, name, and description.
b.option
will return null
if the option was not specified in the build
command, so we're using orelse false
to configure the default value for
the flag.
This will make zig build -Dsimd
or zig build -Dsimd=true
set the simd
variable to true in your build script!
Using Options
Telling Zig to parse options is pretty simple, but actually using those options at compile-time in source code can get a bit confusing. Here's how to do it, in one go:
pub fn build(b: *std.Build) !void {
// ...
const simd = b.option(
bool,
"simd",
"Enables/disables support for SIMD algorithms",
) orelse false;
const options = b.addOptions();
options.addOption(bool, "simd", simd);
// Assuming `exe` was defined earlier
exe.root_module.addOptions("build_options", options);
// ...
}
I'll break it down step-by-step:
- We create a new
Options
instance usingb.addOptions
. Think of it like a key-value data structure that is empty at first.2 - We add an entry for our
simd
flag usingoptions.addOption
. - We tell our build artifact (in this scenario, an executable) to make our
build options importable under the name of
build_options
.
Now, in our source files, we can access the simd
flag:
const build_options = @import("build_options");
pub fn find(needle: u8, haystack: []const u8) ?usize {
if (build_options.simd) {
// Special vectorized byte search implementation
} else {
// Normal byte search algorithm
}
}
Wrap-Up
I hope this post was useful! In the future, once Zig stabilizes, hopefully little things like this will be better documented. But for now, posts like this will have to do. Zig's philosophy for build options makes a lot of sense, but its flexibility is a bit uncanny when coming from other languages like Rust or C++.
As always, if you found any issues or have any suggestions related to this post, open an issue in this website's GitHub repository!
For reference, I'm talking about:
std.Build.option
,std.Build.addOptions
,std.Build.Module.addOptions
, andstd.Build.Step.Options.addOption
. None of them are similar. Don't worry, I'll cover each one individually in the post! ↩︎It actually has no relation to
b.option
! A nice consequence of this is that you can include completely arbitrary metadata in your build options; they don't even have to be tied to command line arguments! ↩︎