Commit 7c05330287

Ryan Liptak <squeek502@hotmail.com>
2024-03-07 06:28:49
Integrate resinator errors with Zig using std.zig.Server and ErrorBundle
This takes the code that was previously in src/Compilation.zig to turn resinator diagnostics into Zig error bundles and puts it in resinator/main.zig, and then makes resinator emit the resulting error bundles via std.zig.Server (which is used by the build runner, etc). Also adds support for turning Aro diagnostics into ErrorBundles.
1 parent 52de280
Changed files (3)
lib
compiler
src
lib/compiler/resinator/main.zig
@@ -25,26 +25,48 @@ pub fn main() !void {
         std.os.exit(1);
     }
     const zig_lib_dir = args[1];
+    var cli_args = args[2..];
+
+    var zig_integration = false;
+    if (cli_args.len > 0 and std.mem.eql(u8, cli_args[0], "--zig-integration")) {
+        zig_integration = true;
+        cli_args = args[3..];
+    }
+
+    var error_handler: ErrorHandler = switch (zig_integration) {
+        true => .{
+            .server = .{
+                .out = std.io.getStdOut(),
+                .in = undefined, // won't be receiving messages
+                .receive_fifo = undefined, // won't be receiving messages
+            },
+        },
+        false => .{
+            .tty = stderr_config,
+        },
+    };
 
     var options = options: {
         var cli_diagnostics = cli.Diagnostics.init(allocator);
         defer cli_diagnostics.deinit();
-        var options = cli.parse(allocator, args[2..], &cli_diagnostics) catch |err| switch (err) {
+        var options = cli.parse(allocator, cli_args, &cli_diagnostics) catch |err| switch (err) {
             error.ParseError => {
-                cli_diagnostics.renderToStdErr(args, stderr_config);
+                try error_handler.emitCliDiagnostics(allocator, cli_args, &cli_diagnostics);
                 std.os.exit(1);
             },
             else => |e| return e,
         };
         try options.maybeAppendRC(std.fs.cwd());
 
-        // print any warnings/notes
-        cli_diagnostics.renderToStdErr(args, stderr_config);
-        // 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");
+        if (!zig_integration) {
+            // print any warnings/notes
+            cli_diagnostics.renderToStdErr(args, stderr_config);
+            // 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");
+            }
         }
         break :options options;
     };
@@ -55,6 +77,9 @@ pub fn main() !void {
         return;
     }
 
