Getting a `build.zig` to build and test defining an options module

I want to be able to:

zig build test
zig build

where the first line does a test run, and the second line builds an executable (fib in this case).

The challenge is that I want to set up an options module that the code can check in both the test and the production code.

My build.zig is:

const std = @import("std");
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const options = b.addOptions();
    const llvm_module = build_llvm_module(b, target, optimize);

    const mod = b.createModule(.{
        .root_source_file = b.path("zag/zag.zig"),
        .target = target,
    });
    mod.addOptions("options", options);
    const exe = b.addExecutable(.{
        .name = "zag",
        .root_module = b.createModule(.{
            .root_source_file = b.path("zag/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "zag", .module = mod },
            },
        }),
    });
    exe.root_module.addOptions("options", options);
    b.installArtifact(exe);
    const run_step = b.step("run", "Run the app");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }
    const mod_tests = b.addTest(.{
        .root_module = mod,
    });
    const run_mod_tests = b.addRunArtifact(mod_tests);
    const exe_tests = b.addTest(.{
        .root_module = exe.root_module,
    });
    const run_exe_tests = b.addRunArtifact(exe_tests);
    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_mod_tests.step);
    test_step.dependOn(&run_exe_tests.step);

    var includeLLVM = false;
    if (b.option(bool, "llvm", "Include LLVM in build") orelse false) {
        includeLLVM = true;
    }
    options.addOption(bool, "includeLLVM", includeLLVM);
    const git_version = b.run(&.{ "git", "log", "--pretty=format:%cI-%h", "-1" });
    options.addOption([]const u8, "git_version", git_version);

    if (includeLLVM) {
        mod.addImport("llvm-build-module", llvm_module);
        exe.root_module.addImport("llvm-build-module", llvm_module);
    }

    const fib = b.addExecutable(.{
        .name = "fib",
        .root_module = b.createModule(.{
            .root_source_file = b.path("experiments/fib.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "zag", .module = mod },
            },
        }),
    });
    //b.step("fib", "Compile fib").dependOn(&b.installArtifact(fib).step);
    b.installArtifact(fib);
}

fn build_llvm_module(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Module {
    const llvm_module = b.addModule("llvm-build-module", .{
        .root_source_file = b.path("./zag/libs/zig-llvm/src/llvm.zig"),
        .target = target,
        .optimize = optimize,
    });
    llvm_module.addCMacro("_FILE_OFFSET_BITS", "64");
    llvm_module.addCMacro("__STDC_CONSTANT_MACROS", "");
    llvm_module.addCMacro("__STDC_FORMAT_MACROS", "");
    llvm_module.addCMacro("__STDC_LIMIT_MACROS", "");
    llvm_module.linkSystemLibrary("z", .{});

    if (target.result.abi != .msvc) {
        llvm_module.link_libc = true;
    } else {
        llvm_module.link_libcpp = true;
    }

    switch (target.result.os.tag) {
        .linux => llvm_module.linkSystemLibrary("LLVM-18", .{}), // Ubuntu
        .macos => {
            llvm_module.addLibraryPath(.{
                .cwd_relative = "/opt/homebrew/opt/llvm/lib",
            });
            llvm_module.linkSystemLibrary("LLVM", .{
                .use_pkg_config = .no,
            });
        },
        else => llvm_module.linkSystemLibrary("LLVM", .{
            .use_pkg_config = .no,
        }),
    }
    return llvm_module;
}

You can safely ignore the LLVM stuff.

I have two files zag.zig:

pub const config = @import("config.zig");
pub const InMemory = @import("object/inMemory.zig");
pub const object = @import("object.zig");
pub const Object = object.Object;
pub const execute = @import("execute.zig");
pub const Context = @import("context.zig");
pub const Process = @import("process.zig");
pub const heap = @import("heap.zig");
pub const globalArena = @import("globalArena.zig");
pub const symbol = @import("symbol.zig");
pub const primitives = @import("primitives.zig").primitives;
pub const utilities = @import("utilities.zig");
pub const llvm = if (config.includeLLVM) @import("llvm-build-module") else null;
pub const threadedFn = @import("threadedFn.zig");

and root.zig which is the same with the addition of:

test "root test" {
    _ = config;
    _ = symbol;
    _ = llvm;
    _ = threadedFn;
}

to run the tests. I want 2 modules to be visible everywhere else: zag and options. They are visible in the fib (default) build but not in the test build.

Help! Nothing I try seems to work. Somebody with good “build fu” can probably see the fix immediately. Thanks.

I think you’re just missing: mod_tests.root_module.addOptions("options", options);

Sadly, no. Still:

error: no module named 'options' available within module 'test'

OK, I think I’ve got it.

Basically I renamed the mod module to zag (irrelevant), and then replaces the test code with:

    const tests = b.addTest(.{
        .root_module = b.createModule(.{
            .root_source_file = b.path("zag/root.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "zag", .module = zag },
            },
        }),
    });
    tests.root_module.addOptions("options", options);
    const run_tests = b.addRunArtifact(tests);
    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_tests.step);

and not it all seems to work properly.

Thanks for the help.