Commit 74931fe25c

Matthew Lugg <mlugg@mlugg.co.uk>
2025-10-28 13:42:05
std.debug.lockStderrWriter: also return ttyconf
`std.Io.tty.Config.detect` may be an expensive check (e.g. involving syscalls), and doing it every time we need to print isn't really necessary; under normal usage, we can compute the value once and cache it for the whole program's execution. Since anyone outputting to stderr may reasonably want this information (in fact they are very likely to), it makes sense to cache it and return it from `lockStderrWriter`. Call sites who do not need it will experience no significant overhead, and can just ignore the TTY config with a `const w, _` destructure.
1 parent 74c23a2
lib/compiler/resinator/cli.zig
@@ -124,10 +124,10 @@ pub const Diagnostics = struct {
         try self.errors.append(self.allocator, error_details);
     }
 
-    pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.Io.tty.Config) void {
-        const stderr = std.debug.lockStderrWriter(&.{});
+    pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8) void {
+        const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
         defer std.debug.unlockStderrWriter();
-        self.renderToWriter(args, stderr, config) catch return;
+        self.renderToWriter(args, stderr, ttyconf) catch return;
     }
 
     pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.Io.Writer, config: std.Io.tty.Config) !void {
lib/compiler/resinator/errors.zig
@@ -67,20 +67,15 @@ pub const Diagnostics = struct {
         return @intCast(index);
     }
 
-    pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.Io.tty.Config, source_mappings: ?SourceMappings) void {
+    pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
         const io = self.io;
-        const stderr = std.debug.lockStderrWriter(&.{});
+        const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
         defer std.debug.unlockStderrWriter();
         for (self.errors.items) |err_details| {
-            renderErrorMessage(io, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
+            renderErrorMessage(io, stderr, ttyconf, cwd, err_details, source, self.strings.items, source_mappings) catch return;
         }
     }
 
-    pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
-        const tty_config = std.Io.tty.detectConfig(std.fs.File.stderr());
-        return self.renderToStdErr(cwd, source, tty_config, source_mappings);
-    }
-
     pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
         for (self.errors.items) |details| {
             if (details.err == err) return true;
lib/compiler/resinator/main.zig
@@ -28,13 +28,11 @@ pub fn main() !void {
     defer arena_state.deinit();
     const arena = arena_state.allocator();
 
-    const stderr = std.fs.File.stderr();
-    const stderr_config = std.Io.tty.detectConfig(stderr);
-
     const args = try std.process.argsAlloc(arena);
 
     if (args.len < 2) {
-        try renderErrorMessage(std.debug.lockStderrWriter(&.{}), stderr_config, .err, "expected zig lib dir as first argument", .{});
+        const w, const ttyconf = std.debug.lockStderrWriter(&.{});
+        try renderErrorMessage(w, ttyconf, .err, "expected zig lib dir as first argument", .{});
         std.process.exit(1);
     }
     const zig_lib_dir = args[1];
@@ -56,9 +54,7 @@ pub fn main() !void {
                 .in = undefined, // won't be receiving messages
             },
         },
-        false => .{
-            .tty = stderr_config,
-        },
+        false => .stderr,
     };
 
     var options = options: {
@@ -75,12 +71,14 @@ pub fn main() !void {
 
         if (!zig_integration) {
             // print any warnings/notes
-            cli_diagnostics.renderToStdErr(cli_args, stderr_config);
+            cli_diagnostics.renderToStdErr(cli_args);
             // If there was something printed, then add an extra newline separator
             // so that there is a clear separation between the cli diagnostics and whatever
             // gets printed after
             if (cli_diagnostics.errors.items.len > 0) {
-                try stderr.writeAll("\n");
+                const stderr, _ = std.debug.lockStderrWriter(&.{});
+                defer std.debug.unlockStderrWriter();
+                try stderr.writeByte('\n');
             }
         }
         break :options options;
@@ -130,17 +128,18 @@ pub fn main() !void {
             const aro_arena = aro_arena_state.allocator();
 
             var stderr_buf: [512]u8 = undefined;
-            var stderr_writer = stderr.writer(&stderr_buf);
-            var diagnostics: aro.Diagnostics = switch (zig_integration) {
-                false => .{ .output = .{ .to_writer = .{
-                    .writer = &stderr_writer.interface,
-                    .color = stderr_config,
-                } } },
-                true => .{ .output = .{ .to_list = .{
-                    .arena = .init(gpa),
-                } } },
-            };
-            defer diagnostics.deinit();
+            var diagnostics: aro.Diagnostics = .{ .output = output: {
+                if (zig_integration) break :output .{ .to_list = .{ .arena = .init(gpa) } };
+                const w, const ttyconf = std.debug.lockStderrWriter(&stderr_buf);
+                break :output .{ .to_writer = .{
+                    .writer = w,
+                    .color = ttyconf,
+                } };
+            } };
+            defer {
+                diagnostics.deinit();
+                if (!zig_integration) std.debug.unlockStderrWriter();
+            }
 
             var comp = aro.Compilation.init(aro_arena, aro_arena, io, &diagnostics, std.fs.cwd());
             defer comp.deinit();
@@ -307,7 +306,7 @@ pub fn main() !void {
 
                 // print any warnings/notes
                 if (!zig_integration) {
-                    diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+                    diagnostics.renderToStdErr(std.fs.cwd(), final_input, mapping_results.mappings);
                 }
 
                 // write the depfile
@@ -660,7 +659,7 @@ const SourceMappings = @import("source_mapping.zig").SourceMappings;
 
 const ErrorHandler = union(enum) {
     server: std.zig.Server,
-    tty: std.Io.tty.Config,
+    stderr,
 
     pub fn emitCliDiagnostics(
         self: *ErrorHandler,
@@ -675,9 +674,7 @@ const ErrorHandler = union(enum) {
 
                 try server.serveErrorBundle(error_bundle);
             },
-            .tty => {
-                diagnostics.renderToStdErr(args, self.tty);
-            },
+            .stderr => diagnostics.renderToStdErr(args),
         }
     }
 
@@ -698,11 +695,11 @@ const ErrorHandler = union(enum) {
 
                 try server.serveErrorBundle(error_bundle);
             },
-            .tty => {
+            .stderr => {
                 // aro errors have already been emitted
-                const stderr = std.debug.lockStderrWriter(&.{});
+                const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
                 defer std.debug.unlockStderrWriter();
-                try renderErrorMessage(stderr, self.tty, .err, "{s}", .{fail_msg});
+                try renderErrorMessage(stderr, ttyconf, .err, "{s}", .{fail_msg});
             },
         }
     }
@@ -722,9 +719,7 @@ const ErrorHandler = union(enum) {
 
                 try server.serveErrorBundle(error_bundle);
             },
-            .tty => {
-                diagnostics.renderToStdErr(cwd, source, self.tty, mappings);
-            },
+            .stderr => diagnostics.renderToStdErr(cwd, source, mappings),
         }
     }
 
@@ -745,10 +740,10 @@ const ErrorHandler = union(enum) {
 
                 try server.serveErrorBundle(error_bundle);
             },
-            .tty => {
-                const stderr = std.debug.lockStderrWriter(&.{});
+            .stderr => {
+                const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
                 defer std.debug.unlockStderrWriter();
-                try renderErrorMessage(stderr, self.tty, msg_type, format, args);
+                try renderErrorMessage(stderr, ttyconf, msg_type, format, args);
             },
         }
     }
