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:

  1. Specify certain options to be parsed in the zig build command
  2. Use those options in the build.zig script
  3. 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:

  1. We create a new Options instance using b.addOptions. Think of it like a key-value data structure that is empty at first.2
  2. We add an entry for our simd flag using options.addOption.
  3. 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!


  1. For reference, I'm talking about: std.Build.option, std.Build.addOptions, std.Build.Module.addOptions, and std.Build.Step.Options.addOption. None of them are similar. Don't worry, I'll cover each one individually in the post! ↩︎

  2. 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! ↩︎