diff --git a/build.zig b/build.zig index 731b825c07..a5f4cd5675 100644 --- a/build.zig +++ b/build.zig @@ -577,6 +577,10 @@ pub fn build(b: *std.Build) !void { } else { update_mingw_step.dependOn(&b.addFail("The -Dmingw-src=... option is required for this step").step); } + + const test_incremental_step = b.step("test-incremental", "Run the incremental compilation test cases"); + try tests.addIncrementalTests(b, test_incremental_step); + test_step.dependOn(test_incremental_step); } fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void { diff --git a/lib/std/io.zig b/lib/std/io.zig index 6455693d67..0a9b2bf867 100644 --- a/lib/std/io.zig +++ b/lib/std/io.zig @@ -442,6 +442,7 @@ pub fn poll( .overlapped = [1]windows.OVERLAPPED{ mem.zeroes(windows.OVERLAPPED), } ** enum_fields.len, + .small_bufs = undefined, .active = .{ .count = 0, .handles_buf = undefined, @@ -481,6 +482,7 @@ pub fn Poller(comptime StreamEnum: type) type { windows: if (is_windows) struct { first_read_done: bool, overlapped: [enum_fields.len]windows.OVERLAPPED, + small_bufs: [enum_fields.len][128]u8, active: struct { count: math.IntFittingRange(0, enum_fields.len), handles_buf: [enum_fields.len]windows.HANDLE, @@ -534,24 +536,31 @@ pub fn Poller(comptime StreamEnum: type) type { const bump_amt = 512; if (!self.windows.first_read_done) { - // Windows Async IO requires an initial call to ReadFile before waiting on the handle + var already_read_data = false; for (0..enum_fields.len) |i| { const handle = self.windows.active.handles_buf[i]; - switch (try windowsAsyncRead( + switch (try windowsAsyncReadToFifoAndQueueSmallRead( handle, &self.windows.overlapped[i], &self.fifos[i], + &self.windows.small_bufs[i], bump_amt, )) { - .pending => { + .populated, .empty => |state| { + if (state == .populated) already_read_data = true; self.windows.active.handles_buf[self.windows.active.count] = handle; self.windows.active.stream_map[self.windows.active.count] = @as(StreamEnum, @enumFromInt(i)); self.windows.active.count += 1; }, .closed => {}, // don't add to the wait_objects list + .closed_populated => { + // don't add to the wait_objects list, but we did already get data + already_read_data = true; + }, } } self.windows.first_read_done = true; + if (already_read_data) return true; } while (true) { @@ -576,32 +585,35 @@ pub fn Poller(comptime StreamEnum: type) type { const active_idx = status - windows.WAIT_OBJECT_0; - const handle = self.windows.active.handles_buf[active_idx]; const stream_idx = @intFromEnum(self.windows.active.stream_map[active_idx]); - var read_bytes: u32 = undefined; - if (0 == windows.kernel32.GetOverlappedResult( - handle, - &self.windows.overlapped[stream_idx], - &read_bytes, - 0, - )) switch (windows.GetLastError()) { - .BROKEN_PIPE => { + const handle = self.windows.active.handles_buf[active_idx]; + + const overlapped = &self.windows.overlapped[stream_idx]; + const stream_fifo = &self.fifos[stream_idx]; + const small_buf = &self.windows.small_bufs[stream_idx]; + + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => { self.windows.active.removeAt(active_idx); continue; }, - else => |err| return windows.unexpectedError(err), + .aborted => unreachable, }; + try stream_fifo.write(small_buf[0..num_bytes_read]); - self.fifos[stream_idx].update(read_bytes); - - switch (try windowsAsyncRead( + switch (try windowsAsyncReadToFifoAndQueueSmallRead( handle, - &self.windows.overlapped[stream_idx], - &self.fifos[stream_idx], + overlapped, + stream_fifo, + small_buf, bump_amt, )) { - .pending => {}, - .closed => self.windows.active.removeAt(active_idx), + .empty => {}, // irrelevant, we already got data from the small buffer + .populated => {}, + .closed, + .closed_populated, // identical, since we already got data from the small buffer + => self.windows.active.removeAt(active_idx), } return true; } @@ -654,25 +666,145 @@ pub fn Poller(comptime StreamEnum: type) type { }; } -fn windowsAsyncRead( +/// The `ReadFile` docuementation states that `lpNumberOfBytesRead` does not have a meaningful +/// result when using overlapped I/O, but also that it cannot be `null` on Windows 7. For +/// compatibility, we point it to this dummy variables, which we never otherwise access. +/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile +var win_dummy_bytes_read: u32 = undefined; + +/// Read as much data as possible from `handle` with `overlapped`, and write it to the FIFO. Before +/// returning, queue a read into `small_buf` so that `WaitForMultipleObjects` returns when more data +/// is available. `handle` must have no pending asynchronous operation. +fn windowsAsyncReadToFifoAndQueueSmallRead( handle: windows.HANDLE, overlapped: *windows.OVERLAPPED, fifo: *PollFifo, + small_buf: *[128]u8, bump_amt: usize, -) !enum { pending, closed } { +) !enum { empty, populated, closed_populated, closed } { + var read_any_data = false; while (true) { - const buf = try fifo.writableWithSize(bump_amt); - var read_bytes: u32 = undefined; - const read_result = windows.kernel32.ReadFile(handle, buf.ptr, math.cast(u32, buf.len) orelse math.maxInt(u32), &read_bytes, overlapped); - if (read_result == 0) return switch (windows.GetLastError()) { - .IO_PENDING => .pending, - .BROKEN_PIPE => .closed, - else => |err| windows.unexpectedError(err), + const fifo_read_pending = while (true) { + const buf = try fifo.writableWithSize(bump_amt); + const buf_len = math.cast(u32, buf.len) orelse math.maxInt(u32); + + if (0 == windows.kernel32.ReadFile( + handle, + buf.ptr, + buf_len, + &win_dummy_bytes_read, + overlapped, + )) switch (windows.GetLastError()) { + .IO_PENDING => break true, + .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, + else => |err| return windows.unexpectedError(err), + }; + + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => unreachable, + }; + + read_any_data = true; + fifo.update(num_bytes_read); + + if (num_bytes_read == buf_len) { + // We filled the buffer, so there's probably more data available. + continue; + } else { + // We didn't fill the buffer, so assume we're out of data. + // There is no pending read. + break false; + } }; - fifo.update(read_bytes); + + if (fifo_read_pending) cancel_read: { + // Cancel the pending read into the FIFO. + _ = windows.kernel32.CancelIo(handle); + + // We have to wait for the handle to be signalled, i.e. for the cancellation to complete. + switch (windows.kernel32.WaitForSingleObject(handle, windows.INFINITE)) { + windows.WAIT_OBJECT_0 => {}, + windows.WAIT_FAILED => return windows.unexpectedError(windows.GetLastError()), + else => unreachable, + } + + // If it completed before we canceled, make sure to tell the FIFO! + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, true)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => break :cancel_read, + }; + read_any_data = true; + fifo.update(num_bytes_read); + } + + // Try to queue the 1-byte read. + if (0 == windows.kernel32.ReadFile( + handle, + small_buf, + small_buf.len, + &win_dummy_bytes_read, + overlapped, + )) switch (windows.GetLastError()) { + .IO_PENDING => { + // 1-byte read pending as intended + return if (read_any_data) .populated else .empty; + }, + .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, + else => |err| return windows.unexpectedError(err), + }; + + // We got data back this time. Write it to the FIFO and run the main loop again. + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => unreachable, + }; + try fifo.write(small_buf[0..num_bytes_read]); + read_any_data = true; } } +/// Simple wrapper around `GetOverlappedResult` to determine the result of a `ReadFile` operation. +/// If `!allow_aborted`, then `aborted` is never returned (`OPERATION_ABORTED` is considered unexpected). +/// +/// The `ReadFile` documentation states that the number of bytes read by an overlapped `ReadFile` must be determined using `GetOverlappedResult`, even if the +/// operation immediately returns data: +/// "Use NULL for [lpNumberOfBytesRead] if this is an asynchronous operation to avoid potentially +/// erroneous results." +/// "If `hFile` was opened with `FILE_FLAG_OVERLAPPED`, the following conditions are in effect: [...] +/// The lpNumberOfBytesRead parameter should be set to NULL. Use the GetOverlappedResult function to +/// get the actual number of bytes read." +/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile +fn windowsGetReadResult( + handle: windows.HANDLE, + overlapped: *windows.OVERLAPPED, + allow_aborted: bool, +) !union(enum) { + success: u32, + closed, + aborted, +} { + var num_bytes_read: u32 = undefined; + if (0 == windows.kernel32.GetOverlappedResult( + handle, + overlapped, + &num_bytes_read, + 0, + )) switch (windows.GetLastError()) { + .BROKEN_PIPE => return .closed, + .OPERATION_ABORTED => |err| if (allow_aborted) { + return .aborted; + } else { + return windows.unexpectedError(err); + }, + else => |err| return windows.unexpectedError(err), + }; + return .{ .success = num_bytes_read }; +} + /// Given an enum, returns a struct with fields of that enum, each field /// representing an I/O stream for polling. pub fn PollFiles(comptime StreamEnum: type) type { diff --git a/test/incremental/add_decl b/test/incremental/add_decl index 1f9788f481..6b3a0dad84 100644 --- a/test/incremental/add_decl +++ b/test/incremental/add_decl @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/incremental/add_decl_namespaced b/test/incremental/add_decl_namespaced index 7063e3fa74..48ed5cfd2e 100644 --- a/test/incremental/add_decl_namespaced +++ b/test/incremental/add_decl_namespaced @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/incremental/delete_comptime_decls b/test/incremental/delete_comptime_decls index 54411797cf..03ddc68128 100644 --- a/test/incremental/delete_comptime_decls +++ b/test/incremental/delete_comptime_decls @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig pub fn main() void {} diff --git a/test/incremental/hello b/test/incremental/hello index 9419b3d186..3526b47f7c 100644 --- a/test/incremental/hello +++ b/test/incremental/hello @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/incremental/modify_inline_fn b/test/incremental/modify_inline_fn index 69ab1f429a..cd5361fb17 100644 --- a/test/incremental/modify_inline_fn +++ b/test/incremental/modify_inline_fn @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/incremental/move_src b/test/incremental/move_src index 8a3a24a269..908135485c 100644 --- a/test/incremental/move_src +++ b/test/incremental/move_src @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/incremental/remove_enum_field b/test/incremental/remove_enum_field index c163e5e168..3a882ae0f1 100644 --- a/test/incremental/remove_enum_field +++ b/test/incremental/remove_enum_field @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const MyEnum = enum(u8) { diff --git a/test/incremental/type_becomes_comptime_only b/test/incremental/type_becomes_comptime_only index 3878e0c61b..2da31ec5f2 100644 --- a/test/incremental/type_becomes_comptime_only +++ b/test/incremental/type_becomes_comptime_only @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const SomeType = u32; diff --git a/test/incremental/unreferenced_error b/test/incremental/unreferenced_error index 2031975986..3dfe0ab758 100644 --- a/test/incremental/unreferenced_error +++ b/test/incremental/unreferenced_error @@ -1,5 +1,6 @@ #target=x86_64-linux-selfhosted #target=x86_64-linux-cbe +#target=x86_64-windows-cbe #update=initial version #file=main.zig const std = @import("std"); diff --git a/test/tests.zig b/test/tests.zig index 1e98a48489..017f9478a9 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -1509,3 +1509,31 @@ pub fn addDebuggerTests(b: *std.Build, options: DebuggerContext.Options) ?*Step }); return step; } + +pub fn addIncrementalTests(b: *std.Build, test_step: *Step) !void { + const incr_check = b.addExecutable(.{ + .name = "incr-check", + .root_source_file = b.path("tools/incr-check.zig"), + .target = b.graph.host, + .optimize = .Debug, + }); + + var dir = try b.build_root.handle.openDir("test/incremental", .{ .iterate = true }); + defer dir.close(); + + var it = try dir.walk(b.graph.arena); + while (try it.next()) |entry| { + if (entry.kind != .file) continue; + + const run = b.addRunArtifact(incr_check); + run.setName(b.fmt("incr-check '{s}'", .{entry.basename})); + + run.addArg(b.graph.zig_exe); + run.addFileArg(b.path("test/incremental/").path(b, entry.path)); + run.addArgs(&.{ "--zig-lib-dir", b.fmt("{}", .{b.graph.zig_lib_directory}) }); + + run.addCheck(.{ .expect_term = .{ .Exited = 0 } }); + + test_step.dependOn(&run.step); + } +} diff --git a/tools/incr-check.zig b/tools/incr-check.zig index 047b129276..b5329f134e 100644 --- a/tools/incr-check.zig +++ b/tools/incr-check.zig @@ -1,11 +1,12 @@ const std = @import("std"); -const fatal = std.process.fatal; const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; -const usage = "usage: incr-check [--zig-lib-dir lib] [--debug-zcu] [--debug-link] [--zig-cc-binary /path/to/zig]"; +const usage = "usage: incr-check [--zig-lib-dir lib] [--debug-zcu] [--debug-link] [--preserve-tmp] [--zig-cc-binary /path/to/zig]"; pub fn main() !void { + const fatal = std.process.fatal; + var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena_instance.deinit(); const arena = arena_instance.allocator(); @@ -16,6 +17,7 @@ pub fn main() !void { var opt_cc_zig: ?[]const u8 = null; var debug_zcu = false; var debug_link = false; + var preserve_tmp = false; var arg_it = try std.process.argsWithAllocator(arena); _ = arg_it.skip(); @@ -27,6 +29,8 @@ pub fn main() !void { debug_zcu = true; } else if (std.mem.eql(u8, arg, "--debug-link")) { debug_link = true; + } else if (std.mem.eql(u8, arg, "--preserve-tmp")) { + preserve_tmp = true; } else if (std.mem.eql(u8, arg, "--zig-cc-binary")) { opt_cc_zig = arg_it.next() orelse fatal("expect arg after '--zig-cc-binary'\n{s}", .{usage}); } else { @@ -48,15 +52,29 @@ pub fn main() !void { const input_file_bytes = try std.fs.cwd().readFileAlloc(arena, input_file_name, std.math.maxInt(u32)); const case = try Case.parse(arena, input_file_bytes); + // Check now: if there are any targets using the `cbe` backend, we need the lib dir. + if (opt_lib_dir == null) { + for (case.targets) |target| { + if (target.backend == .cbe) { + fatal("'--zig-lib-dir' requried when using backend 'cbe'", .{}); + } + } + } + const prog_node = std.Progress.start(.{}); defer prog_node.end(); const rand_int = std.crypto.random.int(u64); const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int); - const tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{}); - - const child_prog_node = prog_node.start("zig build-exe", 0); - defer child_prog_node.end(); + var tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{}); + defer { + tmp_dir.close(); + if (!preserve_tmp) { + std.fs.cwd().deleteTree(tmp_dir_path) catch |err| { + std.log.warn("failed to delete tree '{s}': {s}", .{ tmp_dir_path, @errorName(err) }); + }; + } + } // Convert paths to be relative to the cwd of the subprocess. const resolved_zig_exe = try std.fs.path.relative(arena, tmp_dir_path, zig_exe); @@ -65,10 +83,21 @@ pub fn main() !void { else null; + const host = try std.zig.system.resolveTargetQuery(.{}); + const debug_log_verbose = debug_zcu or debug_link; for (case.targets) |target| { - std.log.scoped(.status).info("target: '{s}-{s}'", .{ target.query, @tagName(target.backend) }); + const target_prog_node = node: { + var name_buf: [std.Progress.Node.max_name_len]u8 = undefined; + const name = std.fmt.bufPrint(&name_buf, "{s}-{s}", .{ target.query, @tagName(target.backend) }) catch &name_buf; + break :node prog_node.start(name, case.updates.len); + }; + defer target_prog_node.end(); + + if (debug_log_verbose) { + std.log.scoped(.status).info("target: '{s}-{s}'", .{ target.query, @tagName(target.backend) }); + } var child_args: std.ArrayListUnmanaged([]const u8) = .empty; try child_args.appendSlice(arena, &.{ @@ -81,7 +110,7 @@ pub fn main() !void { "--cache-dir", ".local-cache", "--global-cache-dir", - ".global_cache", + ".global-cache", "--listen=-", }); if (opt_resolved_lib_dir) |resolved_lib_dir| { @@ -100,11 +129,14 @@ pub fn main() !void { try child_args.appendSlice(arena, &.{ "--debug-log", "link", "--debug-log", "link_state", "--debug-log", "link_relocs" }); } + const zig_prog_node = target_prog_node.start("zig build-exe", 0); + defer zig_prog_node.end(); + var child = std.process.Child.init(child_args.items, arena); child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; - child.progress_node = child_prog_node; + child.progress_node = zig_prog_node; child.cwd_dir = tmp_dir; child.cwd = tmp_dir_path; @@ -121,7 +153,7 @@ pub fn main() !void { "-target", target.query, "-I", - opt_resolved_lib_dir orelse fatal("'--zig-lib-dir' required when using backend 'cbe'", .{}), + opt_resolved_lib_dir.?, // verified earlier "-o", }); } @@ -129,11 +161,13 @@ pub fn main() !void { var eval: Eval = .{ .arena = arena, .case = case, + .host = host, .target = target, .tmp_dir = tmp_dir, .tmp_dir_path = tmp_dir_path, .child = &child, .allow_stderr = debug_log_verbose, + .preserve_tmp_on_fatal = preserve_tmp, .cc_child_args = &cc_child_args, }; @@ -146,7 +180,7 @@ pub fn main() !void { defer poller.deinit(); for (case.updates) |update| { - var update_node = prog_node.start(update.name, 0); + var update_node = target_prog_node.start(update.name, 0); defer update_node.end(); if (debug_log_verbose) { @@ -160,18 +194,20 @@ pub fn main() !void { try eval.end(&poller); - waitChild(&child); + waitChild(&child, &eval); } } const Eval = struct { arena: Allocator, + host: std.Target, case: Case, target: Case.Target, tmp_dir: std.fs.Dir, tmp_dir_path: []const u8, child: *std.process.Child, allow_stderr: bool, + preserve_tmp_on_fatal: bool, /// When `target.backend == .cbe`, this contains the first few arguments to `zig cc` to build the generated binary. /// The arguments `out.c in.c` must be appended before spawning the subprocess. cc_child_args: *std.ArrayListUnmanaged([]const u8), @@ -186,12 +222,12 @@ const Eval = struct { .sub_path = full_contents.name, .data = full_contents.bytes, }) catch |err| { - fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) }); + eval.fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) }); }; } for (update.deletes) |doomed_name| { eval.tmp_dir.deleteFile(doomed_name) catch |err| { - fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) }); + eval.fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) }); }; } } @@ -233,7 +269,7 @@ const Eval = struct { if (eval.allow_stderr) { std.log.info("error_bundle included stderr:\n{s}", .{stderr_data}); } else { - fatal("error_bundle included unexpected stderr:\n{s}", .{stderr_data}); + eval.fatal("error_bundle included unexpected stderr:\n{s}", .{stderr_data}); } } if (result_error_bundle.errorMessageCount() != 0) { @@ -252,7 +288,7 @@ const Eval = struct { if (eval.allow_stderr) { std.log.info("emit_digest included stderr:\n{s}", .{stderr_data}); } else { - fatal("emit_digest included unexpected stderr:\n{s}", .{stderr_data}); + eval.fatal("emit_digest included unexpected stderr:\n{s}", .{stderr_data}); } } @@ -268,14 +304,7 @@ const Eval = struct { const name = std.fs.path.stem(std.fs.path.basename(eval.case.root_source_file)); const bin_name = try std.zig.binNameAlloc(arena, .{ .root_name = name, - .target = try std.zig.system.resolveTargetQuery(try std.Build.parseTargetQuery(.{ - .arch_os_abi = eval.target.query, - .object_format = switch (eval.target.backend) { - .sema => unreachable, - .selfhosted, .llvm => null, - .cbe => "c", - }, - })), + .target = eval.target.resolved, .output_mode = .Exe, }); const bin_path = try std.fs.path.join(arena, &.{ result_dir, bin_name }); @@ -296,16 +325,15 @@ const Eval = struct { if (eval.allow_stderr) { std.log.info("update '{s}' included stderr:\n{s}", .{ update.name, stderr_data }); } else { - fatal("update '{s}' failed:\n{s}", .{ update.name, stderr_data }); + eval.fatal("update '{s}' failed:\n{s}", .{ update.name, stderr_data }); } } - waitChild(eval.child); - fatal("update '{s}': compiler failed to send error_bundle or emit_bin_path", .{update.name}); + waitChild(eval.child, eval); + eval.fatal("update '{s}': compiler failed to send error_bundle or emit_bin_path", .{update.name}); } fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void { - _ = eval; switch (update.outcome) { .unknown => return, .compile_errors => |expected_errors| { @@ -317,7 +345,7 @@ const Eval = struct { .stdout, .exit_code => { const color: std.zig.Color = .auto; error_bundle.renderToStdErr(color.renderOptions()); - fatal("update '{s}': unexpected compile errors", .{update.name}); + eval.fatal("update '{s}': unexpected compile errors", .{update.name}); }, } } @@ -325,7 +353,7 @@ const Eval = struct { fn checkSuccessOutcome(eval: *Eval, update: Case.Update, opt_emitted_path: ?[]const u8, prog_node: std.Progress.Node) !void { switch (update.outcome) { .unknown => return, - .compile_errors => fatal("expected compile errors but compilation incorrectly succeeded", .{}), + .compile_errors => eval.fatal("expected compile errors but compilation incorrectly succeeded", .{}), .stdout, .exit_code => {}, } const emitted_path = opt_emitted_path orelse { @@ -344,27 +372,73 @@ const Eval = struct { }, }; + var argv_buf: [2][]const u8 = undefined; + const argv: []const []const u8, const is_foreign: bool = switch (std.zig.system.getExternalExecutor( + eval.host, + &eval.target.resolved, + .{ .link_libc = eval.target.backend == .cbe }, + )) { + .bad_dl, .bad_os_or_cpu => { + // This binary cannot be executed on this host. + if (eval.allow_stderr) { + std.log.warn("skipping execution because host '{s}' cannot execute binaries for foreign target '{s}'", .{ + try eval.host.zigTriple(eval.arena), + try eval.target.resolved.zigTriple(eval.arena), + }); + } + return; + }, + .native, .rosetta => argv: { + argv_buf[0] = binary_path; + break :argv .{ argv_buf[0..1], false }; + }, + .qemu, .wine, .wasmtime, .darling => |executor_cmd| argv: { + argv_buf[0] = executor_cmd; + argv_buf[1] = binary_path; + break :argv .{ argv_buf[0..2], true }; + }, + }; + + const run_prog_node = prog_node.start("run generated executable", 0); + defer run_prog_node.end(); + const result = std.process.Child.run(.{ .allocator = eval.arena, - .argv = &.{binary_path}, + .argv = argv, .cwd_dir = eval.tmp_dir, .cwd = eval.tmp_dir_path, }) catch |err| { - fatal("update '{s}': failed to run the generated executable '{s}': {s}", .{ + if (is_foreign) { + // Chances are the foreign executor isn't available. Skip this evaluation. + if (eval.allow_stderr) { + std.log.warn("update '{s}': skipping execution of '{s}' via executor for foreign target '{s}': {s}", .{ + update.name, + binary_path, + try eval.target.resolved.zigTriple(eval.arena), + @errorName(err), + }); + } + return; + } + eval.fatal("update '{s}': failed to run the generated executable '{s}': {s}", .{ update.name, binary_path, @errorName(err), }); }; - if (result.stderr.len != 0) { + + // Some executors (looking at you, Wine) like throwing some stderr in, just for fun. + // Therefore, we'll ignore stderr when using a foreign executor. + if (!is_foreign and result.stderr.len != 0) { std.log.err("update '{s}': generated executable '{s}' had unexpected stderr:\n{s}", .{ update.name, binary_path, result.stderr, }); } + switch (result.term) { .Exited => |code| switch (update.outcome) { .unknown, .compile_errors => unreachable, .stdout => |expected_stdout| { if (code != 0) { - fatal("update '{s}': generated executable '{s}' failed with code {d}", .{ + eval.fatal("update '{s}': generated executable '{s}' failed with code {d}", .{ update.name, binary_path, code, }); } @@ -373,12 +447,13 @@ const Eval = struct { .exit_code => |expected_code| try std.testing.expectEqual(expected_code, result.term.Exited), }, .Signal, .Stopped, .Unknown => { - fatal("update '{s}': generated executable '{s}' terminated unexpectedly", .{ + eval.fatal("update '{s}': generated executable '{s}' terminated unexpectedly", .{ update.name, binary_path, }); }, } - if (result.stderr.len != 0) std.process.exit(1); + + if (!is_foreign and result.stderr.len != 0) std.process.exit(1); } fn requestUpdate(eval: *Eval) !void { @@ -390,7 +465,7 @@ const Eval = struct { } fn end(eval: *Eval, poller: *Poller) !void { - requestExit(eval.child); + requestExit(eval.child, eval); const Header = std.zig.Server.Message.Header; const stdout = poller.fifo(.stdout); @@ -410,7 +485,7 @@ const Eval = struct { if (stderr.readableLength() > 0) { const stderr_data = try stderr.toOwnedSlice(); - fatal("unexpected stderr:\n{s}", .{stderr_data}); + eval.fatal("unexpected stderr:\n{s}", .{stderr_data}); } } @@ -430,7 +505,7 @@ const Eval = struct { .cwd = eval.tmp_dir_path, .progress_node = child_prog_node, }) catch |err| { - fatal("update '{s}': failed to spawn zig cc for '{s}': {s}", .{ + eval.fatal("update '{s}': failed to spawn zig cc for '{s}': {s}", .{ update.name, c_path, @errorName(err), }); }; @@ -441,7 +516,7 @@ const Eval = struct { update.name, result.stderr, }); } - fatal("update '{s}': zig cc for '{s}' failed with code {d}", .{ + eval.fatal("update '{s}': zig cc for '{s}' failed with code {d}", .{ update.name, c_path, code, }); }, @@ -451,12 +526,22 @@ const Eval = struct { update.name, result.stderr, }); } - fatal("update '{s}': zig cc for '{s}' terminated unexpectedly", .{ + eval.fatal("update '{s}': zig cc for '{s}' terminated unexpectedly", .{ update.name, c_path, }); }, } } + + fn fatal(eval: *Eval, comptime fmt: []const u8, args: anytype) noreturn { + eval.tmp_dir.close(); + if (!eval.preserve_tmp_on_fatal) { + std.fs.cwd().deleteTree(eval.tmp_dir_path) catch |err| { + std.log.warn("failed to delete tree '{s}': {s}", .{ eval.tmp_dir_path, @errorName(err) }); + }; + } + std.process.fatal(fmt, args); + } }; const Case = struct { @@ -466,6 +551,7 @@ const Case = struct { const Target = struct { query: []const u8, + resolved: std.Target, backend: Backend, const Backend = enum { /// Run semantic analysis only. Runtime output will not be tested, but we still verify @@ -511,6 +597,8 @@ const Case = struct { }; fn parse(arena: Allocator, bytes: []const u8) !Case { + const fatal = std.process.fatal; + var targets: std.ArrayListUnmanaged(Target) = .empty; var updates: std.ArrayListUnmanaged(Update) = .empty; var changes: std.ArrayListUnmanaged(FullContents) = .empty; @@ -521,18 +609,32 @@ const Case = struct { if (std.mem.startsWith(u8, line, "#")) { var line_it = std.mem.splitScalar(u8, line, '='); const key = line_it.first()[1..]; - const val = line_it.rest(); + const val = std.mem.trimRight(u8, line_it.rest(), "\r"); // windows moment if (val.len == 0) { fatal("line {d}: missing value", .{line_n}); } else if (std.mem.eql(u8, key, "target")) { const split_idx = std.mem.lastIndexOfScalar(u8, val, '-') orelse fatal("line {d}: target does not include backend", .{line_n}); + const query = val[0..split_idx]; + const backend_str = val[split_idx + 1 ..]; const backend: Target.Backend = std.meta.stringToEnum(Target.Backend, backend_str) orelse fatal("line {d}: invalid backend '{s}'", .{ line_n, backend_str }); + + const parsed_query = std.Build.parseTargetQuery(.{ + .arch_os_abi = query, + .object_format = switch (backend) { + .sema, .selfhosted, .llvm => null, + .cbe => "c", + }, + }) catch fatal("line {d}: invalid target query '{s}'", .{ line_n, query }); + + const resolved = try std.zig.system.resolveTargetQuery(parsed_query); + try targets.append(arena, .{ .query = query, + .resolved = resolved, .backend = backend, }); } else if (std.mem.eql(u8, key, "update")) { @@ -603,7 +705,7 @@ const Case = struct { } }; -fn requestExit(child: *std.process.Child) void { +fn requestExit(child: *std.process.Child, eval: *Eval) void { if (child.stdin == null) return; const header: std.zig.Client.Message.Header = .{ @@ -612,7 +714,7 @@ fn requestExit(child: *std.process.Child) void { }; child.stdin.?.writeAll(std.mem.asBytes(&header)) catch |err| switch (err) { error.BrokenPipe => {}, - else => fatal("failed to send exit: {s}", .{@errorName(err)}), + else => eval.fatal("failed to send exit: {s}", .{@errorName(err)}), }; // Send EOF to stdin. @@ -620,11 +722,11 @@ fn requestExit(child: *std.process.Child) void { child.stdin = null; } -fn waitChild(child: *std.process.Child) void { - requestExit(child); - const term = child.wait() catch |err| fatal("child process failed: {s}", .{@errorName(err)}); +fn waitChild(child: *std.process.Child, eval: *Eval) void { + requestExit(child, eval); + const term = child.wait() catch |err| eval.fatal("child process failed: {s}", .{@errorName(err)}); switch (term) { - .Exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}), - .Signal, .Stopped, .Unknown => fatal("compiler terminated unexpectedly", .{}), + .Exited => |code| if (code != 0) eval.fatal("compiler failed with code {d}", .{code}), + .Signal, .Stopped, .Unknown => eval.fatal("compiler terminated unexpectedly", .{}), } }