Conditional compilation with build time enums?

Is there a way to have build time user defined enum values? I want to be able to do conditional compilation similar to what builtin gives us.

For example, with builtin we can do conditional compilation using cpu.arch and os.tag enums like this:

if (builtin.cpu.arch == .aarch64) {
  // do this
}
if (builtin.os.tag == .windows) {
  // do that
}

and I want something similar:

$ zig build -Dtask=both
if (conditions.task == .first or conditions.task == .both) {
  task1()
}
if (conditions.task == .second or conditions.task == .both) {
  task2()
}

You can find it here: Zig Build System ⚡ Zig Programming Language

1 Like

@hizani Just be aware when you look at that:

  • std.Build.option() (i.e. b.option() in this example) creates a command-line switch / parameter for zig build.
  • std.Build.addOptions() (i.e. b.addOptions) creates a container for key-value pairs that can be converted to Zig code.
  • std.Build.Step.Options.addOption() (i.e. options.addOption() adds key-value pairs to that container.
  • std.Build.Module.addOptions() (i.e. exe.root_module.addOptions()) adds the key-value container, as a Zig code, to the project so it can be @imported.

IMHO somebody needs to buy a thesaurus and find some alternative nouns to “option” for that API. I found it all highly confusing the first time I read it.

8 Likes

something like addConstants makes the most sense since it’s literally doing just that, adding constants. I don’t think Zig will allow adding anything else beside constants even in the future.

Just throwing a few out there:

  • Flag
  • BuildFlag
  • UserFlag
  • UserConfigOption
  • UserConfigFlag
1 Like

I agree with this, the syntactic distance between “options” and “option” is too close. We could name it after its functionality (OptionsBuilder), its abstract data structure (OptionsMap, OptionsTable), or after its result (OptionsModule).

None of these is exactly satisfactory, I know. But Options is just too terse, especially since b.option has no necessary connection to any given Options builder. It took me several tries before I would correctly construct and add b.option values to an Options module, without getting it wrong and being confused about why my options aren’t in my Options module.

The separation is necessary, I have no issue with how the system works[1]. But it could certainly be more clear than it is.


  1. Arguably Module.addOptions is redundant and we should just use options.createModule() directly. But that’s a minor point. ↩︎

3 Likes

I like OptionsModule. It’s pretty descriptive.

1 Like

Trying this build script

const std = @import("std");

const TaskOption = enum {
    all,
    first,
    second,
};

pub fn build(b: *std.Build) void {
    b.top_level_steps = .{};

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

    const task_option = b.option(TaskOption, "task", "What task to run. Default: all");

    const build_time_options = b.addOptions();
    if (task_option) |task| {
        build_time_options.addOption(TaskOption, "task", task);
    } else {
        build_time_options.addOption(TaskOption, "task", TaskOption.all);
    }

    const exe = b.addExecutable(.{
        .name = "foo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/path/to/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.root_module.addOptions("config", build_time_options);

    const run = b.addRunArtifact(exe);
    b.default_step = &run.step;
}

But it doesn’t work. When I run zig build I get these errors

getPath() was called on a GeneratedFile that wasn't built yet.
  source package path: /home/myuser/projects/zig/fooproject
  Is there a missing Step dependency on step 'options'?
    The step was created by this stack trace:
name: 'options'. creation stack trace:
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build/Step/Options.zig:23:22: 0x12feccc in create (std.zig)
        .step = .init(.{
                     ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:764:31: 0x12c561a in addOptions (std.zig)
    return Step.Options.create(b);
                              ^
/home/myuser/projects/zig/fooproject/build.zig:17:44: 0x1293fae in build (build.zig)
    const build_time_options = b.addOptions();
                                           ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:2258:33: 0x124bcba in runBuild__anon_31549 (std.zig)
        .void => build_zig.build(b),
                                ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/compiler/build_runner.zig:459:29: 0x122f4ed in main (build_runner.zig)
        try builder.runBuild(root);
                            ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/start.zig:680:88: 0x1234c82 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/start.zig:190:5: 0x12193f1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
    The step 'compile exe foo Debug native' that is missing a dependency on the above step was created by this stack trace:
name: 'compile exe foo Debug native'. creation stack trace:
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build/Step/Compile.zig:412:22: 0x130098f in create (std.zig)
        .step = .init(.{
                     ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:785:19: 0x12c76e2 in addExecutable (std.zig)
    return .create(b, .{
                  ^
/home/myuser/projects/zig/fooproject/build.zig:24:32: 0x1294471 in build (build.zig)
    const exe = b.addExecutable(.{
                               ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:2258:33: 0x124bcba in runBuild__anon_31549 (std.zig)
        .void => build_zig.build(b),
                                ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/compiler/build_runner.zig:459:29: 0x122f4ed in main (build_runner.zig)
        try builder.runBuild(root);
                            ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/start.zig:680:88: 0x1234c82 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/start.zig:190:5: 0x12193f1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
    Proceeding to panic.
thread 20811 panic: misconfigured build script
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:2515:25: 0x1432b40 in getPath4 (std.zig)
                        @panic("misconfigured build script");
                        ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:2483:24: 0x13b1dcd in getPath3 (std.zig)
        return getPath4(lazy_path, src_builder, asking_step) catch |err| switch (err) {
                       ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build.zig:2477:27: 0x13af40d in getPath2 (std.zig)
        const p = getPath3(lazy_path, src_builder, asking_step);
                          ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build/Step/Compile.zig:1298:48: 0x131d471 in getZigArgs (std.zig)
                        const src = lp.getPath2(mod.owner, step);
                                               ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build/Step/Compile.zig:1753:36: 0x138dc3f in make (std.zig)
    const zig_args = try getZigArgs(compile, false);
                                   ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Build/Step.zig:276:33: 0x1308d50 in make (std.zig)
    const make_result = s.makeFn(s, options);
                                ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/compiler/build_runner.zig:1336:26: 0x12ced0e in makeStep (build_runner.zig)
        } else if (s.make(.{
                         ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Io.zig:1107:17: 0x129bd8b in start (std.zig)
                return @call(.auto, function, args_casted.*);
                ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Io/Threaded.zig:322:37: 0x1100a90 in start (std.zig)
            const result = task.func(task.contextPointer());
                                    ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Io/Threaded.zig:1408:29: 0x1153c1a in worker (std.zig)
            runnable.startFn(runnable, &thread, t);
                            ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Thread.zig:561:13: 0x112c745 in callFn__anon_15088 (std.zig)
            @call(.auto, f, args);
            ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/Thread.zig:1506:30: 0x10ff810 in entryFn (std.zig)
                return callFn(f, self.fn_args);
                             ^
/home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib/std/os/linux/x86_64.zig:105:5: 0x112c825 in clone (std.zig)
    asm volatile (
    ^
error: the following build command terminated with signal ABRT:
.zig-cache/o/bfb9a6ad10ac5e76eae8ff0a1bca2c07/build /home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/zig /home/myuser/.local/share/zigup/0.16.0-dev.2223+4f16e80ce/files/lib /home/myuser/projects/zig/fooproject .zig-cache /home/myuser/.local/cache/zig --seed 0x1286ab62 -Zdf15ffca8d5047a4

It successfully builds and runs when I comment out exe.root_module.addOptions("config", build_time_options); , so I think there is a problem with exe step, but not sure where exactly.

This can be simplified:

const task_option = b.option(
    TaskOption, 
    "task", 
    "What task to run. Default: all",
    ) orelse TaskOption.all;
const build_time_options = b.addOptions();
build_time_options.addOption(TaskOption, "task", task_option);

I sometimes put the call to b.option directly inside the call to addOption, when I think that’s clearer.

I think you want:

const run = b.addRunArtifact(exe);
b.default_step.dependsOn(&run.step);

I’ve literally never used b.default_step but I don’t think you can just replace it like that.

Probably you’re better off making an explicit run step like this:

const run = b.addRunArtifact(exe);
const run_step = b.addStep("run", "run the executable");
run_step.dependsOn(&run.step);
1 Like

Yes, it works when I add explicit run step, but I wonder if there is a way to do it with b.default_step to run a program without additional step specifications…

I got it! Just assigned run step to b.default_step and now it works

final version:

const std = @import("std");

const TaskOption = enum {
    all,
    first,
    second,
};

pub fn build(b: *std.Build) void {
    b.top_level_steps = .{};

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

    const task_option = b.option(TaskOption, "task", "What task to run. Default: all") orelse TaskOption.all;

    const build_time_options = b.addOptions();
    build_time_options.addOption(TaskOption, "task", task_option);

    const exe = b.addExecutable(.{
        .name = "foo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/path/to/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.root_module.addOptions("config", build_time_options);

    const run = b.addRunArtifact(exe);

    const run_step = b.step("run", "run the tasks example");
    run_step.dependOn(&run.step);

    b.default_step = run_step;
}

Well there you go. And zig build -h identifies the run step as the default, which is a nice touch.

You should ask yourself though: is it? Is running the executable from the build script actually the default action? Fairly uncommon in my experience, I almost always run an executable from somewhere in my $PATH, after installing it.

Except when I’m developing, of course. But “default” doesn’t mean “thing I would like to have be the least typing while I’m developing”. I have shell abbreviations zbt and zbr anyway, so nothing with two words is going to be faster than that. If that’s your goal, an abbreviation works for all repos, without making the build script do unexpected things.

And it would be unexpected. Build tools uniformly do not execute artifacts unless instructed, I’ve never run make and had a program run as a result. It’s in the name: “build”, “make”, not “buildrun”, “makerun”.

Up to you though. If you think building and running the executable is the appropriate default, then make it the default, by all means.

I’m doing a small reference project that runs different zig code examples. i.e. I can do zig build -Dexample=comptime -Dsubexample=all and it will run all the snippets on comptime subject. I think this b.default_step approach makes sense here.