authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-13 09:46:57+00:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2025-11-14 21:50:24+01:00
logc6b5945356568f6ec70ca00f9a844bd180f8ca62
tree24d4350469cf7fae42cbbabd697eb43b65e90640
parentb38fb4bff31b76cbaa27157784139a71e290f2e9

std.Build: don't force all children to inherit color option

The build runner was previously forcing child processes to have their stderr colorization match the build runner by setting `CLICOLOR_FORCE` or `NO_COLOR`. This is a nice idea in some cases---for instance a simple `Run` step which we just expect to exit with code 0 and whose stderr is not being programmatically inspected---but is a bad idea in others, for instance if there is a check on stderr or if stderr is captured, in which case forcing color on the child could cause checks to fail. Instead, this commit adds a field to `std.Build.Step.Run` which specifies a behavior for the build runner to employ in terms of assigning the `CLICOLOR_FORCE` and `NO_COLOR` environment variables. The default behavior is to set `CLICOLOR_FORCE` if the build runner's output is colorized and the step's stderr is not captured, and to set `NO_COLOR` otherwise. Alternatively, colors can be always enabled, always disabled, always match the build runner, or the environment variables can be left untouched so they can be manually controlled through `env_map`. Notably, this fixes a failure when running `zig build test-cli` in a TTY (or with colors explicitly enabled). GitHub CI hadn't caught this because it does not request color, but Codeberg CI now does, and we were seeing a failure in the `zig init` test because the actual output had color escape codes in it due to 6d280dc.

6 files changed, 76 insertions(+), 19 deletions(-)

