How to see output of custom build step?

I’m using a custom build step called “precompile” to generate some Zig source files with Zig 0.16.

The precompile step has 2 parts: One part uses a Python script to generate a Zig file, the other part uses a Zig program to generate two other Zig files.

This is an excerpt of the build.zig file:

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const precompile_step = b.step("precompile", "Generate Zig sources from Python.");
    const precompile_cmd = b.addSystemCommand(&.{ "cmd.exe", "/C", "precompile.cmd", "x" });
    precompile_cmd.addFileInput(b.path("precompile.cmd"));
precompile_cmd.addFileInput(b.path("src/builtins/autogen/errors.py"));
precompile_cmd.addFileInput(b.path("src/builtins/autogen/errors_input.py"));
    precompile_step.dependOn(&precompile_cmd.step);
    const wrap_raylib = b.addExecutable(.{ .name = "wrap_raylib", .root_module = b.createModule(.{
        .root_source_file = b.path("tools/wrap_raylib.zig"),
        .target = b.graph.host,
        .optimize = .Debug,
    }) });
    const wrap_raylib_step = b.addRunArtifact(wrap_raylib);
    const generated_raymath_zig = wrap_raylib_step.addOutputFileArg("raymath.zig");
    const generated_raylib_zig = wrap_raylib_step.addOutputFileArg("raylib.zig");
    const wf = b.addUpdateSourceFiles();
    wf.addCopyFileToSource(generated_raymath_zig, "src/std/raymath.zig");
    wf.addCopyFileToSource(generated_raylib_zig, "src/std/raylib.zig");
    wf.step.dependOn(&wrap_raylib_step.step);
    precompile_step.dependOn(&wf.step);


I can see the output of the Python script when I call zig build precompile.

The Zig part of the precompile step also works as expected and I can see the output of the Zig program, but then in the end the zig build precompile says:

failed command: ".\\.zig-cache\\o\\…\\wrap_raylib.exe" ...

Ok, I thought, so maybe the build system doesn’t like writing to stderr, just as if my custom build step was a test, and I know the opinionated Zig build system does not allow any stderr output in tests because it interpretes this as an error symtom (which causes some confusion for users every now and then).

So I tried to write to switch to std.log instead (which is cleaner anyway for my purpose) and write my own little logFn which should write to stdout instead of stderr, like this:

pub const std_options: std.Options = .{
    .logFn = myLog,
};

pub fn myLog(
    comptime level: std.log.Level,
    comptime scope: @EnumLiteral(),
    comptime format: []const u8,
    args: anytype,
) void {
    const io = Io.Threaded.global_single_threaded.io();
    // const prev = io.swapCancelProtection(.blocked);
    // defer _ = io.swapCancelProtection(prev);
    var buffer: [64]u8 = undefined;
    var stdout = Io.File.stdout().writer(io, &buffer);
    const terminal: Io.Terminal = .{
        .writer = &stdout.interface,
        .mode = Io.Terminal.Mode.escape_codes,
    };
    return std.log.defaultLogFileTerminal(level, scope, format, args, terminal) catch @panic("IO");
}

Using this approach avoids the “failed command: …” error message, but it doesn’t show any output.

It seems weird that it is either impossible or unreasonable complicated to show some output in a custom build step.

Forgot to mention that, before using std.log, the program used std.debug.print.

I think your custom logging function is the issue. With a build.zig like:

const custom_exe = b.addExecutable(.{ .root_module = b.createModule(.{ .root_source_file = b.path("custom.zig"), .optimize = optimize, .target = target }), .name = "custom" });
const custom_run = b.addRunArtifact(custom_exe);
const custom = b.step("custom", "");
custom.dependOn(&custom_run.step);

and custom.zig:

const std = @import("std");

pub fn main() !void {
    std.log.debug("hello world", .{});
}

Running zig build custom gets me:

debug: hello world

Log level doesn’t seem to make a difference. If I use your custom function, however, the output disappears.

Actually, yours works fine too. You just have to call std.log.defaultLogFileTerminal instead of returning it, and then call stdout.interface.flush().

I found out that whether there is output or not depends on the .optimize value, which you didn’t specify, so it defaults to .Debug.

With .ReleaseSafe, there is no output with std.log.debug (and no “failed command: …”, btw.):

const custom_exe = b.addExecutable(.{ .root_module = b.createModule(.{ .root_source_file = b.path("custom.zig"), .optimize = .ReleaseSafe, .target = target }), .name = "custom" },);
const custom_run = b.addRunArtifact(custom_exe);
const custom = b.step("custom", "");
custom.dependOn(&custom_run.step);

Only with .info or .warn, there is output.

With std.debug.print (and \n of course), I also get output and no “failed command”, which is strange, because I don’t see the difference to my wrap_raylib step.

However, if I add wrap_raylib_step.has_side_effects = true; then I get output with or without my custom log fn and no “failed command: …”.

Without .has_side_effects = truesetting and without my custom log fn, the log messages are uncoloured and “failed command:”.

Without .has_side_effects = truesetting and with my custom log fn (including flush), there is no output.

Unfortunately, this option is not clearly documented in the code. Looking into Step/Run.zig didn’t really help me. For example, I couldn’t find where the “failed command: …” error message comes from.

The only hint I found is the doc comment for fn hasSideEffects.

Anyway, since setting .has_side_effects = truehas the desired effect, I’ll mark this as the solution.