+    // Don't allow verbose when integrating with Zig via stdout
+    options.verbose = false;
+
     const stdout_writer = std.io.getStdOut().writer();
     if (options.verbose) {
         try options.dumpVerbose(stdout_writer);
@@ -86,13 +111,13 @@ pub fn main() !void {
                 else => |e| {
                     switch (e) {
                         error.MsvcIncludesNotFound => {
-                            try renderErrorMessage(stderr.writer(), stderr_config, .err, "MSVC include paths could not be automatically detected", .{});
+                            try error_handler.emitMessage(allocator, .err, "MSVC include paths could not be automatically detected", .{});
                         },
                         error.MingwIncludesNotFound => {
-                            try renderErrorMessage(stderr.writer(), stderr_config, .err, "MinGW include paths could not be automatically detected", .{});
+                            try error_handler.emitMessage(allocator, .err, "MinGW include paths could not be automatically detected", .{});
                         },
                     }
-                    try renderErrorMessage(stderr.writer(), stderr_config, .note, "to disable auto includes, use the option /:auto-includes none", .{});
+                    try error_handler.emitMessage(allocator, .note, "to disable auto includes, use the option /:auto-includes none", .{});
                     std.os.exit(1);
                 },
             };
@@ -117,20 +142,16 @@ pub fn main() !void {
 
             preprocess.preprocess(&comp, preprocessed_buf.writer(), argv.items, maybe_dependencies_list) catch |err| switch (err) {
                 error.GeneratedSourceError => {
-                    // extra newline to separate this line from the aro errors
-                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessor setup (this is always a bug):\n", .{});
-                    aro.Diagnostics.render(&comp, stderr_config);
+                    try error_handler.emitAroDiagnostics(allocator, "failed during preprocessor setup (this is always a bug):", &comp);
                     std.os.exit(1);
                 },
                 // ArgError can occur if e.g. the .rc file is not found
                 error.ArgError, error.PreprocessError => {
-                    // extra newline to separate this line from the aro errors
-                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing:\n", .{});
-                    aro.Diagnostics.render(&comp, stderr_config);
+                    try error_handler.emitAroDiagnostics(allocator, "failed during preprocessing:", &comp);
                     std.os.exit(1);
                 },
                 error.StreamTooLong => {
-                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing: maximum file size exceeded", .{});
+                    try error_handler.emitMessage(allocator, .err, "failed during preprocessing: maximum file size exceeded", .{});
                     std.os.exit(1);
                 },
                 error.OutOfMemory => |e| return e,
@@ -139,7 +160,7 @@ pub fn main() !void {
             break :full_input try preprocessed_buf.toOwnedSlice();
         } else {
             break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| {
-                try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
+                try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
                 std.os.exit(1);
             };
         }
@@ -159,14 +180,14 @@ pub fn main() !void {
 
     const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) {
         error.InvalidSourceMappingCollapse => {
-            try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during comment removal; this is a known bug", .{});
+            try error_handler.emitMessage(allocator, .err, "failed during comment removal; this is a known bug", .{});
             std.os.exit(1);
         },
         else => |e| return e,
     };
 
     var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
-        try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
+        try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
         std.os.exit(1);
     };
     var output_file_closed = false;
@@ -193,7 +214,7 @@ pub fn main() !void {
         .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
     }) catch |err| switch (err) {
         error.ParseError, error.CompileError => {
-            diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+            try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings);
             // Delete the output file on error
             output_file.close();
             output_file_closed = true;
@@ -207,12 +228,14 @@ pub fn main() !void {
     try output_buffered_stream.flush();
 
     // print any warnings/notes
-    diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+    if (!zig_integration) {
+        diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+    }
 
     // write the depfile
     if (options.depfile_path) |depfile_path| {
         var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| {
-            try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
+            try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
             std.os.exit(1);
         };
         defer depfile.close();
@@ -296,3 +319,390 @@ fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.A
         }
     }
 }
+
+const ErrorBundle = std.zig.ErrorBundle;
+const SourceMappings = @import("source_mapping.zig").SourceMappings;
+
+const ErrorHandler = union(enum) {
+    server: std.zig.Server,
+    tty: std.io.tty.Config,
+
+    pub fn emitCliDiagnostics(
+        self: *ErrorHandler,
+        allocator: std.mem.Allocator,
+        args: []const []const u8,
+        diagnostics: *cli.Diagnostics,
+    ) !void {
+        switch (self.*) {
+            .server => |*server| {
+                var error_bundle = try cliDiagnosticsToErrorBundle(allocator, diagnostics);
+                defer error_bundle.deinit(allocator);
+
+                try server.serveErrorBundle(error_bundle);
+            },
+            .tty => {
+                diagnostics.renderToStdErr(args, self.tty);
+            },
+        }
+    }
+
+    pub fn emitAroDiagnostics(
+        self: *ErrorHandler,
+        allocator: std.mem.Allocator,
+        fail_msg: []const u8,
+        comp: *aro.Compilation,
+    ) !void {
+        switch (self.*) {
+            .server => |*server| {
+                var error_bundle = try aroDiagnosticsToErrorBundle(allocator, fail_msg, comp);
+                defer error_bundle.deinit(allocator);
+
+                try server.serveErrorBundle(error_bundle);
+            },
+            .tty => {
+                // extra newline to separate this line from the aro errors
+                try renderErrorMessage(std.io.getStdErr().writer(), self.tty, .err, "{s}\n", .{fail_msg});
+                aro.Diagnostics.render(comp, self.tty);
+            },
+        }
+    }
+
+    pub fn emitDiagnostics(
+        self: *ErrorHandler,
+        allocator: std.mem.Allocator,
+        cwd: std.fs.Dir,
+        source: []const u8,
+        diagnostics: *Diagnostics,
+        mappings: SourceMappings,
+    ) !void {
+        switch (self.*) {
+            .server => |*server| {
+                var error_bundle = try diagnosticsToErrorBundle(allocator, source, diagnostics, mappings);
+                defer error_bundle.deinit(allocator);
+
+                try server.serveErrorBundle(error_bundle);
+            },
+            .tty => {
+                diagnostics.renderToStdErr(cwd, source, self.tty, mappings);
+            },
+        }
+    }
+
+    pub fn emitMessage(
+        self: *ErrorHandler,
+        allocator: std.mem.Allocator,
+        msg_type: @import("utils.zig").ErrorMessageType,
+        comptime format: []const u8,
+        args: anytype,
+    ) !void {
+        switch (self.*) {
+            .server => |*server| {
+                // only emit errors
+                if (msg_type != .err) return;
+
+                var error_bundle = try errorStringToErrorBundle(allocator, format, args);
+                defer error_bundle.deinit(allocator);
+
+                try server.serveErrorBundle(error_bundle);
+            },
+            .tty => {
+                try renderErrorMessage(std.io.getStdErr().writer(), self.tty, msg_type, format, args);
+            },
+        }
+    }
+};
+
+fn cliDiagnosticsToErrorBundle(
+    gpa: std.mem.Allocator,
+    diagnostics: *cli.Diagnostics,
+) !ErrorBundle {
+    @setCold(true);
+
+    var bundle: ErrorBundle.Wip = undefined;
+    try bundle.init(gpa);
+    errdefer bundle.deinit();
+
+    try bundle.addRootErrorMessage(.{
+        .msg = try bundle.addString("invalid command line option(s)"),
+    });
+
+    var cur_err: ?ErrorBundle.ErrorMessage = null;
+    var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
+    defer cur_notes.deinit(gpa);
+    for (diagnostics.errors.items) |err_details| {
+        switch (err_details.type) {
+            .err => {
+                if (cur_err) |err| {
+                    try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+                }
+                cur_err = .{
+                    .msg = try bundle.addString(err_details.msg.items),
+                };
+                cur_notes.clearRetainingCapacity();
+            },
+            .warning => cur_err = null,
+            .note => {
+                if (cur_err == null) continue;
+                cur_err.?.notes_len += 1;
+                try cur_notes.append(gpa, .{
+                    .msg = try bundle.addString(err_details.msg.items),
+                });
+            },
+        }
+    }
+    if (cur_err) |err| {
+        try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+    }
+
+    return try bundle.toOwnedBundle("");
+}
+
+fn diagnosticsToErrorBundle(
+    gpa: std.mem.Allocator,
+    source: []const u8,
+    diagnostics: *Diagnostics,
+    mappings: SourceMappings,
+) !ErrorBundle {
+    @setCold(true);
+
+    var bundle: ErrorBundle.Wip = undefined;
+    try bundle.init(gpa);
+    errdefer bundle.deinit();
+
+    var msg_buf: std.ArrayListUnmanaged(u8) = .{};
+    defer msg_buf.deinit(gpa);
+    var cur_err: ?ErrorBundle.ErrorMessage = null;
+    var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
+    defer cur_notes.deinit(gpa);
+    for (diagnostics.errors.items) |err_details| {
+        switch (err_details.type) {
+            .hint => continue,
+            // Clear the current error so that notes don't bleed into unassociated errors
+            .warning => {
+                cur_err = null;
+                continue;
+            },
+            .note => if (cur_err == null) continue,
+            .err => {},
+        }
+        const corresponding_span = mappings.getCorrespondingSpan(err_details.token.line_number).?;
+        const err_line = corresponding_span.start_line;
+        const err_filename = mappings.files.get(corresponding_span.filename_offset);
+
+        const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
+        // Treat tab stops as 1 column wide for error display purposes,
+        // and add one to get a 1-based column
+        const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
+
+        msg_buf.clearRetainingCapacity();
+        try err_details.render(msg_buf.writer(gpa), source, diagnostics.strings.items);
+
+        const src_loc = src_loc: {
+            var src_loc: ErrorBundle.SourceLocation = .{
+                .src_path = try bundle.addString(err_filename),
+                .line = @intCast(err_line - 1), // 1-based -> 0-based
+                .column = @intCast(column - 1), // 1-based -> 0-based
+                .span_start = 0,
+                .span_main = 0,
+                .span_end = 0,
+            };
+            if (err_details.print_source_line) {
+                const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
+                const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
+                src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);
+                src_loc.span_main = @intCast(visual_info.point_offset);
+                src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);
+                src_loc.source_line = try bundle.addString(source_line);
+            }
+            break :src_loc try bundle.addSourceLocation(src_loc);
+        };
+
+        switch (err_details.type) {
+            .err => {
+                if (cur_err) |err| {
+                    try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+                }
+                cur_err = .{
+                    .msg = try bundle.addString(msg_buf.items),
+                    .src_loc = src_loc,
+                };
+                cur_notes.clearRetainingCapacity();
+            },
+            .note => {
+                cur_err.?.notes_len += 1;
+                try cur_notes.append(gpa, .{
+                    .msg = try bundle.addString(msg_buf.items),
+                    .src_loc = src_loc,
+                });
+            },
+            .warning, .hint => unreachable,
+        }
+    }
+    if (cur_err) |err| {
+        try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+    }
+
+    return try bundle.toOwnedBundle("");
+}
+
+fn flushErrorMessageIntoBundle(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void {
+    try wip.addRootErrorMessage(msg);
+    const notes_start = try wip.reserveNotes(@intCast(notes.len));
+    for (notes_start.., notes) |i, note| {
+        wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note));
+    }
+}
+
+fn errorStringToErrorBundle(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) !ErrorBundle {
+    @setCold(true);
+    var bundle: ErrorBundle.Wip = undefined;
+    try bundle.init(allocator);
+    errdefer bundle.deinit();
+    try bundle.addRootErrorMessage(.{
+        .msg = try bundle.printString(format, args),
+    });
+    return try bundle.toOwnedBundle("");
+}
+
+fn aroDiagnosticsToErrorBundle(
+    gpa: std.mem.Allocator,
+    fail_msg: []const u8,
+    comp: *aro.Compilation,
+) !ErrorBundle {
+    @setCold(true);
+
+    var bundle: ErrorBundle.Wip = undefined;
+    try bundle.init(gpa);
+    errdefer bundle.deinit();
+
+    try bundle.addRootErrorMessage(.{
+        .msg = try bundle.addString(fail_msg),
+    });
+
+    var msg_writer = MsgWriter.init(gpa);
+    defer msg_writer.deinit();
+    var cur_err: ?ErrorBundle.ErrorMessage = null;
+    var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
+    defer cur_notes.deinit(gpa);
+    for (comp.diagnostics.list.items) |msg| {
+        switch (msg.kind) {
+            // Clear the current error so that notes don't bleed into unassociated errors
+            .off, .warning => {
+                cur_err = null;
+                continue;
+            },
+            .note => if (cur_err == null) continue,
+            .@"fatal error", .@"error" => {},
+            .default => unreachable,
+        }
+        msg_writer.resetRetainingCapacity();
+        aro.Diagnostics.renderMessage(comp, &msg_writer, msg);
+
+        const src_loc = src_loc: {
+            if (msg_writer.path) |src_path| {
+                var src_loc: ErrorBundle.SourceLocation = .{
+                    .src_path = try bundle.addString(src_path),
+                    .line = msg_writer.line - 1, // 1-based -> 0-based
+                    .column = msg_writer.col - 1, // 1-based -> 0-based
+                    .span_start = 0,
+                    .span_main = 0,
+                    .span_end = 0,
+                };
+                if (msg_writer.source_line) |source_line| {
+                    src_loc.span_start = msg_writer.span_main;
+                    src_loc.span_main = msg_writer.span_main;
+                    src_loc.span_end = msg_writer.span_main;
+                    src_loc.source_line = try bundle.addString(source_line);
+                }
+                break :src_loc try bundle.addSourceLocation(src_loc);
+            }
+            break :src_loc ErrorBundle.SourceLocationIndex.none;
+        };
+
+        switch (msg.kind) {
+            .@"fatal error", .@"error" => {
+                if (cur_err) |err| {
+                    try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+                }
+                cur_err = .{
+                    .msg = try bundle.addString(msg_writer.buf.items),
+                    .src_loc = src_loc,
+                };
+                cur_notes.clearRetainingCapacity();
+            },
+            .note => {
+                cur_err.?.notes_len += 1;
+                try cur_notes.append(gpa, .{
+                    .msg = try bundle.addString(msg_writer.buf.items),
+                    .src_loc = src_loc,
+                });
+            },
+            .off, .warning, .default => unreachable,
+        }
+    }
+    if (cur_err) |err| {
+        try flushErrorMessageIntoBundle(&bundle, err, cur_notes.items);
+    }
+
+    return try bundle.toOwnedBundle("");
+}
+
+// Similar to aro.Diagnostics.MsgWriter but:
+// - Writers to an ArrayList
+// - Only prints the message itself (no location, source line, error: prefix, etc)
+// - Keeps track of source path/line/col instead
+const MsgWriter = struct {
+    buf: std.ArrayList(u8),
+    path: ?[]const u8 = null,
+    // 1-indexed
+    line: u32 = undefined,
+    col: u32 = undefined,
+    source_line: ?[]const u8 = null,
+    span_main: u32 = undefined,
+
+    fn init(allocator: std.mem.Allocator) MsgWriter {
+        return .{
+            .buf = std.ArrayList(u8).init(allocator),
+        };
+    }
+
+    fn deinit(m: *MsgWriter) void {
+        m.buf.deinit();
+    }
+
+    fn resetRetainingCapacity(m: *MsgWriter) void {
+        m.buf.clearRetainingCapacity();
+        m.path = null;
+        m.source_line = null;
+    }
+
+    pub fn print(m: *MsgWriter, comptime fmt: []const u8, args: anytype) void {
+        m.buf.writer().print(fmt, args) catch {};
+    }
+
+    pub fn write(m: *MsgWriter, msg: []const u8) void {
+        m.buf.writer().writeAll(msg) catch {};
+    }
+
+    pub fn setColor(m: *MsgWriter, color: std.io.tty.Color) void {
+        _ = m;
+        _ = color;
+    }
+
+    pub fn location(m: *MsgWriter, path: []const u8, line: u32, col: u32) void {
+        m.path = path;
+        m.line = line;
+        m.col = col;
+    }
+
+    pub fn start(m: *MsgWriter, kind: aro.Diagnostics.Kind) void {
+        _ = m;
+        _ = kind;
+    }
+
+    pub fn end(m: *MsgWriter, maybe_line: ?[]const u8, col: u32, end_with_splice: bool) void {
+        _ = end_with_splice;
+        m.source_line = maybe_line;
+        m.span_main = col;
+    }
+};
lib/compiler/resinator/utils.zig
@@ -82,9 +82,11 @@ pub fn isNonAsciiDigit(c: u21) bool {
     };
 }
 
