How to print path to a freshly build executable in zig-cache?

Is there some built-in way to make Step.Compile print the path inside .zig-cache where the binary is generated to?

I want to build a Zig binary and run it, but I want to run the binary directly, not through zig build. One way I could do this is by adding an install step, such that I do ./zig/zig build && ./zig-out/bin/my-binary, but that doesn’t work if there’s concurrency. I run several ./zig/zig build -Dmy-option=value commands in parallel, which build different flavors of my-binary, and I want to run each flavor.

I think I know how to do this by writing my custom addPrintCompileStep, which would depend on Compile.Step, and, in its make function, call its compile_step.getEmittedBin and then print it to stdout. But writing custom steps is a bit of a pain, so I am wondering if there’s some easy way to instruct build system to inform the caller about what actually got built?

1 Like

You can call the getEmittedBin or addRunArtifact on the Step.Compile that addExecutable returns (for the exe artifact).

You must not call installArtifact or addInstallArtifact (these copy the exe artifact from .zig-cache to prefix/bin).

Example:

   const tester_exe = b.addExecutable(.{
        .name = "tester",
        .root_source_file = src_tester,
        .target = target,
        .optimize = optimize,
    });
    const run_tester = b.addRunArtifact(tester_exe);
    run_tester.has_side_effects = true;
    run_tester.step.dependOn(&tester_exe.step);
1 Like

But I had to plug that into my own custom step, right? There’s no way I can just convince the build-runner to print the path once it realized, right?

or addRunArtifact

This sadly doesn’t work for me, I need to run the binary directly (the X problem is that we run a bunch of fuzzers concurrently on a machine, and, as each fuzzer uses a random, and sometimes quite high amount of RAM, some fuzzers get OOM killed. So I want to determine if the process was sigkilled by OOM and not treat it as a fuzzing failure. But if I run the process through addRunArtifcat and not directly, then it’s not my code that waits for OOMed child, but actually the code in build_runner.zig. I guess an alternative solution here would be to make addRunArtifact to exec the process in-place, but, AFAIR, that’s also not possible?)

1 Like

You can call getEmmitedBin like:

const bin_file = artifact.getEmittedBin();
bin_file.addStepDependencies(&run.step);

where artifact is a *Step.Compile.


The problem might be that getEmmitedBin returns a LazyPath propably the .generated one.
See getPath3 but note that the documentation says: β€œIntended to be used during the make phase only.”


EDIT:
If you are going to use addSystemCommand you can also use addArtifactArg (that calls getEmmitedBin).

There’s no builtin way that I know of, see zigwin32gen/build.zig at 1213c28c588cd366f7ff6431a624c1f3850af532 Β· marlersoft/zigwin32gen Β· GitHub

    b.step(
        "show-path",
        "Print the zigwin32 cache directory",
    ).dependOn(&PrintLazyPath.create(b, gen_out_dir).step);

Maybe Andrew would be amenable to adding something like b.addPrintLazyPath to std.Build?

Here’s the implementation I’m using:

const PrintLazyPath = struct {
    step: Step,
    lazy_path: Build.LazyPath,
    pub fn create(
        b: *Build,
        lazy_path: Build.LazyPath,
    ) *PrintLazyPath {
        const print = b.allocator.create(PrintLazyPath) catch unreachable;
        print.* = .{
            .step = Step.init(.{
                .id = .custom,
                .name = "print the given lazy path",
                .owner = b,
                .makeFn = make,
            }),
            .lazy_path = lazy_path,
        };
        lazy_path.addStepDependencies(&print.step);
        return print;
    }
    fn make(step: *Step, prog_node: std.Progress.Node) !void {
        _ = prog_node;
        const print: *PrintLazyPath = @fieldParentPtr("step", step);
        try std.io.getStdOut().writer().print(
            "{s}\n",
            .{print.lazy_path.getPath(step.owner)},
        );
    }
};
3 Likes

I think my first approach would be to just encode the options as a suffix of the executable name like so:

const strip = b.option(
    bool,
    "strip",
    "Strip debug info to reduce binary size, defaults to true in release modes",
) orelse false;

const Mode = enum { normal, fuzz, debug, interactive };
const mode = b.option(
    Mode,
    "mode",
    "compile in special modes",
) orelse .normal;

const name = try std.fmt.allocPrint(b.allocator, "example-{s}{s}", .{ if (strip) "s" else "u", @tagName(mode) });

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

So you end up with a bunch of executables in the zig-out/bin like this:

.
β”œβ”€β”€ bin
β”‚   β”œβ”€β”€ example-sfuzz
β”‚   β”œβ”€β”€ example-snormal
β”‚   β”œβ”€β”€ example-ufuzz
β”‚   β”œβ”€β”€ example-uinteractive
β”‚   └── example-unormal

Or if you have a more complex situation you could create separate directories like this:

const strip = b.option(
    bool,
    "strip",
    "Strip debug info to reduce binary size, defaults to true in release modes",
) orelse false;

const Mode = enum { normal, fuzz, debug, interactive };
const mode = b.option(
    Mode,
    "mode",
    "compile in special modes",
) orelse .normal;

const fuzzer = try std.fmt.allocPrint(b.allocator, "{s}{s}", .{ if (strip) "s" else "u", @tagName(mode) });

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

const install_wf = b.addWriteFiles();
_ = install_wf.addCopyFile(exe.getEmittedBin(), "example");
// can even add meta info files besides the executable
var string = std.ArrayList(u8).init(b.allocator);
try std.json.stringify(.{
    .strip = strip,
    .mode = mode,
}, .{}, string.writer());
_ = install_wf.add("config.json", string.items);
const install = b.addInstallDirectory(.{
    .source_dir = install_wf.getDirectory(),
    .install_dir = .prefix,
    .install_subdir = b.pathJoin(&.{ "fuzzers", fuzzer }),
});
b.getInstallStep().dependOn(&install.step);

And end up with this:

.
└── fuzzers
    β”œβ”€β”€ sfuzz
    β”‚   β”œβ”€β”€ config.json
    β”‚   └── example
    β”œβ”€β”€ sinteractive
    β”‚   β”œβ”€β”€ config.json
    β”‚   └── example
    β”œβ”€β”€ snormal
    β”‚   β”œβ”€β”€ config.json
    β”‚   └── example
    β”œβ”€β”€ udebug
    β”‚   β”œβ”€β”€ config.json
    β”‚   └── example
    β”œβ”€β”€ ufuzz
    β”‚   β”œβ”€β”€ config.json
    β”‚   └── example
    └── uinteractive
        β”œβ”€β”€ config.json
        └── example

Where config contains something like this:

{"strip":false,"mode":"fuzz"}

So I don’t think you need to give up on using installation directories, just create your own directory structure for them.
Or is there any downside to this that I haven’t realized?

If you don’t want to encode the options you could just have an option -Dfuzzer-name=someuniqueid and use that as the name of the folder in the second example and then your process can create the path zig-out/fuzzer/someuniqueid/example to invoke the executable.

Perhaps Run steps can gain a β€œdry run” option.

3 Likes