From 9788450cca71aa65d41a99e1ad6f7a793b93f33a Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 6 Aug 2026 16:43:34 +0200 Subject: [PATCH 1/6] Maker: rename `serveBSPHandshake` to `serveBspHandshake` --- lib/compiler/Maker.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 4d4577f2f179ce0b9af9390a19de77324ad8535f..fe3db3d508eba625d2072d339782079ef9bf2665 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -695,7 +695,7 @@ pub fn main(init: process.Init.Minimal) !void { .in = &stdin_reader.interface, .out = &stdout_writer.interface, }; - try serveBSPHandshake(&protocol_server_allocation); + try serveBspHandshake(&protocol_server_allocation); break :s &protocol_server_allocation; } else null; @@ -3256,7 +3256,7 @@ fn cleanTmpFiles(maker: *Maker, steps: []const Configuration.Step.Index) void { } } -fn serveBSPHandshake(s: *const std.zig.Server) !void { +fn serveBspHandshake(s: *const std.zig.Server) !void { const handshake_header: Server.Message.Handshake = .{ .version = Server.build_system_version, .flags = .{ -- 2.54.0 From 5926dd027df2f35d4ff3610c16cfcad8e0b3a4b5 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 6 Aug 2026 16:44:23 +0200 Subject: [PATCH 2/6] Maker: reuse global buffer for stdout --- lib/compiler/Maker.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index fe3db3d508eba625d2072d339782079ef9bf2665..7eac68eebb7bd09afb8cac07d6ab1e688e6c4bc1 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -681,9 +681,8 @@ pub fn main(init: process.Init.Minimal) !void { } else null; var stdin_buffer: [256]u8 = undefined; - var stdout_buffer: [256]u8 = undefined; var stdin_reader = Io.File.stdin().reader(io, &stdin_buffer); - var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer); + const stdout_writer = initStdoutWriter(io); var protocol_server_allocation: AvoidableServer = undefined; const protocol_server: ?*AvoidableServer = if (listen) s: { @@ -693,7 +692,7 @@ pub fn main(init: process.Init.Minimal) !void { if (step_names.items.len > 0) fatal("build steps must be provided over the protocol instead of using CLI arguments", .{}); protocol_server_allocation = .{ .in = &stdin_reader.interface, - .out = &stdout_writer.interface, + .out = stdout_writer, }; try serveBspHandshake(&protocol_server_allocation); break :s &protocol_server_allocation; -- 2.54.0 From 81be5b22a34424972c6b75201c510f5159f5d4f3 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 29 Aug 2026 00:45:49 +0200 Subject: [PATCH 3/6] Maker: fix leaks on process exit Can be observed with `--debug-maker-leaks`. --- lib/compiler/Maker.zig | 38 ++++++++++++++++++------------------- lib/compiler/Maker/Step.zig | 9 +++++++++ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 7eac68eebb7bd09afb8cac07d6ab1e688e6c4bc1..5c7f4a94d4eeee79ef158262a5b51508c93bd0c3 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -63,9 +63,9 @@ protocol_server_mutex: Io.Mutex, web_server: ?*AvoidableWebServer, /// Allocated into `gpa`. memory_blocked_steps: std.ArrayList(Configuration.Step.Index), -/// Allocated into `gpa`. +/// Allocated into `gpa` during `prepare`. initial_steps: std.array_hash_map.Auto(Configuration.Step.Index, void), -/// Allocated into `gpa`. +/// Allocated into `gpa` during `prepare`. step_stack: std.array_hash_map.Auto(Configuration.Step.Index, void), pkg_config: PkgConfig, @@ -762,10 +762,8 @@ pub fn main(init: process.Init.Minimal) !void { .include = install_include_path, }, - // Extra step at the end which is the autogenerated placeholder - // step which indicates that we need to reconfigure. - .steps = try arena.alloc(Step, scanned_config.configuration.steps.len + 1), - .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len), + .steps = &.{}, + .generated_files = &.{}, .run_args = run_args, .available_rss = max_rss, @@ -792,11 +790,7 @@ pub fn main(init: process.Init.Minimal) !void { else .failures, }; - defer { - maker.memory_blocked_steps.deinit(gpa); - maker.initial_steps.deinit(gpa); - maker.step_stack.deinit(gpa); - } + defer maker.deinit(); if (maker.available_rss == 0) { maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64); @@ -863,6 +857,9 @@ pub fn main(init: process.Init.Minimal) !void { &watch.?, if (in_debounce) .{ .ms = debounce_interval_ms } else .none, }); + } else if (watch) |*w| { + w.deinit(); + watch = null; } continue :loop try select.await(); @@ -915,7 +912,7 @@ pub fn main(init: process.Init.Minimal) !void { if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{native_os}); break :w try .init(&maker); }; - defer w.deinit(); + defer if (watch_flag) w.deinit(); if (web_server) |ws| try ws.updateConfiguration(&maker); @@ -2229,6 +2226,11 @@ fn prepare( const step_stack = &maker.step_stack; const c = &maker.scanned_config.configuration; + // Extra step at the end which is the autogenerated placeholder + // step which indicates that we need to reconfigure. + maker.steps = try arena.alloc(Step, c.steps.len + 1); + maker.generated_files = try arena.alloc(Path, c.generated_files_len); + // The last element is a reserved special pseudostep which contains the // watch inputs for the configurer executable. for (maker.steps[0 .. maker.steps.len - 1], 0..) |*step, step_index_usize| { @@ -2549,7 +2551,7 @@ fn makeSteps( }; if (code == 0) { removePoisonedConfiguration(io, maker.scanned_config); - if (debugMakerLeaks()) return deinit(maker); + if (debugMakerLeaks()) return; } cleanup_task.await(io); // There is a defer above but an exit below. _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; @@ -2558,12 +2560,10 @@ fn makeSteps( fn deinit(maker: *Maker) void { const gpa = maker.gpa; - for (maker.steps) |*step| { - step.clearResultStderr(gpa); - step.clearFailedCommand(gpa); - step.clearErrorBundle(gpa); - step.inputs.deinit(gpa); - } + for (maker.steps) |*step| step.deinit(gpa); + maker.memory_blocked_steps.deinit(gpa); + maker.initial_steps.deinit(gpa); + maker.step_stack.deinit(gpa); } fn stepReady( diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig index 0fd82226cce06989c0acea46f4f22e4dcd6d6e58..fcb60233f0f8f458e146b4a71bae60ddd8525c9b 100644 --- a/lib/compiler/Maker/Step.zig +++ b/lib/compiler/Maker/Step.zig @@ -309,6 +309,15 @@ pub fn make( } } +pub fn deinit(step: *Step, gpa: Allocator) void { + step.clearResultStderr(gpa); + step.clearFailedCommand(gpa); + step.clearErrorBundle(gpa); + step.inputs.deinit(gpa); + if (step.getZigProcess()) |zp| gpa.destroy(zp); + step.* = undefined; +} + /// Prepares the step for being re-evaluated. pub fn reset(step: *Step, maker: *Maker) void { assert(step.state == .precheck_done); -- 2.54.0 From 13cc8a4de6cc26a225b6c9a49bd95d85275d1c66 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 7 Aug 2026 21:42:18 +0200 Subject: [PATCH 4/6] Maker: always detect gpa leaks in debug mode The build runner used to run in debug mode and had leaks so `--debug-maker-leaks` was needed to opt into reporting them. Since the leaks are now fixed in the maker which runs in release mode by default, `--debug-maker-leaks` becomes unnecessary in favor of just `ZIG_DEBUG_CMD`. --- lib/compiler/Maker.zig | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 5c7f4a94d4eeee79ef158262a5b51508c93bd0c3..197dc61ef19b32d504d240085d2e5a5149dd002f 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -163,7 +163,6 @@ pub fn main(init: process.Init.Minimal) !void { var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); defer arena_instance.deinit(); - defer if (debugMakerLeaks()) log.debug("used {Bi} of arena", .{arena_instance.queryCapacity()}); const arena = arena_instance.allocator(); const args = try init.args.toSlice(arena); @@ -444,8 +443,6 @@ pub fn main(init: process.Init.Minimal) !void { } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| { graph.debug_compiler_runtime_libs = stringToEnum(std.lang.Optimize, rest) orelse fatal("unrecognized optimization mode: {s}", .{rest}); - } else if (is_debug_mode and mem.eql(u8, arg, "--debug-maker-leaks")) { - debug_maker_leaks = true; } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) { // --glibc-runtimes was the old name of the flag; kept for compatibility for now. graph.libc_runtimes_dir = nextArgOrFatal(args, &arg_i); @@ -819,10 +816,7 @@ pub fn main(init: process.Init.Minimal) !void { .message => |payload| { const header: Client.Message.Header = try payload; switch (header.tag) { - .exit => { - cleanExit(io, &scanned_config); - process.exit(0); - }, + .exit => return cleanExit(io, &scanned_config), .bsp_build_steps => { // Cancel existing file watching select.cancelDiscard(); @@ -2550,8 +2544,7 @@ fn makeSteps( break :code 2; // failure; do not print build command }; if (code == 0) { - removePoisonedConfiguration(io, maker.scanned_config); - if (debugMakerLeaks()) return; + return cleanExit(io, maker.scanned_config); } cleanup_task.await(io); // There is a defer above but an exit below. _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; @@ -3639,11 +3632,6 @@ fn removePoisonedConfiguration(io: Io, scanned_config: *const ScannedConfig) voi } } -inline fn debugMakerLeaks() bool { - if (!is_debug_mode) return false; - return debug_maker_leaks; -} - const BuildRoot = struct { directory: Cache.Directory, close_directory: bool, -- 2.54.0 From 7c14270ed41314a97f17788569ec813755c07d4c Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 6 Aug 2026 23:46:56 +0200 Subject: [PATCH 5/6] Maker: do not write error messages to stderr when running over protocol --- lib/compiler/Maker.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 197dc61ef19b32d504d240085d2e5a5149dd002f..fc39fceec4ea8fa41125c1941d1f269a0880d1d2 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -2689,9 +2689,10 @@ fn makeStep( } // No matter the result, we want to display error/warning messages. - if (make_step.result_error_bundle.errorMessageCount() > 0 or - make_step.result_error_msgs.items.len > 0 or - make_step.result_stderr.len > 0) + if (maker.protocol_server == null and + (make_step.result_error_bundle.errorMessageCount() > 0 or + make_step.result_error_msgs.items.len > 0 or + make_step.result_stderr.len > 0)) { const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); defer io.unlockStderr(); -- 2.54.0 From 88d0dbe9355fb17d24cab9861c692e89f32bbc7c Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 29 Aug 2026 00:31:51 +0200 Subject: [PATCH 6/6] Maker: workaround file watching not supporting cancellation Related https://codeberg.org/ziglang/zig/issues/31693 --- lib/compiler/Maker.zig | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index fc39fceec4ea8fa41125c1941d1f269a0880d1d2..44ad6b6cfbbb94bf3bb1c672c11a520a4b843dd8 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -809,6 +809,11 @@ pub fn main(init: process.Init.Minimal) !void { var select: Io.Select(Event) = .init(io, &select_buffer); defer select.cancelDiscard(); + // File watching cannot be canceled without blocking + // https://codeberg.org/ziglang/zig/issues/31693 + var is_fs_watching = false; + defer if (is_fs_watching) @panic("(zig build system) TODO file watching cannot be canceled without blocking"); + try select.concurrent(.message, Server.receiveMessage, .{s}); var in_debounce = false; @@ -816,8 +821,14 @@ pub fn main(init: process.Init.Minimal) !void { .message => |payload| { const header: Client.Message.Header = try payload; switch (header.tag) { - .exit => return cleanExit(io, &scanned_config), + .exit => { + // exit early until file watching supports cancelation without blocking + if (is_fs_watching) std.process.exit(0); + return cleanExit(io, &scanned_config); + }, .bsp_build_steps => { + if (is_fs_watching) @panic("(zig build system) TODO file watching cannot be canceled without blocking"); + // Cancel existing file watching select.cancelDiscard(); in_debounce = false; @@ -851,6 +862,7 @@ pub fn main(init: process.Init.Minimal) !void { &watch.?, if (in_debounce) .{ .ms = debounce_interval_ms } else .none, }); + is_fs_watching = true; } else if (watch) |*w| { w.deinit(); watch = null; @@ -862,6 +874,7 @@ pub fn main(init: process.Init.Minimal) !void { } }, .fs_event => |payload| { + is_fs_watching = false; if (!Watch.have_impl) unreachable; switch (payload catch |err| switch (err) { error.MustReconfigure => { @@ -883,6 +896,7 @@ pub fn main(init: process.Init.Minimal) !void { &watch.?, if (in_debounce) .{ .ms = debounce_interval_ms } else .none, }); + is_fs_watching = true; continue :loop try select.await(); }, } -- 2.54.0