authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-11 21:42:47+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-11 21:42:47+02:00
log41bc507688e21f8f8baffbb0da7026d6328beb1a
tree49f87ea24bb3efcf4ef7a0b15857598908ccae91
parent3223d3a1ac0aa7a5ffb9252c2e8d871f0bf70ec0
parent81be7f62ecbeca828543bff3a28af4c183e1054c

Merge pull request 'Debug I/O color detection fixes/improvements for Windows/POSIX/WASI/Emscripten + tests' (#31108) from castholm/zig:wasm-env-vars into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31108 Reviewed-by: Andrew Kelley <andrew@ziglang.org>

5 files changed, 123 insertions(+), 14 deletions(-)

lib/std/Io/Threaded.zig+17-14
...@@ -1671,20 +1671,23 @@ pub fn init(...@@ -1671,20 +1671,23 @@ pub fn init(
1671/// When initialized this way:1671/// When initialized this way:
1672/// * cancel requests have no effect.1672/// * cancel requests have no effect.
1673/// * `deinit` is safe, but unnecessary to call.1673/// * `deinit` is safe, but unnecessary to call.
1674pub const init_single_threaded: Threaded = .{1674pub const init_single_threaded: Threaded = init: {
1675 .allocator = .failing,1675 const env_block: process.Environ.Block = if (is_windows) .global else .empty;
1676 .stack_size = std.Thread.SpawnConfig.default_stack_size,1676 break :init .{
1677 .async_limit = .nothing,1677 .allocator = .failing,
1678 .cpu_count_error = null,1678 .stack_size = std.Thread.SpawnConfig.default_stack_size,
1679 .concurrent_limit = .nothing,1679 .async_limit = .nothing,
1680 .old_sig_io = undefined,1680 .cpu_count_error = null,
1681 .old_sig_pipe = undefined,1681 .concurrent_limit = .nothing,
1682 .have_signal_handler = false,1682 .old_sig_io = undefined,
1683 .argv0 = .empty,1683 .old_sig_pipe = undefined,
1684 .environ_initialized = true,1684 .have_signal_handler = false,
1685 .environ = .empty,1685 .argv0 = .empty,
1686 .worker_threads = .init(null),1686 .environ_initialized = env_block.isEmpty(),
1687 .disable_memory_mapping = false,1687 .environ = .{ .process_environ = .{ .block = env_block } },
1688 .worker_threads = .init(null),
1689 .disable_memory_mapping = false,
1690 };
1688};1691};
16891692
1690var global_single_threaded_instance: Threaded = .init_single_threaded;1693var global_single_threaded_instance: Threaded = .init_single_threaded;
lib/std/start.zig+1
...@@ -631,6 +631,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8)...@@ -631,6 +631,7 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [:null]?[*:0]u8)
631 if (std.Options.debug_threaded_io) |t| {631 if (std.Options.debug_threaded_io) |t| {
632 if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0];632 if (@sizeOf(std.Io.Threaded.Argv0) != 0) t.argv0.value = argv[0];
633 t.environ = .{ .process_environ = .{ .block = env_block } };633 t.environ = .{ .process_environ = .{ .block = env_block } };
634 t.environ_initialized = env_block.isEmpty();
634 }635 }
635 std.Thread.maybeAttachSignalStack();636 std.Thread.maybeAttachSignalStack();
636 std.debug.maybeEnableSegfaultHandler();637 std.debug.maybeEnableSegfaultHandler();
test/standalone/build.zig.zon+3
...@@ -199,6 +199,9 @@...@@ -199,6 +199,9 @@
199 .posix = .{199 .posix = .{
200 .path = "posix",200 .path = "posix",
201 },201 },
202 .debug_io_color = .{
203 .path = "debug_io_color",
204 },
202 },205 },
203 .paths = .{206 .paths = .{
204 "build.zig",207 "build.zig",
test/standalone/debug_io_color/build.zig created+95
...@@ -0,0 +1,95 @@
1const std = @import("std");
2
3pub fn build(b: *std.Build) void {
4 const test_step = b.step("test", "Test");
5 b.default_step = test_step;
6
7 // Most targets handle color the same way, regardless of whether libc is linked.
8 const native_target = b.graph.host;
9 addTestCases(test_step, native_target, false);
10 addTestCases(test_step, native_target, true);
11
12 // WASI behaves differently depending on whether libc is linked.
13 if (b.enable_wasmtime) {
14 const wasi_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .wasi });
15 addTestCases(test_step, wasi_target, false);
16 addTestCases(test_step, wasi_target, true);
17 }
18}
19
20fn addTestCases(
21 test_step: *std.Build.Step,
22 target: std.Build.ResolvedTarget,
23 link_libc: bool,
24) void {
25 const b = test_step.owner;
26 const exe = b.addExecutable(.{
27 .name = b.fmt("{s}{s}", .{ @tagName(target.result.os.tag), if (link_libc) "-libc" else "" }),
28 .root_module = b.createModule(.{
29 .root_source_file = b.path("main.zig"),
30 .target = target,
31 .link_libc = link_libc,
32 }),
33 });
34
35 // Should reflect 'std.process.Environ.Block' and 'std.Io.Threaded.init_single_threaded'.
36 const debug_io_can_read_environ = switch (target.result.os.tag) {
37 .windows => true,
38 .wasi, .emscripten => link_libc,
39 .freestanding, .other => false,
40 else => true,
41 };
42
43 // Don't forget to account for whether the build process's stderr supports color.
44 const parent_stderr_color_enabled = (std.Io.Terminal.Mode.detect(b.graph.io, .stderr(), false, false) catch unreachable) != .no_color;
45
46 _ = addTestCase(test_step, exe, "neither", .inherit, .manual, parent_stderr_color_enabled);
47 _ = addTestCase(test_step, exe, "neither", .redirect, .manual, false);
48 _ = addTestCase(test_step, exe, "no_color", .inherit, .disable, if (debug_io_can_read_environ) false else parent_stderr_color_enabled);
49 _ = addTestCase(test_step, exe, "no_color", .redirect, .disable, false);
50 _ = addTestCase(test_step, exe, "clicolor_force", .inherit, .enable, if (debug_io_can_read_environ) true else parent_stderr_color_enabled);
51 _ = addTestCase(test_step, exe, "clicolor_force", .redirect, .enable, debug_io_can_read_environ);
52
53 const both = addTestCase(test_step, exe, "both", .inherit, .manual, if (debug_io_can_read_environ) false else parent_stderr_color_enabled);
54 both.setEnvironmentVariable("NO_COLOR", "1");
55 both.setEnvironmentVariable("CLICOLOR_FORCE", "1");
56
57 const both_redirected = addTestCase(test_step, exe, "both", .redirect, .manual, false);
58 both_redirected.setEnvironmentVariable("NO_COLOR", "1");
59 both_redirected.setEnvironmentVariable("CLICOLOR_FORCE", "1");
60}
61
62fn addTestCase(
63 test_step: *std.Build.Step,
64 exe: *std.Build.Step.Compile,
65 test_case_name: []const u8,
66 stderr: enum { inherit, redirect },
67 run_step_color: std.Build.Step.Run.Color,
68 expected_color_enabled: bool,
69) *std.Build.Step.Run {
70 const b = test_step.owner;
71 const step_name = b.fmt("{s} {s}{s}", .{
72 exe.name,
73 test_case_name,
74 if (stderr == .redirect) "-redirect" else "",
75 });
76 const run_exe = b.addRunArtifact(exe);
77 run_exe.setName(b.fmt("run {s}", .{step_name}));
78
79 run_exe.failing_to_execute_foreign_is_an_error = false;
80 if (stderr == .redirect) run_exe.expectStdErrMatch("");
81
82 run_exe.clearEnvironment();
83 run_exe.color = run_step_color;
84
85 // Build system quirk: Currently, Run step stdout checks will also redirect stderr, so as a
86 // workaround we use a CheckFile step instead. We must also mark the Run step as having side
87 // effects, to ensure the parent stderr is inherited when not explicitly redirected.
88 run_exe.has_side_effects = true;
89 const stdout = run_exe.captureStdOut(.{});
90 const check_file = b.addCheckFile(stdout, .{ .expected_exact = if (expected_color_enabled) "true" else "false" });
91 check_file.setName(b.fmt("check {s}", .{step_name}));
92 test_step.dependOn(&check_file.step);
93
94 return run_exe;
95}
test/standalone/debug_io_color/main.zig created+7
...@@ -0,0 +1,7 @@
1const std = @import("std");
2
3pub fn main() !void {
4 const stderr = std.debug.lockStderr(&.{});
5 defer std.debug.unlockStderr();
6 try std.Io.File.stdout().writeStreamingAll(std.Options.debug_io, if (stderr.terminal_mode != .no_color) "true" else "false");
7}