lib/compiler/build_runner.zig
@@ -442,8 +442,7 @@ pub fn main() !void {
         if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
     }
 
-    const stderr: std.fs.File = .stderr();
-    const ttyconf = get_tty_conf(color, stderr);
+    const ttyconf = color.detectTtyConf();
     switch (ttyconf) {
         .no_color => try graph.env_map.put("NO_COLOR", "1"),
         .escape_codes => try graph.env_map.put("CLICOLOR_FORCE", "1"),
@@ -522,9 +521,9 @@ pub fn main() !void {
         .error_style = error_style,
         .multiline_errors = multiline_errors,
         .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
-        .ttyconf = ttyconf,
-        .stderr = stderr,
         .thread_pool = undefined,
+
+        .ttyconf = ttyconf,
     };
     defer {
         run.memory_blocked_steps.deinit(gpa);
@@ -563,9 +562,9 @@ pub fn main() !void {
         break :ws .init(.{
             .gpa = gpa,
             .thread_pool = &run.thread_pool,
+            .ttyconf = ttyconf,
             .graph = &graph,
             .all_steps = run.step_stack.keys(),
-            .ttyconf = run.ttyconf,
             .root_prog_node = main_progress_node,
             .watch = watch,
             .listen_address = listen_address,
@@ -578,7 +577,7 @@ pub fn main() !void {
     }
 
     rebuild: while (true) : (if (run.error_style.clearOnUpdate()) {
-        const bw = std.debug.lockStderrWriter(&stdio_buffer_allocation);
+        const bw, _ = std.debug.lockStderrWriter(&stdio_buffer_allocation);
         defer std.debug.unlockStderrWriter();
         try bw.writeAll("\x1B[2J\x1B[3J\x1B[H");
     }) {
@@ -682,13 +681,14 @@ const Run = struct {
     /// Allocated into `gpa`.
     step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
     thread_pool: std.Thread.Pool,
+    /// Similar to the `tty.Config` returned by `std.debug.lockStderrWriter`,
+    /// but also respects the '--color' flag.
+    ttyconf: tty.Config,
 
     claimed_rss: usize,
     error_style: ErrorStyle,
     multiline_errors: MultilineErrors,
     summary: Summary,
-    ttyconf: tty.Config,
-    stderr: File,
 };
 
 fn prepare(
@@ -834,8 +834,6 @@ fn runStepNames(
         }
     }
 
-    const ttyconf = run.ttyconf;
-
     if (fuzz) |mode| blk: {
         switch (builtin.os.tag) {
             // Current implementation depends on two things that need to be ported to Windows:
@@ -863,9 +861,9 @@ fn runStepNames(
             gpa,
             io,
             thread_pool,
+            run.ttyconf,
             step_stack.keys(),
             parent_prog_node,
-            ttyconf,
             mode,
         ) catch |err| fatal("failed to start fuzzer: {s}", .{@errorName(err)});
         defer f.deinit();
@@ -890,8 +888,9 @@ fn runStepNames(
             .none => break :summary,
         }
 
-        const w = std.debug.lockStderrWriter(&stdio_buffer_allocation);
+        const w, _ = std.debug.lockStderrWriter(&stdio_buffer_allocation);
         defer std.debug.unlockStderrWriter();
+        const ttyconf = run.ttyconf;
 
         const total_count = success_count + failure_count + pending_count + skipped_count;
         ttyconf.setColor(w, .cyan) catch {};
@@ -1399,9 +1398,10 @@ fn workerMakeOneStep(
     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) {
-        const bw = std.debug.lockStderrWriter(&stdio_buffer_allocation);
+        const bw, _ = std.debug.lockStderrWriter(&stdio_buffer_allocation);
         defer std.debug.unlockStderrWriter();
-        printErrorMessages(run.gpa, s, .{ .ttyconf = run.ttyconf }, bw, run.error_style, run.multiline_errors) catch {};
+        const ttyconf = run.ttyconf;
+        printErrorMessages(run.gpa, s, .{}, bw, ttyconf, run.error_style, run.multiline_errors) catch {};
     }
 
     handle_result: {
@@ -1465,11 +1465,10 @@ pub fn printErrorMessages(
     failing_step: *Step,
     options: std.zig.ErrorBundle.RenderOptions,
     stderr: *Writer,
+    ttyconf: tty.Config,
     error_style: ErrorStyle,
     multiline_errors: MultilineErrors,
 ) !void {
-    const ttyconf = options.ttyconf;
-
     if (error_style.verboseContext()) {
         // Provide context for where these error messages are coming from by
         // printing the corresponding Step subtree.
@@ -1513,7 +1512,7 @@ pub fn printErrorMessages(
         }
     }
 
-    try failing_step.result_error_bundle.renderToWriter(options, stderr);
+    try failing_step.result_error_bundle.renderToWriter(options, stderr, ttyconf);
 
     for (failing_step.result_error_msgs.items) |msg| {
         try ttyconf.setColor(stderr, .red);
@@ -1759,14 +1758,6 @@ const ErrorStyle = enum {
 const MultilineErrors = enum { indent, newline, none };
 const Summary = enum { all, new, failures, line, none };
 
-fn get_tty_conf(color: Color, stderr: File) tty.Config {
-    return switch (color) {
-        .auto => tty.detectConfig(stderr),
-        .on => .escape_codes,
-        .off => .no_color,
-    };
-}
-
 fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
     std.debug.print(f ++ "\n  access the help menu with 'zig build -h'\n", args);
     process.exit(1);
lib/compiler/std-docs.zig
@@ -394,8 +394,7 @@ fn buildWasmBinary(
     }
 
     if (result_error_bundle.errorMessageCount() > 0) {
-        const color = std.zig.Color.auto;
-        result_error_bundle.renderToStdErr(color.renderOptions());
+        result_error_bundle.renderToStdErr(.{}, true);
         std.log.err("the following command failed with {d} compilation errors:\n{s}", .{
             result_error_bundle.errorMessageCount(),
             try std.Build.Step.allocPrintCmd(arena, null, argv.items),
lib/std/Build/Step/Compile.zig
@@ -1056,15 +1056,15 @@ fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking
     const maybe_path: ?*GeneratedFile = @field(compile, tag_name);
 
     const generated_file = maybe_path orelse {
-        const w = std.debug.lockStderrWriter(&.{});
-        std.Build.dumpBadGetPathHelp(&compile.step, w, .detect(.stderr()), compile.step.owner, asking_step) catch {};
+        const w, const ttyconf = std.debug.lockStderrWriter(&.{});
+        std.Build.dumpBadGetPathHelp(&compile.step, w, ttyconf, compile.step.owner, asking_step) catch {};
         std.debug.unlockStderrWriter();
         @panic("missing emit option for " ++ tag_name);
     };
 
     const path = generated_file.path orelse {
-        const w = std.debug.lockStderrWriter(&.{});
-        std.Build.dumpBadGetPathHelp(&compile.step, w, .detect(.stderr()), compile.step.owner, asking_step) catch {};
+        const w, const ttyconf = std.debug.lockStderrWriter(&.{});
+        std.Build.dumpBadGetPathHelp(&compile.step, w, ttyconf, compile.step.owner, asking_step) catch {};
         std.debug.unlockStderrWriter();
         @panic(tag_name ++ " is null. Is there a missing step dependency?");
     };
@@ -2027,10 +2027,9 @@ fn checkCompileErrors(compile: *Compile) !void {
         var aw: std.Io.Writer.Allocating = .init(arena);
         defer aw.deinit();
         try actual_eb.renderToWriter(.{
-            .ttyconf = .no_color,
             .include_reference_trace = false,
             .include_source_line = false,
-        }, &aw.writer);
+        }, &aw.writer, .no_color);
         break :ae try aw.toOwnedSlice();
     };
 
lib/std/Build/Fuzz.zig
@@ -16,6 +16,7 @@ const build_runner = @import("root");
 
 gpa: Allocator,
 io: Io,
+ttyconf: tty.Config,
 mode: Mode,
 
 /// Allocated into `gpa`.
@@ -25,7 +26,6 @@ wait_group: std.Thread.WaitGroup,
 root_prog_node: std.Progress.Node,
 prog_node: std.Progress.Node,
 thread_pool: *std.Thread.Pool,
-ttyconf: tty.Config,
 
 /// Protects `coverage_files`.
 coverage_mutex: std.Thread.Mutex,
@@ -79,9 +79,9 @@ pub fn init(
     gpa: Allocator,
     io: Io,
     thread_pool: *std.Thread.Pool,
+    ttyconf: tty.Config,
     all_steps: []const *Build.Step,
     root_prog_node: std.Progress.Node,
-    ttyconf: tty.Config,
     mode: Mode,
 ) Allocator.Error!Fuzz {
     const run_steps: []const *Step.Run = steps: {
@@ -115,11 +115,11 @@ pub fn init(
     return .{
         .gpa = gpa,
         .io = io,
+        .ttyconf = ttyconf,
         .mode = mode,
         .run_steps = run_steps,
         .wait_group = .{},
         .thread_pool = thread_pool,
-        .ttyconf = ttyconf,
         .root_prog_node = root_prog_node,
         .prog_node = .none,
         .coverage_files = .empty,
@@ -158,7 +158,7 @@ pub fn deinit(fuzz: *Fuzz) void {
     fuzz.gpa.free(fuzz.run_steps);
 }
 
-fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, ttyconf: std.Io.tty.Config, parent_prog_node: std.Progress.Node) void {
+fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, ttyconf: tty.Config, parent_prog_node: std.Progress.Node) void {
     rebuildTestsWorkerRunFallible(run, gpa, ttyconf, parent_prog_node) catch |err| {
         const compile = run.producer.?;
         log.err("step '{s}': failed to rebuild in fuzz mode: {s}", .{
@@ -167,7 +167,7 @@ fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, ttyconf: std.Io.tty.Con
     };
 }
 
-fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, ttyconf: std.Io.tty.Config, parent_prog_node: std.Progress.Node) !void {
+fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, ttyconf: tty.Config, parent_prog_node: std.Progress.Node) !void {
     const compile = run.producer.?;
     const prog_node = parent_prog_node.start(compile.step.name, 0);
     defer prog_node.end();
@@ -180,9 +180,9 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, ttyconf: std.Io
 
     if (show_error_msgs or show_compile_errors or show_stderr) {
         var buf: [256]u8 = undefined;
-        const w = std.debug.lockStderrWriter(&buf);
+        const w, _ = std.debug.lockStderrWriter(&buf);
         defer std.debug.unlockStderrWriter();
-        build_runner.printErrorMessages(gpa, &compile.step, .{ .ttyconf = ttyconf }, w, .verbose, .indent) catch {};
+        build_runner.printErrorMessages(gpa, &compile.step, .{}, w, ttyconf, .verbose, .indent) catch {};
     }
 
     const rebuilt_bin_path = result catch |err| switch (err) {
@@ -206,9 +206,9 @@ fn fuzzWorkerRun(
     run.rerunInFuzzMode(fuzz, unit_test_index, prog_node) catch |err| switch (err) {
         error.MakeFailed => {
             var buf: [256]u8 = undefined;
-            const w = std.debug.lockStderrWriter(&buf);
+            const w, _ = std.debug.lockStderrWriter(&buf);
             defer std.debug.unlockStderrWriter();
-            build_runner.printErrorMessages(gpa, &run.step, .{ .ttyconf = fuzz.ttyconf }, w, .verbose, .indent) catch {};
+            build_runner.printErrorMessages(gpa, &run.step, .{}, w, fuzz.ttyconf, .verbose, .indent) catch {};
             return;
         },
         else => {
lib/std/Build/WebServer.zig
@@ -54,9 +54,9 @@ pub fn notifyUpdate(ws: *WebServer) void {
 pub const Options = struct {
     gpa: Allocator,
     thread_pool: *std.Thread.Pool,
+    ttyconf: Io.tty.Config,
     graph: *const std.Build.Graph,
     all_steps: []const *Build.Step,
-    ttyconf: Io.tty.Config,
     root_prog_node: std.Progress.Node,
     watch: bool,
     listen_address: net.IpAddress,
@@ -101,10 +101,10 @@ pub fn init(opts: Options) WebServer {
     return .{
         .gpa = opts.gpa,
         .thread_pool = opts.thread_pool,
+        .ttyconf = opts.ttyconf,
         .graph = opts.graph,
         .all_steps = all_steps,
         .listen_address = opts.listen_address,
-        .ttyconf = opts.ttyconf,
         .root_prog_node = opts.root_prog_node,
         .watch = opts.watch,
 
@@ -236,9 +236,9 @@ pub fn finishBuild(ws: *WebServer, opts: struct {
             ws.gpa,
             ws.graph.io,
             ws.thread_pool,
+            ws.ttyconf,
             ws.all_steps,
             ws.root_prog_node,
-            ws.ttyconf,
             .{ .forever = .{ .ws = ws } },
         ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
         ws.fuzz.?.start();
@@ -655,8 +655,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
     }
 
     if (result_error_bundle.errorMessageCount() > 0) {
-        const color = std.zig.Color.auto;
-        result_error_bundle.renderToStdErr(color.renderOptions());
+        result_error_bundle.renderToStdErr(.{}, .auto);
         log.err("the following command failed with {d} compilation errors:\n{s}", .{
             result_error_bundle.errorMessageCount(),
             try Build.Step.allocPrintCmd(arena, null, argv.items),
lib/std/json/dynamic.zig
@@ -47,7 +47,7 @@ pub const Value = union(enum) {
     }
 
     pub fn dump(v: Value) void {
-        const w = std.debug.lockStderrWriter(&.{});
+        const w, _ = std.debug.lockStderrWriter(&.{});
         defer std.debug.unlockStderrWriter();
 
         json.Stringify.value(v, .{}, w) catch return;
lib/std/zig/ErrorBundle.zig
@@ -157,23 +157,22 @@ pub fn nullTerminatedString(eb: ErrorBundle, index: String) [:0]const u8 {
 }
 
 pub const RenderOptions = struct {
-    ttyconf: Io.tty.Config,
     include_reference_trace: bool = true,
     include_source_line: bool = true,
     include_log_text: bool = true,
 };
 
-pub fn renderToStdErr(eb: ErrorBundle, options: RenderOptions) void {
+pub fn renderToStdErr(eb: ErrorBundle, options: RenderOptions, color: std.zig.Color) void {
     var buffer: [256]u8 = undefined;
-    const w = std.debug.lockStderrWriter(&buffer);
+    const w, const ttyconf = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
-    renderToWriter(eb, options, w) catch return;
+    renderToWriter(eb, options, w, color.getTtyConf(ttyconf)) catch return;
 }
 
-pub fn renderToWriter(eb: ErrorBundle, options: RenderOptions, w: *Writer) (Writer.Error || std.posix.UnexpectedError)!void {
+pub fn renderToWriter(eb: ErrorBundle, options: RenderOptions, w: *Writer, ttyconf: Io.tty.Config) (Writer.Error || std.posix.UnexpectedError)!void {
     if (eb.extra.len == 0) return;
     for (eb.getMessages()) |err_msg| {
-        try renderErrorMessageToWriter(eb, options, err_msg, w, "error", .red, 0);
+        try renderErrorMessageToWriter(eb, options, err_msg, w, ttyconf, "error", .red, 0);
     }
 
     if (options.include_log_text) {
@@ -190,11 +189,11 @@ fn renderErrorMessageToWriter(
     options: RenderOptions,
     err_msg_index: MessageIndex,
     w: *Writer,
+    ttyconf: Io.tty.Config,
     kind: []const u8,
     color: Io.tty.Color,
     indent: usize,
 ) (Writer.Error || std.posix.UnexpectedError)!void {
-    const ttyconf = options.ttyconf;
     const err_msg = eb.getErrorMessage(err_msg_index);
     if (err_msg.src_loc != .none) {
         const src = eb.extraData(SourceLocation, @intFromEnum(err_msg.src_loc));
@@ -251,7 +250,7 @@ fn renderErrorMessageToWriter(
             try ttyconf.setColor(w, .reset);
         }
         for (eb.getNotes(err_msg_index)) |note| {
-            try renderErrorMessageToWriter(eb, options, note, w, "note", .cyan, indent);
+            try renderErrorMessageToWriter(eb, options, note, w, ttyconf, "note", .cyan, indent);
         }
         if (src.data.reference_trace_len > 0 and options.include_reference_trace) {
             try ttyconf.setColor(w, .reset);
@@ -300,7 +299,7 @@ fn renderErrorMessageToWriter(
         }
         try ttyconf.setColor(w, .reset);
         for (eb.getNotes(err_msg_index)) |note| {
-            try renderErrorMessageToWriter(eb, options, note, w, "note", .cyan, indent + 4);
+            try renderErrorMessageToWriter(eb, options, note, w, ttyconf, "note", .cyan, indent + 4);
         }
     }
 }
lib/std/zig/parser_test.zig
@@ -6386,7 +6386,7 @@ var fixed_buffer_mem: [100 * 1024]u8 = undefined;
 
 fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *bool) ![]u8 {
     var buffer: [64]u8 = undefined;
-    const stderr = std.debug.lockStderrWriter(&buffer);
+    const stderr, _ = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
 
     var tree = try std.zig.Ast.parse(allocator, source, .zig);
lib/std/Build.zig
@@ -2257,8 +2257,8 @@ pub const GeneratedFile = struct {
 
     pub fn getPath2(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) []const u8 {
         return gen.path orelse {
-            const w = debug.lockStderrWriter(&.{});
-            dumpBadGetPathHelp(gen.step, w, .detect(.stderr()), src_builder, asking_step) catch {};
+            const w, const ttyconf = debug.lockStderrWriter(&.{});
+            dumpBadGetPathHelp(gen.step, w, ttyconf, src_builder, asking_step) catch {};
             debug.unlockStderrWriter();
             @panic("misconfigured build script");
         };
@@ -2466,8 +2466,8 @@ pub const LazyPath = union(enum) {
                 var file_path: Cache.Path = .{
                     .root_dir = Cache.Directory.cwd(),
                     .sub_path = gen.file.path orelse {
-                        const w = debug.lockStderrWriter(&.{});
-                        dumpBadGetPathHelp(gen.file.step, w, .detect(.stderr()), src_builder, asking_step) catch {};
+                        const w, const ttyconf = debug.lockStderrWriter(&.{});
+                        dumpBadGetPathHelp(gen.file.step, w, ttyconf, src_builder, asking_step) catch {};
                         debug.unlockStderrWriter();
                         @panic("misconfigured build script");
                     },
@@ -2558,13 +2558,11 @@ fn dumpBadDirnameHelp(
     comptime msg: []const u8,
     args: anytype,
 ) anyerror!void {
-    const w = debug.lockStderrWriter(&.{});
+    const w, const tty_config = debug.lockStderrWriter(&.{});
     defer debug.unlockStderrWriter();
 
     try w.print(msg, args);
 
-    const tty_config = std.Io.tty.detectConfig(.stderr());
-
     if (fail_step) |s| {
         tty_config.setColor(w, .red) catch {};
         try w.writeAll("    The step was created by this stack trace:\n");
lib/std/debug.zig
@@ -272,7 +272,7 @@ pub fn unlockStdErr() void {
     std.Progress.unlockStdErr();
 }
 
-/// Allows the caller to freely write to stderr until `unlockStdErr` is called.
+/// Allows the caller to freely write to stderr until `unlockStderrWriter` is called.
 ///
 /// During the lock, any `std.Progress` information is cleared from the terminal.
 ///
@@ -282,8 +282,16 @@ pub fn unlockStdErr() void {
 ///
 /// The returned `Writer` does not need to be manually flushed: flushing is performed automatically
 /// when the matching `unlockStderrWriter` call occurs.
-pub fn lockStderrWriter(buffer: []u8) *Writer {
-    return std.Progress.lockStderrWriter(buffer);
+pub fn lockStderrWriter(buffer: []u8) struct { *Writer, tty.Config } {
+    const global = struct {
+        var conf: ?tty.Config = null;
+    };
+    const w = std.Progress.lockStderrWriter(buffer);
+    // The stderr lock also locks access to `global.conf`.
+    if (global.conf == null) {
+        global.conf = .detect(.stderr());
+    }
+    return .{ w, global.conf.? };
 }
 
 pub fn unlockStderrWriter() void {
@@ -297,7 +305,7 @@ pub fn unlockStderrWriter() void {
 /// function returns.
 pub fn print(comptime fmt: []const u8, args: anytype) void {
     var buffer: [64]u8 = undefined;
-    const bw = lockStderrWriter(&buffer);
+    const bw, _ = lockStderrWriter(&buffer);
     defer unlockStderrWriter();
     nosuspend bw.print(fmt, args) catch return;
 }
@@ -314,9 +322,8 @@ pub inline fn getSelfDebugInfo() !*SelfInfo {
 /// Tries to print a hexadecimal view of the bytes, unbuffered, and ignores any error returned.
 /// Obtains the stderr mutex while dumping.
 pub fn dumpHex(bytes: []const u8) void {
-    const bw = lockStderrWriter(&.{});
+    const bw, const ttyconf = lockStderrWriter(&.{});
     defer unlockStderrWriter();
-    const ttyconf = tty.detectConfig(.stderr());
     dumpHexFallible(bw, ttyconf, bytes) catch {};
 }
 
@@ -538,9 +545,7 @@ pub fn defaultPanic(
             _ = panicking.fetchAdd(1, .seq_cst);
 
             trace: {
-                const tty_config = tty.detectConfig(.stderr());
-
-                const stderr = lockStderrWriter(&.{});
+                const stderr, const tty_config = lockStderrWriter(&.{});
                 defer unlockStderrWriter();
 
                 if (builtin.single_threaded) {
@@ -743,8 +748,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri
 }
 /// A thin wrapper around `writeCurrentStackTrace` which writes to stderr and ignores write errors.
 pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void {
-    const tty_config = tty.detectConfig(.stderr());
-    const stderr = lockStderrWriter(&.{});
+    const stderr, const tty_config = lockStderrWriter(&.{});
     defer unlockStderrWriter();
     writeCurrentStackTrace(.{
         .first_address = a: {
@@ -809,8 +813,7 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.C
 }
 /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors.
 pub fn dumpStackTrace(st: *const StackTrace) void {
-    const tty_config = tty.detectConfig(.stderr());
-    const stderr = lockStderrWriter(&.{});
+    const stderr, const tty_config = lockStderrWriter(&.{});
     defer unlockStderrWriter();
     writeStackTrace(st, stderr, tty_config) catch |err| switch (err) {
         error.WriteFailed => {},
@@ -1552,9 +1555,7 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?CpuContex
             _ = panicking.fetchAdd(1, .seq_cst);
 
             trace: {
-                const tty_config = tty.detectConfig(.stderr());
-
-                const stderr = lockStderrWriter(&.{});
+                const stderr, const tty_config = lockStderrWriter(&.{});
                 defer unlockStderrWriter();
 
                 if (addr) |a| {
@@ -1612,7 +1613,7 @@ test "manage resources correctly" {
         &di,
         &discarding.writer,
         S.showMyTrace(),
-        tty.detectConfig(.stderr()),
+        .no_color,
     );
 }
 
@@ -1674,8 +1675,7 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize
         pub fn dump(t: @This()) void {
             if (!enabled) return;
 
-            const tty_config = tty.detectConfig(.stderr());
-            const stderr = lockStderrWriter(&.{});
+            const stderr, const tty_config = lockStderrWriter(&.{});
             defer unlockStderrWriter();
             const end = @min(t.index, size);
             for (t.addrs[0..end], 0..) |frames_array, i| {
lib/std/log.zig
@@ -151,7 +151,7 @@ pub fn defaultLog(
     const level_txt = comptime message_level.asText();
     const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
     var buffer: [64]u8 = undefined;
-    const stderr = std.debug.lockStderrWriter(&buffer);
+    const stderr, _ = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
     nosuspend stderr.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return;
 }
lib/std/testing.zig
@@ -355,7 +355,7 @@ test expectApproxEqRel {
 /// This function is intended to be used only in tests. When the two slices are not
 /// equal, prints diagnostics to stderr to show exactly how they are not equal (with
 /// the differences highlighted in red), then returns a test failure error.
-/// The colorized output is optional and controlled by the return of `std.Io.tty.detectConfig()`.
+/// The colorized output is optional and controlled by the return of `std.Io.tty.Config.detect`.
 /// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
 pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {
     const diff_index: usize = diff_index: {
@@ -367,9 +367,9 @@ pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const
         break :diff_index if (expected.len == actual.len) return else shortest;
     };
     if (!backend_can_print) return error.TestExpectedEqual;
-    const stderr_w = std.debug.lockStderrWriter(&.{});
+    const stderr_w, const ttyconf = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
-    failEqualSlices(T, expected, actual, diff_index, stderr_w) catch {};
+    failEqualSlices(T, expected, actual, diff_index, stderr_w, ttyconf) catch {};
     return error.TestExpectedEqual;
 }
 
@@ -379,6 +379,7 @@ fn failEqualSlices(
     actual: []const T,
     diff_index: usize,
     w: *std.Io.Writer,
+    ttyconf: std.Io.tty.Config,
 ) !void {
     try w.print("slices differ. first difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index });
 
@@ -398,7 +399,6 @@ fn failEqualSlices(
     const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)];
     const actual_truncated = window_start + actual_window.len < actual.len;
 
-    const ttyconf = std.Io.tty.detectConfig(.stderr());
     var differ = if (T == u8) BytesDiffer{
         .expected = expected_window,
         .actual = actual_window,
lib/std/zig.zig
@@ -53,17 +53,18 @@ pub const Color = enum {
     /// Assume stderr is a terminal.
     on,
 
-    pub fn get_tty_conf(color: Color) Io.tty.Config {
+    pub fn getTtyConf(color: Color, detected: Io.tty.Config) Io.tty.Config {
         return switch (color) {
-            .auto => Io.tty.detectConfig(std.fs.File.stderr()),
+            .auto => detected,
             .on => .escape_codes,
             .off => .no_color,
         };
     }
-
-    pub fn renderOptions(color: Color) std.zig.ErrorBundle.RenderOptions {
-        return .{
-            .ttyconf = get_tty_conf(color),
+    pub fn detectTtyConf(color: Color) Io.tty.Config {
+        return switch (color) {
+            .auto => .detect(.stderr()),
+            .on => .escape_codes,
+            .off => .no_color,
         };
     }
 };
@@ -606,7 +607,7 @@ pub fn printAstErrorsToStderr(gpa: Allocator, tree: Ast, path: []const u8, color
 
     var error_bundle = try wip_errors.toOwnedBundle("");
     defer error_bundle.deinit(gpa);
-    error_bundle.renderToStdErr(color.renderOptions());
+    error_bundle.renderToStdErr(.{}, color);
 }
 
 pub fn putAstErrorsIntoBundle(
src/Air/print.zig
@@ -73,13 +73,13 @@ pub fn writeInst(
 }
 
 pub fn dump(air: Air, pt: Zcu.PerThread, liveness: ?Air.Liveness) void {
-    const stderr_bw = std.debug.lockStderrWriter(&.{});
+    const stderr_bw, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
     air.write(stderr_bw, pt, liveness);
 }
 
 pub fn dumpInst(air: Air, inst: Air.Inst.Index, pt: Zcu.PerThread, liveness: ?Air.Liveness) void {
-    const stderr_bw = std.debug.lockStderrWriter(&.{});
+    const stderr_bw, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
     air.writeInst(stderr_bw, inst, pt, liveness);
 }
src/codegen/aarch64/Select.zig
@@ -11188,7 +11188,7 @@ fn initValueAdvanced(
 }
 pub fn dumpValues(isel: *Select, which: enum { only_referenced, all }) void {
     errdefer |err| @panic(@errorName(err));
-    const stderr = std.debug.lockStderrWriter(&.{});
+    const stderr, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
 
     const zcu = isel.pt.zcu;
src/libs/mingw/def.zig
@@ -1028,7 +1028,7 @@ fn testParse(machine_type: std.coff.IMAGE.FILE.MACHINE, source: [:0]const u8, ex
     const module = parse(std.testing.allocator, source, machine_type, .mingw, &diagnostics) catch |err| switch (err) {
         error.OutOfMemory => |e| return e,
         error.ParseError => {
-            const stderr = std.debug.lockStderrWriter(&.{});
+            const stderr, _ = std.debug.lockStderrWriter(&.{});
             defer std.debug.unlockStderrWriter();
             try diagnostics.writeMsg(stderr, source);
             try stderr.writeByte('\n');
src/libs/mingw.zig
@@ -312,7 +312,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
     const include_dir = try comp.dirs.zig_lib.join(arena, &.{ "libc", "mingw", "def-include" });
 
     if (comp.verbose_cc) print: {
-        var stderr = std.debug.lockStderrWriter(&.{});
+        var stderr, _ = std.debug.lockStderrWriter(&.{});
         defer std.debug.unlockStderrWriter();
         nosuspend stderr.print("def file: {s}\n", .{def_file_path}) catch break :print;
         nosuspend stderr.print("include dir: {s}\n", .{include_dir}) catch break :print;
@@ -332,11 +332,11 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
 
     if (aro_comp.diagnostics.output.to_list.messages.items.len != 0) {
         var buffer: [64]u8 = undefined;
-        const w = std.debug.lockStderrWriter(&buffer);
+        const w, const ttyconf = std.debug.lockStderrWriter(&buffer);
         defer std.debug.unlockStderrWriter();
         for (aro_comp.diagnostics.output.to_list.messages.items) |msg| {
             if (msg.kind == .@"fatal error" or msg.kind == .@"error") {
-                msg.write(w, .detect(std.fs.File.stderr()), true) catch {};
+                msg.write(w, ttyconf, true) catch {};
                 return error.AroPreprocessorFailed;
             }
         }
@@ -356,7 +356,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void {
             error.OutOfMemory => |e| return e,
             error.ParseError => {
                 var buffer: [64]u8 = undefined;
-                const w = std.debug.lockStderrWriter(&buffer);
+                const w, _ = std.debug.lockStderrWriter(&buffer);
                 defer std.debug.unlockStderrWriter();
                 try w.writeAll("error: ");
                 try def_diagnostics.writeMsg(w, input);
src/link/Coff.zig
@@ -2335,7 +2335,7 @@ pub fn deleteExport(coff: *Coff, exported: Zcu.Exported, name: InternPool.NullTe
 }
 
 pub fn dump(coff: *Coff, tid: Zcu.PerThread.Id) void {
-    const w = std.debug.lockStderrWriter(&.{});
+    const w, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
     coff.printNode(tid, w, .root, 0) catch {};
 }
src/link/Elf2.zig
@@ -1965,7 +1965,7 @@ pub fn deleteExport(elf: *Elf, exported: Zcu.Exported, name: InternPool.NullTerm
 }
 
 pub fn dump(elf: *Elf, tid: Zcu.PerThread.Id) void {
-    const w = std.debug.lockStderrWriter(&.{});
+    const w, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
     elf.printNode(tid, w, .root, 0) catch {};
 }
src/Package/Fetch.zig
@@ -2043,7 +2043,7 @@ const UnpackResult = struct {
         defer errors.deinit(gpa);
         var aw: Io.Writer.Allocating = .init(gpa);
         defer aw.deinit();
-        try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer);
+        try errors.renderToWriter(.{}, &aw.writer, .no_color);
         try std.testing.expectEqualStrings(
             \\error: unable to unpack
             \\    note: unable to create symlink from 'dir2/file2' to 'filename': SymlinkError
@@ -2360,7 +2360,7 @@ const TestFetchBuilder = struct {
         }
         var aw: Io.Writer.Allocating = .init(std.testing.allocator);
         defer aw.deinit();
-        try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer);
+        try errors.renderToWriter(.{}, &aw.writer, .no_color);
         try std.testing.expectEqualStrings(msg, aw.written());
     }
 };
src/Zcu/PerThread.zig
@@ -4473,7 +4473,7 @@ fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) e
     defer if (liveness) |*l| l.deinit(gpa);
 
     if (build_options.enable_debug_extensions and comp.verbose_air) {
-        const stderr = std.debug.lockStderrWriter(&.{});
+        const stderr, _ = std.debug.lockStderrWriter(&.{});
         defer std.debug.unlockStderrWriter();
         stderr.print("# Begin Function AIR: {f}:\n", .{fqn.fmt(ip)}) catch {};
         air.write(stderr, pt, liveness);
src/Compilation.zig
@@ -2093,7 +2093,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
 
         if (options.verbose_llvm_cpu_features) {
             if (options.root_mod.resolved_target.llvm_cpu_features) |cf| print: {
-                const stderr_w = std.debug.lockStderrWriter(&.{});
+                const stderr_w, _ = std.debug.lockStderrWriter(&.{});
                 defer std.debug.unlockStderrWriter();
                 stderr_w.print("compilation: {s}\n", .{options.root_name}) catch break :print;
                 stderr_w.print("  target: {s}\n", .{try target.zigTriple(arena)}) catch break :print;
@@ -4270,7 +4270,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle {
             // However, we haven't reported any such error.
             // This is a compiler bug.
             print_ctx: {
-                var stderr_w = std.debug.lockStderrWriter(&.{});
+                var stderr_w, _ = std.debug.lockStderrWriter(&.{});
                 defer std.debug.unlockStderrWriter();
                 stderr_w.writeAll("referenced transitive analysis errors, but none actually emitted\n") catch break :print_ctx;
                 stderr_w.print("{f} [transitive failure]\n", .{zcu.fmtAnalUnit(failed_unit)}) catch break :print_ctx;
@@ -7752,7 +7752,7 @@ pub fn lockAndSetMiscFailure(
 
 pub fn dump_argv(argv: []const []const u8) void {
     var buffer: [64]u8 = undefined;
-    const stderr = std.debug.lockStderrWriter(&buffer);
+    const stderr, _ = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
     nosuspend {
         for (argv, 0..) |arg, i| {
src/crash_report.zig
@@ -95,7 +95,7 @@ fn dumpCrashContext() Io.Writer.Error!void {
 
     // TODO: this does mean that a different thread could grab the stderr mutex between the context
     // and the actual panic printing, which would be quite confusing.
-    const stderr = std.debug.lockStderrWriter(&.{});
+    const stderr, _ = std.debug.lockStderrWriter(&.{});
     defer std.debug.unlockStderrWriter();
 
     try stderr.writeAll("Compiler crash context:\n");
src/fmt.zig
@@ -124,7 +124,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
                     try wip_errors.addZirErrorMessages(zir, tree, source_code, "<stdin>");
                     var error_bundle = try wip_errors.toOwnedBundle("");
                     defer error_bundle.deinit(gpa);
-                    error_bundle.renderToStdErr(color.renderOptions());
+                    error_bundle.renderToStdErr(.{}, color);
                     process.exit(2);
                 }
             } else {
@@ -138,7 +138,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
                     try wip_errors.addZoirErrorMessages(zoir, tree, source_code, "<stdin>");
                     var error_bundle = try wip_errors.toOwnedBundle("");
                     defer error_bundle.deinit(gpa);
-                    error_bundle.renderToStdErr(color.renderOptions());
+                    error_bundle.renderToStdErr(.{}, color);
                     process.exit(2);
                 }
             }
@@ -317,7 +317,7 @@ fn fmtPathFile(
                     try wip_errors.addZirErrorMessages(zir, tree, source_code, file_path);
                     var error_bundle = try wip_errors.toOwnedBundle("");
                     defer error_bundle.deinit(gpa);
-                    error_bundle.renderToStdErr(fmt.color.renderOptions());
+                    error_bundle.renderToStdErr(.{}, fmt.color);
                     fmt.any_error = true;
                 }
             },
@@ -332,7 +332,7 @@ fn fmtPathFile(
                     try wip_errors.addZoirErrorMessages(zoir, tree, source_code, file_path);
                     var error_bundle = try wip_errors.toOwnedBundle("");
                     defer error_bundle.deinit(gpa);
-                    error_bundle.renderToStdErr(fmt.color.renderOptions());
+                    error_bundle.renderToStdErr(.{}, fmt.color);
                     fmt.any_error = true;
                 }
             },
src/InternPool.zig
@@ -11330,7 +11330,7 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
 
 fn dumpAllFallible(ip: *const InternPool) anyerror!void {
     var buffer: [4096]u8 = undefined;
-    const stderr_bw = std.debug.lockStderrWriter(&buffer);
+    const stderr_bw, _ = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
     for (ip.locals, 0..) |*local, tid| {
         const items = local.shared.items.view();
@@ -11462,7 +11462,7 @@ pub fn dumpGenericInstancesFallible(ip: *const InternPool, allocator: Allocator)
     }
 
     var buffer: [4096]u8 = undefined;
-    const stderr_bw = std.debug.lockStderrWriter(&buffer);
+    const stderr_bw, _ = std.debug.lockStderrWriter(&buffer);
     defer std.debug.unlockStderrWriter();
 
     const SortContext = struct {
src/link.zig
@@ -2215,7 +2215,7 @@ fn resolvePathInputLib(
             var error_bundle = try wip_errors.toOwnedBundle("");
             defer error_bundle.deinit(gpa);
 
-            error_bundle.renderToStdErr(color.renderOptions());
+            error_bundle.renderToStdErr(.{}, color);
 
             std.process.exit(1);
         }
src/main.zig
@@ -4520,7 +4520,7 @@ fn updateModule(comp: *Compilation, color: Color, prog_node: std.Progress.Node)
     defer errors.deinit(comp.gpa);
 
     if (errors.errorMessageCount() > 0) {
-        errors.renderToStdErr(color.renderOptions());
+        errors.renderToStdErr(.{}, color);
         return error.CompileErrorsReported;
     }
 }
@@ -4573,7 +4573,7 @@ fn cmdTranslateC(
                 return;
             } else {
                 const color: Color = .auto;
-                result.errors.renderToStdErr(color.renderOptions());
+                result.errors.renderToStdErr(.{}, color);
                 process.exit(1);
             }
         }
@@ -5199,7 +5199,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8)
 
                 if (fetch.error_bundle.root_list.items.len > 0) {
                     var errors = try fetch.error_bundle.toOwnedBundle("");
-                    errors.renderToStdErr(color.renderOptions());
+                    errors.renderToStdErr(.{}, color);
                     process.exit(1);
                 }
 
@@ -6135,7 +6135,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void {
                 try wip_errors.init(arena);
                 try wip_errors.addZirErrorMessages(zir, tree, source, display_path);
                 var error_bundle = try wip_errors.toOwnedBundle("");
-                error_bundle.renderToStdErr(color.renderOptions());
+                error_bundle.renderToStdErr(.{}, color);
                 if (zir.loweringFailed()) {
                     process.exit(1);
                 }
@@ -6206,7 +6206,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void {
                 try wip_errors.init(arena);
                 try wip_errors.addZoirErrorMessages(zoir, tree, source, display_path);
                 var error_bundle = try wip_errors.toOwnedBundle("");
-                error_bundle.renderToStdErr(color.renderOptions());
+                error_bundle.renderToStdErr(.{}, color);
                 process.exit(1);
             }
 
@@ -6479,7 +6479,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void {
         try wip_errors.init(arena);
         try wip_errors.addZirErrorMessages(old_zir, old_tree, old_source, old_source_path);
         var error_bundle = try wip_errors.toOwnedBundle("");
-        error_bundle.renderToStdErr(color.renderOptions());
+        error_bundle.renderToStdErr(.{}, color);
         process.exit(1);
     }
 
@@ -6491,7 +6491,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void {
         try wip_errors.init(arena);
         try wip_errors.addZirErrorMessages(new_zir, new_tree, new_source, new_source_path);
         var error_bundle = try wip_errors.toOwnedBundle("");
-        error_bundle.renderToStdErr(color.renderOptions());
+        error_bundle.renderToStdErr(.{}, color);
         process.exit(1);
     }
 
@@ -6948,7 +6948,7 @@ fn cmdFetch(
 
     if (fetch.error_bundle.root_list.items.len > 0) {
         var errors = try fetch.error_bundle.toOwnedBundle("");
-        errors.renderToStdErr(color.renderOptions());
+        errors.renderToStdErr(.{}, color);
         process.exit(1);
     }
 
@@ -7304,7 +7304,7 @@ fn loadManifest(
 
         var error_bundle = try wip_errors.toOwnedBundle("");
         defer error_bundle.deinit(gpa);
-        error_bundle.renderToStdErr(options.color.renderOptions());
+        error_bundle.renderToStdErr(.{}, options.color);
 
         process.exit(2);
     }
src/Sema.zig
@@ -2631,7 +2631,7 @@ pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg
         Compilation.addModuleErrorMsg(zcu, &wip_errors, err_msg.*, false) catch @panic("out of memory");
         std.debug.print("compile error during Sema:\n", .{});
         var error_bundle = wip_errors.toOwnedBundle("") catch @panic("out of memory");
-        error_bundle.renderToStdErr(.{ .ttyconf = .no_color });
+        error_bundle.renderToStdErr(.{}, .auto);
         std.debug.panicExtra(@returnAddress(), "unexpected compile error occurred", .{});
     }
 
tools/gen_spirv_spec.zig
@@ -89,10 +89,9 @@ pub fn main() !void {
     const output = allocating.written()[0 .. allocating.written().len - 1 :0];
 
     var tree = try std.zig.Ast.parse(allocator, output, .zig);
-    var color: std.zig.Color = .on;
 
     if (tree.errors.len != 0) {
-        try std.zig.printAstErrorsToStderr(allocator, tree, "", color);
+        try std.zig.printAstErrorsToStderr(allocator, tree, "", .auto);
         return;
     }
 
@@ -104,7 +103,7 @@ pub fn main() !void {
         try wip_errors.addZirErrorMessages(zir, tree, output, "");
         var error_bundle = try wip_errors.toOwnedBundle("");
         defer error_bundle.deinit(allocator);
-        error_bundle.renderToStdErr(color.renderOptions());
+        error_bundle.renderToStdErr(.{}, .auto);
     }
 
     const formatted_output = try tree.renderAlloc(allocator);
@@ -931,7 +930,7 @@ fn parseHexInt(text: []const u8) !u31 {
 }
 
 fn usageAndExit(arg0: []const u8, code: u8) noreturn {
-    const stderr = std.debug.lockStderrWriter(&.{});
+    const stderr, _ = std.debug.lockStderrWriter(&.{});
     stderr.print(
         \\Usage: {s} <SPIRV-Headers repository path> <path/to/zig/src/codegen/spirv/extinst.zig.grammar.json>
         \\
tools/generate_linux_syscalls.zig
@@ -177,7 +177,9 @@ pub fn main() !void {
 
     const args = try std.process.argsAlloc(gpa);
     if (args.len < 2 or mem.eql(u8, args[1], "--help")) {
-        usage(std.debug.lockStderrWriter(&.{}), args[0]) catch std.process.exit(2);
+        const w, _ = std.debug.lockStderrWriter(&.{});
+        defer std.debug.unlockStderrWriter();
+        usage(w, args[0]) catch std.process.exit(2);
         std.process.exit(1);
     }
     const linux_path = args[1];
tools/incr-check.zig
@@ -340,8 +340,7 @@ const Eval = struct {
             .unknown => return,
             .compile_errors => |ce| ce,
             .stdout, .exit_code => {
-                const color: std.zig.Color = .auto;
-                error_bundle.renderToStdErr(color.renderOptions());
+                error_bundle.renderToStdErr(.{}, .auto);
                 eval.fatal("update '{s}': unexpected compile errors", .{update.name});
             },
         };
@@ -350,8 +349,7 @@ const Eval = struct {
 
         for (error_bundle.getMessages()) |err_idx| {
             if (expected_idx == expected.errors.len) {
-                const color: std.zig.Color = .auto;
-                error_bundle.renderToStdErr(color.renderOptions());
+                error_bundle.renderToStdErr(.{}, .auto);
                 eval.fatal("update '{s}': more errors than expected", .{update.name});
             }
             try eval.checkOneError(update, error_bundle, expected.errors[expected_idx], false, err_idx);
@@ -359,8 +357,7 @@ const Eval = struct {
 
             for (error_bundle.getNotes(err_idx)) |note_idx| {
                 if (expected_idx == expected.errors.len) {
-                    const color: std.zig.Color = .auto;
-                    error_bundle.renderToStdErr(color.renderOptions());
+                    error_bundle.renderToStdErr(.{}, .auto);
                     eval.fatal("update '{s}': more error notes than expected", .{update.name});
                 }
                 try eval.checkOneError(update, error_bundle, expected.errors[expected_idx], true, note_idx);
@@ -369,8 +366,7 @@ const Eval = struct {
         }
 
         if (!std.mem.eql(u8, error_bundle.getCompileLogOutput(), expected.compile_log_output)) {
-            const color: std.zig.Color = .auto;
-            error_bundle.renderToStdErr(color.renderOptions());
+            error_bundle.renderToStdErr(.{}, .auto);
             eval.fatal("update '{s}': unexpected compile log output", .{update.name});
         }
     }
@@ -404,8 +400,7 @@ const Eval = struct {
             expected.column != src.column + 1 or
             !std.mem.eql(u8, expected.msg, msg))
         {
-            const color: std.zig.Color = .auto;
-            eb.renderToStdErr(color.renderOptions());
+            eb.renderToStdErr(.{}, .auto);
             eval.fatal("update '{s}': compile error did not match expected error", .{update.name});
         }
     }
tools/update_clang_options.zig
@@ -961,7 +961,9 @@ fn objectLessThan(context: void, a: *json.ObjectMap, b: *json.ObjectMap) bool {
 }
 
 fn printUsageAndExit(arg0: []const u8) noreturn {
-    printUsage(std.debug.lockStderrWriter(&.{}), arg0) catch std.process.exit(2);
+    const w, _ = std.debug.lockStderrWriter(&.{});
+    defer std.debug.unlockStderrWriter();
+    printUsage(w, arg0) catch std.process.exit(2);
     std.process.exit(1);
 }
 
tools/update_cpu_features.zig
@@ -2167,7 +2167,7 @@ fn processOneTarget(job: Job) void {
 }
 
 fn usageAndExit(arg0: []const u8, code: u8) noreturn {
-    const stderr = std.debug.lockStderrWriter(&.{});
+    const stderr, _ = std.debug.lockStderrWriter(&.{});
     stderr.print(
         \\Usage: {s} /path/to/llvm-tblgen /path/git/llvm-project /path/git/zig [zig_name filter]
         \\
tools/update_crc_catalog.zig
@@ -190,7 +190,9 @@ pub fn main() anyerror!void {
 }
 
 fn printUsageAndExit(arg0: []const u8) noreturn {
-    printUsage(std.debug.lockStderrWriter(&.{}), arg0) catch std.process.exit(2);
+    const w, _ = std.debug.lockStderrWriter(&.{});
+    defer std.debug.unlockStderrWriter();
+    printUsage(w, arg0) catch std.process.exit(2);
     std.process.exit(1);
 }