+pub const ErrorMessageType = enum { err, warning, note };
+
 /// Used for generic colored errors/warnings/notes, more context-specific error messages
 /// are handled elsewhere.
-pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: enum { err, warning, note }, comptime format: []const u8, args: anytype) !void {
+pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type: ErrorMessageType, comptime format: []const u8, args: anytype) !void {
     switch (msg_type) {
         .err => {
             try config.setColor(writer, .bold);
src/Compilation.zig
@@ -4921,6 +4921,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
         try argv.appendSlice(&.{
             self_exe_path,
             "rc",
+            "--zig-integration",
             "/:depfile",
             out_dep_path,
             "/:depfile-fmt",
@@ -4940,30 +4941,78 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
         try argv.appendSlice(rc_src.extra_flags);
         try argv.appendSlice(&.{ "--", rc_src.src_path, out_res_path });
 
-        var child = std.ChildProcess.init(argv.items, arena);
-        child.stdin_behavior = .Ignore;
-        child.stdout_behavior = .Ignore;
-        child.stderr_behavior = .Pipe;
+        {
+            var child = std.ChildProcess.init(argv.items, arena);
+            child.stdin_behavior = .Ignore;
+            child.stdout_behavior = .Pipe;
+            child.stderr_behavior = .Pipe;
 
-        try child.spawn();
+            child.spawn() catch |err| {
+                return comp.failWin32Resource(win32_resource, "unable to spawn {s} rc: {s}", .{ argv.items[0], @errorName(err) });
+            };
 
-        const stderr_reader = child.stderr.?.reader();
-        const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
-        const term = child.wait() catch |err| {
-            return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-        };
+            var poller = std.io.poll(comp.gpa, enum { stdout }, .{
+                .stdout = child.stdout.?,
+            });
+            defer poller.deinit();
 
-        switch (term) {
-            .Exited => |code| {
-                if (code != 0) {
-                    log.err("zig rc failed with stderr:\n{s}", .{stderr});
-                    return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
+            const stdout = poller.fifo(.stdout);
+
+            poll: while (true) {
+                while (stdout.readableLength() < @sizeOf(std.zig.Server.Message.Header)) {
+                    if (!(try poller.poll())) break :poll;
                 }
-            },
-            else => {
-                log.err("zig rc terminated with stderr:\n{s}", .{stderr});
-                return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
-            },
+                const header = stdout.reader().readStruct(std.zig.Server.Message.Header) catch unreachable;
+                while (stdout.readableLength() < header.bytes_len) {
+                    if (!(try poller.poll())) break :poll;
+                }
+                const body = stdout.readableSliceOfLen(header.bytes_len);
+
+                switch (header.tag) {
+                    // We expect exactly one ErrorBundle, and if any error_bundle header is
+                    // sent then it's a fatal error.
+                    .error_bundle => {
+                        const EbHdr = std.zig.Server.Message.ErrorBundle;
+                        const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
+                        const extra_bytes =
+                            body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
+                        const string_bytes =
+                            body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
+                        const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
+                        const extra_array = try comp.gpa.alloc(u32, unaligned_extra.len);
+                        @memcpy(extra_array, unaligned_extra);
+                        const error_bundle = .{
+                            .string_bytes = try comp.gpa.dupe(u8, string_bytes),
+                            .extra = extra_array,
+                        };
+                        return comp.failWin32ResourceWithOwnedBundle(win32_resource, error_bundle);
+                    },
+                    else => {}, // ignore other messages
+                }
+
+                stdout.discard(body.len);
+            }
+
+            // Just in case there's a failure that didn't send an ErrorBundle (e.g. an error return trace)
+            const stderr_reader = child.stderr.?.reader();
+            const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
+
+            const term = child.wait() catch |err| {
+                return comp.failWin32Resource(win32_resource, "unable to wait for {s} rc: {s}", .{ argv.items[0], @errorName(err) });
+            };
+
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        log.err("zig rc failed with stderr:\n{s}", .{stderr});
+                        return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
+                    }
+                },
+                else => {
+                    log.err("zig rc terminated with stderr:\n{s}", .{stderr});
+                    return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
+                },
+            }
         }
 
         // Read depfile and update cache manifest