Using Text From a System Command as part of a Path During Build

Just encountered something I haven’t done yet with Zig’s build system, but have with others (Cmake/Meson/Makefiles). I’m trying to get the output of a system command, and then use said output to determine a path to include in my build. For example I know how to generate a file at install time that contains this output:

// Getting the output of some system command
const get_a_string_cmd = b.addSystemCommand(&.{ "echo", "some/partial/path" });
const get_a_string_stdout = get_a_string_cmd.captureStdOut();
const get_a_string_install_file = b.addInstallFile(get_a_string_stdout, "output.txt");

This generates a file zig-out/output.txt as expected, the trick now is I want to use that string value in an include path. For instance, I want to do something to the effect of:

const my_command_output = "some/partial/path"; // This needs to come from system command instead!
exe.addLibraryPath(.{ .path = b.fmt("/home/whatever/something/{s}", .{my_command_output}) });

Once figured out I’ll add to the build system tricks and tips post!

As an aside, are there any issues open to make this kind of thing easier? It seems like there’s a LOT of hoops to jump through to get behavior that can be achieved with a single line in a Makefile:

VARIABLE = $(shell echo helloworld 2>&1)

In other words, you need to run a system command during the configure phase.

This is what is for.

Note that this is generally discouraged as it adds a dependency on the system to your build script, making it less portable, and more of a pain in the ass to set up a dev environment.

Much appreciated, this is perfect!

Note that this is generally discouraged as it adds a dependency on the system to of your build script, making it less portable, and more of a pain in the ass to set up a dev environment.

Completely agreed, the context for needing this ugliness for anyone interested is explained in this Makefile:

Basically I need to use the arm-none-eabi-gcc compiler to tell me where the processor specific pre-compiled libc libraries are based on the -mcpu, etc arguments so I can manually link them in to my executable. In this case, the dev environment absolutely must have an installation of the arm-none-eabi-gcc compiler somewhere.


I wonder if you could make your own wrapper around the Compile step that adds extra include paths at build time instead of configure time? The only thing I’m not sure of is if there are other parts of configuration that rely on knowing all the include paths, I have a feeling this isn’t the case so this should be possible.

You just create your own Compile struct with a std.Build.Step.Compile step inside of it, and inside your make function you add any extra include paths that are missing (might need some synchronization though since I don’t think Compile is meant to be thread-safe).

Interesting! Any good examples or notes you could point me towards on differentiating what is executed at “configure” vs “build” time with Zigs build system? With Cmake its relatively easy to figure out through docs and because you actually have to run a “configure” and then “build” command separately.

I guess I’m confused on the distinction with Zigs build system and why you would potentially want this step (finding paths from a system command) in one versus the other.

The “configure” phase is during the execution of your pub fn build(b: *std.Build) void { ... } function. You could, for example, put defer std.debug.print("end of configure phase; start of make phase\n", .{}); at the top.

The “make” phase is what happens after that, and is implemented in lib/compiler/build_runner.zig.

It is planned for these phases to run in separate processes in the future. Run build.zig logic in a WebAssembly sandbox · Issue #14286 · ziglang/zig · GitHub


Ahh awesome thank you! Makes sense, you’re setting up the structure of what “will” run in the make phase during the configure phase in build().

yeah, in retrospect, I should have called that function “configure” rather than “build”.


Oooo I like that :slight_smile: Not too late to change. You could easily support both for a smooth transition.


It makes the signature read more like a sentence too:

pub fn configure(b: *std.Build) void {

So we configure the build.