Can I avoid repetitive names while using the build system?

I’m not sure what you mean by “single file scripts”.

So far, I don’t mind having to maintain both build.zig and build.zig.zon. But I’d like to avoid updating 3 different places for each imported module, before being able to do @import("mymodule"):

  • First in build.zig.zon:
    .ws = .{
      .url = "git+",
      .hash = "1220376acac4a7fa2c1772d0bd2a1d030513fb3599322552f11c5bd743de58d0e486",
  • Then in
    const ws = b.dependency("ws", .{});
  • Finally, still in
    exe.root_module.addImport("ws", ws.module("websocket"));

To be honest, I haven’t really thought about the problem, and the solution. Coming from Go or Rust, it just feels repetitive. Is there a reason for the current design, or is there a possibility to eliminate the repetition for the more common use case?

The code above illustrating the “problem” is from bork/build.zig at master · kristoff-it/bork · GitHub.

I moved this post, because we are essentially asking different questions.

The first and second “ws” are just arbitrary names that need to match.
The third “ws” is also an arbitrary name that needs to match, with a forth “ws” in your @import statement.

So I would say, we have 2 pairs of keys that are used to lookup things.
One is to lookup a dependency, the other to lookup a module.

I think if you want to make things less verbose, it will only become confusing once you actually need the flexibility, when you are dealing with many dependencies and many modules.

I think it’s just WIP for the moment. The zig core team is pretty small but also very ambitious (and rightly so!) and the build system has been put on the back burner a little it seems.

But as @Sze said in the other post. It should not be that hard to have a utility function to call in your build.zig that would just parse your build.zig.zon and just call b.dependency(...) and addImport.

pub fn populateDependencies(b: Build, exe: ???) !void {
  // pseudo code!
  const package = try parse(build.zig.zon);
  for (package.dependencies) |dependency| {
    b.dependency(, .{});
    exe.root_module.addImport(, dependency);

Keep in mind that there is an overarching theme in Zig of favoring explicitness over magic. The steps you point out each serve a fundamentally different purpose and are all necessary steps toward adding a module as an import.

.ws = .{
    .url = "git+",
    .hash = "1220376acac4a7fa2c1772d0bd2a1d030513fb3599322552f11c5bd743de58d0e486",

Adding a dependency to your build.zig.zon package manifest instructs the compiler how to download and validate the contents of the package when you invoke zig build, before the build function of your build.zig script is invoked.

const ws = b.dependency("ws", .{});

This instantiates the dependency. Note that I use the word “instantiate”, because you can create multiple instances of the dependency and its exposed artifacts and modules with different options:

const ws_debug = b.dependency("ws", .{ .optimize = .Debug });
const ws_release = b.dependency("ws", .{ .optimize = .ReleaseSafe });
const ws_wasi = b.dependency("ws", .{ .target = b.resolveTargetQuery({ .cpu_arch = .wasm32, .os_tag = .wasi  }) });

Each of these variables are different instances of the same dependency created with different sets of options and can be used for different purposes in your build script.

const websocket = ws.module("websocket");

This resolves the module defined by the instance of the dependency and returns a reference to it. Note that a package can export multiple modules (or none), so just because you have a dependency doesn’t mean it’s obvious which of its exported modules you want to reference.

exe.root_module.addImport("ws", websocket);

Finally, this adds the websocket module to table of modules that can be @imported by exe’s root module, under the name ws.