From b6f68ca4ba477213ea89bc0ac6c9422f78116692 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Tue, 22 Sep 2026 13:57:40 -0400 Subject: [PATCH] Runner: implement cbe backend --- build.zig | 2 +- lib/compiler/Maker/Step.zig | 18 +- lib/compiler/Maker/Step/Compile.zig | 11 +- lib/compiler/Maker/Step/Run.zig | 7 +- lib/std/lang.zig | 2 +- lib/std/zig/Server.zig | 30 +- lib/std/zig/system.zig | 56 +- src/main.zig | 68 +- test/src/Cases.zig | 1 + test/src/Runner.zig | 1147 +++++++++++++++++++++++++++ test/src/runner.zig | 918 --------------------- test/tests.zig | 12 +- tools/doctest.zig | 1 + 13 files changed, 1269 insertions(+), 1004 deletions(-) create mode 100644 test/src/Runner.zig delete mode 100644 test/src/runner.zig diff --git a/build.zig b/build.zig index bc09ce6393c0598245d43f16e07bd5fbf72d72b7..aad5a316a9ab7036a4a283582253eb36dfa055c0 100644 --- a/build.zig +++ b/build.zig @@ -791,7 +791,7 @@ pub fn build(b: *std.Build) !void { const runner = b.addExecutable(.{ .name = "runner", .root_module = b.createModule(.{ - .root_source_file = b.path("test/src/runner.zig"), + .root_source_file = b.path("test/src/Runner.zig"), .target = b.graph.host, }), }); diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig index 97537845a7d52f2e35cbaddd999c40f4fdc6da0e..ebf2a4672892d24cf96db6947d9a04e07da1e69e 100644 --- a/lib/compiler/Maker/Step.zig +++ b/lib/compiler/Maker/Step.zig @@ -624,6 +624,7 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi else => |e| return e, }; const body = client.in.take(header.bytes_len) catch unreachable; + var body_r: std.Io.Reader = .fixed(body); switch (header.tag) { .zig_version => { @@ -635,16 +636,22 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi ); } }, + .config => switch (s.extended) { + else => unreachable, + .compile => |*compile| compile.config = + body_r.takeStruct(std.zig.Server.Message.Config, .little) catch unreachable, + .translate_c => {}, + }, .error_bundle => { s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); // This message indicates the end of the update. if (watch) break; }, .emit_digest => { - const EmitDigest = std.zig.Server.Message.EmitDigest; - const emit_digest: *align(1) const EmitDigest = @ptrCast(body); + const emit_digest = body_r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable; + const digest = body_r.takeArray(Cache.bin_digest_len) catch unreachable; s.result_cached = emit_digest.flags.cache_hit; - result = .{ .bin = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len].* }; + result = .{ .bin = digest.* }; }, .file_system_inputs => { clearWatchInputs(s, maker); @@ -704,8 +711,7 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi } }, .time_report => if (maker.web_server) |ws| { - const TimeReport = std.zig.Server.Message.TimeReport; - const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); + const tr = body_r.takeStruct(std.zig.Server.Message.TimeReport, .little) catch unreachable; ws.updateTimeReportCompile(.{ .compile_step = step_index, .use_llvm = tr.flags.use_llvm, @@ -714,7 +720,7 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi .llvm_pass_timings_len = tr.llvm_pass_timings_len, .files_len = tr.files_len, .decls_len = tr.decls_len, - .trailing = body[@sizeOf(TimeReport)..], + .trailing = body_r.buffered(), }); }, else => {}, // ignore other messages diff --git a/lib/compiler/Maker/Step/Compile.zig b/lib/compiler/Maker/Step/Compile.zig index b65019ed6e41890671ab9ea232e0df6317e13dec..1c8cdc9db4c94aae76542ef3e43d36f1fc6c190c 100644 --- a/lib/compiler/Maker/Step/Compile.zig +++ b/lib/compiler/Maker/Step/Compile.zig @@ -21,7 +21,7 @@ zig_process: ?*Step.ZigProcess = null, /// Populated by InstallArtifact. installed_path: ?Path = null, /// Populated by `make`, used by `Run`. -is_linking_libc: bool = false, +config: ?std.zig.Server.Message.Config = null, pub fn make( compile: *Compile, @@ -29,6 +29,7 @@ pub fn make( maker: *Maker, progress_node: std.Progress.Node, ) Step.ExtendedMakeError!void { + _ = compile; // only accessed by `Step.evalZigProcess`. const graph = maker.graph; const gpa = maker.gpa; const conf = &maker.scanned_config.configuration; @@ -42,7 +43,7 @@ pub fn make( var argv: std.ArrayList([]const u8) = .empty; defer argv.deinit(gpa); - try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, false); + try lowerZigArgs(arena, compile_index, maker, progress_node, &argv, false); const incremental = conf_comp.flags4.incremental.toBool() orelse graph.incremental == true; @@ -158,7 +159,6 @@ const ModuleListContext = struct { fn lowerZigArgs( arena: Allocator, - compile: *Compile, compile_index: Configuration.Step.Index, maker: *Maker, progress_node: std.Progress.Node, @@ -575,8 +575,6 @@ fn lowerZigArgs( try zig_args.ensureUnusedCapacity(gpa, 2); if (is_linking_libcpp) zig_args.appendAssumeCapacity("-lc++"); if (is_linking_libc) zig_args.appendAssumeCapacity("-lc"); - - compile.is_linking_libc = is_linking_libc; } if (conf_comp.win32_manifest.value) |manifest_file| { @@ -944,6 +942,7 @@ pub fn rebuildInFuzzMode( compile_index: Configuration.Step.Index, progress_node: std.Progress.Node, ) !Step.OptCacheDigest { + _ = compile; const gpa = maker.gpa; const step = maker.stepByIndex(compile_index); @@ -962,7 +961,7 @@ pub fn rebuildInFuzzMode( var argv: std.ArrayList([]const u8) = .empty; defer argv.deinit(gpa); - try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, true); + try lowerZigArgs(arena, compile_index, maker, progress_node, &argv, true); return Step.evalZigProcess(compile_index, maker, argv.items, progress_node, false); } diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 8c4d7b5572999568fd7799d208fedcbf1291d33f..c33cdeb2936e18c0e0031a2d78da9634e6f0a81e 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -2212,14 +2212,14 @@ fn runCommand( const root_module = producer.root_module.get(conf); const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf); const root_target = root_module_target.unwrapTarget(conf); - const link_libc = maker.stepByIndex(producer_index).extended.compile.is_linking_libc; + const config = maker.stepByIndex(producer_index).extended.compile.config.?; const host: std.Target = std.zig.system.resolveTargetQuery(io, .{}) catch |he| switch (he) { error.Canceled => |e| return e, else => builtin.target, }; - const need_cross_libc = link_libc and root_target.os.tag == .linux and + const need_cross_libc = config.flags.link_libc and root_target.os.tag == .linux and switch (producer.flags2.linkage) { .static => false, .dynamic => true, @@ -2229,7 +2229,8 @@ fn runCommand( .host_cpu_arch = host.cpu.arch, .host_os_tag = host.os.tag, .qemu_fixes_dl = need_cross_libc and graph.libc_runtimes_dir != null, - .link_libc = link_libc, + .link_mode = config.flags.link_mode, + .link_libc = config.flags.link_libc, })) { .native, .rosetta => { if (allow_skip) return error.MakeSkipped; diff --git a/lib/std/lang.zig b/lib/std/lang.zig index fbe36760bca652bf37909ed033be562cdb2da489..b8dba4354d5d4dfa4fe05bf7786806e3e01dddbd 100644 --- a/lib/std/lang.zig +++ b/lib/std/lang.zig @@ -971,7 +971,7 @@ pub const Signedness = enum(u1) { /// This data structure is used by the Zig language code generation and /// therefore must be kept in sync with the compiler implementation. -pub const OutputMode = enum { +pub const OutputMode = enum(u2) { Exe, Lib, Obj, diff --git a/lib/std/zig/Server.zig b/lib/std/zig/Server.zig index 6175821dbd1f4f4263f01b998672b2c9f5ef8fde..7fb59ac5a45a762d5bfc38f979e883f765a01c9c 100644 --- a/lib/std/zig/Server.zig +++ b/lib/std/zig/Server.zig @@ -78,6 +78,8 @@ pub const Message = struct { /// - root_dir: InputDir, /// - sub_path: [:0]u8, discovered_inputs, + /// Body is a Config. + config, /// The first message sent by the server over the build system protocol. /// Body is a `Handshake`. @@ -116,7 +118,7 @@ pub const Message = struct { pub const Flags = packed struct(u32) { file_system_watch_supported: bool, - _: u31 = 0, + unused: u31 = 0, }; }; @@ -202,7 +204,7 @@ pub const Message = struct { flags: Flags, pub const Flags = packed struct(u32) { use_llvm: bool, - _: u31 = 0, + unused: u31 = 0, }; }; @@ -224,6 +226,21 @@ pub const Message = struct { /// Numbered in the order that `.input_dir`s appear in the `.args` message. _, }; + + /// Properties of the output binary target that are decided by the compiler. + pub const Config = extern struct { + flags: Flags, + + pub const Flags = packed struct(u8) { + output_mode: std.lang.OutputMode, + link_mode: std.lang.LinkMode, + link_libc: bool, + link_libcpp: bool, + link_libunwind: bool, + pie: bool, + unused: u1 = 0, + }; + }; }; pub fn receiveMessage(s: *Server) !InMessage.Header { @@ -377,3 +394,12 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void { try s.out.writeAll(test_metadata.string_bytes); try s.out.flush(); } + +pub fn serveConfig(s: *Server, config: OutMessage.Config) !void { + try s.serveMessageHeader(.{ + .tag = .config, + .bytes_len = @intCast(@sizeOf(OutMessage.Config)), + }); + try s.out.writeStruct(config, .little); + try s.out.flush(); +} diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 857ae1d2ff1c35feac45eb5b2f40cca4986d74f3..41de483284a972e2f6e87668b739fba5592415ae 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -33,13 +33,9 @@ pub const Executor = union(enum) { pub const GetExternalExecutorOptions = struct { host_cpu_arch: std.Target.Cpu.Arch, host_os_tag: std.Target.Os.Tag, - allow_darling: bool = true, - allow_qemu: bool = true, - allow_rosetta: bool = true, - allow_wasmtime: bool = true, - allow_wine: bool = true, qemu_fixes_dl: bool = false, - link_libc: bool = false, + link_mode: std.lang.LinkMode, + link_libc: bool, }; /// Return whether or not the given host is capable of running executables of @@ -71,7 +67,7 @@ pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExt var bad_result: Executor = .bad_os_or_cpu; if (os_match and cpu_ok) native: { - if (options.link_libc) { + if (options.link_libc and options.link_mode == .dynamic) { if (candidate.dynamic_linker.get()) |candidate_dl| { Io.Dir.cwd().access(io, candidate_dl, .{}) catch { bad_result = .{ .bad_dl = candidate_dl }; @@ -84,9 +80,7 @@ pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExt // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2 // to emulate the foreign architecture. - if (options.allow_rosetta and os_match and - (host_os_tag == .maccatalyst or host_os_tag == .macos) and host_cpu_arch == .aarch64) - { + if (os_match and (host_os_tag == .maccatalyst or host_os_tag == .macos) and host_cpu_arch == .aarch64) { switch (candidate.cpu.arch) { .x86_64 => return .rosetta, else => return bad_result, @@ -94,7 +88,7 @@ pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExt } // If the OS matches, we can use QEMU to emulate a foreign architecture. - if (options.allow_qemu and os_match and (!cpu_ok or options.qemu_fixes_dl)) { + if (os_match and (!cpu_ok or options.qemu_fixes_dl)) { return switch (candidate.cpu.arch) { inline .aarch64, .arm, @@ -167,38 +161,32 @@ pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExt }; } - if (options.allow_wasmtime and candidate.cpu.arch.isWasm()) { + if (candidate.cpu.arch.isWasm()) { return .{ .wasmtime = "wasmtime" }; } switch (candidate.os.tag) { .windows => { - if (options.allow_wine) { - const wine_supported = switch (candidate.cpu.arch) { - .thumb => switch (host_cpu_arch) { - .arm, .thumb, .aarch64 => true, - else => false, - }, - .aarch64 => host_cpu_arch == .aarch64, - .x86 => host_cpu_arch.isX86(), - .x86_64 => host_cpu_arch == .x86_64, + const wine_supported = switch (candidate.cpu.arch) { + .thumb => switch (host_cpu_arch) { + .arm, .thumb, .aarch64 => true, else => false, - }; - return if (wine_supported) .{ .wine = "wine" } else bad_result; - } - return bad_result; + }, + .aarch64 => host_cpu_arch == .aarch64, + .x86 => host_cpu_arch.isX86(), + .x86_64 => host_cpu_arch == .x86_64, + else => false, + }; + return if (wine_supported) .{ .wine = "wine" } else bad_result; }, .driverkit, .macos => { - if (options.allow_darling) { - // This check can be loosened once darling adds a QEMU-based emulation - // layer for non-host architectures: - // https://github.com/darlinghq/darling/issues/863 - if (candidate.cpu.arch != host_cpu_arch) { - return bad_result; - } - return .{ .darling = "darling" }; + // This check can be loosened once darling adds a QEMU-based emulation + // layer for non-host architectures: + // https://github.com/darlinghq/darling/issues/863 + if (candidate.cpu.arch != host_cpu_arch) { + return bad_result; } - return bad_result; + return .{ .darling = "darling" }; }, else => return bad_result, } diff --git a/src/main.zig b/src/main.zig index 5ad21b548e12b1453aa4e1345c74fa7c17af5036..f09b81e0ff7ecaaa19d5cec8ed7a852dca376f46 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4000,11 +4000,9 @@ fn buildOutputType( test_exec_args.items, self_exe_path_or_argv0, arg_mode, - target, &comp_destroyed, all_args, runtime_args_start, - create_module.resolved_options.link_libc, test_execve, environ_map, ); @@ -4409,6 +4407,15 @@ fn serve( var server: Server = .{ .in = in, .out = out }; try server.serveStringMessage(.zig_version, build_options.version); + try server.serveConfig(.{ .flags = .{ + .output_mode = comp.config.output_mode, + .link_mode = comp.config.link_mode, + .link_libc = comp.config.link_libc, + .link_libcpp = comp.config.link_libcpp, + .link_libunwind = comp.config.link_libunwind, + .pie = comp.config.pie, + } }); + var child_pid: ?std.process.Child.Id = null; const main_progress_node = std.Progress.start(io, .{}); @@ -4481,8 +4488,6 @@ fn serve( // test_exec_args, // self_exe_path.?, // arg_mode, - // target, - // true, // &comp_destroyed, // all_args, // runtime_args_start, @@ -4633,11 +4638,9 @@ fn runOrTest( test_exec_args: []const ?[]const u8, self_exe_path: []const u8, arg_mode: ArgMode, - target: *const std.Target, comp_destroyed: *bool, all_args: []const []const u8, runtime_args_start: ?usize, - link_libc: bool, test_execve: bool, environ_map: *process.Environ.Map, ) !void { @@ -4685,7 +4688,12 @@ fn runOrTest( _ = try io.lockStderr(&.{}, .no_color); const err = process.replace(io, .{ .argv = argv.items, .environ_map = environ_map }); io.unlockStderr(); - try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, .{ + .arg_mode = arg_mode, + .target = comp.getTarget(), + .link_mode = comp.config.link_mode, + .link_libc = comp.config.link_libc, + }); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{t}':\n{s}", .{ err, cmd }); } else if (!process.can_spawn) { @@ -4715,7 +4723,12 @@ fn runOrTest( }); const term = term_result catch |err| { - try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, .{ + .arg_mode = arg_mode, + .target = comp.getTarget(), + .link_mode = comp.config.link_mode, + .link_libc = comp.config.link_libc, + }); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed with {t}:\n{s}", .{ err, cmd }); }; @@ -5866,33 +5879,32 @@ fn prefixedIntArg(arg: []const u8, prefix: []const u8) ?u64 { return std.fmt.parseUnsigned(u64, number, 0) catch |err| fatal("unable to parse {q}: {t}", .{ arg, err }); } -fn warnAboutForeignBinaries( - io: Io, - arena: Allocator, +fn warnAboutForeignBinaries(io: Io, arena: Allocator, opts: struct { arg_mode: ArgMode, target: *const std.Target, + link_mode: std.lang.LinkMode, link_libc: bool, -) !void { - const host_query: std.Target.Query = .{}; - const host_target = std.zig.resolveTargetQueryOrFatal(io, host_query); +}) !void { + const host_target = std.zig.resolveTargetQueryOrFatal(io, .{}); - switch (std.zig.system.getExternalExecutor(io, target, .{ + switch (std.zig.system.getExternalExecutor(io, opts.target, .{ .host_cpu_arch = host_target.cpu.arch, .host_os_tag = host_target.os.tag, - .link_libc = link_libc, + .link_mode = opts.link_mode, + .link_libc = opts.link_libc, })) { .native => return, .rosetta => { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); + const foreign_name = try opts.target.zigTriple(arena); warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}). Consider installing Rosetta.", .{ host_name, foreign_name, }); }, .qemu => |qemu| { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); - switch (arg_mode) { + const foreign_name = try opts.target.zigTriple(arena); + switch (opts.arg_mode) { .zig_test => warn( "the host system ({s}) does not appear to be capable of executing binaries " ++ "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ @@ -5908,8 +5920,8 @@ fn warnAboutForeignBinaries( }, .wine => |wine| { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); - switch (arg_mode) { + const foreign_name = try opts.target.zigTriple(arena); + switch (opts.arg_mode) { .zig_test => warn( "the host system ({s}) does not appear to be capable of executing binaries " ++ "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ @@ -5925,8 +5937,8 @@ fn warnAboutForeignBinaries( }, .wasmtime => |wasmtime| { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); - switch (arg_mode) { + const foreign_name = try opts.target.zigTriple(arena); + switch (opts.arg_mode) { .zig_test => warn( "the host system ({s}) does not appear to be capable of executing binaries " ++ "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ @@ -5942,8 +5954,8 @@ fn warnAboutForeignBinaries( }, .darling => |darling| { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); - switch (arg_mode) { + const foreign_name = try opts.target.zigTriple(arena); + switch (opts.arg_mode) { .zig_test => warn( "the host system ({s}) does not appear to be capable of executing binaries " ++ "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++ @@ -5959,7 +5971,7 @@ fn warnAboutForeignBinaries( }, .bad_dl => |foreign_dl| { const host_dl = host_target.dynamic_linker.get() orelse "(none)"; - const tip_suffix = switch (arg_mode) { + const tip_suffix = switch (opts.arg_mode) { .zig_test => ", '--test-no-exec', or '--test-cmd'", else => "", }; @@ -5969,8 +5981,8 @@ fn warnAboutForeignBinaries( }, .bad_os_or_cpu => { const host_name = try host_target.zigTriple(arena); - const foreign_name = try target.zigTriple(arena); - const tip_suffix = switch (arg_mode) { + const foreign_name = try opts.target.zigTriple(arena); + const tip_suffix = switch (opts.arg_mode) { .zig_test => ". Consider using '--test-no-exec' or '--test-cmd'", else => "", }; diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 5dbff266e78f9b3f585bdbd87b99ac8e2f27f09f..b4fe4e65337b070c8ae99e867c418657b607b0d8 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -626,6 +626,7 @@ pub fn lowerToBuildSteps( if (getExternalExecutor(io, &case.target.result, .{ .host_cpu_arch = host.result.cpu.arch, .host_os_tag = host.result.os.tag, + .link_mode = .dynamic, // TODO: this emulates old behavior until this file is deleted .link_libc = true, }) != .native) { // We wouldn't be able to run the compiled C code. diff --git a/test/src/Runner.zig b/test/src/Runner.zig new file mode 100644 index 0000000000000000000000000000000000000000..599f7b001aaf38e642c1e8bf5e231f0dd0b522b1 --- /dev/null +++ b/test/src/Runner.zig @@ -0,0 +1,1147 @@ +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + + var test_path_arg: ?[]const u8 = null; + var zig_path_arg: ?[]const u8 = null; + var lib_path_arg: ?[]const u8 = null; + var target_args: std.ArrayList([]const u8) = .empty; + defer target_args.deinit(init.gpa); + var keep_src = false; + var enable_qemu = false; + var enable_wine = false; + var enable_wasmtime = false; + var enable_darling = false; + + var arg_it = try init.minimal.args.iterateAllocator(arena); + const self = arg_it.next().?; + while (arg_it.next()) |arg| { + if (std.mem.eql(u8, arg, "--listen=-")) { + var stdin_buffer: [512]u8 = undefined; + var stdin: std.Io.File.Reader = .initStreaming(.stdin(), init.io, &stdin_buffer); + var stdout_buffer: [512]u8 = undefined; + var stdout: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); + var runner: Runner = .{ + .gpa = init.gpa, + .arena = init.arena, + .io = init.io, + .prog_node = std.Progress.start(init.io, .{}), + .args = .{ + .@"test" = null, + .manifest_file = null, + .zig_exe = null, + .lib_dir = null, + .src_dir = null, + .targets = .empty, + .target_bytes = .empty, + .enable_qemu = false, + .enable_wine = false, + .enable_wasmtime = false, + .enable_darling = false, + }, + .host = try std.zig.system.resolveTargetQuery(init.io, .{}), + .server = .{ + .in = &stdin.interface, + .out = &stdout.interface, + }, + }; + defer runner.deinit(); + return runner.protocol(); + } else if (std.mem.eql(u8, arg, "--zig")) { + zig_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); + } else if (std.mem.cutPrefix(u8, arg, "--zig=")) |zig_path| { + zig_path_arg = zig_path; + } else if (std.mem.eql(u8, arg, "--lib")) { + lib_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); + } else if (std.mem.cutPrefix(u8, arg, "--lib=")) |lib_path| { + lib_path_arg = lib_path; + } else if (std.mem.eql(u8, arg, "--target")) { + try target_args.append( + init.gpa, + try arena.dupe(u8, arg_it.next() orelse fatal("missing arg after '{s}'", .{arg})), + ); + } else if (std.mem.cutPrefix(u8, arg, "--target=")) |target| { + try target_args.append(init.gpa, try arena.dupe(u8, target)); + } else if (std.mem.eql(u8, arg, "--keep-src")) { + keep_src = true; + } else if (std.mem.eql(u8, arg, "-fqemu")) { + enable_qemu = true; + } else if (std.mem.eql(u8, arg, "-fwine")) { + enable_wine = true; + } else if (std.mem.eql(u8, arg, "-fwasmtime")) { + enable_wasmtime = true; + } else if (std.mem.eql(u8, arg, "-fdarling")) { + enable_darling = true; + } else { + if (test_path_arg) |_| fatal("unknown arg '{s}'", .{arg}); + test_path_arg = arg; + } + } + + const test_path = test_path_arg orelse fatal("missing 'path/to/test'", .{}); + const zig_path = zig_path_arg orelse fatal("missing '--zig=path/to/zig'", .{}); + const lib_path = lib_path_arg orelse fatal("missing '--lib=path/to/lib'", .{}); + + const cwd: std.Io.Dir = .cwd(); + const test_file = try cwd.openFile(init.io, test_path, .{ + .allow_directory = true, + }); + const zig_exe = try cwd.openFile(init.io, zig_path, .{}); + defer zig_exe.close(init.io); + const lib_dir = try cwd.openDir(init.io, lib_path, .{}); + defer lib_dir.close(init.io); + + const src_dir_path = "src_" ++ std.fmt.hex(rand_int: { + var rand_int: u64 = undefined; + init.io.random(@ptrCast(&rand_int)); + break :rand_int rand_int; + }); + var src_dir = try cwd.createDirPathOpen(init.io, src_dir_path, .{}); + defer { + src_dir.close(init.io); + if (!keep_src) cwd.deleteTree(init.io, src_dir_path) catch |err| { + std.log.warn("failed to delete tree '{s}': {t}", .{ src_dir_path, err }); + }; + } + + var stdout_buffer: [512]u8 = undefined; + var stdout_fw: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); + const stdout = &stdout_fw.interface; + + var child = try std.process.spawn(init.io, .{ + .argv = &.{ self, "--listen=-" }, + .stdin = .pipe, + .stdout = .pipe, + .inherit_dirs = &.{ lib_dir, src_dir }, + .inherit_files = &.{ zig_exe, test_file }, + }); + + var child_reader_buffer: [512]u8 = undefined; + var child_reader = child.stdout.?.readerStreaming(init.io, &child_reader_buffer); + var child_writer_buffer: [512]u8 = undefined; + var child_writer = child.stdin.?.writerStreaming(init.io, &child_writer_buffer); + var client: std.zig.Client = .{ + .in = &child_reader.interface, + .out = &child_writer.interface, + }; + { + var args: std.ArrayList(u8) = .empty; + defer args.deinit(init.gpa); + const Arg = std.zig.Client.Message.Arg; + + try args.append(init.gpa, @backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { + else => unreachable, + .file => .input_file, + .directory => .input_dir, + }))); + try args.appendSlice(init.gpa, @ptrCast(&test_file.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--zig="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.input_file)); + try args.appendSlice(init.gpa, @ptrCast(&zig_exe.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--lib="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.input_dir)); + try args.appendSlice(init.gpa, @ptrCast(&lib_dir.handle)); + + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--src="); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.output_dir)); + try args.appendSlice(init.gpa, @ptrCast(&src_dir.handle)); + + for (target_args.items) |target_arg| { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "--target"); + try args.append(init.gpa, 0); + + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, target_arg); + try args.append(init.gpa, 0); + } + + if (enable_qemu) { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "-fqemu"); + try args.append(init.gpa, 0); + } + if (enable_wine) { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "-fwine"); + try args.append(init.gpa, 0); + } + if (enable_wasmtime) { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "-fwasmtime"); + try args.append(init.gpa, 0); + } + if (enable_darling) { + try args.append(init.gpa, @backingInt(Arg.string)); + try args.appendSlice(init.gpa, "-fdarling"); + try args.append(init.gpa, 0); + } + + try client.serveMessageHeader(.{ + .tag = .args, + .bytes_len = @intCast(args.items.len), + }); + try client.out.writeAll(args.items); + + var metadata: struct { + index: u32, + len: u32, + names: []u32, + expected_panic_msgs: []u32, + string_bytes: []u8, + + fn name(m: *@This()) []const u8 { + return std.mem.sliceTo(m.string_bytes[m.names[m.index - 1]..], 0); + } + + fn expectedPanicMsg(m: *@This()) ?[:0]const u8 { + return switch (m.expected_panic_msgs[m.index - 1]) { + 0 => null, + else => |offset| m.string_bytes[offset..std.mem.findScalarPos( + u8, + m.string_bytes, + offset, + 0, + ).? :0], + }; + } + + fn next(m: *@This()) ?u32 { + if (m.len - m.index == 0) return null; + defer m.index += 1; + return m.index; + } + } = .{ .index = 0, .len = 0, .names = &.{}, .expected_panic_msgs = &.{}, .string_bytes = &.{} }; + try client.serveBodylessMessage(.query_test_metadata); + while (true) { + const hdr = try client.receiveMessage(); + switch (hdr.tag) { + else => try client.in.discardAll(hdr.bytes_len), + .zig_version => { + const body = try arena.alloc(u8, hdr.bytes_len); + try client.in.readSliceAll(body); + if (!std.mem.eql(u8, @import("builtin").zig_version_string, body)) return fatal( + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ @import("builtin").zig_version_string, body }, + ); + }, + .test_metadata => { + const tm_hdr = + try client.in.takeStruct(std.zig.Server.Message.TestMetadata, .little); + metadata = .{ + .index = 0, + .len = tm_hdr.tests_len, + .names = try arena.alloc(u32, tm_hdr.tests_len), + .expected_panic_msgs = try arena.alloc(u32, tm_hdr.tests_len), + .string_bytes = try arena.alloc(u8, tm_hdr.string_bytes_len), + }; + try client.in.readSliceEndian(u32, metadata.names, .little); + try client.in.readSliceEndian(u32, metadata.expected_panic_msgs, .little); + try client.in.readSliceAll(metadata.string_bytes); + try client.serveRunTest(metadata.next() orelse break); + }, + .test_results => { + const tr = try client.in.takeStruct(std.zig.Server.Message.TestResults, .little); + try stdout.print("[{t}] {s}\n", .{ tr.flags.status, metadata.name() }); + try stdout.flush(); + try client.serveRunTest(metadata.next() orelse break); + }, + } + } + try client.serveBodylessMessage(.exit); + } + _ = try child.wait(init.io); +} +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); +} + +const Runner = @This(); + +gpa: std.mem.Allocator, +arena: *std.heap.ArenaAllocator, +io: std.Io, +prog_node: std.Progress.Node, +args: Args, +host: std.Target, +server: std.zig.Server, + +const Args = struct { + @"test": ?struct { + input_dir: std.zig.Server.Message.InputDir, + dir: std.Io.Dir, + }, + manifest_file: ?std.Io.File, + zig_exe: ?std.Io.File, + lib_dir: ?std.Io.Dir, + src_dir: ?std.Io.Dir, + targets: std.ArrayList(u32), + target_bytes: std.ArrayList(u8), + enable_qemu: bool, + enable_wine: bool, + enable_wasmtime: bool, + enable_darling: bool, + + fn deinit(args: *Args, gpa: std.mem.Allocator) void { + args.targets.deinit(gpa); + args.target_bytes.deinit(gpa); + args.* = undefined; + } + + fn addTarget(args: *Args, gpa: std.mem.Allocator, target: []const u8) std.mem.Allocator.Error!void { + try args.target_bytes.ensureUnusedCapacity(gpa, target.len + 1); + try args.targets.append(gpa, @intCast(args.target_bytes.items.len)); + args.target_bytes.appendSliceAssumeCapacity(target); + args.target_bytes.appendAssumeCapacity(0); + } +}; + +fn deinit(runner: *Runner) void { + runner.prog_node.end(); + runner.args.deinit(runner.gpa); + runner.* = undefined; +} + +pub fn protocol(runner: *Runner) !void { + try runner.server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); + while (true) { + _ = runner.arena.reset(.retain_capacity); + const arena = runner.arena.allocator(); + const hdr = try runner.server.receiveMessage(); + switch (hdr.tag) { + else => { + std.log.warn("unsupported message: {t}\n", .{hdr.tag}); + std.process.exit(1); + }, + .exit => std.process.exit(0), + .args => { + const args_body = try arena.alloc(u8, hdr.bytes_len); + try runner.server.in.readSliceAll(args_body); + var state: enum { positional, zig, lib, src, target } = .positional; + var input_dir: std.zig.Server.Message.InputDir = .cwd; + var args_body_offset: usize = 0; + while (args_body.len - args_body_offset > 0) { + const arg: std.zig.Client.Message.Arg = + @fromBackingInt(args_body[args_body_offset]); + args_body_offset += 1; + arg: switch (arg) { + .string => { + const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; + const string = args_body[args_body_offset..end]; + args_body_offset = end + 1; + switch (state) { + .positional => if (std.mem.eql(u8, string, "--target")) { + state = .target; + continue; + } else if (std.mem.cutPrefix(u8, string, "--target=")) |target| { + try runner.args.addTarget(runner.gpa, target); + } else if (std.mem.eql(u8, string, "-fqemu")) { + runner.args.enable_qemu = true; + } else if (std.mem.eql(u8, string, "-fwine")) { + runner.args.enable_wine = true; + } else if (std.mem.eql(u8, string, "-fwasmtime")) { + runner.args.enable_wasmtime = true; + } else if (std.mem.eql(u8, string, "-fdarling")) { + runner.args.enable_darling = true; + }, + .zig => unreachable, + .lib => unreachable, + .src => unreachable, + .target => try runner.args.addTarget(runner.gpa, string), + } + }, + .prefix => { + const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; + const string = args_body[args_body_offset..end]; + args_body_offset = end + 1; + state = if (std.mem.eql(u8, string, "--zig=")) + .zig + else if (std.mem.eql(u8, string, "--lib=")) + .lib + else if (std.mem.eql(u8, string, "--src=")) + .src + else + unreachable; + continue; + }, + .suffix => unreachable, + .input_dir => { + input_dir = @fromBackingInt(@backingInt(input_dir) + 1); + continue :arg .output_dir; + }, + .output_dir => { + const dir_handle: *align(1) const std.Io.Dir.Handle = @ptrCast( + args_body[args_body_offset..][0..@sizeOf(std.Io.Dir.Handle)], + ); + args_body_offset += @sizeOf(std.Io.Dir.Handle); + const dir: std.Io.Dir = .{ + .handle = dir_handle.*, + }; + switch (state) { + .positional => { + std.debug.assert(runner.args.@"test" == null); + runner.args.@"test" = .{ .input_dir = input_dir, .dir = dir }; + std.debug.assert(runner.args.manifest_file == null); + const manifest_path = "manifest"; + runner.args.manifest_file = + try dir.openFile(runner.io, manifest_path, .{}); + + try runner.server.serveMessageHeader(.{ + .tag = .discovered_inputs, + .bytes_len = @sizeOf(std.zig.Server.Message.InputDir) + + manifest_path.len + 1, + }); + try runner.server.out.writeInt(u32, @backingInt( + input_dir, + ), .little); + try runner.server.out.writeAll(manifest_path); + try runner.server.out.writeByte(0); + try runner.server.out.flush(); + }, + .zig => unreachable, + .lib => { + std.debug.assert(runner.args.lib_dir == null); + runner.args.lib_dir = dir; + }, + .src => { + std.debug.assert(runner.args.src_dir == null); + runner.args.src_dir = dir; + }, + .target => unreachable, + } + }, + .input_file, .input_file_content, .output_file => { + const file_handle: *align(1) const std.Io.File.Handle = @ptrCast( + args_body[args_body_offset..][0..@sizeOf(std.Io.File.Handle)], + ); + args_body_offset += @sizeOf(std.Io.File.Handle); + const file: std.Io.File = .{ + .handle = file_handle.*, + .flags = .{ .nonblocking = false }, + }; + switch (state) { + .positional => { + std.debug.assert(runner.args.manifest_file == null); + runner.args.manifest_file = file; + }, + .zig => { + std.debug.assert(runner.args.zig_exe == null); + runner.args.zig_exe = file; + }, + .lib => unreachable, + .src => unreachable, + .target => unreachable, + } + }, + } + state = .positional; + } + }, + .query_test_metadata => { + const expected_panic_msgs = try arena.alloc(u32, runner.args.targets.items.len); + @memset(expected_panic_msgs, 0); + try runner.server.serveTestMetadata(.{ + .names = runner.args.targets.items, + .expected_panic_msgs = expected_panic_msgs, + .string_bytes = runner.args.target_bytes.items, + }); + }, + .run_test => { + const test_index = try runner.server.receiveBody_u32(); + try runner.server.serveBodylessMessage(.test_started); + try runner.server.serveTestResults(.{ + .index = test_index, + .flags = .{ + .status = if (runner.testOne(test_index)) .pass else |err| switch (err) { + else => status: { + std.log.err("{t}\n", .{err}); + if (@errorReturnTrace()) |trace| std.debug.dumpErrorReturnTrace(trace); + break :status .fail; + }, + error.SkipZigTest => .skip, + }, + .fuzz = false, + .log_err_count = 0, + .leak_count = 0, + }, + }); + }, + } + } +} +const DelimitedReader = struct { + const Io = std.Io; + + unlimited: *Io.Reader, + remaining: Io.Limit, + delimiter: []const u8, + interface: Io.Reader, + + pub fn init(reader: *Io.Reader, delimiter: []const u8, buffer: []u8) DelimitedReader { + return .{ + .unlimited = reader, + .remaining = .nothing, + .delimiter = delimiter, + .interface = .{ + .vtable = &.{ + .stream = stream, + .discard = discard, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + }; + } + fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const d: *DelimitedReader = @fieldParentPtr("interface", r); + const block = try d.unlimited.peekGreedy(d.delimiter.len); + d.remaining = .limited(std.mem.findPos(u8, block, d.remaining.toInt().?, d.delimiter) orelse + block.len - d.delimiter.len + 1); + if (d.remaining == .nothing) return error.EndOfStream; + const n = try d.unlimited.stream(w, limit.min(d.remaining)); + d.remaining = d.remaining.subtract(n).?; + return n; + } + fn discard(r: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { + const d: *DelimitedReader = @fieldParentPtr("interface", r); + const block = try d.unlimited.peekGreedy(d.delimiter.len); + d.remaining = .limited(std.mem.findPos(u8, block, d.remaining.toInt().?, d.delimiter) orelse + block.len - d.delimiter.len + 1); + if (d.remaining == .nothing) return error.EndOfStream; + const n = try d.unlimited.discard(limit.min(d.remaining)); + d.remaining = d.remaining.subtract(n).?; + return n; + } +}; + +fn testOne(runner: *const Runner, test_index: u32) !void { + const target_query = + std.mem.sliceTo(runner.args.target_bytes.items[runner.args.targets.items[test_index]..], 0); + const src_dir = try (runner.args.src_dir orelse return error.MissingSrcDir).createDirPathOpen( + runner.io, + target_query, + .{}, + ); + const manifest_file = runner.args.manifest_file orelse return error.MissingManifestArg; + var manifest_buffer: [512]u8 = undefined; + var manifest_fr = manifest_file.reader(runner.io, &manifest_buffer); + var allow_skip = true; + var skip_delimiter = false; + var compiler: ?Compiler = null; + var update_prog_node: std.Progress.Node = .none; + defer update_prog_node.end(); + var update_mtime = std.Io.Clock.real.now(runner.io); + var update_num: usize = 0; + while (manifest_fr.interface.takeSentinel('\n')) |untrimmed_line| { + const line = std.mem.trimEnd(u8, untrimmed_line, "\r"); + var line_it = std.mem.tokenizeScalar(u8, line, ' '); + const cmd_str = std.mem.cutPrefix(u8, line_it.next() orelse continue, "#").?; + if (skip_delimiter) { + std.debug.assert(std.mem.eql(u8, cmd_str, "}")); + skip_delimiter = false; + continue; + } + const cmd = std.meta.stringToEnum( + enum { todo, skip, exe, lib, obj, write, delete, update, check }, + cmd_str, + ) orelse return error.UnknownCommand; + var maybe_arg = line_it.next(); + var contents_file: ?std.Io.File = null; + defer if (contents_file) |file| file.close(runner.io); + var contents_buffer: [512]u8 = undefined; + var contents_impl: union { + dr: DelimitedReader, + fr: std.Io.File.Reader, + } = undefined; + const contents_r: *std.Io.Reader = contents_r: switch (cmd) { + .skip, .delete => { + if (line_it.next()) |_| return error.UnexpectedArg; + break :contents_r .ending; + }, + else => { + const contents_path = contents_path: { + const line_rest = line_it.rest(); + if (line_rest.len > 0) break :contents_path line_rest; + const contents_path = maybe_arg orelse break :contents_r .ending; + maybe_arg = null; + break :contents_path contents_path; + }; + if (std.mem.eql(u8, contents_path, "{")) { + contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &contents_buffer) }; + skip_delimiter = true; + break :contents_r &contents_impl.dr.interface; + } + + const args_test = runner.args.@"test" orelse return error.ContentsFileInFileTest; + try runner.server.serveMessageHeader(.{ + .tag = .discovered_inputs, + .bytes_len = @intCast(@sizeOf(std.zig.Server.Message.InputDir) + + contents_path.len + 1), + }); + try runner.server.out.writeInt(u32, @backingInt(args_test.input_dir), .little); + try runner.server.out.writeAll(contents_path); + try runner.server.out.writeByte(0); + try runner.server.out.flush(); + + contents_file = try args_test.dir.openFile(runner.io, contents_path, .{}); + contents_impl = .{ .fr = contents_file.?.reader(runner.io, &contents_buffer) }; + break :contents_r &contents_impl.fr.interface; + }, + }; + + var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + const cmd_prog_node = runner.prog_node.start(std.mem.print(&name_buffer, "#{t} {s}", .{ + cmd, maybe_arg orelse "", + }) catch &name_buffer, 0); + defer cmd_prog_node.end(); + + cmd: switch (cmd) { + .todo => _ = try contents_r.discardRemaining(), + .skip => if (!allow_skip) + return error.NonInitialSkipCommand // #skip must appear first + else if (std.mem.eql(u8, target_query, maybe_arg orelse return error.MissingArg)) + return error.SkipZigTest, + .exe, .lib, .obj => try runner.spawnCompiler(&compiler, src_dir, switch (cmd) { + else => unreachable, + .exe => .Exe, + .lib => .Lib, + .obj => .Obj, + }, contents_r, target: { + const backend_split = std.mem.findScalarLast(u8, target_query, '-') orelse + return error.TargetMissingBackend; + const mode_split = + std.mem.findScalarLast(u8, target_query[0..backend_split], '-') orelse + return error.TargetMissingMode; + const triple = target_query[0..mode_split]; + const backend = std.meta.stringToEnum( + Compiler.Target.Backend, + target_query[backend_split + 1 ..], + ) orelse return error.TargetMissingBackend; + const mode = std.meta.stringToEnum( + Compiler.Target.Mode, + target_query[mode_split + 1 .. backend_split], + ) orelse return error.TargetMissingMode; + break :target .{ + .triple = triple, + .resolved = try std.zig.system.resolveTargetQuery( + runner.io, + try std.Build.parseTargetQuery(.{ + .arch_os_abi = triple, + .object_format = switch (backend) { + .sema, .selfhosted, .llvm => null, + .cbe => "c", + }, + }), + ), + .mode = mode, + .backend = backend, + }; + }), + .write => { + const file = try src_dir.createFile( + runner.io, + maybe_arg orelse return error.MissingFileArg, + .{}, + ); + defer file.close(runner.io); + var fw = file.writer(runner.io, &.{}); + _ = try contents_r.streamRemaining(&fw.interface); + try file.setTimestamps(runner.io, .{ .modify_timestamp = .{ .new = update_mtime } }); + }, + .delete => try src_dir.deleteFile( + runner.io, + maybe_arg orelse return error.MissingFileArg, + ), + .update => { + const comp = &(compiler orelse return error.MissingCompiler); + update_prog_node.end(); + update_prog_node = runner.prog_node.start( + switch (try contents_r.readSliceShort(&name_buffer)) { + 0 => switch (update_num) { + 0 => "initial update", + else => |num| std.mem.print(&name_buffer, "update {d}", .{num}) catch + &name_buffer, + }, + else => |name_len| name_buffer[0..name_len], + }, + 0, + ); + _ = try contents_r.discardRemaining(); + if (comp.state != .idle) return error.MissingCheck; + try comp.client.serveBodylessMessage(.update); + comp.state = .update; + update_mtime = update_mtime.addDuration(.fromSeconds(2)); + update_num += 1; + }, + .check => { + const check = std.meta.stringToEnum( + Compiler.Check, + maybe_arg orelse return error.MissingCheckArg, + ) orelse return error.UnknownCheck; + const expected = try contents_r.allocRemaining(runner.gpa, .unlimited); + defer runner.gpa.free(expected); + + const comp = &(compiler orelse return error.MissingCompiler); + if (comp.state != .update) return error.MissingUpdate; + const stderr = comp.mr.reader(1); + while (try comp.receiveMessage()) |message| switch (message.tag) { + .config => { + var body_r: std.Io.Reader = .fixed(message.body); + comp.config = body_r.takeStruct(std.zig.Server.Message.Config, .little) catch + unreachable; + }, + .emit_digest => { + var body_r: std.Io.Reader = .fixed(message.body); + _ = body_r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch + unreachable; + const digest = body_r.takeArray(std.Build.Cache.bin_digest_len) catch + unreachable; + try comp.checkSuccess(check, expected, src_dir, digest); + }, + .error_bundle => { + const error_bundle = + try std.zig.Server.allocErrorBundle(runner.arena.allocator(), message.body); + switch (check) { + .errors => { + var error_aw: std.Io.Writer.Allocating = .init(runner.gpa); + defer error_aw.deinit(); + try error_bundle.renderToWriter(.{ + .include_reference_trace = false, + .include_source_line = false, + .include_log_text = true, + }, &error_aw.writer); + try std.testing.expectEqualStrings(expected, error_aw.written()); + }, + .stdout, .exit, .lldb => if (error_bundle.errorMessageCount() > 0) { + try error_bundle.renderToStderr(runner.io, .{}, .auto); + fatal("unexpected compile errors", .{}); + }, + } + comp.state = .idle; + break :cmd; + }, + else => {}, // Ignore other messages, + }; + + const buffered_stderr = stderr.buffered(); + if (buffered_stderr.len > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("stderr:\n{s}", .{buffered_stderr}); + } else { + fatal("unexpected stderr:\n{s}", .{buffered_stderr}); + } + } + + try comp.exit(); + fatal("compiler failed to send terminating error_bundle", .{}); + }, + } + std.debug.assert(try contents_r.discardRemaining() == 0); + switch (cmd) { + .todo, .skip => {}, + else => allow_skip = false, + } + } else |err| switch (err) { + else => |e| return e, + error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, + } + if (compiler) |*comp| { + if (comp.state != .idle) return error.MissingCheck; + try comp.exit(); + } +} + +fn spawnCompiler( + runner: *const Runner, + compiler: *?Compiler, + src_dir: std.Io.Dir, + output_mode: std.lang.OutputMode, + args_r: *std.Io.Reader, + target: Compiler.Target, +) !void { + const gpa = runner.gpa; + const arena = runner.arena.allocator(); + if (compiler.*) |*comp| { + try comp.exit(); + compiler.* = null; + } + + var argv: std.ArrayList([]const u8) = .empty; + defer argv.deinit(gpa); + try argv.appendSlice(gpa, &.{ + "zig", + switch (output_mode) { + .Obj => "build-obj", + .Exe => "build-exe", + .Lib => "build-lib", + }, + "-target", + target.triple, + "--cache-dir", + ".zig-cache", + }); + switch (target.mode) { + .whole => {}, + .incremental => try argv.append(gpa, "-fincremental"), + } + var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; + if (target.backend) |b| switch (b) { + .sema => try argv.append(gpa, "-fno-emit-bin"), + .selfhosted => try argv.append(gpa, "-fno-llvm"), + .llvm => try argv.append(gpa, "-fllvm"), + .cbe => try argv.append(gpa, "-ofmt=c"), + } else try argv.appendSlice(gpa, &.{ "-I", path_buffer[0..try std.Io.Dir.readLinkAbsolute( + runner.io, + try arena.print("/proc/self/fd/{d}", .{ + (runner.args.lib_dir orelse return error.MissingLibDir).handle, + }), + &path_buffer, + )] }); + var need: usize = 1; + var root_name: ?[]const u8 = null; + while (true) { + const done = if (args_r.fill(need)) false else |err| switch (err) { + else => |e| return e, + error.EndOfStream => true, + }; + const buffered = args_r.buffered(); + const start = std.mem.findNone(u8, buffered, "\n ") orelse buffered.len; + const end = std.mem.findAnyPos(u8, buffered, start, "\n ") orelse buffered.len; + if (end - start > 0) { + const arg = try arena.dupe(u8, buffered[start..end]); + if (std.mem.cutPrefix(u8, arg, "-M")) |module| { + var module_name_it = std.mem.splitScalar(u8, module, '='); + const module_name = module_name_it.next().?; + root_name = root_name orelse module_name; + } + try argv.append(gpa, arg); + args_r.toss(end); + need = 1; + } else { + args_r.toss(start); + need = buffered.len - start + 1; + } + if (done) break; + } + try argv.append(gpa, "--listen=-"); + + const out_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{ + .root_name = root_name orelse "root", + .cpu_arch = target.resolved.cpu.arch, + .os_tag = target.resolved.os.tag, + .ofmt = target.resolved.ofmt, + .abi = target.resolved.abi, + .output_mode = output_mode, + }); + + var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + const comp_prog_node = runner.prog_node.start(std.mem.print(&name_buffer, "compile {s}", .{ + out_name, + }) catch &name_buffer, 0); + errdefer comp_prog_node.end(); + + const child = try std.process.spawn(runner.io, .{ + .exe = .{ .file = runner.args.zig_exe orelse return error.MissingZigExe }, + .argv = argv.items, + .cwd = .{ .dir = src_dir }, + .progress_node = comp_prog_node, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + }); + compiler.* = .{ + .runner = runner, + .prog_node = comp_prog_node, + .allow_compiler_stderr = true, + .output_mode = output_mode, + .state = .idle, + .child = child, + .fw = undefined, + .mr_buffer = undefined, + .mr = undefined, + .client = undefined, + .target = target, + .out_name = out_name, + .config = null, + }; + const comp = &compiler.*.?; + comp.fw = child.stdin.?.writerStreaming(runner.io, &.{}); + comp.mr.init(gpa, runner.io, comp.mr_buffer.toStreams(), &.{ + child.stdout.?, child.stderr.?, + }); + comp.client = .{ .in = comp.mr.reader(0), .out = &comp.fw.interface }; +} +const Compiler = struct { + runner: *const Runner, + prog_node: std.Progress.Node, + allow_compiler_stderr: bool, + output_mode: std.lang.OutputMode, + state: enum { idle, update }, + child: std.process.Child, + fw: std.Io.File.Writer, + mr_buffer: std.Io.File.MultiReader.Buffer(2), + mr: std.Io.File.MultiReader, + client: std.zig.Client, + target: Target, + out_name: []const u8, + config: ?std.zig.Server.Message.Config, + + const Target = struct { + triple: []const u8, + resolved: std.Target, + mode: Mode, + backend: ?Backend, + + const Mode = enum { + whole, + incremental, + }; + + const Backend = enum { + /// Run semantic analysis only. Runtime output will not be tested, but we still verify + /// that compilation succeeds. Corresponds to `-fno-emit-bin`. + sema, + /// Use the self-hosted code generation backend for this target. + /// Corresponds to `-fno-llvm -fno-lld`. + selfhosted, + /// Use the LLVM backend. + /// Corresponds to `-fllvm -flld`. + llvm, + /// Use the C backend. The output is compiled with `zig cc`. + /// Corresponds to `-ofmt=c`. + cbe, + }; + }; + + fn receiveMessage(comp: *Compiler) !?struct { + tag: std.zig.Server.Message.Tag, + body: []const u8, + } { + const header = comp.client.receiveMessageWithMultiReader( + &comp.mr, + .none, + ) catch |err| switch (err) { + error.Timeout => unreachable, + error.EndOfStream => return null, + else => |e| return e, + }; + const body = comp.client.in.take(header.bytes_len) catch unreachable; + const stderr = comp.mr.reader(1); + if (stderr.bufferedLen() > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("{t} stderr:\n{s}", .{ header.tag, stderr.buffered() }); + } else { + fatal("{t} unexpected stderr:\n{s}", .{ header.tag, stderr.buffered() }); + } + stderr.tossBuffered(); + } + return .{ .tag = header.tag, .body = body }; + } + + const Check = enum { errors, stdout, exit, lldb }; + fn checkSuccess( + comp: *Compiler, + check: Check, + expected: []const u8, + src_dir: std.Io.Dir, + digest: *const std.Build.Cache.BinDigest, + ) !void { + const runner = comp.runner; + const gpa = runner.gpa; + const arena = runner.arena.allocator(); + const io = runner.io; + const config = &comp.config.?; + const out_dir = ".zig-cache" ++ std.Io.Dir.path.sep_str ++ + "o" ++ std.Io.Dir.path.sep_str ++ std.Build.Cache.binToHex(digest.*); + const out_path = try std.Io.Dir.path.join(arena, &.{ out_dir, comp.out_name }); + const bin_path = switch (comp.target.backend.?) { + .sema => return, + .selfhosted, .llvm => out_path, + .cbe => try comp.compileC(src_dir, out_path), + }; + switch (check) { + .errors => return, + .stdout, .exit, .lldb => {}, + } + + const executor = executor: switch (std.zig.system.getExternalExecutor( + io, + &comp.target.resolved, + .{ + .host_cpu_arch = runner.host.cpu.arch, + .host_os_tag = runner.host.os.tag, + .link_mode = config.flags.link_mode, + .link_libc = config.flags.link_libc, + }, + )) { + .bad_dl, .bad_os_or_cpu => { + // This binary cannot be executed on this host. + std.log.warn("skipping execution because host '{s}' cannot execute binaries for " ++ + "foreign target '{s}'", .{ + try runner.host.zigTriple(arena), comp.target.triple, + }); + return; + }, + .native, .rosetta => null, + .qemu => |executor| if (runner.args.enable_qemu) + executor + else + continue :executor .bad_os_or_cpu, + .wine => |executor| if (runner.args.enable_wine) + executor + else + continue :executor .bad_os_or_cpu, + .wasmtime => |executor| if (runner.args.enable_wasmtime) + executor + else + continue :executor .bad_os_or_cpu, + .darling => |executor| if (runner.args.enable_darling) + executor + else + continue :executor .bad_os_or_cpu, + }; + + var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + const run_prog_node = runner.prog_node.start(std.mem.print(&name_buffer, "run {s}", .{ + comp.out_name, + }) catch &name_buffer, 0); + defer run_prog_node.end(); + + const bin_file = try src_dir.openFile(io, bin_path, .{}); + defer bin_file.close(io); + switch (check) { + .errors => unreachable, + .stdout, .exit => { + const result = std.process.run(gpa, io, .{ + .exe = if (executor) |_| .search else .{ .file = bin_file }, + .argv = if (executor) |e| &.{ e, bin_path } else &.{bin_path}, + .cwd = .{ .dir = src_dir }, + .progress_node = run_prog_node, + }) catch |err| if (executor) |_| { + // Chances are the foreign executor isn't available. Skip this evaluation. + std.log.warn("skipping execution of '{s}' via executor for foreign target '{s}': {t}", .{ + bin_path, + comp.target.triple, + err, + }); + return; + } else fatal("failed to run the generated executable '{s}': {t}", .{ bin_path, err }); + defer { + gpa.free(result.stdout); + gpa.free(result.stderr); + } + switch (result.term) { + .exited => |code| switch (check) { + .errors, .lldb => unreachable, + .stdout => { + if (code != 0) fatal("generated executable '{s}' failed with code {d}", .{ bin_path, code }); + try std.testing.expectEqualStrings(expected, result.stdout); + }, + .exit => { + var actual_code_buffer: [std.fmt.count("{d}", .{std.math.maxInt(u8)})]u8 = undefined; + try std.testing.expectEqualStrings( + expected, + std.mem.print(&actual_code_buffer, "{d}", .{code}) catch unreachable, + ); + }, + }, + .signal => |sig| fatal("generated executable '{s}' terminated with signal {t}", .{ bin_path, sig }), + .stopped => |sig| fatal("generated executable '{s}' stopped with signal {t}", .{ bin_path, sig }), + .unknown => fatal("generated executable '{s}' terminated unexpectedly", .{bin_path}), + } + if (executor == null and result.stderr.len > 0) { + std.log.err("generated executable '{s}' had unexpected stderr:\n{s}", .{ + bin_path, result.stderr, + }); + } + }, + .lldb => {}, + } + } + + fn compileC(parent_comp: *Compiler, src_dir: std.Io.Dir, c_path: []const u8) ![]const u8 { + const runner = parent_comp.runner; + const arena = runner.arena.allocator(); + var compiler: ?Compiler = null; + var args_r: std.Io.Reader = .fixed(c_path); + try parent_comp.runner.spawnCompiler(&compiler, src_dir, parent_comp.output_mode, &args_r, .{ + .triple = parent_comp.target.triple, + .resolved = resolved: { + var resolved = parent_comp.target.resolved; + resolved.ofmt = .default(resolved.os.tag, resolved.cpu.arch); + break :resolved resolved; + }, + .mode = .whole, + .backend = null, + }); + const comp = &compiler.?; + try comp.client.serveBodylessMessage(.update); + comp.state = .update; + var out_path: ?[]const u8 = null; + while (try comp.receiveMessage()) |message| switch (message.tag) { + .config => { + var body_r: std.Io.Reader = .fixed(message.body); + comp.config = + body_r.takeStruct(std.zig.Server.Message.Config, .little) catch unreachable; + }, + .emit_digest => { + var body_r: std.Io.Reader = .fixed(message.body); + _ = body_r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable; + const digest = body_r.takeArray(std.Build.Cache.bin_digest_len) catch unreachable; + + const out_dir = ".zig-cache" ++ std.Io.Dir.path.sep_str ++ + "o" ++ std.Io.Dir.path.sep_str ++ std.Build.Cache.binToHex(digest.*); + out_path = try std.Io.Dir.path.join(arena, &.{ out_dir, comp.out_name }); + }, + .error_bundle => { + const error_bundle = try std.zig.Server.allocErrorBundle(arena, message.body); + + if (error_bundle.errorMessageCount() > 0) { + try error_bundle.renderToStderr(runner.io, .{}, .auto); + fatal("unexpected compile errors", .{}); + } + comp.state = .idle; + break; + }, + else => {}, // Ignore other messages, + }; + try comp.exit(); + return out_path.?; + } + + fn exit(comp: *Compiler) !void { + const io = comp.runner.io; + comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { + error.WriteFailed => switch (comp.fw.err.?) { + error.BrokenPipe => {}, + else => |e| fatal("failed to send exit: {t}", .{e}), + }, + }; + comp.child.stdin.?.close(io); + comp.child.stdin = null; + while (try comp.receiveMessage()) |_| {} + if (comp.client.in.bufferedLen() > 0) return error.EndOfStream; + const term = comp.child.wait(io) catch |err| fatal("child process failed: {t}", .{err}); + comp.prog_node.end(); + switch (term) { + .exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}), + .signal => |sig| fatal("compiler terminated with signal {t}", .{sig}), + .stopped => |sig| fatal("compiler stopped unexpectedly with signal {t}", .{sig}), + .unknown => fatal("compiler terminated unexpectedly", .{}), + } + } +}; diff --git a/test/src/runner.zig b/test/src/runner.zig deleted file mode 100644 index 9e37cec171b5d5134486a21c9459e508b31e3025..0000000000000000000000000000000000000000 --- a/test/src/runner.zig +++ /dev/null @@ -1,918 +0,0 @@ -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const arena = init.arena.allocator(); - - var test_path_arg: ?[]const u8 = null; - var zig_path_arg: ?[]const u8 = null; - var lib_path_arg: ?[]const u8 = null; - var target_args: std.ArrayList([]const u8) = .empty; - defer target_args.deinit(init.gpa); - var keep_src = false; - - var arg_it = try init.minimal.args.iterateAllocator(arena); - const self = arg_it.next().?; - while (arg_it.next()) |arg| { - if (std.mem.eql(u8, arg, "--listen=-")) { - return protocol(init.gpa, init.arena, init.io); - } else if (std.mem.eql(u8, arg, "--zig")) { - zig_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); - } else if (std.mem.cutPrefix(u8, arg, "--zig=")) |zig_path| { - zig_path_arg = zig_path; - } else if (std.mem.eql(u8, arg, "--lib")) { - lib_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); - } else if (std.mem.cutPrefix(u8, arg, "--lib=")) |lib_path| { - lib_path_arg = lib_path; - } else if (std.mem.eql(u8, arg, "--target")) { - try target_args.append( - init.gpa, - try arena.dupe(u8, arg_it.next() orelse fatal("missing arg after '{s}'", .{arg})), - ); - } else if (std.mem.cutPrefix(u8, arg, "--target=")) |target| { - try target_args.append(init.gpa, try arena.dupe(u8, target)); - } else if (std.mem.eql(u8, arg, "--keep-src")) { - keep_src = true; - } else { - if (test_path_arg) |_| fatal("unknown arg '{s}'", .{arg}); - test_path_arg = arg; - } - } - - const test_path = test_path_arg orelse fatal("missing 'path/to/test'", .{}); - const zig_path = zig_path_arg orelse fatal("missing '--zig=path/to/zig'", .{}); - const lib_path = lib_path_arg orelse fatal("missing '--lib=path/to/lib'", .{}); - - const cwd: std.Io.Dir = .cwd(); - const test_file = try cwd.openFile(init.io, test_path, .{ - .allow_directory = true, - }); - const zig_exe = try cwd.openFile(init.io, zig_path, .{}); - defer zig_exe.close(init.io); - const lib_dir = try cwd.openDir(init.io, lib_path, .{}); - defer lib_dir.close(init.io); - - const src_dir_path = "src_" ++ std.fmt.hex(rand_int: { - var rand_int: u64 = undefined; - init.io.random(@ptrCast(&rand_int)); - break :rand_int rand_int; - }); - var src_dir = try cwd.createDirPathOpen(init.io, src_dir_path, .{}); - defer { - src_dir.close(init.io); - if (!keep_src) cwd.deleteTree(init.io, src_dir_path) catch |err| { - std.log.warn("fataled to delete tree '{s}': {t}", .{ src_dir_path, err }); - }; - } - - var stdout_buffer: [512]u8 = undefined; - var stdout_fw: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); - const stdout = &stdout_fw.interface; - - var child = try std.process.spawn(init.io, .{ - .argv = &.{ self, "--listen=-" }, - .stdin = .pipe, - .stdout = .pipe, - .inherit_dirs = &.{ lib_dir, src_dir }, - .inherit_files = &.{ zig_exe, test_file }, - }); - - var child_reader_buffer: [512]u8 = undefined; - var child_reader = child.stdout.?.readerStreaming(init.io, &child_reader_buffer); - var child_writer_buffer: [512]u8 = undefined; - var child_writer = child.stdin.?.writerStreaming(init.io, &child_writer_buffer); - var client: std.zig.Client = .{ - .in = &child_reader.interface, - .out = &child_writer.interface, - }; - { - var args: std.ArrayList(u8) = .empty; - defer args.deinit(init.gpa); - const Arg = std.zig.Client.Message.Arg; - - try args.append(init.gpa, @backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { - else => unreachable, - .file => .input_file, - .directory => .input_dir, - }))); - try args.appendSlice(init.gpa, @ptrCast(&test_file.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--zig="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.input_file)); - try args.appendSlice(init.gpa, @ptrCast(&zig_exe.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--lib="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.input_dir)); - try args.appendSlice(init.gpa, @ptrCast(&lib_dir.handle)); - - try args.append(init.gpa, @backingInt(Arg.prefix)); - try args.appendSlice(init.gpa, "--src="); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.output_dir)); - try args.appendSlice(init.gpa, @ptrCast(&src_dir.handle)); - - for (target_args.items) |target_arg| { - try args.append(init.gpa, @backingInt(Arg.string)); - try args.appendSlice(init.gpa, "--target"); - try args.append(init.gpa, 0); - - try args.append(init.gpa, @backingInt(Arg.string)); - try args.appendSlice(init.gpa, target_arg); - try args.append(init.gpa, 0); - } - - try client.serveMessageHeader(.{ - .tag = .args, - .bytes_len = @intCast(args.items.len), - }); - try client.out.writeAll(args.items); - - var metadata: struct { - index: u32, - len: u32, - names: []u32, - expected_panic_msgs: []u32, - string_bytes: []u8, - - fn name(m: *@This()) []const u8 { - return std.mem.sliceTo(m.string_bytes[m.names[m.index - 1]..], 0); - } - - fn expectedPanicMsg(m: *@This()) ?[:0]const u8 { - return switch (m.expected_panic_msgs[m.index - 1]) { - 0 => null, - else => |offset| m.string_bytes[offset..std.mem.findScalarPos( - u8, - m.string_bytes, - offset, - 0, - ).? :0], - }; - } - - fn next(m: *@This()) ?u32 { - if (m.len - m.index == 0) return null; - defer m.index += 1; - return m.index; - } - } = .{ .index = 0, .len = 0, .names = &.{}, .expected_panic_msgs = &.{}, .string_bytes = &.{} }; - try client.serveBodylessMessage(.query_test_metadata); - while (true) { - const hdr = try client.receiveMessage(); - switch (hdr.tag) { - else => try client.in.discardAll(hdr.bytes_len), - .zig_version => { - const body = try arena.alloc(u8, hdr.bytes_len); - try client.in.readSliceAll(body); - if (!std.mem.eql(u8, @import("builtin").zig_version_string, body)) return fatal( - "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", - .{ @import("builtin").zig_version_string, body }, - ); - }, - .test_metadata => { - const tm_hdr = - try client.in.takeStruct(std.zig.Server.Message.TestMetadata, .little); - metadata = .{ - .index = 0, - .len = tm_hdr.tests_len, - .names = try arena.alloc(u32, tm_hdr.tests_len), - .expected_panic_msgs = try arena.alloc(u32, tm_hdr.tests_len), - .string_bytes = try arena.alloc(u8, tm_hdr.string_bytes_len), - }; - try client.in.readSliceEndian(u32, metadata.names, .little); - try client.in.readSliceEndian(u32, metadata.expected_panic_msgs, .little); - try client.in.readSliceAll(metadata.string_bytes); - try client.serveRunTest(metadata.next() orelse break); - }, - .test_results => { - const tr = try client.in.takeStruct(std.zig.Server.Message.TestResults, .little); - try stdout.print("[{t}] {s}\n", .{ tr.flags.status, metadata.name() }); - try stdout.flush(); - try client.serveRunTest(metadata.next() orelse break); - }, - } - } - try client.serveBodylessMessage(.exit); - } - _ = try child.wait(init.io); -} -fn fatal(comptime fmt: []const u8, args: anytype) noreturn { - std.log.err(fmt, args); - std.process.exit(1); -} - -const Args = struct { - @"test": ?struct { - input_dir: std.zig.Server.Message.InputDir, - dir: std.Io.Dir, - }, - manifest_file: ?std.Io.File, - zig_exe: ?std.Io.File, - lib_dir: ?std.Io.Dir, - src_dir: ?std.Io.Dir, - targets: std.ArrayList(u32), - target_bytes: std.ArrayList(u8), - - fn deinit(args: *Args, gpa: std.mem.Allocator) void { - args.targets.deinit(gpa); - args.target_bytes.deinit(gpa); - } - - fn addTarget(args: *Args, gpa: std.mem.Allocator, target: []const u8) std.mem.Allocator.Error!void { - try args.target_bytes.ensureUnusedCapacity(gpa, target.len + 1); - try args.targets.append(gpa, @intCast(args.target_bytes.items.len)); - args.target_bytes.appendSliceAssumeCapacity(target); - args.target_bytes.appendAssumeCapacity(0); - } -}; -pub fn protocol(gpa: std.mem.Allocator, arena_impl: *std.heap.ArenaAllocator, io: std.Io) !void { - const prog_node = std.Progress.start(io, .{}); - defer prog_node.end(); - - var stdin_buffer: [512]u8 = undefined; - var stdout_buffer: [512]u8 = undefined; - var stdin: std.Io.File.Reader = .initStreaming(.stdin(), io, &stdin_buffer); - var stdout: std.Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buffer); - var server: std.zig.Server = .{ - .in = &stdin.interface, - .out = &stdout.interface, - }; - - var args: Args = .{ - .@"test" = null, - .manifest_file = null, - .zig_exe = null, - .lib_dir = null, - .src_dir = null, - .targets = .empty, - .target_bytes = .empty, - }; - defer args.deinit(gpa); - - try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); - while (true) { - _ = arena_impl.reset(.retain_capacity); - const arena = arena_impl.allocator(); - const hdr = try server.receiveMessage(); - switch (hdr.tag) { - else => { - std.debug.print("unsupported message: {t}\n", .{hdr.tag}); - std.process.exit(1); - }, - .exit => std.process.exit(0), - .args => { - const args_body = try arena.alloc(u8, hdr.bytes_len); - try server.in.readSliceAll(args_body); - var state: enum { positional, zig, lib, src, target } = .positional; - var input_dir: std.zig.Server.Message.InputDir = .cwd; - var args_body_offset: usize = 0; - while (args_body.len - args_body_offset > 0) { - const arg: std.zig.Client.Message.Arg = - @fromBackingInt(args_body[args_body_offset]); - args_body_offset += 1; - arg: switch (arg) { - .string => { - const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; - const string = args_body[args_body_offset..end]; - args_body_offset = end + 1; - switch (state) { - .positional => if (std.mem.eql(u8, string, "--target")) { - state = .target; - continue; - } else if (std.mem.cutPrefix(u8, string, "--target=")) |target| { - try args.addTarget(gpa, target); - }, - .zig => unreachable, - .lib => unreachable, - .src => unreachable, - .target => try args.addTarget(gpa, string), - } - }, - .prefix => { - const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; - const string = args_body[args_body_offset..end]; - args_body_offset = end + 1; - state = if (std.mem.eql(u8, string, "--zig=")) - .zig - else if (std.mem.eql(u8, string, "--lib=")) - .lib - else if (std.mem.eql(u8, string, "--src=")) - .src - else - unreachable; - continue; - }, - .suffix => unreachable, - .input_dir => { - input_dir = @fromBackingInt(@backingInt(input_dir) + 1); - continue :arg .output_dir; - }, - .output_dir => { - const dir_handle: *align(1) const std.Io.Dir.Handle = @ptrCast( - args_body[args_body_offset..][0..@sizeOf(std.Io.Dir.Handle)], - ); - args_body_offset += @sizeOf(std.Io.Dir.Handle); - const dir: std.Io.Dir = .{ - .handle = dir_handle.*, - }; - switch (state) { - .positional => { - std.debug.assert(args.@"test" == null); - args.@"test" = .{ .input_dir = input_dir, .dir = dir }; - std.debug.assert(args.manifest_file == null); - const manifest_path = "manifest"; - args.manifest_file = try dir.openFile(io, manifest_path, .{}); - - try server.serveMessageHeader(.{ - .tag = .discovered_inputs, - .bytes_len = @sizeOf(std.zig.Server.Message.InputDir) + - manifest_path.len + 1, - }); - try server.out.writeInt(u32, @backingInt(input_dir), .little); - try server.out.writeAll(manifest_path); - try server.out.writeByte(0); - try server.out.flush(); - }, - .zig => unreachable, - .lib => { - std.debug.assert(args.lib_dir == null); - args.lib_dir = dir; - }, - .src => { - std.debug.assert(args.src_dir == null); - args.src_dir = dir; - }, - .target => unreachable, - } - }, - .input_file, .input_file_content, .output_file => { - const file_handle: *align(1) const std.Io.File.Handle = @ptrCast( - args_body[args_body_offset..][0..@sizeOf(std.Io.File.Handle)], - ); - args_body_offset += @sizeOf(std.Io.File.Handle); - const file: std.Io.File = .{ - .handle = file_handle.*, - .flags = .{ .nonblocking = false }, - }; - switch (state) { - .positional => { - std.debug.assert(args.manifest_file == null); - args.manifest_file = file; - }, - .zig => { - std.debug.assert(args.zig_exe == null); - args.zig_exe = file; - }, - .lib => unreachable, - .src => unreachable, - .target => unreachable, - } - }, - } - state = .positional; - } - }, - .query_test_metadata => { - const expected_panic_msgs = try arena.alloc(u32, args.targets.items.len); - @memset(expected_panic_msgs, 0); - try server.serveTestMetadata(.{ - .names = args.targets.items, - .expected_panic_msgs = expected_panic_msgs, - .string_bytes = args.target_bytes.items, - }); - }, - .run_test => { - const test_index = try server.receiveBody_u32(); - const target_query = - std.mem.sliceTo(args.target_bytes.items[args.targets.items[test_index]..], 0); - try server.serveBodylessMessage(.test_started); - const test_result = - runTest(gpa, arena, io, prog_node, &server, &args, target_query); - try server.serveTestResults(.{ - .index = test_index, - .flags = .{ - .status = if (test_result) .pass else |err| switch (err) { - else => status: { - std.debug.print("{t}\n", .{err}); - if (@errorReturnTrace()) |trace| std.debug.dumpErrorReturnTrace(trace); - break :status .fail; - }, - error.SkipZigTest => .skip, - }, - .fuzz = false, - .log_err_count = 0, - .leak_count = 0, - }, - }); - }, - } - } -} -fn runTest( - gpa: std.mem.Allocator, - arena: std.mem.Allocator, - io: std.Io, - prog_node: std.Progress.Node, - server: *std.zig.Server, - args: *const Args, - target_string: []const u8, -) !void { - const manifest_file = args.manifest_file orelse return error.MissingManifestArg; - const src_dir = - try (args.src_dir orelse return error.MissingSrcDir).createDirPathOpen(io, target_string, .{}); - const target: Compiler.Target = target: { - const backend_split = std.mem.findScalarLast(u8, target_string, '-') orelse - return error.TargetMissingBackend; - const mode_split = - std.mem.findScalarLast(u8, target_string[0..backend_split], '-') orelse - return error.TargetMissingMode; - const triple = target_string[0..mode_split]; - const backend = std.meta.stringToEnum( - Compiler.Target.Backend, - target_string[backend_split + 1 ..], - ) orelse return error.TargetMissingBackend; - const mode = std.meta.stringToEnum( - Compiler.Target.Mode, - target_string[mode_split + 1 .. backend_split], - ) orelse return error.TargetMissingMode; - break :target .{ - .triple = triple, - .resolved = try std.zig.system.resolveTargetQuery(io, try std.Build.parseTargetQuery(.{ - .arch_os_abi = triple, - .object_format = switch (backend) { - .sema, .selfhosted, .llvm => null, - .cbe => "c", - }, - })), - .mode = mode, - .backend = backend, - }; - }; - var manifest_buffer: [512]u8 = undefined; - var manifest_fr = manifest_file.reader(io, &manifest_buffer); - var allow_skip = true; - var skip_delimiter = false; - var compiler: ?Compiler = null; - var update_mtime = std.Io.Clock.real.now(io); - var update_num: usize = 0; - while (manifest_fr.interface.takeSentinel('\n')) |untrimmed_line| { - const line = std.mem.trimEnd(u8, untrimmed_line, "\r"); - var line_it = std.mem.tokenizeScalar(u8, line, ' '); - const cmd_str = std.mem.cutPrefix(u8, line_it.next() orelse continue, "#").?; - if (skip_delimiter) { - std.debug.assert(std.mem.eql(u8, cmd_str, "}")); - skip_delimiter = false; - continue; - } - const cmd = std.meta.stringToEnum( - enum { todo, skip, exe, lib, obj, write, delete, update, check }, - cmd_str, - ) orelse return error.UnknownCommand; - var maybe_arg = line_it.next(); - var contents_file: ?std.Io.File = null; - defer if (contents_file) |file| file.close(io); - var contents_buffer: [512]u8 = undefined; - var contents_impl: union { - dr: DelimitedReader, - fr: std.Io.File.Reader, - } = undefined; - const contents_r: *std.Io.Reader = contents_r: switch (cmd) { - .skip, .delete => { - if (line_it.next()) |_| return error.UnexpectedArg; - break :contents_r .ending; - }, - else => { - const contents_path = contents_path: { - const line_rest = line_it.rest(); - if (line_rest.len > 0) break :contents_path line_rest; - const contents_path = maybe_arg orelse break :contents_r .ending; - maybe_arg = null; - break :contents_path contents_path; - }; - if (std.mem.eql(u8, contents_path, "{")) { - contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &contents_buffer) }; - skip_delimiter = true; - break :contents_r &contents_impl.dr.interface; - } - - const @"test" = args.@"test" orelse return error.ContentsFileInFileTest; - try server.serveMessageHeader(.{ - .tag = .discovered_inputs, - .bytes_len = @intCast(@sizeOf(std.zig.Server.Message.InputDir) + - contents_path.len + 1), - }); - try server.out.writeInt(u32, @backingInt(@"test".input_dir), .little); - try server.out.writeAll(contents_path); - try server.out.writeByte(0); - try server.out.flush(); - - contents_file = try @"test".dir.openFile(io, contents_path, .{}); - contents_impl = .{ .fr = contents_file.?.reader(io, &contents_buffer) }; - break :contents_r &contents_impl.fr.interface; - }, - }; - const cmd_prog_node = cmd_prog_node: { - var cmd_name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; - break :cmd_prog_node prog_node.start(std.mem.print(&cmd_name_buffer, "#{t} {s}", .{ - cmd, maybe_arg orelse "", - }) catch &cmd_name_buffer, 0); - }; - defer cmd_prog_node.end(); - cmd: switch (cmd) { - .todo => _ = try contents_r.discardRemaining(), - .skip => if (!allow_skip) - return error.NonInitialSkipCommand // #skip must appear first - else if (std.mem.eql(u8, target_string, maybe_arg orelse return error.MissingArg)) - return error.SkipZigTest, - .exe, .lib, .obj => { - if (compiler) |*comp| { - try comp.exit(io); - compiler = null; - } - var argv: std.ArrayList([]const u8) = .empty; - defer argv.deinit(gpa); - try argv.appendSlice(gpa, &.{ - "zig", - switch (cmd) { - else => unreachable, - .obj => "build-obj", - .exe => "build-exe", - .lib => "build-lib", - }, - "-target", - target.triple, - "--cache-dir", - ".zig-cache", - }); - switch (target.mode) { - .whole => {}, - .incremental => try argv.append(gpa, "-fincremental"), - } - switch (target.backend) { - .sema => try argv.append(gpa, "-fno-emit-bin"), - .selfhosted => try argv.append(gpa, "-fno-llvm"), - .llvm => try argv.append(gpa, "-fllvm"), - .cbe => try argv.append(gpa, "-ofmt=c"), - } - try argv.append(gpa, "--listen=-"); - var need: usize = 1; - var root_name: ?[]const u8 = null; - while (contents_r.peekGreedy(need)) |buffered| { - const start = std.mem.findNone(u8, buffered, "\n ") orelse buffered.len; - if (std.mem.findAnyPos(u8, buffered, start, "\n ")) |end| { - const arg = try arena.dupe(u8, buffered[start..end]); - if (std.mem.cutPrefix(u8, arg, "-M")) |module| { - var module_name_it = std.mem.splitScalar(u8, module, '='); - const module_name = module_name_it.next().?; - root_name = root_name orelse module_name; - } - try argv.append(gpa, arg); - contents_r.toss(end); - need = 1; - } else { - contents_r.toss(start); - need = buffered.len - start + 1; - } - } else |err| switch (err) { - else => |e| return e, - error.EndOfStream => {}, - } - const compile_prog_node = prog_node.start("initial update", 0); - const child = try std.process.spawn(io, .{ - .exe = .{ .file = args.zig_exe orelse return error.MissingZigExe }, - .argv = argv.items, - .cwd = .{ .dir = src_dir }, - .progress_node = compile_prog_node, - .stdin = .pipe, - .stdout = .pipe, - .stderr = .pipe, - }); - compiler = .{ - .prog_node = compile_prog_node, - .allow_compiler_stderr = true, - .output_mode = switch (cmd) { - else => unreachable, - .exe => .Exe, - .lib => .Lib, - .obj => .Obj, - }, - .root_name = root_name orelse return error.MissingRootModule, - .state = .idle, - .child = child, - .fw = undefined, - .mr_buffer = undefined, - .mr = undefined, - .client = undefined, - }; - const comp = &compiler.?; - comp.fw = child.stdin.?.writerStreaming(io, &.{}); - comp.mr.init(gpa, io, comp.mr_buffer.toStreams(), &.{ - child.stdout.?, child.stderr.?, - }); - comp.client = .{ .in = comp.mr.reader(0), .out = &comp.fw.interface }; - }, - .write => { - const file = - try src_dir.createFile(io, maybe_arg orelse return error.MissingFileArg, .{}); - defer file.close(io); - var fw = file.writer(io, &.{}); - _ = try contents_r.streamRemaining(&fw.interface); - try file.setTimestamps(io, .{ .modify_timestamp = .{ .new = update_mtime } }); - }, - .delete => try src_dir.deleteFile(io, maybe_arg orelse return error.MissingFileArg), - .update => { - const comp = &(compiler orelse return error.MissingCompiler); - name: { - var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; - comp.prog_node.setName(switch (try contents_r.readSliceShort(&name_buffer)) { - 0 => switch (update_num) { - 0 => break :name, - else => |num| std.mem.print(&name_buffer, "update {d}", .{num}) catch - &name_buffer, - }, - else => |name_len| name_buffer[0..name_len], - }); - } - _ = try contents_r.discardRemaining(); - if (comp.state != .idle) return error.MissingCheck; - try comp.client.serveBodylessMessage(.update); - comp.state = .update; - update_mtime = update_mtime.addDuration(.fromSeconds(2)); - update_num += 1; - }, - .check => { - const check = std.meta.stringToEnum( - enum { errors, stdout, exit, lldb }, - maybe_arg orelse return error.MissingCheckArg, - ) orelse return error.UnknownCheck; - const expected = try contents_r.allocRemaining(gpa, .unlimited); - defer gpa.free(expected); - - const comp = &(compiler orelse return error.MissingCompiler); - if (comp.state != .update) return error.MissingUpdate; - const stderr = comp.mr.reader(1); - while (true) { - const header = comp.client.receiveMessageWithMultiReader( - &comp.mr, - .none, - ) catch |err| switch (err) { - error.Timeout => unreachable, - error.EndOfStream => break, - else => |e| return e, - }; - const body = comp.client.in.take(header.bytes_len) catch unreachable; - switch (header.tag) { - .emit_digest => { - var r: std.Io.Reader = .fixed(body); - _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch - unreachable; - - if (stderr.bufferedLen() > 0) { - if (comp.allow_compiler_stderr) { - std.log.info("emit_digest stderr:\n{s}", .{stderr.buffered()}); - } else { - fatal("emit_digest unexpected stderr:\n{s}", .{stderr.buffered()}); - } - stderr.tossBuffered(); - } - switch (target.backend) { - .sema => continue, - .selfhosted, .llvm, .cbe => {}, - } - switch (check) { - .errors => continue, - .stdout, .exit, .lldb => {}, - } - const digest = r.takeArray(std.Build.Cache.bin_digest_len) catch - unreachable; - const bin_dir = ".zig-cache" ++ std.Io.Dir.path.sep_str ++ - "o" ++ std.Io.Dir.path.sep_str ++ std.Build.Cache.binToHex(digest.*); - const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{ - .root_name = comp.root_name, - .cpu_arch = target.resolved.cpu.arch, - .os_tag = target.resolved.os.tag, - .ofmt = target.resolved.ofmt, - .abi = target.resolved.abi, - .output_mode = comp.output_mode, - }); - const bin_path = - try std.Io.Dir.path.join(arena, &.{ bin_dir, bin_name }); - const bin_file = try src_dir.openFile(io, bin_path, .{}); - defer bin_file.close(io); - switch (check) { - .errors => unreachable, - .stdout, .exit => { - const result = std.process.run(gpa, io, .{ - .exe = .{ .file = bin_file }, - .argv = &.{bin_path}, - }) catch continue; - defer { - gpa.free(result.stdout); - gpa.free(result.stderr); - } - switch (result.term) { - .exited => |code| switch (check) { - .errors, .lldb => unreachable, - .stdout => { - if (code != 0) fatal("generated executable '{s}' failed with code {d}", .{ bin_path, code }); - try std.testing.expectEqualStrings(expected, result.stdout); - }, - .exit => { - var actual_code_buffer: [std.fmt.count("{d}", .{std.math.maxInt(u8)})]u8 = undefined; - try std.testing.expectEqualStrings( - expected, - std.mem.print(&actual_code_buffer, "{d}", .{code}) catch unreachable, - ); - }, - }, - .signal => |sig| fatal("generated executable '{s}' terminated with signal {t}", .{ bin_path, sig }), - .stopped => |sig| fatal("generated executable '{s}' stopped with signal {t}", .{ bin_path, sig }), - .unknown => fatal("generated executable '{s}' terminated unexpectedly", .{bin_path}), - } - }, - .lldb => {}, - } - }, - .error_bundle => { - const error_bundle = try std.zig.Server.allocErrorBundle(arena, body); - if (stderr.bufferedLen() > 0) { - if (comp.allow_compiler_stderr) { - std.log.info("error_bundle stderr:\n{s}", .{stderr.buffered()}); - } else { - fatal("error_bundle unexpected stderr:\n{s}", .{stderr.buffered()}); - } - stderr.tossBuffered(); - } - switch (check) { - .errors => { - var error_aw: std.Io.Writer.Allocating = .init(gpa); - defer error_aw.deinit(); - try error_bundle.renderToWriter(.{ - .include_reference_trace = false, - .include_source_line = false, - .include_log_text = true, - }, &error_aw.writer); - try std.testing.expectEqualStrings(expected, error_aw.written()); - }, - .stdout, .exit, .lldb => if (error_bundle.errorMessageCount() != 0) { - try error_bundle.renderToStderr(io, .{}, .auto); - fatal("unexpected compile errors", .{}); - }, - } - comp.state = .idle; - break :cmd; - }, - else => {}, // Ignore other messages, - } - } - - const buffered_stderr = stderr.buffered(); - if (buffered_stderr.len > 0) { - if (comp.allow_compiler_stderr) { - std.log.info("stderr:\n{s}", .{buffered_stderr}); - } else { - fatal("unexpected stderr:\n{s}", .{buffered_stderr}); - } - } - - try comp.exit(io); - fatal("compiler failed to send terminating error_bundle", .{}); - }, - } - std.debug.assert(try contents_r.discardRemaining() == 0); - switch (cmd) { - .todo, .skip => {}, - else => allow_skip = false, - } - } else |err| switch (err) { - else => |e| return e, - error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, - } - if (compiler) |*comp| { - if (comp.state != .idle) return error.MissingCheck; - try comp.exit(io); - } -} -const DelimitedReader = struct { - const Io = std.Io; - - unlimited: *Io.Reader, - remaining: Io.Limit, - delimiter: []const u8, - interface: Io.Reader, - - pub fn init(reader: *Io.Reader, delimiter: []const u8, buffer: []u8) DelimitedReader { - return .{ - .unlimited = reader, - .remaining = .nothing, - .delimiter = delimiter, - .interface = .{ - .vtable = &.{ - .stream = stream, - .discard = discard, - }, - .buffer = buffer, - .seek = 0, - .end = 0, - }, - }; - } - fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { - const d: *DelimitedReader = @fieldParentPtr("interface", r); - const block = try d.unlimited.peekGreedy(d.delimiter.len); - d.remaining = .limited(std.mem.findPos(u8, block, d.remaining.toInt().?, d.delimiter) orelse - block.len - d.delimiter.len + 1); - if (d.remaining == .nothing) return error.EndOfStream; - const n = try d.unlimited.stream(w, limit.min(d.remaining)); - d.remaining = d.remaining.subtract(n).?; - return n; - } - fn discard(r: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { - const d: *DelimitedReader = @fieldParentPtr("interface", r); - const block = try d.unlimited.peekGreedy(d.delimiter.len); - d.remaining = .limited(std.mem.findPos(u8, block, d.remaining.toInt().?, d.delimiter) orelse - block.len - d.delimiter.len + 1); - if (d.remaining == .nothing) return error.EndOfStream; - const n = try d.unlimited.discard(limit.min(d.remaining)); - d.remaining = d.remaining.subtract(n).?; - return n; - } -}; -const Compiler = struct { - prog_node: std.Progress.Node, - allow_compiler_stderr: bool, - output_mode: std.lang.OutputMode, - root_name: []const u8, - state: enum { idle, update }, - child: std.process.Child, - fw: std.Io.File.Writer, - mr_buffer: std.Io.File.MultiReader.Buffer(2), - mr: std.Io.File.MultiReader, - client: std.zig.Client, - - const Target = struct { - triple: []const u8, - resolved: std.Target, - mode: Mode, - backend: Backend, - - const Mode = enum { - whole, - incremental, - }; - - const Backend = enum { - /// Run semantic analysis only. Runtime output will not be tested, but we still verify - /// that compilation succeeds. Corresponds to `-fno-emit-bin`. - sema, - /// Use the self-hosted code generation backend for this target. - /// Corresponds to `-fno-llvm -fno-lld`. - selfhosted, - /// Use the LLVM backend. - /// Corresponds to `-fllvm -flld`. - llvm, - /// Use the C backend. The output is compiled with `zig cc`. - /// Corresponds to `-ofmt=c`. - cbe, - }; - }; - - fn exit(comp: *Compiler, io: std.Io) !void { - comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { - error.WriteFailed => switch (comp.fw.err.?) { - error.BrokenPipe => {}, - else => |e| fatal("failed to send exit: {t}", .{e}), - }, - }; - comp.child.stdin.?.close(io); - comp.child.stdin = null; - while (true) { - const header = comp.client.receiveMessageWithMultiReader( - &comp.mr, - .none, - ) catch |err| switch (err) { - error.Timeout => unreachable, - error.EndOfStream => |e| { - if (comp.client.in.bufferedLen() == 0) break; - return e; - }, - else => |e| return e, - }; - try comp.client.in.discardAll(header.bytes_len); - } - const term = comp.child.wait(io) catch |err| fatal("child process failed: {t}", .{err}); - comp.prog_node.end(); - switch (term) { - .exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}), - .signal => |sig| fatal("compiler terminated with signal {t}", .{sig}), - .stopped => |sig| fatal("compiler stopped unexpectedly with signal {t}", .{sig}), - .unknown => fatal("compiler terminated unexpectedly", .{}), - } - } -}; diff --git a/test/tests.zig b/test/tests.zig index d92863cc88bded40c15102941b11d3ec5f748178..ac6cca3ab78d3325daac58214975254b7ef3da9f 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3356,6 +3356,10 @@ pub fn addIncrementalTests( defer tests_dir.close(b.graph.io); var test_it = tests_dir.iterate(); while (try test_it.next(b.graph.io)) |@"test"| { + for (options.test_filters) |filter| { + if (std.mem.find(u8, @"test".name, filter) != null) break; + } else if (options.test_filters.len > 0) continue; + const test_path = tests_path.path(b, @"test".name); switch (@"test".kind) { else => continue, @@ -3400,11 +3404,9 @@ pub fn addIncrementalTests( test_target.backend, }); - if (options.test_target_filters.len > 0) { - for (options.test_target_filters) |filter| { - if (std.mem.find(u8, target_str, filter) != null) break; - } else continue; - } + for (options.test_target_filters) |filter| { + if (std.mem.find(u8, target_str, filter) != null) break; + } else if (options.test_target_filters.len > 0) continue; run.addArgs(&.{ "--target", target_str }); } diff --git a/tools/doctest.zig b/tools/doctest.zig index 18b73798d496f29e46660c213e19da67e8afcfa7..f39c56d4d12c65cca732f4c9dd9aea620b5f6ba9 100644 --- a/tools/doctest.zig +++ b/tools/doctest.zig @@ -314,6 +314,7 @@ fn printOutput( switch (getExternalExecutor(io, &target, .{ .host_cpu_arch = host.cpu.arch, .host_os_tag = host.os.tag, + .link_mode = code.link_mode orelse .dynamic, .link_libc = code.link_libc, })) { .native => {}, -- 2.54.0