How to access target options and build prefix in makeFn

I am building a cross-platform app and want to be able to retain a build for every platform. To that end I am trying to impose the following structure on the output directory:

Instead of compiling the binary to zig-out/bin, I want to compile to e.g. bin/macos_aarch64.

The first part is taken care of by invoking the compiler via zig build -p bin.

The second part is partially taken care of by the Step.InstallArtifact.Options:

const target = b.standardTargetOptions(.{});
const artifact = b.addInstallArtifact(exe, .{
    .dest_dir = .{
        .override = .{
            .custom = try std.fmt.allocPrint(b.allocator, "{s}_{s}", .{
                @tagName(target.result.os.tag),
                @tagName(target.result.cpu.arch),
            }),
        },
    },
});
b.getInstallStep().dependOn(&artifact.step);

I have a problem in my custom build step however. Calling step.owner.standardTargetOptions(.{}) in the makeFn yields a panic:

var make_symlink = b.step("link-data", "Create symlink to data folder");
make_symlink.makeFn = struct {
    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
        const output_target = step.owner.standardTargetOptions(.{});
        const output_dir = try std.fmt.allocPrint(
            step.owner.allocator, "bin/{s}_{s}", .{
                @tagName(output_target.result.os.tag),
                @tagName(output_target.result.cpu.arch),
            }
        );
        const exe_dir = try std.fs.cwd().openDir(output_dir, .{});
        exe_dir.deleteFile("data") catch |err| switch (err) {
            error.FileNotFound => {},
            else => return err,
        };
        try exe_dir.symLink("../../data", "data", .{ .is_directory = true });
    }
}.make;
make_symlink.dependOn(b.getInstallStep());
thread 136342 panic: Option 'target' declared twice
[...]
[...]/build.zig:45:67: 0x101054313 in make (build)
            const output_target = step.owner.standardTargetOptions(.{});

What am I doing wrong here? Is it illegal to call standardTargetOptions twice? How else do I get access to those in the make function?
And how do I access the build prefix – bin/ in my case?

A hint in the right direction would be greatly appreciated!

If you want to access additional context from within a step’s makeFn, you can use @fieldParentPtr:

const StepContainer = struct {
    step: std.Build.Step,
    target: std.Build.ResolvedTarget,
};

const container = b.allocator.create(StepContainer) catch @panic("OOM");

container.* = .{
    .step = .init(.{
        .id = .custom,
        .name = "link-data-intermediate-step",
        .owner = b,
        .makeFn = struct {
            fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
                const parent: *StepContainer = @fieldParentPtr("step", step);
                const output_target = parent.target;
                const output_dir = try std.fmt.allocPrint(step.owner.allocator, "bin/{s}_{s}", .{
                    @tagName(output_target.result.os.tag),
                    @tagName(output_target.result.cpu.arch),
                });
                const exe_dir = try std.fs.cwd().openDir(output_dir, .{});
                exe_dir.deleteFile("data") catch |err| switch (err) {
                    error.FileNotFound => {},
                    else => return err,
                };
                try exe_dir.symLink("../../data", "data", .{ .is_directory = true });
            }
        }.make,
    }),
    .target = target,
};

container.step.dependOn(b.getInstallStep());

var make_symlink = b.step("link-data", "Create symlink to data folder");
make_symlink.dependOn(&container.step);

If you want to access the build prefix, it is stored as a string in b.install_prefix.

You can also construct a path relative to the prefix like so: b.getInstallPath(.prefix, "my/relative/path").

1 Like

Awesome! That’s exactly what I was missing. Thank you!