Build option inheritance

I just resolved a build issue in a Zig project of mine that kept me busy for much too long. The mistake was that I didnt’t pass the .optimize and .target parameters down to a module that the program included, so it ended up with a bunch of link errors.

Now, in order to learn the right lessons from this, my question is: When is it necessary to pass these along and when are they inherited (if that’s the right word) from a project’s root module? Short of studying the source code, is this documented somewhere?

Modules can be initiated with null target and optimization, in which case these will be inherited.
However, in order for you to compile a module, at least the target needs to be explicitly specified, and the optimization mode will default to debug if not specified.
Also note that standardOptimizeOption and its target counterpart do not return optionals. If you don’t explicitly specify these options in your build command, zig will give you a default value, which is not null.
Put it all together, and you get this footgun:

const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOption(.{});
const exported_module = b.addModule("foo", .{
  .root_source_file = b.path("src/root.zig"),

  // Here you have given the module a non-null
  // optimize and target, so they will not be inherited.
  // If you don't specify them when importing your module
  // you'll end up with .Debug and native target
  .optimize = optimize,
  .target = target.
});

The proper way of doing it is:

// This is suitable for importing, but unsuitable for compilation.
const exported_module = b.addModule("foo", .{
  .root_source_file = b.path("src/root.zig"),
});

const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOption(.{});

// This is suitable for compilation, but unsuitable for importing.
const compiled_module = b.createModule(
  .root_source_file = b.path("src/root.zig"),
  .optimize = optimize,
  .target = target
);
// Only `compiled_module` can be used for compilation.
const exe = b.addExecutable(.{
  .name = "foo",
  .root_module = compiled_module,
});

// Tests are compiled as well
const test_exe = b.addTest(.{
  .name = "foo",
  .root_module = compiled_module,
});
4 Likes

My rule of thumb is always passing optimize and target into any dependencies and/or modules I use. There are sometimes exceptions to this but you can usually tell when you want something to always be Debug or always be a wasm target.

1 Like