lib/compiler/build_runner.zig+1-5
...@@ -443,11 +443,6 @@ pub fn main() !void {...@@ -443,11 +443,6 @@ pub fn main() !void {
443 }443 }
444444
445 const ttyconf = color.detectTtyConf();445 const ttyconf = color.detectTtyConf();
446 switch (ttyconf) {
447 .no_color => try graph.env_map.put("NO_COLOR", "1"),
448 .escape_codes => try graph.env_map.put("CLICOLOR_FORCE", "1"),
449 .windows_api => {},
450 }
451446
452 const main_progress_node = std.Progress.start(.{447 const main_progress_node = std.Progress.start(.{
453 .disable_printing = (color == .off),448 .disable_printing = (color == .off),
...@@ -1389,6 +1384,7 @@ fn workerMakeOneStep(...@@ -1389,6 +1384,7 @@ fn workerMakeOneStep(
1389 .thread_pool = thread_pool,1384 .thread_pool = thread_pool,
1390 .watch = run.watch,1385 .watch = run.watch,
1391 .web_server = if (run.web_server) |*ws| ws else null,1386 .web_server = if (run.web_server) |*ws| ws else null,
1387 .ttyconf = run.ttyconf,
1392 .unit_test_timeout_ns = run.unit_test_timeout_ns,1388 .unit_test_timeout_ns = run.unit_test_timeout_ns,
1393 .gpa = run.gpa,1389 .gpa = run.gpa,
1394 });1390 });
lib/std/Build/Step.zig+1
...@@ -118,6 +118,7 @@ pub const MakeOptions = struct {...@@ -118,6 +118,7 @@ pub const MakeOptions = struct {
118 // it currently breaks because `std.net.Address` doesn't work there. Work around for now.118 // it currently breaks because `std.net.Address` doesn't work there. Work around for now.
119 .wasm32 => void,119 .wasm32 => void,
120 },120 },
121 ttyconf: std.Io.tty.Config,
121 /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.122 /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.
122 unit_test_timeout_ns: ?u64,123 unit_test_timeout_ns: ?u64,
123 /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.124 /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.
lib/std/Build/Step/Run.zig+55-10
...@@ -24,6 +24,21 @@ cwd: ?Build.LazyPath,...@@ -24,6 +24,21 @@ cwd: ?Build.LazyPath,
24/// Override this field to modify the environment, or use setEnvironmentVariable24/// Override this field to modify the environment, or use setEnvironmentVariable
25env_map: ?*EnvMap,25env_map: ?*EnvMap,
2626
27/// Controls the `NO_COLOR` and `CLICOLOR_FORCE` environment variables.
28color: enum {
29 /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset.
30 enable,
31 /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset.
32 disable,
33 /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`.
34 inherit,
35 /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`.
36 auto,
37 /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables.
38 /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`.
39 manual,
40} = .auto,
41
27/// When `true` prevents `ZIG_PROGRESS` environment variable from being passed42/// When `true` prevents `ZIG_PROGRESS` environment variable from being passed
28/// to the child process, which otherwise would be used for the child to send43/// to the child process, which otherwise would be used for the child to send
29/// progress updates to the parent.44/// progress updates to the parent.
...@@ -525,7 +540,7 @@ pub fn setCwd(run: *Run, cwd: Build.LazyPath) void {...@@ -525,7 +540,7 @@ pub fn setCwd(run: *Run, cwd: Build.LazyPath) void {
525pub fn clearEnvironment(run: *Run) void {540pub fn clearEnvironment(run: *Run) void {
526 const b = run.step.owner;541 const b = run.step.owner;
527 const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM");542 const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM");
528 new_env_map.* = EnvMap.init(b.allocator);543 new_env_map.* = .init(b.allocator);
529 run.env_map = new_env_map;544 run.env_map = new_env_map;
530}545}
531546
...@@ -806,6 +821,9 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -806,6 +821,9 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
806 }821 }
807 }822 }
808823
824 man.hash.add(run.color);
825 man.hash.add(run.disable_zig_progress);
826
809 for (run.argv.items) |arg| {827 for (run.argv.items) |arg| {
810 switch (arg) {828 switch (arg) {
811 .bytes => |bytes| {829 .bytes => |bytes| {
...@@ -1130,6 +1148,7 @@ pub fn rerunInFuzzMode(...@@ -1130,6 +1148,7 @@ pub fn rerunInFuzzMode(
1130 .thread_pool = undefined, // not used by `runCommand`1148 .thread_pool = undefined, // not used by `runCommand`
1131 .watch = undefined, // not used by `runCommand`1149 .watch = undefined, // not used by `runCommand`
1132 .web_server = null, // only needed for time reports1150 .web_server = null, // only needed for time reports
1151 .ttyconf = fuzz.ttyconf,
1133 .unit_test_timeout_ns = null, // don't time out fuzz tests for now1152 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
1134 .gpa = undefined, // not used by `runCommand`1153 .gpa = undefined, // not used by `runCommand`
1135 }, .{1154 }, .{
...@@ -1234,9 +1253,40 @@ fn runCommand(...@@ -1234,9 +1253,40 @@ fn runCommand(
1234 var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);1253 var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);
1235 defer interp_argv.deinit();1254 defer interp_argv.deinit();
12361255
1237 var env_map = run.env_map orelse &b.graph.env_map;1256 var env_map: EnvMap = env: {
1257 const orig = run.env_map orelse &b.graph.env_map;
1258 break :env try orig.clone(gpa);
1259 };
1260 defer env_map.deinit();
1261
1262 color: switch (run.color) {
1263 .manual => {},
1264 .enable => {
1265 try env_map.put("CLICOLOR_FORCE", "1");
1266 env_map.remove("NO_COLOR");
1267 },
1268 .disable => {
1269 try env_map.put("NO_COLOR", "1");
1270 env_map.remove("CLICOLOR_FORCE");
1271 },
1272 .inherit => switch (options.ttyconf) {
1273 .no_color, .windows_api => continue :color .disable,
1274 .escape_codes => continue :color .enable,
1275 },
1276 .auto => {
1277 const capture_stderr = run.captured_stderr != null or switch (run.stdio) {
1278 .check => |checks| checksContainStderr(checks.items),
1279 .infer_from_args, .inherit, .zig_test => false,
1280 };
1281 if (capture_stderr) {
1282 continue :color .disable;
1283 } else {
1284 continue :color .inherit;
1285 }
1286 },
1287 }
12381288
1239 const opt_generic_result = spawnChildAndCollect(run, argv, env_map, has_side_effects, options, fuzz_context) catch |err| term: {1289 const opt_generic_result = spawnChildAndCollect(run, argv, &env_map, has_side_effects, options, fuzz_context) catch |err| term: {
1240 // InvalidExe: cpu arch mismatch1290 // InvalidExe: cpu arch mismatch
1241 // FileNotFound: can happen with a wrong dynamic linker path1291 // FileNotFound: can happen with a wrong dynamic linker path
1242 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {1292 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
...@@ -1273,12 +1323,7 @@ fn runCommand(...@@ -1273,12 +1323,7 @@ fn runCommand(
1273 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but1323 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
1274 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.1324 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
1275 if (env_map.get("WINEDEBUG") == null) {1325 if (env_map.get("WINEDEBUG") == null) {
1276 // We don't own `env_map` at this point, so create a copy in order to modify it.1326 try env_map.put("WINEDEBUG", "-all");
1277 const new_env_map = arena.create(EnvMap) catch @panic("OOM");
1278 new_env_map.hash_map = try env_map.hash_map.cloneWithAllocator(arena);
1279 try new_env_map.put("WINEDEBUG", "-all");
1280
1281 env_map = new_env_map;
1282 }1327 }
1283 } else {1328 } else {
1284 return failForeign(run, "-fwine", argv[0], exe);1329 return failForeign(run, "-fwine", argv[0], exe);
...@@ -1377,7 +1422,7 @@ fn runCommand(...@@ -1377,7 +1422,7 @@ fn runCommand(
1377 step.result_failed_command = null;1422 step.result_failed_command = null;
1378 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);1423 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
13791424
1380 break :term spawnChildAndCollect(run, interp_argv.items, env_map, has_side_effects, options, fuzz_context) catch |e| {1425 break :term spawnChildAndCollect(run, interp_argv.items, &env_map, has_side_effects, options, fuzz_context) catch |e| {
1381 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;1426 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
1382 if (e == error.MakeFailed) return error.MakeFailed; // error already reported1427 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1383 return step.fail("unable to spawn interpreter {s}: {s}", .{1428 return step.fail("unable to spawn interpreter {s}: {s}", .{
lib/std/Io/tty.zig+2-4
...@@ -37,7 +37,7 @@ pub const Color = enum {...@@ -37,7 +37,7 @@ pub const Color = enum {
37pub const Config = union(enum) {37pub const Config = union(enum) {
38 no_color,38 no_color,
39 escape_codes,39 escape_codes,
40 windows_api: if (native_os == .windows) WindowsContext else void,40 windows_api: if (native_os == .windows) WindowsContext else noreturn,
4141
42 /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr).42 /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr).
43 /// This includes feature checks for ANSI escape codes and the Windows console API, as well as43 /// This includes feature checks for ANSI escape codes and the Windows console API, as well as
...@@ -105,7 +105,7 @@ pub const Config = union(enum) {...@@ -105,7 +105,7 @@ pub const Config = union(enum) {
105 };105 };
106 try w.writeAll(color_string);106 try w.writeAll(color_string);
107 },107 },
108 .windows_api => |ctx| if (native_os == .windows) {108 .windows_api => |ctx| {
109 const attributes = switch (color) {109 const attributes = switch (color) {
110 .black => 0,110 .black => 0,
111 .red => windows.FOREGROUND_RED,111 .red => windows.FOREGROUND_RED,
...@@ -130,8 +130,6 @@ pub const Config = union(enum) {...@@ -130,8 +130,6 @@ pub const Config = union(enum) {
130 };130 };
131 try w.flush();131 try w.flush();
132 try windows.SetConsoleTextAttribute(ctx.handle, attributes);132 try windows.SetConsoleTextAttribute(ctx.handle, attributes);
133 } else {
134 unreachable;
135 },133 },
136 };134 };
137 }135 }
lib/std/process.zig+16
...@@ -206,6 +206,22 @@ pub const EnvMap = struct {...@@ -206,6 +206,22 @@ pub const EnvMap = struct {
206 return self.hash_map.iterator();206 return self.hash_map.iterator();
207 }207 }
208208
209 /// Returns a full copy of `em` allocated with `gpa`, which is not necessarily
210 /// the same allocator used to allocate `em`.
211 pub fn clone(em: *const EnvMap, gpa: Allocator) Allocator.Error!EnvMap {
212 var new: EnvMap = .init(gpa);
213 errdefer new.deinit();
214 // Since we need to dupe the keys and values, the only way for error handling to not be a
215 // nightmare is to add keys to an empty map one-by-one. This could be avoided if this
216 // abstraction were a bit less... OOP-esque.
217 try new.hash_map.ensureUnusedCapacity(em.hash_map.count());
218 var it = em.hash_map.iterator();
219 while (it.next()) |entry| {
220 try new.put(entry.key_ptr.*, entry.value_ptr.*);
221 }
222 return new;
223 }
224
209 fn free(self: EnvMap, value: []const u8) void {225 fn free(self: EnvMap, value: []const u8) void {
210 self.hash_map.allocator.free(value);226 self.hash_map.allocator.free(value);
211 }227 }
test/standalone/empty_env/build.zig+1
...@@ -31,6 +31,7 @@ pub fn build(b: *std.Build) void {...@@ -31,6 +31,7 @@ pub fn build(b: *std.Build) void {
31 const run = b.addRunArtifact(main);31 const run = b.addRunArtifact(main);
32 run.clearEnvironment();32 run.clearEnvironment();
33 run.disable_zig_progress = true;33 run.disable_zig_progress = true;
34 run.color = .manual;
3435
35 test_step.dependOn(&run.step);36 test_step.dependOn(&run.step);
36}37}