Idiomatic way of using Zig build system with multiple binaries

Hi all.

I have been diving deeper into Zig build system, so I don’t have to use Makefiles anymore.

I have read the official guide and the system seems quite powerful, but I do have one question when it comes to multiple binaries and how to compile just one binary and not everything.

In my testing project, I have two programs (build.zig looks like this).

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const runtime = b.addExecutable(.{
        .name = "runtime",
        .root_source_file = b.path("runtime/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(runtime);

    const sandbox = b.addExecutable(.{
        .name = "sandbox",
        .root_source_file = b.path("sandbox/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(sandbox);
}

I can of course build both of them with zig build. What I am wondering is, what is the best way to compile just one of them. Something like zig build sandbox.

Is there a common way to achieve this, maybe a pattern, I don’t know.

Thank you in advance!

You need to add two steps (build targets):

    const runtime_step = b.step("runtime", "Build runtime");
    const sandbox_step = b.step("sandbox", "Build sandbox");

These steps are listed, with their name and description, when running: zig build -l


EDIT: added calls to addInstallArtifact that copies the executable to zig-out/bin

const install_runtime = b.addInstallArtifact(runtime, .{});
const install_sandbox = b.addInstallArtifact(sandbox, .{});

Finally you need to add the step dependencies:

    runtime_step.dependOn(&install_runtime.step);
    sandbox_step.dependOn(&install_sandbox.step);

Then you can build:

  • runtime by running zig build runtime,
  • sandbox by running zig build sandbox,
  • both by running zig build runtime sandbox,
  • both (via the default install step) by running zig build.

See also: Build System Tricks

2 Likes

This does not build the binary for me. Doing zig build builds both of them, and the steps are listed when I do zig build -l.

$ zig build -l
  install (default)            Copy build artifacts to prefix path
  uninstall                    Remove build artifacts from prefix path
  runtime                      Build runtime
  sandbox                      Build sandbox

My code after I added the suggested solution looks like:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const runtime = b.addExecutable(.{
        .name = "runtime",
        .root_source_file = b.path("runtime/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(runtime);

    const sandbox = b.addExecutable(.{
        .name = "sandbox",
        .root_source_file = b.path("sandbox/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(sandbox);

    const runtime_step = b.step("runtime", "Build runtime");
    const sandbox_step = b.step("sandbox", "Build sandbox");

    runtime_step.dependOn(&runtime.step);
    sandbox_step.dependOn(&sandbox.step);
}

I have tried deleting zig-out folder and then also recreating en empty zig-out/bin folder just to test if maybe that would be a problem, and no dice.

If I do zig build runtime or zig build sandbox nothing happens. No errors are displayed.

I also checked the status of last command which was zig build runtime and I got:

$ echo $?
0

Am I missing something?

It can be quite difficult to understand what’s happening under the hood. Try using the --summary all or new options to get an output of what’s actually been built versus retrieved from cache.

This usually means the artifacts were cached and their inputs were unchanged, so no rebuild happened. Use the --summary option to see what zig build actually did.

I was indeed being cached, and I have managed to fix it with providing a new cache directory with:

$ zig build sandbox --summary all --cache-dir ./zig-cache
Build Summary: 2/2 steps succeeded
sandbox success
└─ zig build-exe sandbox Debug native success 969ms MaxRSS:189M

Note: even when I changed the source file, it was still treating it like nothing changed. When I provided a custom cache directory, it started working.

I guess because I did zig build before adding new executables, it somehow got confused. I am using version 0.14.

This is ok now, and I will continue providing --cache-dir just to be sure.

The issue with this version is that b.addExecutable only builds the executable (artifact), but doesn’t do anything with it. To get the executable to appear in zig-out/bin, you additionally need an install step:

const install_runtime = b.addInstallArtifact(runtime, .{});
runtime_step.dependOn(&install_runtime.step);

This is actually what b.installArtifact(runtime) is doing under the hood for the default “install” step. The entire implementation of installArtifact is

pub fn installArtifact(b: *Build, artifact: *Step.Compile) void {
    b.getInstallStep().dependOn(&b.addInstallArtifact(artifact, .{}).step);
}

That is, it’s just creating an install step for the artifact and adding it as a dependency of the “install” step. By creating your own install steps, you can add them selectively as dependencies of other steps, as you’re looking to do here.

3 Likes

@ianprime0509 you are correct, Install artifact was indeed missing. Now it’s working without me providing any other arguments.

This is what I have now, and it’s been working. I’m pasting it here if somebody will also wonder about this.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Runtime.
    const runtime = b.addExecutable(.{
        .name = "runtime",
        .root_source_file = b.path("runtime/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(runtime);

    const runtime_step = b.step("runtime", "Build runtime");
    runtime_step.dependOn(&runtime.step);

    const install_runtime = b.addInstallArtifact(runtime, .{});
    runtime_step.dependOn(&install_runtime.step);

    // Sandbox.
    const sandbox = b.addExecutable(.{
        .name = "sandbox",
        .root_source_file = b.path("sandbox/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(sandbox);

    const sandbox_step = b.step("sandbox", "Build sandbox");
    sandbox_step.dependOn(&sandbox.step);

    const install_sandbox = b.addInstallArtifact(sandbox, .{});
    sandbox_step.dependOn(&install_sandbox.step);
}

I can now build each one of the seperately or all together with the following commands.

# all together
$ zig build --summary all

# just runtime
$ zig build runtime --summary all

# just sandbox
$ zig build sandbox --summary all
3 Likes

Just to add on to this, you also no longer need the explicit runtime_step.dependOn(&runtime.step), since runtime is already a dependency of install_runtime (to install any artifact, it needs to be built first), and hence transitively of runtime_step (due to runtime_step.dependOn(&install_runtime.step)). The resulting build graph will end up looking like

runtime_step <- install_runtime <- runtime
4 Likes

related topic