From d524a0fe645cfd4d9ae27bd8451b934dcf2ebaef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 13:43:59 -0700 Subject: [PATCH] libghostty: guard release builds against std debug Io Release libghostty-vt builds carefully avoid referencing std.Options.debug_io because its default implementation is std.Io.Threaded, and referencing that vtable keeps every operation Threaded supports linked into the binary: roughly 110KB of unreachable code. Nothing references it today, but any std.debug.print, std.debug.lockStderr, or std.log default-handler call added to release-reachable code would silently reintroduce all of it. Declare std_options_debug_io in the root module so std uses our value instead of constructing the Threaded default. Development builds (Debug, ReleaseSafe, tests) forward the std default so std.debug.print and friends work normally. ReleaseFast and ReleaseSmall builds declare it as a @compileError: since std only analyzes the declaration lazily, at the moment something references a debug Io code path, the error fires exactly at the offending reference, turning a silent size regression into a build failure with a message explaining the alternatives. Release binaries are byte-identical when the guard is not tripped. --- src/lib_vt.zig | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/lib_vt.zig b/src/lib_vt.zig index ffd56939f..5b78b8cbe 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -426,19 +426,45 @@ pub const std_options: std.Options = opts: { break :opts options; }; +/// True for builds where we keep the full std debug machinery (stack +/// traces on panic, std.debug.print, etc.). These builds are for +/// development, where the roughly 160KB of binary size it costs is +/// worth it. +const debug_machinery: bool = builtin.is_test or switch (builtin.mode) { + .Debug, .ReleaseSafe => true, + .ReleaseFast, .ReleaseSmall => false, +}; + /// The panic handler for when this file is the root module. /// /// In ReleaseFast and ReleaseSmall builds we print the panic message to /// stderr and trap, but do not attempt to unwind the stack to print a /// stack trace. -pub const panic: type = if (builtin.is_test or switch (builtin.mode) { - .Debug, .ReleaseSafe => true, - .ReleaseFast, .ReleaseSmall => false, -}) +pub const panic: type = if (debug_machinery) std.debug.FullPanic(std.debug.defaultPanic) else std.debug.FullPanic(tinyPanicImpl); +/// Guards release builds against accidentally reintroducing the std +/// debug Io machinery. +/// +/// `std.Options.debug_io` defaults to `std.Io.Threaded`, and anything +/// that reaches it (std.debug.print, std.debug.lockStderr, the default +/// std.log handler, etc.) pins Threaded's entire vtable into the binary: +/// roughly 110KB of unreachable code. +/// +/// This verifies nothing ever touches it. +pub const std_options_debug_io: std.Io = if (debug_machinery) + std.Io.Threaded.global_single_threaded.io() +else + @compileError( + \\The std debug Io machinery (std.debug.print, std.debug.lockStderr, + \\std.log's default handler, ...) is disabled in libghostty-vt release + \\builds because it costs ~110KB of binary size. Use std.log (routed + \\through our logFn), os/stderr.zig for raw diagnostic writes, or + \\gate the code on debug builds. + ); + /// Prints the panic message to stderr (best-effort) and traps. /// /// This intentionally avoids `std.debug.lockStderr`, which routes through