Idiomatic way to compile for several targets simultaneously in build.zig?

With build.zig, you can use -Dtarget=aarch64-linux and such for easy cross compilation to a single target. Is there some idiom to compile to many targets at the same time?

I can run zig build several times, for each target I need, but that’s not parallel.

I can for(targets) inside build.zig to create a step per target and tie them all into build install command, but that requires somewhat non-trivial refactor of build.zig.

Is there some way to take a trivial build.zig generated by zig init-exe, and somehow make that compile to a bunch of targets simultaneosly in a single command in parallel?

I don’t know if there’s a command like that. So, this doesn’t answer your question, but here’s how I’d make a trivial cross-compiling build.zig example:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});
    const root_source_file = std.Build.FileSource.relative("src/main.zig");

    const exe_step = b.step("exe", "Install executable for all targets");

    for (TARGETS) |TARGET| {
        const exe = b.addExecutable(.{
            .name = "exe",
            .target = TARGET,
            .optimize = optimize,
            .root_source_file = root_source_file,
        });

        const exe_install = b.addInstallArtifact(exe, .{});
        exe_step.dependOn(&exe_install.step);
    }

    b.default_step.dependOn(exe_step);
}

const TARGETS = [_]std.zig.CrossTarget{
    .{ .cpu_arch = .aarch64, .os_tag = .linux },
    .{ .cpu_arch = .aarch64, .os_tag = .macos },
    .{ .cpu_arch = .aarch64, .os_tag = .windows },
};
1 Like

Hm, I think this needs something else to avoid name clash in the install dir? As written, I think linux and mac binaries would both race to overwirte zig-out/bin/exe?

1 Like

Oh yeah, forgot about that. Just the name change will do:

const std = @import("std");

pub fn build(b: *std.Build) !void {
    const optimize = b.standardOptimizeOption(.{});
    const root_source_file = std.Build.FileSource.relative("src/main.zig");

    const exe_step = b.step("exe", "Install executable for all targets");

    for (TARGETS) |TARGET| {
        const name = try std.fmt.allocPrint(b.allocator, "{s}-{s}", .{
            @tagName(TARGET.cpu_arch.?),
            @tagName(TARGET.os_tag.?),
        });

        const exe = b.addExecutable(.{
            .name = name,
            .target = TARGET,
            .optimize = optimize,
            .root_source_file = root_source_file,
        });

        const exe_install = b.addInstallArtifact(exe, .{});
        exe_step.dependOn(&exe_install.step);
    }

    b.default_step.dependOn(exe_step);
}

const TARGETS = [_]std.zig.CrossTarget{
    .{ .cpu_arch = .aarch64, .os_tag = .linux },
    .{ .cpu_arch = .aarch64, .os_tag = .macos },
    .{ .cpu_arch = .aarch64, .os_tag = .windows },
};
5 Likes

We could update the build_runner to accept multiple targets and then invoke the build fn for each one. This could also be generalized to any set of build options, i.e.

zig build --instance -Dtarget=x86_64-windows --instance -Dsome-other-option

This would be equivalent to invoking zig build twice we each set of corresponding options. The advantage of this over executing zig build each time is you can take full advantage of zig build’s parallelism. Seems like a feature worth considering.

2 Likes