# How to see output of custom build step?

**URL:** <https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534>\
**Category:** Help\
**Tags:** build-system\
**Created:** [September 8, 2026, 5:45pm UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534 "2026-09-08T17:45:36Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![hvbargen](https://ziggit.dev/user_avatar/ziggit.dev/hvbargen/32/5696_2.png) [@hvbargen](https://ziggit.dev/u/hvbargen)\
**Post date:** [September 8, 2026, 5:45pm UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534/1 "2026-09-08T17:45:36Z")

</div>

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:

```zig
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:

```zig
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:

```zig
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.

---

<div class="post-metadata">

**Author:** ![hvbargen](https://ziggit.dev/user_avatar/ziggit.dev/hvbargen/32/5696_2.png) [@hvbargen](https://ziggit.dev/u/hvbargen)\
**Post date:** [September 8, 2026, 6:17pm UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534/2 "2026-09-08T18:17:12Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![candela](https://ziggit.dev/user_avatar/ziggit.dev/candela/32/2272_2.png) [@candela](https://ziggit.dev/u/candela)\
**Post date:** [September 9, 2026, 12:05am UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534/3 "2026-09-09T00:05:28Z")

</div>

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

```zig
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`:

```zig
const std = @import("std");

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

```

Running `zig build custom` gets me:

```zig
debug: hello world

```

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

---

<div class="post-metadata">

**Author:** ![candela](https://ziggit.dev/user_avatar/ziggit.dev/candela/32/2272_2.png) [@candela](https://ziggit.dev/u/candela)\
**Post date:** [September 9, 2026, 12:33am UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534/4 "2026-09-09T00:33:14Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![hvbargen](https://ziggit.dev/user_avatar/ziggit.dev/hvbargen/32/5696_2.png) [@hvbargen](https://ziggit.dev/u/hvbargen)\
**Post date:** [September 9, 2026, 8:15am UTC](https://ziggit.dev/t/how-to-see-output-of-custom-build-step/17534/5 "2026-09-09T08:15:59Z")

</div>

> [@candela](#):
>
> ```zig
> const std = @import("std");
> 
> pub fn main() !void {
> std.log.debug("hello world", .{});
> }
> 
> ```

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.):

```zig
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 = true`setting and without my custom log fn, the log messages are uncoloured and “failed command:”.

Without `.has_side_effects = true`setting 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 = true`has the desired effect, I’ll mark this as the solution.
