How to print Step.Run stderr only of the step fails?

I have a Run step which always prints something to stderr. However, I want to see stderr only if the step fails.

If I don’t configure the behavior specially, stderr is inherited, which is not what I want.

If I do step.addCheck(.{ .expect_term = .{ .Exited = 0 } });, the stderr is collected, which is good because it at least doesn’t lock build’s global stdio mutex, but it is still printing unconditionally at the end of the build.

If I also step.addCheck(.{ .expect_stderr_match = "" });, then stderr isn’t printed even if the step is considered failed.

Looking at the relevant build_runner.zig code, it seems like this use-case isn’t actually supported at all:

    // No matter the result, we want to display error/warning messages.
    const show_compile_errors = !run.prominent_compile_errors and
        s.result_error_bundle.errorMessageCount() > 0;
    const show_error_msgs = s.result_error_msgs.items.len > 0;
    const show_stderr = s.result_stderr.len > 0;

    if (show_error_msgs or show_compile_errors or show_stderr) {
        var bw = std.debug.lockStdErr2();
        defer std.debug.unlockStdErr();

        const gpa = b.allocator;
        const options: std.zig.ErrorBundle.RenderOptions = .{
            .ttyconf = run.ttyconf,
            .include_reference_trace = (b.reference_trace orelse 0) > 0,
        };
        printErrorMessages(gpa, s, options, &bw, run.prominent_compile_errors) catch {};
    }

But is there perhaps some out-of-the-box way to do what I want here?

I believe Run.captureStdErr might do the trick. You’ll need logic in the build to open and print the file, though. I’ve only used it to discard spurious output.

Got it working with

fn hide_stderr(run: *std.Build.Step.Run) void {
    const b = run.step.owner;

    run.addCheck(.{ .expect_term = .{ .Exited = 0 } });
    run.has_side_effects = true;

    const override = struct {
        var global_map: std.AutoHashMapUnmanaged(usize, std.Build.Step.MakeFn) = .{};

        fn make(step: *std.Build.Step, prog_node: std.Progress.Node) anyerror!void {
            const original = global_map.get(@intFromPtr(step)).?;
            try original(step, prog_node);
            assert(step.result_error_msgs.items.len == 0);
            step.result_stderr = "";
        }
    };

    const original = run.step.makeFn;
    override.global_map.put(b.allocator, @intFromPtr(&run.step), original) catch @panic("OOM");
    run.step.makeFn = &override.make;
}

Please don’t show this code to Andrew, otherwise he might reconsider private fields.

7 Likes

Non-private fields are precisely so you can do things that API doesn’t expose

1 Like

This seems like a reasonable use case for us to support. Maybe file an enhancement issue?