Run an artifact with a dependency on a file as a run step

Hi, I’m working on upgrading a [program] packaged(https://codeberg.org/AlexBlais/stlink) with Zig 0.16.0 to Zig 0.17.0.

To compile the st-flash program, you used to be required to define a macro that has the path to a chips directory which contains information needed to flash a microcontroller properly. I used to use b.install_prefix in the Zig 0.16.0 version. I wasn’t able to do something similar in the Zig 0.17.0, I’m guessing it’s caused by the maker and configurer process separation which makes the install prefix not known at configure time.

Luckily, the original stlink project added a commit to make it resolve that directory at runtime (it looks at ../share/other/stuff/chips which solves my usecase.

However, I’m running into another issue. My package also exposes the compiled artifact so it can be used as part of a build step like so:

const stlink_dep = b.dependency("stlink", .{ .optimize = .safe });
const stflash = stlink_dep.artifact("st-flash");

const flash_mcu = b.addRunArtifact(stflash);
flash_mcu.addArg("write");
flash_mcu.addFileArg(firmware);
flash_mcu.addArg("0x08000000");

This used to work correctly, but now, the program fails to find it’s required chips directory.
I’ve tracked down the issue to the Build.addInstallDirectory function which is used to install the chips from the source to prefix/share/....
In Zig 0.16.0, that directory used to be installed in .zig-cache/i/... and the st-flash executable could find it because it had it’s absolute path.
In Zig 0.17.0, that directory is installed in the prefix and the executable is at .zig-cache/o/{hash}/st-flash and so cannot find the chips directory with the usual relative path.

So it comes down to this: How can I run a compiled artifact that has a dependency on a directory with a fixed relative path to that artifact? I’d also prefer if it didn’t pollute the installed files of the main project.

Thanks!

I did some digging and it seems the st tools also supports passing the path as an environment variable.

Unfortunately the build system still doesn’t support adding lazy paths as env vars, but that is easy to work around with a small script. You may want to expose a helper function so that dependents don’t have to set all that up themselves.

I saw the st tools supported passing the path as an environment variable, but gave up when I saw setting one required a []const u8.
Making a wrapper script and giving it the path using addFileArg, making it call st-flash and setting the env vars appropriatly should do the trick.

Thanks!

I implemented it like so if this can help someone else:

In the stlink package:

pub fn addRunArtifact(b: *std.Build, stlink_dep: *std.Build.Dependency, target_exe: *Step.Compile) *Step.Run {
    const env_setter_wrapper = b.addExecutable(.{
        .name = "env_setter_wrapper",
        .root_module = b.createModule(.{
            .root_source_file = stlink_dep.path("env_setter_wrapper.zig"),
            .optimize = .debug,
            .target = b.graph.host,
        }),
    });

    const run_artifact = b.addRunArtifact(env_setter_wrapper);
    run_artifact.addFileArg2(stlink_dep.namedLazyPath("chips_dir"), .{ .make_absolute = true });
    run_artifact.addFileArg2(target_exe.getEmittedBin(), .{ .make_absolute = true });
    return run_artifact;
}

In env_setter_wrapper.zig

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const gpa = init.gpa;

    const args = try init.minimal.args.toSlice(init.arena.allocator());
    if (args.len < 3) {
        std.debug.print("Usage: {s} path_to_chips_dir exe_to_execute\n", .{args[0]});
        return error.NotEnoughArgs;
    }

    var environ_map = std.process.Environ.Map.init(gpa);
    defer environ_map.deinit();
    try environ_map.put("STLINK_CHIPS_DIR", args[1]);

    var child = try std.process.spawn(io, .{
        .argv = args[2..],
        .environ_map = &environ_map,
    });
    const term = try child.wait(io);

    const exit_code = switch (term) {
        .exited => |code| code,
        else => 1,
    };
    std.process.exit(exit_code);
}

In the package that depends on stlink:

const stlink_dep = b.dependency("stlink", .{ .optimize = .ReleaseFast });
const stflash = stlink_dep.artifact("st-flash");

const flash_mcu = @import("stlink").addRunArtifact(b, stlink_dep, stflash);
flash_mcu.addArg("write");
flash_mcu.addFileArg(firmware);
flash_mcu.addArg("0x08000000");