From 11958a306f30268b17454a25bc365e67aad84efd Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sun, 13 Sep 2026 06:31:29 -0400 Subject: [PATCH 01/23] test: implement new runner --- build.zig | 21 + lib/compiler/Maker/Step.zig | 5 +- lib/compiler/Maker/Step/Run.zig | 615 ++++++++++++++---- lib/std/Build.zig | 5 +- lib/std/Build/Configuration.zig | 3 +- lib/std/Build/Serialize.zig | 2 +- lib/std/Build/Step/Run.zig | 23 +- lib/std/Io/Reader/Limited.zig | 6 +- lib/std/Io/Threaded.zig | 67 +- lib/std/bit_set.zig | 9 +- lib/std/process.zig | 2 + lib/std/zig/Client.zig | 28 + .../change_panic_handler_explicit/0-main.zig | 46 ++ .../change_panic_handler_explicit/1-main.zig | 46 ++ .../change_panic_handler_explicit/2-main.zig | 46 ++ .../change_panic_handler_explicit/manifest | 23 + test/incremental2/hello | 28 + test/src/runner.zig | 279 ++++++++ test/standalone/build.zig | 1 + test/tests.zig | 87 ++- tools/runner-check.zig | 151 +++++ 21 files changed, 1323 insertions(+), 170 deletions(-) create mode 100644 test/incremental2/change_panic_handler_explicit/0-main.zig create mode 100644 test/incremental2/change_panic_handler_explicit/1-main.zig create mode 100644 test/incremental2/change_panic_handler_explicit/2-main.zig create mode 100644 test/incremental2/change_panic_handler_explicit/manifest create mode 100644 test/incremental2/hello create mode 100644 test/src/runner.zig create mode 100644 tools/runner-check.zig diff --git a/build.zig b/build.zig index 6b94dd1d195da678e7ef1335d68464e8fd9dcadc..df065484e061352b3f5a33eebcc3c969d7d87b93 100644 --- a/build.zig +++ b/build.zig @@ -804,6 +804,27 @@ pub fn build(b: *std.Build) !void { .skip_wasm = skip_wasm, .max_rss = 4_300_000_000, })) |test_libc_nsz_step| test_step.dependOn(test_libc_nsz_step); + + const runner = b.addExecutable(.{ + .name = "runner", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/src/runner.zig"), + .target = b.graph.host, + }), + }); + test_step.dependOn(try tests.addNewIncrementalTests(b, runner, .{ + .test_filters = test_filters, + .test_target_filters = test_target_filters, + .skip_non_native = skip_non_native, + .skip_wasm = skip_wasm, + .skip_freebsd = skip_freebsd, + .skip_netbsd = skip_netbsd, + .skip_openbsd = skip_openbsd, + .skip_windows = skip_windows, + .skip_darwin = skip_darwin, + .skip_linux = skip_linux, + .skip_llvm = skip_llvm, + })); } fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void { diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig index f52640d0a490cb148467672db759d1051765a12a..97537845a7d52f2e35cbaddd999c40f4fdc6da0e 100644 --- a/lib/compiler/Maker/Step.zig +++ b/lib/compiler/Maker/Step.zig @@ -270,7 +270,10 @@ pub fn make( .compile => break :t null, .run => { const run_flags: Configuration.Step.Run.Flags = @bitCast(flags); - if (run_flags.stdio == .zig_test) break :t null; + switch (run_flags.stdio) { + .infer_from_args, .inherit, .check => {}, + .zig_test, .protocol => break :t null, + } }, else => {}, } diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 7f5afff5cbb1968bb25537a716a715a9efb20506..0dc9b7acd60aba42460f5e326cd0143dd740693f 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -73,18 +73,24 @@ pub fn make( man.hash.add(conf_run.flags.color); man.hash.add(conf_run.flags.disable_zig_progress); - var any_dep_files = false; + var any_discovered_inputs = switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => false, + .protocol => true, + }; var any_output_args = false; var any_cli_positionals = false; - for (conf_run.args.slice) |arg_index| { + for (switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => conf_run.args.slice, + .protocol => conf_run.args.slice[0..1], // arguments communicated over protocol + }) |arg_index| { const arg = arg_index.get(conf); try argv_list.ensureUnusedCapacity(gpa, 1); switch (arg.flags.tag) { .string => { - const prefix = arg.prefix.value.?.slice(conf); - argv_list.appendAssumeCapacity(prefix); - man.hash.addBytesZ(prefix); + const string = arg.prefix.value.?.slice(conf); + argv_list.appendAssumeCapacity(string); + man.hash.addBytesZ(string); }, .path_file => { const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; @@ -118,14 +124,14 @@ pub fn make( result.writer.writeAll(prefix) catch return error.OutOfMemory; const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); defer file.close(io); var file_reader = file.reader(io, &.{}); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => switch (file_reader.err.?) { error.Canceled => |e| return e, - else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), + else => |e| return step.fail(maker, "failed to read from {qf}: {t}", .{ file_path, e }), }, error.WriteFailed => return error.OutOfMemory, }; @@ -133,8 +139,8 @@ pub fn make( argv_list.appendAssumeCapacity(result.written()); man.hash.addBytesZ(prefix); - man.hash.addBytesZ(suffix); _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); }, .artifact => { const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; @@ -168,7 +174,7 @@ pub fn make( man.hash.addBytesZ(suffix); man.hash.add(arg.flags.dep_file); - any_dep_files = any_dep_files or arg.flags.dep_file; + if (arg.flags.dep_file) any_discovered_inputs = true; any_output_args = true; // Add a placeholder into the argument list because we need the @@ -176,9 +182,10 @@ pub fn make( // object directory is computed. try output_placeholders.append(gpa, .{ .index = @intCast(argv_list.items.len), + .offset = 0, .arg_index = arg_index, }); - argv_list.items.len += 1; + _ = argv_list.addOneAssumeCapacity(); }, .passthru => { any_cli_positionals = true; @@ -195,14 +202,285 @@ pub fn make( } } - man.hash.add(conf_run.flags.test_runner_mode); - if (conf_run.flags.test_runner_mode) { - const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); - - try argv_list.ensureUnusedCapacity(gpa, 3); - argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); - argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); - argv_list.appendAssumeCapacity("--listen=-"); + var owned_dirs: std.bit_set.Dynamic = .{}; + var inherit_dirs: std.ArrayList(Io.Dir) = .empty; + var owned_files: std.bit_set.Dynamic = .{}; + var inherit_files: std.ArrayList(Io.File) = .empty; + var protocol_args: std.ArrayList(u8) = .empty; + defer { + for (inherit_dirs.items, 0..) |inherit_dir, inherit_dir_index| + if (owned_dirs.isSet(inherit_dir_index)) inherit_dir.close(io); + for (inherit_files.items, 0..) |inherit_file, inherit_file_index| + if (owned_files.isSet(inherit_file_index)) inherit_file.close(io); + owned_dirs.deinit(gpa); + inherit_dirs.deinit(gpa); + owned_files.deinit(gpa); + inherit_files.deinit(gpa); + protocol_args.deinit(gpa); + } + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => {}, + .zig_test => { + const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); + + try argv_list.ensureUnusedCapacity(gpa, 3); + argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); + argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); + argv_list.appendAssumeCapacity("--listen=-"); + }, + .protocol => for (conf_run.args.slice[1..]) |arg_index| { + const arg = arg_index.get(conf); + switch (arg.flags.tag) { + .string => { + const string = arg.prefix.value.?.slice(conf); + + man.hash.addBytesZ(string); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + string.len + 1); + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); + protocol_args.appendSliceAssumeCapacity(string); + protocol_args.appendAssumeCapacity(0); + }, + .path_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .path_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const dir_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + const has_sub_path = dir_path.sub_path.len > 0; + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, has_sub_path); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + const dir = if (has_sub_path) + dir_path.root_dir.handle.openDir(io, dir_path.sub_path, .{ + .iterate = true, + }) catch |err| return step.fail(maker, "unable to open input dir {qf}: {t}", .{ dir_path, err }) + else + dir_path.root_dir.handle; + inherit_dirs.appendAssumeCapacity(dir); + + man.hash.addBytesZ(prefix); + man.hash.addOptionalBytes(dir_path.root_dir.path); + man.hash.addBytes(dir_path.sub_path); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_dir)); + const dir_handle: *align(1) Io.Dir.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle))); + dir_handle.* = dir.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file_content)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + + const file_path = producer_make_comp.installed_path orelse + maker.generatedPath(producer.generated_bin.value.?); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input artifact {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_file)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_files.resize(gpa, owned_files.bit_length + 1, false); + try inherit_files.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_files.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_files.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_dir)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, false); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_dirs.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_dirs.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .passthru => if (maker.run_args) |run_args| { + man.hash.addListOfBytes(run_args); + + for (run_args) |run_arg| { + try protocol_args.ensureUnusedCapacity(gpa, 1 + run_arg.len + 1); + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); + protocol_args.appendSliceAssumeCapacity(run_arg); + protocol_args.appendAssumeCapacity(0); + } + }, + .enable_darling => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), + .enable_qemu => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), + .enable_rosetta => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), + .enable_wasmtime => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), + .enable_wine => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + } + }, } switch (conf_run.stdin.u) { @@ -227,7 +505,7 @@ pub fn make( } switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .zig_test => {}, + .infer_from_args, .inherit, .zig_test, .protocol => {}, .check => { man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else ""); man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else ""); @@ -256,7 +534,7 @@ pub fn make( conf_run.captured_stdout.value == null and conf_run.captured_stderr.value == null, .inherit => true, - .check, .zig_test => false, + .check, .zig_test, .protocol => false, }; if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) { @@ -268,13 +546,22 @@ pub fn make( return; } - if (!any_dep_files) { + if (!any_discovered_inputs) { // We already know the final output paths; use them directly. const digest = if (has_side_effects) man.hash.final() else man.missDigestHex(); const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; try populateGeneratedStdIo(maker, &conf_run, &digest); - try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, argv_list.items); - try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, output_dir_path, null); + try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => .{ .argv = argv_list.items }, + .protocol => .{ .protocol = .{ + .owned_dirs = &owned_dirs, + .inherit_dirs = inherit_dirs.items, + .owned_files = &owned_files, + .inherit_files = inherit_files.items, + .args = protocol_args.items, + } }, + }); + try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, inherit_dirs.items, inherit_files.items, protocol_args.items, has_side_effects, output_dir_path, null); if (!has_side_effects) try step.finalizeManifestAndWatch(maker, &man); return; } @@ -284,8 +571,17 @@ pub fn make( io.random(@ptrCast(&rand_int)); const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, argv_list.items); - try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, tmp_dir_path, null); + try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => .{ .argv = argv_list.items }, + .protocol => .{ .protocol = .{ + .owned_dirs = &owned_dirs, + .inherit_dirs = inherit_dirs.items, + .owned_files = &owned_files, + .inherit_files = inherit_files.items, + .args = protocol_args.items, + } }, + }); + try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, inherit_dirs.items, inherit_files.items, protocol_args.items, has_side_effects, tmp_dir_path, null); for (output_placeholders.items) |placeholder| { const arg = placeholder.arg_index.get(conf); @@ -296,10 +592,10 @@ pub fn make( var diagnostic: Cache.DepTokenizer.Token = undefined; man.addInputDepFile(generated_path, &diagnostic) catch |err| switch (err) { error.OutOfMemory, error.Canceled => |e| return e, - error.InvalidDepFile => return step.fail(maker, "failed adding dep file {f} to cache: {f}", .{ + error.InvalidDepFile => return step.fail(maker, "failed adding dep file {qf} to cache: {f}", .{ generated_path, diagnostic, }), - else => |e| return step.fail(maker, "failed adding dep file {f} to cache: {t}", .{ + else => |e| return step.fail(maker, "failed adding dep file {qf} to cache: {t}", .{ generated_path, e, }), }; @@ -307,13 +603,13 @@ pub fn make( var diagnostic: Cache.Manifest.AddDiscoveredDepFileDiagnostic = undefined; man.addDiscoveredDepFile(generated_path, &diagnostic) catch |err| switch (err) { error.OutOfMemory, error.Canceled => |e| return e, - error.InvalidDepFile => return step.fail(maker, "failed adding dep file {f} to cache: {f}", .{ + error.InvalidDepFile => return step.fail(maker, "failed adding dep file {qf} to cache: {f}", .{ generated_path, diagnostic.dep_tokenizer, }), - error.FileSystemFailure => return step.fail(maker, "failed adding a path from dep file {f} to cache: {f}", .{ + error.FileSystemFailure => return step.fail(maker, "failed adding a path from dep file {qf} to cache: {f}", .{ generated_path, diagnostic.add_discovered_path, }), - else => |e| return step.fail(maker, "failed adding dep file {f} to cache: {t}", .{ + else => |e| return step.fail(maker, "failed adding dep file {qf} to cache: {t}", .{ generated_path, e, }), }; @@ -342,7 +638,7 @@ pub fn make( ) catch |err| switch (err) { error.DirNotEmpty => { dst_path.root_dir.handle.deleteTree(io, dst_path.sub_path) catch |del_err| - return step.fail(maker, "failed to remove tree {f}: {t}", .{ dst_path, del_err }); + return step.fail(maker, "failed to remove tree {qf}: {t}", .{ dst_path, del_err }); Dir.rename( tmp_path.root_dir.handle, @@ -350,11 +646,11 @@ pub fn make( dst_path.root_dir.handle, dst_path.sub_path, io, - ) catch |retry_err| return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ + ) catch |retry_err| return step.fail(maker, "failed to rename directory {qf} to {qf}: {t}", .{ tmp_path, dst_path, retry_err, }); }, - else => return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ + else => return step.fail(maker, "failed to rename directory {qf} to {qf}: {t}", .{ tmp_path, dst_path, err, }), }; @@ -386,19 +682,30 @@ fn thirdPartyToggle( enable: ?Configuration.String, disable: ?Configuration.String, ) void { - if (setting) { - if (enable) |string| { - const slice = string.slice(conf); - if (man_hash) |h| h.addBytesZ(slice); - argv_list.appendAssumeCapacity(slice); - } - } else { - if (disable) |string| { - const slice = string.slice(conf); - if (man_hash) |h| h.addBytesZ(slice); - argv_list.appendAssumeCapacity(slice); - } - } + const string = (if (setting) enable else disable) orelse return; + const slice = string.slice(conf); + if (man_hash) |h| h.addBytesZ(slice); + argv_list.appendAssumeCapacity(slice); +} + +fn thirdPartyToggleProtocol( + gpa: Allocator, + man_hash: *Cache.HashHelper, + protocol_args: *std.ArrayList(u8), + conf: *const Configuration, + setting: bool, + enable: ?Configuration.String, + disable: ?Configuration.String, +) Allocator.Error!void { + const string = (if (setting) enable else disable) orelse return; + const slice = string.slice(conf); + + man_hash.addBytesZ(slice); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + slice.len + 1); + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); + protocol_args.appendSliceAssumeCapacity(slice); + protocol_args.appendAssumeCapacity(0); } /// Reads stdout of a Zig test process until a termination condition is reached: @@ -412,6 +719,7 @@ fn waitZigTest( maker: *Maker, child: *process.Child, progress_node: std.Progress.Node, + protocol_args: []const u8, multi_reader: *Io.File.MultiReader, opt_metadata: *?TestMetadata, results: *Step.TestResults, @@ -444,6 +752,14 @@ fn waitZigTest( .out = &stdin_writer.interface, }; + if (protocol_args.len > 0) { + try client.serveMessageHeader(.{ + .tag = .args, + .bytes_len = @intCast(protocol_args.len), + }); + try client.out.writeAll(protocol_args); + } + if (opt_metadata.*) |*md| { // Previous unit test process died or was killed; we're continuing where it left off requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; @@ -492,6 +808,7 @@ fn waitZigTest( var body_r: std.Io.Reader = .fixed(body); switch (header.tag) { + else => {}, // ignore other messages .zig_version => { if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( maker, @@ -583,7 +900,6 @@ fn waitZigTest( requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; }, - else => {}, // ignore other messages } } } @@ -820,6 +1136,7 @@ const FuzzTestRunner = struct { } switch (header.tag) { + else => {}, // ignore other messages .zig_version => { if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( maker, @@ -882,7 +1199,6 @@ const FuzzTestRunner = struct { f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); } }, - else => {}, // ignore other messages } instance.message.clearRetainingCapacity(); @@ -1146,6 +1462,7 @@ fn evalZigTest( maker: *Maker, progress_node: std.Progress.Node, spawn_options: process.SpawnOptions, + protocol_args: []const u8, fuzz_context: ?FuzzContext, ) !void { if (fuzz_context != null) { @@ -1194,6 +1511,7 @@ fn evalZigTest( maker, &child, progress_node, + protocol_args, &multi_reader, &test_metadata, &test_results, @@ -1402,7 +1720,7 @@ fn evalGeneric( var write_buffer: [1024]u8 = undefined; var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { - error.ReadFailed => return step.fail(maker, "failed to read from {f}: {t}", .{ + error.ReadFailed => return step.fail(maker, "failed to read from {qf}: {t}", .{ path, file_reader.err.?, }), error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{ @@ -1490,6 +1808,7 @@ fn evalGeneric( const IndexedOutput = struct { index: u32, + offset: u32, arg_index: Configuration.Step.Run.Arg.Index, }; @@ -1516,13 +1835,16 @@ pub fn rerunInFuzzMode( var argv_list: std.ArrayList([]const u8) = .empty; defer argv_list.deinit(gpa); - for (conf_run.args.slice) |arg_index| { + for (switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => conf_run.args.slice, + .protocol => @panic("protocol fuzz unimplemented"), + }) |arg_index| { const arg = arg_index.get(conf); try argv_list.ensureUnusedCapacity(gpa, 1); switch (arg.flags.tag) { .string => { - const prefix = arg.prefix.value.?.slice(conf); - argv_list.appendAssumeCapacity(prefix); + const string = arg.prefix.value.?.slice(conf); + argv_list.appendAssumeCapacity(string); }, .path_file => { const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; @@ -1550,14 +1872,14 @@ pub fn rerunInFuzzMode( result.writer.writeAll(prefix) catch return error.OutOfMemory; const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); defer file.close(io); var file_reader = file.reader(io, &.{}); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => switch (file_reader.err.?) { error.Canceled => |e| return e, - else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), + else => |e| return step.fail(maker, "failed to read from {qf}: {t}", .{ file_path, e }), }, error.WriteFailed => return error.OutOfMemory, }; @@ -1591,14 +1913,17 @@ pub fn rerunInFuzzMode( .enable_wine => thirdPartyToggle(null, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), } } + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => {}, + .zig_test => { + const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); - if (conf_run.flags.test_runner_mode) { - const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); - - try argv_list.ensureUnusedCapacity(gpa, 3); - argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); - argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); - argv_list.appendAssumeCapacity("--listen=-"); + try argv_list.ensureUnusedCapacity(gpa, 3); + argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); + argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); + argv_list.appendAssumeCapacity("--listen=-"); + }, + .protocol => unreachable, } step.clearFailedCommand(gpa); @@ -1607,7 +1932,7 @@ pub fn rerunInFuzzMode( var rand_int: u64 = undefined; io.random(@ptrCast(&rand_int)); const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, has_side_effects, tmp_dir_path, .{ + try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, &.{}, &.{}, &.{}, has_side_effects, tmp_dir_path, .{ .fuzz = fuzz, }); } @@ -1633,7 +1958,16 @@ fn populateGeneratedPathsCreateDirs( maker: *Maker, output_dir_path: []const u8, output_placeholders: []const IndexedOutput, - argv: [][]const u8, + populate: union(enum) { + argv: [][]const u8, + protocol: struct { + owned_dirs: *std.bit_set.Dynamic, + inherit_dirs: []Io.Dir, + owned_files: *std.bit_set.Dynamic, + inherit_files: []Io.File, + args: []u8, + }, + }, ) !void { const step = maker.stepByIndex(run_index); const conf = &maker.scanned_config.configuration; @@ -1653,16 +1987,52 @@ fn populateGeneratedPathsCreateDirs( const create_path: Path = .{ .root_dir = cache_root, .sub_path = switch (arg.flags.tag) { + else => unreachable, .output_file => Dir.path.dirname(generated_path.sub_path).?, .output_directory => generated_path.sub_path, - else => unreachable, }, }; create_path.root_dir.handle.createDirPath(io, create_path.sub_path) catch |err| - return step.fail(maker, "unable to make path {f}: {t}", .{ create_path, err }); + return step.fail(maker, "unable to make path {qf}: {t}", .{ create_path, err }); - const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path, arg.flags.make_absolute); - argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix }); + switch (populate) { + .argv => |argv| { + const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path, arg.flags.make_absolute); + argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix }); + }, + .protocol => |protocol| switch (arg.flags.tag) { + else => unreachable, + .output_file => { + const file = generated_path.root_dir.handle.createFile( + io, + generated_path.sub_path, + .{ .read = true }, + ) catch |err| return step.fail(maker, "unable to create output file {qf}: {t}", .{ + generated_path, err, + }); + protocol.inherit_files[placeholder.index] = file; + protocol.owned_files.set(placeholder.index); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol.args[placeholder.offset..][0..@sizeOf(Io.File.Handle)]); + file_handle.* = file.handle; + }, + .output_directory => { + assert(generated_path.sub_path.len > 0); + const dir = generated_path.root_dir.handle.openDir( + io, + generated_path.sub_path, + .{ .iterate = true }, + ) catch |err| return step.fail(maker, "unable to open output dir {qf}: {t}", .{ + generated_path, err, + }); + protocol.inherit_dirs[placeholder.index] = dir; + protocol.owned_dirs.set(placeholder.index); + const dir_handle: *align(1) Io.Dir.Handle = + @ptrCast(protocol.args[placeholder.offset..][0..@sizeOf(Io.Dir.Handle)]); + dir_handle.* = dir.handle; + }, + }, + } } } @@ -1711,6 +2081,9 @@ fn runCommand( maker: *Maker, progress_node: std.Progress.Node, argv: []const []const u8, + inherit_dirs: []const Io.Dir, + inherit_files: []const Io.File, + protocol_args: []const u8, has_side_effects: bool, output_dir_path: []const u8, fuzz_context: ?FuzzContext, @@ -1777,10 +2150,14 @@ fn runCommand( progress_node, argv, &environ_map, + inherit_dirs, + inherit_files, + protocol_args, has_side_effects, fuzz_context, ) catch |err| term: { switch (err) { + else => {}, error.InvalidExe, // cpu arch mismatch error.FileNotFound, // can happen with a wrong dynamic linker path => interpret: { @@ -1928,6 +2305,9 @@ fn runCommand( progress_node, interp_argv.items, &environ_map, + inherit_dirs, + inherit_files, + protocol_args, has_side_effects, fuzz_context, ) catch |e| { @@ -1937,21 +2317,21 @@ fn runCommand( }; }, error.MakeFailed, error.OutOfMemory, error.Canceled => |e| return e, - else => {}, } return step.fail(maker, "failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); }; - const generic_result = opt_generic_result orelse { - assert(conf_run.flags.stdio == .zig_test); - // Specific errors have already been reported, and test results are populated. All we need - // to do is report step failure if any test failed. - if (!step.test_results.isSuccess()) return error.MakeFailed; - return; + const generic_result = switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => opt_generic_result.?, + .zig_test, .protocol => { + assert(opt_generic_result == null); + // Specific errors have already been reported, and test results are populated. All we need + // to do is report step failure if any test failed. + if (!step.test_results.isSuccess()) return error.MakeFailed; + return; + }, }; - assert(fuzz_context == null); - assert(conf_run.flags.stdio != .zig_test); // Capture stdout and stderr to GeneratedFile objects. const Stream = struct { @@ -1978,7 +2358,7 @@ fn runCommand( const sub_path_parent = output_path.dirname().?; sub_path_parent.root_dir.handle.createDirPath(io, sub_path_parent.sub_path) catch |err| - return step.fail(maker, "unable to make path {f}: {t}", .{ sub_path_parent, err }); + return step.fail(maker, "unable to make path {qf}: {t}", .{ sub_path_parent, err }); const data = switch (stream.trim_whitespace) { .none => stream.bytes.?, @@ -1989,12 +2369,12 @@ fn runCommand( output_path.root_dir.handle.writeFile(io, .{ .sub_path = output_path.sub_path, .data = data, - }) catch |err| return step.fail(maker, "unable to write file {f}: {t}", .{ output_path, err }); + }) catch |err| return step.fail(maker, "unable to write file {qf}: {t}", .{ output_path, err }); } } switch (conf_run.flags.stdio) { - .zig_test => unreachable, + .zig_test, .protocol => unreachable, .check => { if (conf_run.expect_stderr_exact.value) |bytes| { const expected_bytes = bytes.slice(conf); @@ -2186,6 +2566,9 @@ fn spawnChildAndCollect( progress_node: std.Progress.Node, argv: []const []const u8, environ_map: *EnvMap, + inherit_dirs: []const Io.Dir, + inherit_files: []const Io.File, + protocol_args: []const u8, has_side_effects: bool, fuzz_context: ?FuzzContext, ) !?EvalGenericResult { @@ -2199,7 +2582,10 @@ fn spawnChildAndCollect( if (fuzz_context != null) { assert(!has_side_effects); - assert(conf_run.flags.stdio == .zig_test); + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => unreachable, + .zig_test, .protocol => {}, + } } const child_cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd| @@ -2237,20 +2623,22 @@ fn spawnChildAndCollect( .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, .inherit => .inherit, .check => .ignore, - .zig_test => .pipe, + .zig_test, .protocol => .pipe, }, .stdout = if (conf_run.captured_stdout.value != null) .pipe else switch (conf_run.flags.stdio) { .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, .inherit => .inherit, .check => if (checksContainStdout(&conf_run)) .pipe else .ignore, - .zig_test => .pipe, + .zig_test, .protocol => .pipe, }, .stderr = if (conf_run.captured_stderr.value != null) .pipe else switch (conf_run.flags.stdio) { .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .pipe, .inherit => if (maker.protocol_server == null) .inherit else .pipe, .check => .pipe, - .zig_test => .pipe, + .zig_test, .protocol => .pipe, }, + .inherit_dirs = inherit_dirs, + .inherit_files = inherit_files, }; if (maker.protocol_server != null) { @@ -2263,35 +2651,38 @@ fn spawnChildAndCollect( assert(spawn_options.stderr != .inherit); } - if (conf_run.flags.stdio == .zig_test) { - try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); - const started: Io.Clock.Timestamp = .now(io, .awake); - const result = evalZigTest(run, run_index, maker, progress_node, spawn_options, fuzz_context) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| e, - }; - step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); - try result; - return null; - } else { - const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; - if (!conf_run.flags.disable_zig_progress and !inherit) { - spawn_options.progress_node = progress_node; - } - const terminal_mode: Io.Terminal.Mode = if (inherit) m: { - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - break :m stderr.terminal_mode; - } else .no_color; - defer if (inherit) io.unlockStderr(); - try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode); + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => { + const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; + if (!conf_run.flags.disable_zig_progress and !inherit) { + spawn_options.progress_node = progress_node; + } + const terminal_mode: Io.Terminal.Mode = if (inherit) m: { + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + break :m stderr.terminal_mode; + } else .no_color; + defer if (inherit) io.unlockStderr(); + try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode); - const started: Io.Clock.Timestamp = .now(io, .awake); - const result = evalGeneric(arena, run_index, maker, spawn_options) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| e, - }; - step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); - return try result; + const started: Io.Clock.Timestamp = .now(io, .awake); + const result = evalGeneric(arena, run_index, maker, spawn_options) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| e, + }; + step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); + return try result; + }, + .zig_test, .protocol => { + try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); + const started: Io.Clock.Timestamp = .now(io, .awake); + const result = evalZigTest(run, run_index, maker, progress_node, spawn_options, protocol_args, fuzz_context) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| e, + }; + step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); + try result; + return null; + }, } } @@ -2341,7 +2732,7 @@ fn setColorEnvironmentVariables( .auto => { const capture_stderr = conf_run.captured_stderr.value != null or switch (conf_run.flags.stdio) { .check => checksContainStderr(conf_run), - .infer_from_args, .inherit, .zig_test => false, + .infer_from_args, .inherit, .zig_test, .protocol => false, }; if (capture_stderr) { continue :color .disable; diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 47993f66d4edb8905c6c497288825c103e80ba9a..730c6bcba58f9c216929acd6157f30c497508a6b 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -768,9 +768,8 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { const run_step = Step.Run.create(b, step_name); run_step.producer = exe; + run_step.addArtifactArg(exe); if (exe.kind == .@"test") { - run_step.addArtifactArg(exe); - const test_server_mode: bool = s: { if (exe.test_runner) |r| break :s r.mode == .server; if (exe.use_llvm == false) { @@ -802,8 +801,6 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { // communicate failure via its exit code. run_step.expectExitCode(0); } - } else { - run_step.addArtifactArg(exe); } return run_step; diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig index efcd9d86ed32f16d6cb1b24427f098fbcbf94747..f0c3ff44d92678e6fbdf945e9d12883b75f95049 100644 --- a/lib/std/Build/Configuration.zig +++ b/lib/std/Build/Configuration.zig @@ -665,7 +665,7 @@ pub const Step = extern struct { pub const Tag = enum(u2) { none, bytes, lazy_path }; }; pub const TrimWhitespace = enum(u2) { none, all, leading, trailing }; - pub const StdIo = enum(u2) { infer_from_args, inherit, check, zig_test }; + pub const StdIo = enum(u3) { infer_from_args, inherit, check, zig_test, protocol }; pub const ExpectTermStatus = enum(u2) { exited, signal, stopped, unknown }; @@ -675,7 +675,6 @@ pub const Step = extern struct { skip_foreign_checks: bool, failing_to_execute_foreign_is_an_error: bool, has_side_effects: bool, - test_runner_mode: bool, color: Color, stdin: StdIn.Tag, stdio: StdIo, diff --git a/lib/std/Build/Serialize.zig b/lib/std/Build/Serialize.zig index eae93e66e46515fc5d5f3e33634966cc6cc9bbaf..802bf4cbcff33db51ca5c406828e5325bb707895 100644 --- a/lib/std/Build/Serialize.zig +++ b/lib/std/Build/Serialize.zig @@ -462,13 +462,13 @@ pub fn write(b: *std.Build, wc: *Configuration.Wip, writer: *std.Io.Writer) !voi .skip_foreign_checks = run.skip_foreign_checks, .failing_to_execute_foreign_is_an_error = run.failing_to_execute_foreign_is_an_error, .has_side_effects = run.has_side_effects, - .test_runner_mode = run.test_runner_mode, .color = run.color, .stdio = switch (run.stdio) { .infer_from_args => .infer_from_args, .inherit => .inherit, .check => .check, .zig_test => .zig_test, + .protocol => .protocol, }, .stdin = switch (run.stdin) { .none => .none, diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 7d271e10e1461606574b96a6966332975b39b706..e8a26ea36ba163c115df3be63abed8db84ec59d2 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -92,7 +92,6 @@ captured_stdout: ?*CapturedStdIo, captured_stderr: ?*CapturedStdIo, has_side_effects: bool, -test_runner_mode: bool = false, /// If this Run step was produced by a Compile step, it is tracked here. producer: ?*Step.Compile, @@ -127,7 +126,10 @@ pub const StdIo = union(enum) { check: std.ArrayList(Check), /// This Run step is running a zig unit test binary and will communicate /// extra metadata over the IPC protocol. + /// Deprecated, use `.protocol`. zig_test, + /// This Run step is running a binary that will communicate over the IPC protocol. + protocol, pub const Check = union(enum) { expect_stderr_exact: []const u8, @@ -243,10 +245,13 @@ pub fn setName(run: *Run, name: []const u8) void { run.rename_step_with_output_arg = false; } +/// Deprecated, use `enableProtocolMode`. pub fn enableTestRunnerMode(run: *Run) void { - if (run.test_runner_mode) return; run.stdio = .zig_test; - run.test_runner_mode = true; +} + +pub fn enableProtocolMode(run: *Run) void { + run.stdio = .protocol; } pub const ArgOptions = struct { @@ -750,8 +755,10 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void { } pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath { - assert(run.stdio != .inherit); - assert(run.stdio != .zig_test); + switch (run.stdio) { + .infer_from_args, .check => {}, + .inherit, .zig_test, .protocol => unreachable, + } const b = run.step.owner; const graph = b.graph; @@ -771,8 +778,10 @@ pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa } pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath { - assert(run.stdio != .inherit); - assert(run.stdio != .zig_test); + switch (run.stdio) { + .infer_from_args, .check => {}, + .inherit, .zig_test, .protocol => unreachable, + } const b = run.step.owner; const graph = b.graph; diff --git a/lib/std/Io/Reader/Limited.zig b/lib/std/Io/Reader/Limited.zig index bc372de06b1e023638f648c8cbd7533f3fd7dec3..fc1e219176dfa8b5ffb37ba6d2759a34f3d2f69b 100644 --- a/lib/std/Io/Reader/Limited.zig +++ b/lib/std/Io/Reader/Limited.zig @@ -28,8 +28,7 @@ pub fn init(reader: *Reader, limit: Limit, buffer: []u8) Limited { fn stream(r: *Reader, w: *Writer, limit: Limit) Reader.StreamError!usize { const l: *Limited = @fieldParentPtr("interface", r); if (l.remaining == .nothing) return error.EndOfStream; - const combined_limit = limit.min(l.remaining); - const n = try l.unlimited.stream(w, combined_limit); + const n = try l.unlimited.stream(w, limit.min(l.remaining)); l.remaining = l.remaining.subtract(n).?; return n; } @@ -53,8 +52,7 @@ test stream { fn discard(r: *Reader, limit: Limit) Reader.Error!usize { const l: *Limited = @fieldParentPtr("interface", r); if (l.remaining == .nothing) return error.EndOfStream; - const combined_limit = limit.min(l.remaining); - const n = try l.unlimited.discard(combined_limit); + const n = try l.unlimited.discard(limit.min(l.remaining)); l.remaining = l.remaining.subtract(n).?; return n; } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3b688ecc93c36631fa0c0bad423155abb8020eeb..58216a3497264988026f05449f5df4450f11e49f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4462,7 +4462,7 @@ fn dirCreateFilePosix( var fl_flags: usize = fl: { const syscall: Syscall = try .start(); while (true) { - const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(u32, 0)); switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); @@ -5058,7 +5058,7 @@ fn dirOpenFilePosix( var fl_flags: usize = fl: { const syscall: Syscall = try .start(); while (true) { - const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(u32, 0)); switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); @@ -15230,16 +15230,12 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null); for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeSentinel(u8, arg, 0)).ptr; - const prog_fileno = 3; - comptime assert(@max(posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO) + 1 == prog_fileno); - const env_block = env_block: { - const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno; if (options.environ_map) |environ_map| break :env_block try environ_map.createPosixBlock(arena, .{ - .zig_progress_fd = prog_fd, + .zig_progress_fd = prog_pipe[1], }); break :env_block try t.environ.process_environ.createPosixBlock(arena, .{ - .zig_progress_fd = prog_fd, + .zig_progress_fd = prog_pipe[1], }); }; @@ -15267,23 +15263,42 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked; const ep1 = err_pipe[1]; - setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(ep1, err); - setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(ep1, err); - setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(ep1, err); - + // Must happen befor clobbering fds below. switch (options.cwd) { .inherit => {}, - .dir => |cwd| { - fchdir(cwd.handle) catch |err| forkBail(ep1, err); - }, - .path => |cwd| { - chdir(cwd) catch |err| forkBail(ep1, err); - }, + .dir => |cwd| fchdir(cwd.handle) catch |err| forkBail(ep1, err), + .path => |cwd| chdir(cwd) catch |err| forkBail(ep1, err), } - // Must happen after fchdir above, the cwd file descriptor might be - // equal to prog_fileno and be clobbered by this dup2 call. - if (prog_pipe[1] != -1) dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(ep1, err); + for (options.inherit_dirs) |dir| switch (posix.errno( + posix.system.fcntl(dir.handle, posix.F.SETFD, @as(u32, 0)), + )) { + .SUCCESS => {}, + else => |err| forkBail(ep1, posix.unexpectedErrno(err)), + }; + for (options.inherit_files) |file| switch (posix.errno( + posix.system.fcntl(file.handle, posix.F.SETFD, @as(u32, 0)), + )) { + .SUCCESS => {}, + else => |err| forkBail(ep1, posix.unexpectedErrno(err)), + }; + if (prog_pipe[1] != -1) switch (posix.errno( + posix.system.fcntl(prog_pipe[1], posix.F.SETFD, @as(u32, 0)), + )) { + .SUCCESS => {}, + else => |err| forkBail(ep1, posix.unexpectedErrno(err)), + }; + for ( + [_]process.SpawnOptions.StdIo{ options.stdin, options.stdout, options.stderr }, + [_]posix.fd_t{ stdin_pipe[0], stdout_pipe[1], stderr_pipe[1] }, + [_]posix.fd_t{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }, + ) |stdio, pipe_fd, std_fd| switch (stdio) { + .pipe => dup2(pipe_fd, std_fd) catch |err| forkBail(ep1, err), + .close => closeFd(std_fd), + .inherit => {}, + .ignore => dup2(dev_null_fd, std_fd) catch |err| forkBail(ep1, err), + .file => |file| dup2(file.handle, std_fd) catch |err| forkBail(ep1, err), + }; if (options.gid) |gid| { switch (posix.errno(posix.system.setregid(gid, gid))) { @@ -15746,16 +15761,6 @@ fn destroyPipe(pipe: [2]posix.fd_t) void { if (pipe[0] != pipe[1]) closeFd(pipe[1]); } -fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { - switch (stdio) { - .pipe => try dup2(pipe_fd, std_fileno), - .close => closeFd(std_fileno), - .inherit => {}, - .ignore => try dup2(dev_null_fd, std_fileno), - .file => |file| try dup2(file.handle, std_fileno), - } -} - fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { const t: *Threaded = @ptrCast(@alignCast(userdata)); diff --git a/lib/std/bit_set.zig b/lib/std/bit_set.zig index f3e477cff91fb5ea7da76b9235ac7d3cfa47d512..b758697d63d6f6e288520e8d3d881a79a8a66d85 100644 --- a/lib/std/bit_set.zig +++ b/lib/std/bit_set.zig @@ -723,13 +723,8 @@ pub const Dynamic = struct { // That slot holds the size of the true allocation, which // is needed by Zig's allocator interface in case a shrink // fails. - - // Don't modify this value. Ideally it would go in const data so - // modifications would cause a bus error, but the only way - // to discard a const qualifier is through intFromPtr, which - // cannot currently round trip at comptime. - var empty_masks_data = [_]MaskInt{ 0, undefined }; - const empty_masks_ptr = empty_masks_data[1..2]; + const empty_masks_data = [_]MaskInt{ 0, undefined }; + const empty_masks_ptr = @constCast(empty_masks_data[1..2]); /// Creates a bit set with no elements present. /// If bit_length is not zero, deinit must eventually be called. diff --git a/lib/std/process.zig b/lib/std/process.zig index 651d854edb623b25437e418640fa54f7f4e6c67a..716c02f6f0875f524f830566ed099bf35dcb013b 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -390,6 +390,8 @@ pub const SpawnOptions = struct { stdin: StdIo = .inherit, stdout: StdIo = .inherit, stderr: StdIo = .inherit, + inherit_dirs: []const Dir = &.{}, + inherit_files: []const File = &.{}, /// Set to true to obtain rusage information for the child process. /// Depending on the target platform and implementation status, the diff --git a/lib/std/zig/Client.zig b/lib/std/zig/Client.zig index cedda191b99042affaba76eec838fd9f332abdbb..d2902a53cbe30a37a0b32ee4184e0081df8185dc 100644 --- a/lib/std/zig/Client.zig +++ b/lib/std/zig/Client.zig @@ -60,6 +60,7 @@ pub const Message = struct { start_fuzzing, /// The message body has the same format as in Server. new_fuzz_input, + args, /// Asks the server to run a list of steps. /// Body is a `BuildSteps`. @@ -69,6 +70,33 @@ pub const Message = struct { _, }; + pub const Arg = enum(u8) { + /// Trailing: + /// * string: [:0]const u8, + string, + /// Trailing: + /// * string: [:0]const u8, + prefix, + /// Trailing: + /// * string: [:0]const u8, + suffix, + /// Trailing: + /// * handle: Handle, + input_file, + /// Trailing: + /// * handle: Handle, + input_dir, + /// Trailing: + /// * handle: Handle, + input_file_content, + /// Trailing: + /// * handle: Handle, + output_file, + /// Trailing: + /// * handle: Handle, + output_dir, + }; + /// Trailing: /// * step_indices: [step_count]std.Build.Configuration.Step.Index, pub const BuildSteps = extern struct { diff --git a/test/incremental2/change_panic_handler_explicit/0-main.zig b/test/incremental2/change_panic_handler_explicit/0-main.zig new file mode 100644 index 0000000000000000000000000000000000000000..608b4a8f1beac504fd564bc89705d2c676076b7b --- /dev/null +++ b/test/incremental2/change_panic_handler_explicit/0-main.zig @@ -0,0 +1,46 @@ +pub fn main() !u8 { + var a: u8 = undefined; + a = 255; + _ = a + 1; + return 1; +} +const no_panic = std.debug.no_panic; +pub const panic = struct { + pub const call = myPanic; + pub fn integerOverflow() noreturn { + @panic("integer overflow"); + } + pub const sentinelMismatch = no_panic.sentinelMismatch; + pub const unwrapError = no_panic.unwrapError; + pub const outOfBounds = no_panic.outOfBounds; + pub const startGreaterThanEnd = no_panic.startGreaterThanEnd; + pub const inactiveUnionField = no_panic.inactiveUnionField; + pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; + pub const reachedUnreachable = no_panic.reachedUnreachable; + pub const unwrapNull = no_panic.unwrapNull; + pub const castToNull = no_panic.castToNull; + pub const incorrectAlignment = no_panic.incorrectAlignment; + pub const invalidErrorCode = no_panic.invalidErrorCode; + pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; + pub const integerOutOfBounds = no_panic.integerOutOfBounds; + pub const shlOverflow = no_panic.shlOverflow; + pub const shrOverflow = no_panic.shrOverflow; + pub const divideByZero = no_panic.divideByZero; + pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; + pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; + pub const corruptSwitch = no_panic.corruptSwitch; + pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; + pub const invalidEnumValue = no_panic.invalidEnumValue; + pub const forLenMismatch = no_panic.forLenMismatch; + pub const copyLenMismatch = no_panic.copyLenMismatch; + pub const memcpyAlias = no_panic.memcpyAlias; + pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; +}; +fn myPanic(msg: []const u8, _: ?usize) noreturn { + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + stdout_writer.interface.print("panic message: {s}\n", .{msg}) catch {}; + std.process.exit(0); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental2/change_panic_handler_explicit/1-main.zig b/test/incremental2/change_panic_handler_explicit/1-main.zig new file mode 100644 index 0000000000000000000000000000000000000000..a1a8742a597b78f18e449dbc60348c4974bb5874 --- /dev/null +++ b/test/incremental2/change_panic_handler_explicit/1-main.zig @@ -0,0 +1,46 @@ +pub fn main() !u8 { + var a: u8 = undefined; + a = 255; + _ = a + 1; + return 1; +} +const no_panic = std.debug.no_panic; +pub const panic = struct { + pub const call = myPanic; + pub fn integerOverflow() noreturn { + @panic("integer overflow"); + } + pub const sentinelMismatch = no_panic.sentinelMismatch; + pub const unwrapError = no_panic.unwrapError; + pub const outOfBounds = no_panic.outOfBounds; + pub const startGreaterThanEnd = no_panic.startGreaterThanEnd; + pub const inactiveUnionField = no_panic.inactiveUnionField; + pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; + pub const reachedUnreachable = no_panic.reachedUnreachable; + pub const unwrapNull = no_panic.unwrapNull; + pub const castToNull = no_panic.castToNull; + pub const incorrectAlignment = no_panic.incorrectAlignment; + pub const invalidErrorCode = no_panic.invalidErrorCode; + pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; + pub const integerOutOfBounds = no_panic.integerOutOfBounds; + pub const shlOverflow = no_panic.shlOverflow; + pub const shrOverflow = no_panic.shrOverflow; + pub const divideByZero = no_panic.divideByZero; + pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; + pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; + pub const corruptSwitch = no_panic.corruptSwitch; + pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; + pub const invalidEnumValue = no_panic.invalidEnumValue; + pub const forLenMismatch = no_panic.forLenMismatch; + pub const copyLenMismatch = no_panic.copyLenMismatch; + pub const memcpyAlias = no_panic.memcpyAlias; + pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; +}; +fn myPanic(msg: []const u8, _: ?usize) noreturn { + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + stdout_writer.interface.print("new panic message: {s}\n", .{msg}) catch {}; + std.process.exit(0); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental2/change_panic_handler_explicit/2-main.zig b/test/incremental2/change_panic_handler_explicit/2-main.zig new file mode 100644 index 0000000000000000000000000000000000000000..9b6143b2fa2dc39db42a6bdccc82acaf428986b7 --- /dev/null +++ b/test/incremental2/change_panic_handler_explicit/2-main.zig @@ -0,0 +1,46 @@ +pub fn main() !u8 { + var a: u8 = undefined; + a = 255; + _ = a + 1; + return 1; +} +const no_panic = std.debug.no_panic; +pub const panic = struct { + pub const call = myPanicNew; + pub fn integerOverflow() noreturn { + @panic("integer overflow"); + } + pub const sentinelMismatch = no_panic.sentinelMismatch; + pub const unwrapError = no_panic.unwrapError; + pub const outOfBounds = no_panic.outOfBounds; + pub const startGreaterThanEnd = no_panic.startGreaterThanEnd; + pub const inactiveUnionField = no_panic.inactiveUnionField; + pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; + pub const reachedUnreachable = no_panic.reachedUnreachable; + pub const unwrapNull = no_panic.unwrapNull; + pub const castToNull = no_panic.castToNull; + pub const incorrectAlignment = no_panic.incorrectAlignment; + pub const invalidErrorCode = no_panic.invalidErrorCode; + pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; + pub const integerOutOfBounds = no_panic.integerOutOfBounds; + pub const shlOverflow = no_panic.shlOverflow; + pub const shrOverflow = no_panic.shrOverflow; + pub const divideByZero = no_panic.divideByZero; + pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; + pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; + pub const corruptSwitch = no_panic.corruptSwitch; + pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; + pub const invalidEnumValue = no_panic.invalidEnumValue; + pub const forLenMismatch = no_panic.forLenMismatch; + pub const copyLenMismatch = no_panic.copyLenMismatch; + pub const memcpyAlias = no_panic.memcpyAlias; + pub const noreturnReturned = no_panic.noreturnReturned; + pub const loadUninstantiableType = no_panic.loadUninstantiableType; +}; +fn myPanicNew(msg: []const u8, _: ?usize) noreturn { + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + stdout_writer.interface.print("third panic message: {s}\n", .{msg}) catch {}; + std.process.exit(0); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental2/change_panic_handler_explicit/manifest b/test/incremental2/change_panic_handler_explicit/manifest new file mode 100644 index 0000000000000000000000000000000000000000..2a873447048a981cefea243c2a33c8e2e323dd7b --- /dev/null +++ b/test/incremental2/change_panic_handler_explicit/manifest @@ -0,0 +1,23 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig 0-main.zig +#update +#check stdout { +panic message: integer overflow +#} + +#write main.zig 1-main.zig +#update { +change the panic handler body#} +#check stdout { +new panic message: integer overflow +#} + +#write main.zig 2-main.zig +#update { +change the panic handler function value#} +#check stdout { +third panic message: integer overflow +#} diff --git a/test/incremental2/hello b/test/incremental2/hello new file mode 100644 index 0000000000000000000000000000000000000000..74a45d2a8b81afebe334968569949b6c82d4c397 --- /dev/null +++ b/test/incremental2/hello @@ -0,0 +1,28 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig { +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); +pub fn main() !void { + try std.Io.File.stdout().writeStreamingAll(io, "good morning\n"); +} +#} +#update +#check stdout { +good morning +#} + +#write main.zig { +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); +pub fn main() !void { + try std.Io.File.stdout().writeStreamingAll(io, "おはようございます\n"); +} +#} +#update { +change the string#} +#check stdout { +おはようございます +#} diff --git a/test/src/runner.zig b/test/src/runner.zig new file mode 100644 index 0000000000000000000000000000000000000000..2b5778a8553bc0adb0b348f9e964a762e38c6154 --- /dev/null +++ b/test/src/runner.zig @@ -0,0 +1,279 @@ +const std = @import("std"); +const Args = struct { + test_dir: ?std.Io.Dir, + manifest_file: ?std.Io.File, + zig_file: ?std.Io.File, + lib_dir: ?std.Io.Dir, + src_dir: ?std.Io.Dir, +}; +pub fn main(init: std.process.Init) !void { + var stdin_buffer: [512]u8 = undefined; + var stdout_buffer: [512]u8 = undefined; + var stderr_buffer: [512]u8 = undefined; + var stdin: std.Io.File.Reader = .initStreaming(.stdin(), init.io, &stdin_buffer); + var stdout: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); + var stderr: std.Io.File.Writer = .initStreaming(.stderr(), init.io, &stderr_buffer); + var server: std.zig.Server = .{ + .in = &stdin.interface, + .out = &stdout.interface, + }; + try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); + var args: Args = .{ + .test_dir = null, + .manifest_file = null, + .zig_file = null, + .lib_dir = null, + .src_dir = null, + }; + while (true) { + 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 init.arena.allocator().alloc(u8, hdr.bytes_len); + try server.in.readSliceAll(args_body); + var state: enum { positional, zig, lib, src } = .positional; + 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; + 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; + _ = 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, .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_dir == null); + args.test_dir = dir; + std.debug.assert(args.manifest_file == null); + args.manifest_file = try dir.openFile(init.io, "manifest", .{}); + }, + .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; + }, + } + }, + .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_file == null); + args.zig_file = file; + }, + .lib, .src => unreachable, + } + }, + } + state = .positional; + } + }, + .query_test_metadata => try server.serveTestMetadata(.{ + .names = &.{0}, + .expected_panic_msgs = &.{0}, + .string_bytes = "runner\x00", + }), + .run_test => { + const test_index = try server.receiveBody_u32(); + try server.serveBodylessMessage(.test_started); + const test_result = runTest(init.io, &stderr.interface, &args); + try stderr.flush(); + try server.serveTestResults(.{ + .index = test_index, + .flags = .{ + .status = if (test_result) .pass else |err| switch (err) { + else => status: { + if (@errorReturnTrace()) |trace| std.debug.dumpErrorReturnTrace(trace); + break :status .fail; + }, + error.SkipZigTest => .skip, + }, + .fuzz = false, + .log_err_count = 0, + .leak_count = 0, + }, + }); + }, + } + } +} +fn runTest(io: std.Io, stderr: *std.Io.Writer, args: *const Args) anyerror!void { + const manifest_file = args.manifest_file orelse return error.MissingManifestArg; + const src_dir = args.src_dir orelse return error.MissingSrcDir; + var manifest_buffer: [512]u8 = undefined; + var manifest_fr = manifest_file.reader(io, &manifest_buffer); + var skip_delimiter = false; + 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 { obj, exe, lib, 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_impl: union { + dr: DelimitedReader, + fr: std.Io.File.Reader, + } = undefined; + const contents_r: *std.Io.Reader = contents_r: { + switch (cmd) { + else => {}, + .delete => break :contents_r .ending, + } + 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, "#}", &.{}) }; + skip_delimiter = true; + break :contents_r &contents_impl.dr.interface; + } + const test_dir = args.test_dir orelse return error.ContentsFileInFileTest; + contents_file = try test_dir.openFile(io, contents_path, .{}); + contents_impl = .{ .fr = contents_file.?.reader(io, &.{}) }; + break :contents_r &contents_impl.fr.interface; + }; + switch (cmd) { + else => { + try stderr.print("{t} {s} {{\n", .{ cmd, maybe_arg orelse "" }); + _ = try contents_r.streamRemaining(stderr); + try stderr.writeAll("\n}\n"); + }, + .write => { + const file = try src_dir.createFile(io, maybe_arg orelse return error.MissingArg, .{}); + 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.MissingArg), + .update => { + var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; + const name = 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], + }; + _ = try contents_r.discardRemaining(); + try stderr.print("update({q})\n", .{name}); + update_mtime = update_mtime.addDuration(.fromSeconds(2)); + update_num += 1; + }, + } + std.debug.assert(try contents_r.discardRemaining() == 0); + } else |err| switch (err) { + else => |e| return e, + error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, + } +} +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; + } +}; diff --git a/test/standalone/build.zig b/test/standalone/build.zig index ab4262c8b90686517cf32f90b7b99263c72d3969..93488b0ad94b3126ae8d76ea73f9f64612e96120 100644 --- a/test/standalone/build.zig +++ b/test/standalone/build.zig @@ -44,6 +44,7 @@ pub fn build(b: *std.Build) void { "../../tools/generate_linux_syscalls.zig", "../../tools/process_headers.zig", "../../tools/migrate_langref.zig", + "../../tools/runner-check.zig", "../../tools/unskip_spirv_tests.zig", "../../tools/update-linux-headers.zig", "../../tools/update_clang_options.zig", diff --git a/test/tests.zig b/test/tests.zig index ff475109fb084df3c5948f05c7d9b0f0a4ffd0b5..db2973e413b4e1babb231a79435a6cd1b8f3cd07 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3365,7 +3365,7 @@ pub fn addIncrementalTests( var it = try dir.walk(b.graph.arena); while (try it.next(io)) |entry| { - if (std.mem.endsWith(u8, entry.basename, ".swp")) continue; + if (isEditorFileName(entry.basename)) continue; for (options.test_filters) |test_filter| { if (std.mem.find(u8, entry.path, test_filter)) |_| break; @@ -3435,6 +3435,91 @@ pub fn addIncrementalTests( } } +pub fn addNewIncrementalTests( + b: *std.Build, + runner: *std.Build.Step.Compile, + options: IncrementalTestOptions, +) !*Step { + const tests_step = b.step("test-new-incremental", "Run the new incremental compilation test cases"); + + const tests_path = b.path("test/incremental2"); + b.dependOnDirectoryContents(tests_path); + + var tests_dir = try b.root.openDir(b.graph.io, "test/incremental2", .{ .iterate = true }); + defer tests_dir.close(b.graph.io); + var test_it = tests_dir.iterate(); + while (try test_it.next(b.graph.io)) |@"test"| { + const test_path = tests_path.path(b, @"test".name); + switch (@"test".kind) { + else => continue, + .file => if (isEditorFileName(@"test".name)) continue, + .directory => {}, + } + + for (incremental_targets) |test_target| { + const resolved_target = b.resolveTargetQuery(test_target.target); + + if (options.skip_non_native and !isNative(&resolved_target, &b.graph.host.result)) + continue; + + const target = &resolved_target.result; + + if (options.skip_wasm and target.cpu.arch.isWasm()) continue; + + if (options.skip_freebsd and target.os.tag == .freebsd) continue; + if (options.skip_netbsd and target.os.tag == .netbsd) continue; + if (options.skip_openbsd and target.os.tag == .openbsd) continue; + if (options.skip_windows and target.os.tag == .windows) continue; + if (options.skip_darwin and target.os.tag.isDarwin()) continue; + if (options.skip_linux and target.os.tag == .linux) continue; + + if (options.skip_llvm and test_target.backend == .llvm) continue; + + const target_str = b.fmt("{s}-{t}", .{ + resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"), + 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; + } + + const run = b.addRunArtifact(runner); + run.setName(@"test".name); + run.enableProtocolMode(); + + switch (@"test".kind) { + else => continue, + .file => run.addFileArg(test_path), + .directory => run.addDirectoryArg(test_path), + } + run.addPrefixedFileArg("--zig=", .zig_exe); + run.addPrefixedDirectoryArg("--lib=", .zig_lib); + _ = run.addPrefixedOutputDirectoryArg("--src=", "src"); + run.addArgs(&.{ "--target", target_str }); + + run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc + + run.addThirdPartyEnabledArgDarling(.{ .enabled = "-fdarling" }); + run.addThirdPartyEnabledArgQemu(.{ .enabled = "-fqemu" }); + run.addThirdPartyEnabledArgRosetta(.{ .enabled = "-frosetta" }); + run.addThirdPartyEnabledArgWasmtime(.{ .enabled = "-fwasmtime" }); + run.addThirdPartyEnabledArgWine(.{ .enabled = "-fwine" }); + + tests_step.dependOn(&run.step); + } + } + + return tests_step; +} + +fn isEditorFileName(name: []const u8) bool { + return (std.mem.startsWith(u8, name, "#") and std.mem.endsWith(u8, name, "#")) or + std.mem.endsWith(u8, name, "~") or std.mem.endsWith(u8, name, ".swp"); +} + pub fn addLlvmIrTests(b: *std.Build, options: LlvmIrContext.Options) ?*Step { const step = b.step("test-llvm-ir", "Run the LLVM IR tests"); diff --git a/tools/runner-check.zig b/tools/runner-check.zig new file mode 100644 index 0000000000000000000000000000000000000000..0291d93814f99242322930cf3722f4d10af3c418 --- /dev/null +++ b/tools/runner-check.zig @@ -0,0 +1,151 @@ +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 keep_src = false; + + var arg_it = try init.minimal.args.iterateAllocator(arena); + std.debug.assert(arg_it.skip()); + while (arg_it.next()) |arg| { + if (std.mem.eql(u8, arg, "--zig")) { + zig_path_arg = arg_it.next() orelse fail("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 fail("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, "--keep-src")) { + keep_src = true; + } else { + if (test_path_arg) |_| fail("unknown arg '{s}'", .{arg}); + test_path_arg = arg; + } + } + + const test_path = test_path_arg orelse fail("missing 'path/to/test'", .{}); + const zig_path = zig_path_arg orelse fail("missing '--zig=path/to/zig'", .{}); + const lib_path = lib_path_arg orelse fail("missing '--lib=path/to/lib'", .{}); + + const test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ + .allow_directory = true, + }); + const zig_file = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); + defer zig_file.close(init.io); + const lib_dir = try std.Io.Dir.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 std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); + defer { + src_dir.close(init.io); + if (!keep_src) std.Io.Dir.cwd().deleteTree(init.io, src_dir_path) catch |err| { + std.log.warn("failed to delete tree '{s}': {t}", .{ src_dir_path, err }); + }; + } + + var child = try std.process.spawn(init.io, .{ + .argv = &.{ + zig_path, + "run", + try std.fs.path.resolveAlloc(arena, &.{ lib_path, "..", "test", "src", "runner.zig" }), + }, + .stdin = .pipe, + .stdout = .pipe, + .inherit_dirs = &.{ lib_dir, src_dir }, + .inherit_files = &.{ zig_file, test_file }, + }); + + var stdout_buffer: [512]u8 = undefined; + var stdout_reader = child.stdout.?.readerStreaming(init.io, &stdout_buffer); + var stdin_buffer: [512]u8 = undefined; + var stdin_writer = child.stdin.?.writerStreaming(init.io, &stdin_buffer); + var client: std.zig.Client = .{ + .in = &stdout_reader.interface, + .out = &stdin_writer.interface, + }; + { + const Arg = std.zig.Client.Message.Arg; + var args: std.ArrayList(u8) = .initBuffer(try arena.alloc(u8, 0 + + @sizeOf(Arg) + @sizeOf(std.Io.File.Handle) + + @sizeOf(Arg) + "--zig=\x00".len + + @sizeOf(Arg) + @sizeOf(std.Io.File.Handle) + + @sizeOf(Arg) + "--lib=\x00".len + + @sizeOf(Arg) + @sizeOf(std.Io.Dir.Handle) + + @sizeOf(Arg) + "--src=\x00".len + + @sizeOf(Arg) + @sizeOf(std.Io.Dir.Handle))); + + args.appendAssumeCapacity(@backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { + else => unreachable, + .file => .input_file, + .directory => .input_dir, + }))); + args.appendSliceAssumeCapacity(@ptrCast(&test_file.handle)); + + args.appendAssumeCapacity(@backingInt(Arg.prefix)); + args.appendSliceAssumeCapacity("--zig="); + args.appendAssumeCapacity(0); + + args.appendAssumeCapacity(@backingInt(Arg.input_file)); + args.appendSliceAssumeCapacity(@ptrCast(&zig_file.handle)); + + args.appendAssumeCapacity(@backingInt(Arg.prefix)); + args.appendSliceAssumeCapacity("--lib="); + args.appendAssumeCapacity(0); + + args.appendAssumeCapacity(@backingInt(Arg.input_dir)); + args.appendSliceAssumeCapacity(@ptrCast(&lib_dir.handle)); + + args.appendAssumeCapacity(@backingInt(Arg.prefix)); + args.appendSliceAssumeCapacity("--src="); + args.appendAssumeCapacity(0); + + args.appendAssumeCapacity(@backingInt(Arg.output_dir)); + args.appendSliceAssumeCapacity(@ptrCast(&src_dir.handle)); + + std.debug.assert(args.unusedCapacitySlice().len == 0); + + try client.serveMessageHeader(.{ + .tag = .args, + .bytes_len = @intCast(args.items.len), + }); + try client.out.writeAll(args.items); + + 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 fail( + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ @import("builtin").zig_version_string, body }, + ); + }, + .test_metadata => { + try client.in.discardAll(hdr.bytes_len); + try client.serveRunTest(0); + }, + .test_results => { + try client.in.discardAll(hdr.bytes_len); + break; + }, + } + } + try client.serveBodylessMessage(.exit); + } + _ = try child.wait(init.io); +} +fn fail(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); +} -- 2.54.0 From f63e8e802f5dc2aebe888c0051f685cab6b2571e Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 14 Sep 2026 01:28:28 -0400 Subject: [PATCH 02/23] Build: implement discovered inputs --- lib/compiler/Maker/Step/Run.zig | 564 ++++++++++++++++++-------------- lib/std/zig/Client.zig | 7 +- lib/std/zig/Server.zig | 12 + test/src/runner.zig | 57 +++- 4 files changed, 373 insertions(+), 267 deletions(-) diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 0dc9b7acd60aba42460f5e326cd0143dd740693f..6b0c40dcd9e014ce741591ccc0eeca5ba7ed2c83 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -207,6 +207,7 @@ pub fn make( var owned_files: std.bit_set.Dynamic = .{}; var inherit_files: std.ArrayList(Io.File) = .empty; var protocol_args: std.ArrayList(u8) = .empty; + var input_dirs: std.ArrayList(Cache.Path) = .empty; defer { for (inherit_dirs.items, 0..) |inherit_dir, inherit_dir_index| if (owned_dirs.isSet(inherit_dir_index)) inherit_dir.close(io); @@ -217,6 +218,7 @@ pub fn make( owned_files.deinit(gpa); inherit_files.deinit(gpa); protocol_args.deinit(gpa); + input_dirs.deinit(gpa); } switch (conf_run.flags.stdio) { .infer_from_args, .inherit, .check => {}, @@ -228,257 +230,264 @@ pub fn make( argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); argv_list.appendAssumeCapacity("--listen=-"); }, - .protocol => for (conf_run.args.slice[1..]) |arg_index| { - const arg = arg_index.get(conf); - switch (arg.flags.tag) { - .string => { - const string = arg.prefix.value.?.slice(conf); - - man.hash.addBytesZ(string); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + string.len + 1); - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); - protocol_args.appendSliceAssumeCapacity(string); - protocol_args.appendAssumeCapacity(0); - }, - .path_file => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .path_directory => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const dir_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - - const has_sub_path = dir_path.sub_path.len > 0; - try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, has_sub_path); - try inherit_dirs.ensureUnusedCapacity(gpa, 1); - const dir = if (has_sub_path) - dir_path.root_dir.handle.openDir(io, dir_path.sub_path, .{ - .iterate = true, - }) catch |err| return step.fail(maker, "unable to open input dir {qf}: {t}", .{ dir_path, err }) - else - dir_path.root_dir.handle; - inherit_dirs.appendAssumeCapacity(dir); - - man.hash.addBytesZ(prefix); - man.hash.addOptionalBytes(dir_path.root_dir.path); - man.hash.addBytes(dir_path.sub_path); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.Dir.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_dir)); - const dir_handle: *align(1) Io.Dir.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle))); - dir_handle.* = dir.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .file_content => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file_content)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .artifact => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const producer_index = arg.producer.value.?; - const producer_step = producer_index.ptr(conf); - const producer = producer_step.extended.get(conf.extra).compile; - const producer_make_comp_step = maker.stepByIndex(producer_index); - const producer_make_comp = &producer_make_comp_step.extended.compile; - - const file_path = producer_make_comp.installed_path orelse - maker.generatedPath(producer.generated_bin.value.?); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input artifact {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .output_file => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const basename = arg.basename.value.?.slice(conf); - - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(basename); - man.hash.addBytesZ(suffix); - assert(!arg.flags.dep_file); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_file)); - - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try owned_files.resize(gpa, owned_files.bit_length + 1, false); - try inherit_files.ensureUnusedCapacity(gpa, 1); - try output_placeholders.append(gpa, .{ - .index = @intCast(inherit_files.items.len), - .offset = @intCast(protocol_args.items.len), - .arg_index = arg_index, - }); - _ = inherit_files.addOneAssumeCapacity(); - - _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle)); - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .output_directory => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const basename = arg.basename.value.?.slice(conf); - - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(basename); - man.hash.addBytesZ(suffix); - assert(!arg.flags.dep_file); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.Dir.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_dir)); - - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, false); - try inherit_dirs.ensureUnusedCapacity(gpa, 1); - try output_placeholders.append(gpa, .{ - .index = @intCast(inherit_dirs.items.len), - .offset = @intCast(protocol_args.items.len), - .arg_index = arg_index, - }); - _ = inherit_dirs.addOneAssumeCapacity(); - - _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle)); - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .passthru => if (maker.run_args) |run_args| { - man.hash.addListOfBytes(run_args); - - for (run_args) |run_arg| { - try protocol_args.ensureUnusedCapacity(gpa, 1 + run_arg.len + 1); + .protocol => { + try input_dirs.append(gpa, if (conf_run.cwd.value) |lazy_cwd| + try maker.resolveLazyPathIndex(arena, lazy_cwd, run_index) + else + .cwd()); + for (conf_run.args.slice[1..]) |arg_index| { + const arg = arg_index.get(conf); + switch (arg.flags.tag) { + .string => { + const string = arg.prefix.value.?.slice(conf); + + man.hash.addBytesZ(string); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + string.len + 1); protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); - protocol_args.appendSliceAssumeCapacity(run_arg); + protocol_args.appendSliceAssumeCapacity(string); protocol_args.appendAssumeCapacity(0); - } - }, - .enable_darling => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), - .enable_qemu => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), - .enable_rosetta => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), - .enable_wasmtime => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), - .enable_wine => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + }, + .path_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .path_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const dir_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + try input_dirs.append(gpa, dir_path); + + const has_sub_path = dir_path.sub_path.len > 0; + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, has_sub_path); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + const dir = if (has_sub_path) + dir_path.root_dir.handle.openDir(io, dir_path.sub_path, .{ + .iterate = true, + }) catch |err| return step.fail(maker, "unable to open input dir {qf}: {t}", .{ dir_path, err }) + else + dir_path.root_dir.handle; + inherit_dirs.appendAssumeCapacity(dir); + + man.hash.addBytesZ(prefix); + man.hash.addOptionalBytes(dir_path.root_dir.path); + man.hash.addBytes(dir_path.sub_path); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_dir)); + const dir_handle: *align(1) Io.Dir.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle))); + dir_handle.* = dir.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file_content)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + + const file_path = producer_make_comp.installed_path orelse + maker.generatedPath(producer.generated_bin.value.?); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input artifact {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_file)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_files.resize(gpa, owned_files.bit_length + 1, false); + try inherit_files.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_files.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_files.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_dir)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, false); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_dirs.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_dirs.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .passthru => if (maker.run_args) |run_args| { + man.hash.addListOfBytes(run_args); + + for (run_args) |run_arg| { + try protocol_args.ensureUnusedCapacity(gpa, 1 + run_arg.len + 1); + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); + protocol_args.appendSliceAssumeCapacity(run_arg); + protocol_args.appendAssumeCapacity(0); + } + }, + .enable_darling => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), + .enable_qemu => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), + .enable_rosetta => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), + .enable_wasmtime => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), + .enable_wine => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + } } }, } @@ -561,7 +570,7 @@ pub fn make( .args = protocol_args.items, } }, }); - try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, inherit_dirs.items, inherit_files.items, protocol_args.items, has_side_effects, output_dir_path, null); + try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, output_dir_path, null); if (!has_side_effects) try step.finalizeManifestAndWatch(maker, &man); return; } @@ -581,7 +590,22 @@ pub fn make( .args = protocol_args.items, } }, }); - try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, inherit_dirs.items, inherit_files.items, protocol_args.items, has_side_effects, tmp_dir_path, null); + try runCommand( + arena, + run, + run_index, + maker, + progress_node, + argv_list.items, + inherit_dirs.items, + inherit_files.items, + protocol_args.items, + input_dirs.items, + &man, + has_side_effects, + tmp_dir_path, + null, + ); for (output_placeholders.items) |placeholder| { const arg = placeholder.arg_index.get(conf); @@ -720,6 +744,8 @@ fn waitZigTest( child: *process.Child, progress_node: std.Progress.Node, protocol_args: []const u8, + input_dirs: []const Cache.Path, + man: ?*Cache.Manifest, multi_reader: *Io.File.MultiReader, opt_metadata: *?TestMetadata, results: *Step.TestResults, @@ -900,6 +926,16 @@ fn waitZigTest( requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; }, + .discovered_inputs => while (body_r.takeEnum( + std.zig.Server.Message.InputDir, + .little, + )) |input_dir| try man.?.addDiscoveredPath(.{ .discovered_path = .{ + .unresolved = try input_dirs[@backingInt(input_dir)] + .join(arena, try body_r.takeSentinel(0)), + } }) else |err| switch (err) { + else => |e| return e, + error.EndOfStream => {}, + }, } } } @@ -1463,6 +1499,8 @@ fn evalZigTest( progress_node: std.Progress.Node, spawn_options: process.SpawnOptions, protocol_args: []const u8, + input_dirs: []const Cache.Path, + man: ?*Cache.Manifest, fuzz_context: ?FuzzContext, ) !void { if (fuzz_context != null) { @@ -1512,6 +1550,8 @@ fn evalZigTest( &child, progress_node, protocol_args, + input_dirs, + man, &multi_reader, &test_metadata, &test_results, @@ -1932,7 +1972,7 @@ pub fn rerunInFuzzMode( var rand_int: u64 = undefined; io.random(@ptrCast(&rand_int)); const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, &.{}, &.{}, &.{}, has_side_effects, tmp_dir_path, .{ + try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, tmp_dir_path, .{ .fuzz = fuzz, }); } @@ -2084,6 +2124,8 @@ fn runCommand( inherit_dirs: []const Io.Dir, inherit_files: []const Io.File, protocol_args: []const u8, + input_dirs: []const Cache.Path, + man: ?*Cache.Manifest, has_side_effects: bool, output_dir_path: []const u8, fuzz_context: ?FuzzContext, @@ -2153,6 +2195,8 @@ fn runCommand( inherit_dirs, inherit_files, protocol_args, + input_dirs, + man, has_side_effects, fuzz_context, ) catch |err| term: { @@ -2308,6 +2352,8 @@ fn runCommand( inherit_dirs, inherit_files, protocol_args, + input_dirs, + man, has_side_effects, fuzz_context, ) catch |e| { @@ -2569,6 +2615,8 @@ fn spawnChildAndCollect( inherit_dirs: []const Io.Dir, inherit_files: []const Io.File, protocol_args: []const u8, + input_dirs: []const Cache.Path, + man: ?*Cache.Manifest, has_side_effects: bool, fuzz_context: ?FuzzContext, ) !?EvalGenericResult { @@ -2675,7 +2723,17 @@ fn spawnChildAndCollect( .zig_test, .protocol => { try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); const started: Io.Clock.Timestamp = .now(io, .awake); - const result = evalZigTest(run, run_index, maker, progress_node, spawn_options, protocol_args, fuzz_context) catch |err| switch (err) { + const result = evalZigTest( + run, + run_index, + maker, + progress_node, + spawn_options, + protocol_args, + input_dirs, + man, + fuzz_context, + ) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| e, }; diff --git a/lib/std/zig/Client.zig b/lib/std/zig/Client.zig index d2902a53cbe30a37a0b32ee4184e0081df8185dc..786f23479de219f3fe4c2096af40bbb4cab63e60 100644 --- a/lib/std/zig/Client.zig +++ b/lib/std/zig/Client.zig @@ -60,6 +60,7 @@ pub const Message = struct { start_fuzzing, /// The message body has the same format as in Server. new_fuzz_input, + /// Body is a sequence of Arg args, /// Asks the server to run a list of steps. @@ -72,13 +73,13 @@ pub const Message = struct { pub const Arg = enum(u8) { /// Trailing: - /// * string: [:0]const u8, + /// * string: [:0]u8, string, /// Trailing: - /// * string: [:0]const u8, + /// * string: [:0]u8, prefix, /// Trailing: - /// * string: [:0]const u8, + /// * string: [:0]u8, suffix, /// Trailing: /// * handle: Handle, diff --git a/lib/std/zig/Server.zig b/lib/std/zig/Server.zig index 4c7e3a85441554dba1a6fe6d92b530d90edcbf39..6175821dbd1f4f4263f01b998672b2c9f5ef8fde 100644 --- a/lib/std/zig/Server.zig +++ b/lib/std/zig/Server.zig @@ -74,6 +74,10 @@ pub const Message = struct { broadcast_fuzz_input, /// Body is a TimeReport. time_report, + /// Body is a sequence of: + /// - root_dir: InputDir, + /// - sub_path: [:0]u8, + discovered_inputs, /// The first message sent by the server over the build system protocol. /// Body is a `Handshake`. @@ -212,6 +216,14 @@ pub const Message = struct { reserved: u7 = 0, }; }; + + /// A reference to one of the `.input_dir`s from the `.args` message. + pub const InputDir = enum(u32) { + /// Server cwd relative path, or absolute path. + cwd, + /// Numbered in the order that `.input_dir`s appear in the `.args` message. + _, + }; }; pub fn receiveMessage(s: *Server) !InMessage.Header { diff --git a/test/src/runner.zig b/test/src/runner.zig index 2b5778a8553bc0adb0b348f9e964a762e38c6154..49b0bcead722760dcbc8388b66f2df2753e86045 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -1,6 +1,9 @@ const std = @import("std"); const Args = struct { - test_dir: ?std.Io.Dir, + @"test": ?struct { + input_dir: std.zig.Server.Message.InputDir, + dir: std.Io.Dir, + }, manifest_file: ?std.Io.File, zig_file: ?std.Io.File, lib_dir: ?std.Io.Dir, @@ -19,7 +22,7 @@ pub fn main(init: std.process.Init) !void { }; try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); var args: Args = .{ - .test_dir = null, + .@"test" = null, .manifest_file = null, .zig_file = null, .lib_dir = null, @@ -39,12 +42,13 @@ pub fn main(init: std.process.Init) !void { const args_body = try init.arena.allocator().alloc(u8, hdr.bytes_len); try server.in.readSliceAll(args_body); var state: enum { positional, zig, lib, src } = .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; - switch (arg) { + arg: switch (arg) { .string => { const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; const string = args_body[args_body_offset..end]; @@ -66,7 +70,11 @@ pub fn main(init: std.process.Init) !void { continue; }, .suffix => unreachable, - .input_dir, .output_dir => { + .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)], ); @@ -76,10 +84,21 @@ pub fn main(init: std.process.Init) !void { }; switch (state) { .positional => { - std.debug.assert(args.test_dir == null); - args.test_dir = dir; + std.debug.assert(args.@"test" == null); + args.@"test" = .{ .input_dir = input_dir, .dir = dir }; std.debug.assert(args.manifest_file == null); - args.manifest_file = try dir.openFile(init.io, "manifest", .{}); + const manifest_path = "manifest"; + args.manifest_file = try dir.openFile(init.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 => { @@ -125,7 +144,7 @@ pub fn main(init: std.process.Init) !void { .run_test => { const test_index = try server.receiveBody_u32(); try server.serveBodylessMessage(.test_started); - const test_result = runTest(init.io, &stderr.interface, &args); + const test_result = runTest(init.io, &stderr.interface, &server, &args); try stderr.flush(); try server.serveTestResults(.{ .index = test_index, @@ -146,7 +165,12 @@ pub fn main(init: std.process.Init) !void { } } } -fn runTest(io: std.Io, stderr: *std.Io.Writer, args: *const Args) anyerror!void { +fn runTest( + io: std.Io, + stderr: *std.Io.Writer, + server: *std.zig.Server, + args: *const Args, +) anyerror!void { const manifest_file = args.manifest_file orelse return error.MissingManifestArg; const src_dir = args.src_dir orelse return error.MissingSrcDir; var manifest_buffer: [512]u8 = undefined; @@ -179,6 +203,7 @@ fn runTest(io: std.Io, stderr: *std.Io.Writer, args: *const Args) anyerror!void else => {}, .delete => break :contents_r .ending, } + const contents_path = contents_path: { const line_rest = line_it.rest(); if (line_rest.len > 0) break :contents_path line_rest; @@ -191,8 +216,18 @@ fn runTest(io: std.Io, stderr: *std.Io.Writer, args: *const Args) anyerror!void skip_delimiter = true; break :contents_r &contents_impl.dr.interface; } - const test_dir = args.test_dir orelse return error.ContentsFileInFileTest; - contents_file = try test_dir.openFile(io, contents_path, .{}); + + 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, &.{}) }; break :contents_r &contents_impl.fr.interface; }; -- 2.54.0 From 207c883bfbdc3099997087869234d6e109bdb0cc Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 14 Sep 2026 02:10:39 -0400 Subject: [PATCH 03/23] runner: implement skip commands --- test/src/runner.zig | 140 +++++++++++++++++++++++++++-------------- test/tests.zig | 6 +- tools/runner-check.zig | 84 ++++++++++++++++--------- 3 files changed, 152 insertions(+), 78 deletions(-) diff --git a/test/src/runner.zig b/test/src/runner.zig index 49b0bcead722760dcbc8388b66f2df2753e86045..bc9d65b0d572d0aa8d23e3650cff1d562b7d2989 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -8,8 +8,24 @@ const Args = struct { zig_file: ?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 main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + var stdin_buffer: [512]u8 = undefined; var stdout_buffer: [512]u8 = undefined; var stderr_buffer: [512]u8 = undefined; @@ -20,14 +36,19 @@ pub fn main(init: std.process.Init) !void { .in = &stdin.interface, .out = &stdout.interface, }; - try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); + var args: Args = .{ .@"test" = null, .manifest_file = null, .zig_file = null, .lib_dir = null, .src_dir = null, + .targets = .empty, + .target_bytes = .empty, }; + defer args.deinit(init.gpa); + + try server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); while (true) { const hdr = try server.receiveMessage(); switch (hdr.tag) { @@ -35,13 +56,11 @@ pub fn main(init: std.process.Init) !void { std.debug.print("unsupported message: {t}\n", .{hdr.tag}); std.process.exit(1); }, - .exit => { - std.process.exit(0); - }, + .exit => std.process.exit(0), .args => { - const args_body = try init.arena.allocator().alloc(u8, hdr.bytes_len); + const args_body = try arena.alloc(u8, hdr.bytes_len); try server.in.readSliceAll(args_body); - var state: enum { positional, zig, lib, src } = .positional; + 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) { @@ -53,7 +72,18 @@ pub fn main(init: std.process.Init) !void { 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; - _ = string; + 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(init.gpa, target); + }, + .zig => unreachable, + .lib => unreachable, + .src => unreachable, + .target => try args.addTarget(init.gpa, string), + } }, .prefix => { const end = std.mem.findScalarPos(u8, args_body, args_body_offset, 0).?; @@ -109,6 +139,7 @@ pub fn main(init: std.process.Init) !void { std.debug.assert(args.src_dir == null); args.src_dir = dir; }, + .target => unreachable, } }, .input_file, .input_file_content, .output_file => { @@ -129,22 +160,30 @@ pub fn main(init: std.process.Init) !void { std.debug.assert(args.zig_file == null); args.zig_file = file; }, - .lib, .src => unreachable, + .lib => unreachable, + .src => unreachable, + .target => unreachable, } }, } state = .positional; } }, - .query_test_metadata => try server.serveTestMetadata(.{ - .names = &.{0}, - .expected_panic_msgs = &.{0}, - .string_bytes = "runner\x00", - }), + .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 = + std.mem.sliceTo(args.target_bytes.items[args.targets.items[test_index]..], 0); try server.serveBodylessMessage(.test_started); - const test_result = runTest(init.io, &stderr.interface, &server, &args); + const test_result = runTest(init.io, &stderr.interface, &server, &args, target); try stderr.flush(); try server.serveTestResults(.{ .index = test_index, @@ -170,11 +209,13 @@ fn runTest( stderr: *std.Io.Writer, server: *std.zig.Server, args: *const Args, -) anyerror!void { + target: []const u8, +) !void { const manifest_file = args.manifest_file orelse return error.MissingManifestArg; const src_dir = args.src_dir orelse return error.MissingSrcDir; var manifest_buffer: [512]u8 = undefined; var manifest_fr = manifest_file.reader(io, &manifest_buffer); + var allow_skip = true; var skip_delimiter = false; var update_mtime = std.Io.Clock.real.now(io); var update_num: usize = 0; @@ -188,7 +229,7 @@ fn runTest( continue; } const cmd = std.meta.stringToEnum( - enum { obj, exe, lib, write, delete, update, check }, + enum { skip, obj, exe, lib, write, delete, update, check }, cmd_str, ) orelse return error.UnknownCommand; var maybe_arg = line_it.next(); @@ -198,38 +239,40 @@ fn runTest( dr: DelimitedReader, fr: std.Io.File.Reader, } = undefined; - const contents_r: *std.Io.Reader = contents_r: { - switch (cmd) { - else => {}, - .delete => break :contents_r .ending, - } + 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, "#}", &.{}) }; + skip_delimiter = true; + break :contents_r &contents_impl.dr.interface; + } - 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, "#}", &.{}) }; - 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(); - 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, &.{}) }; - break :contents_r &contents_impl.fr.interface; + contents_file = try @"test".dir.openFile(io, contents_path, .{}); + contents_impl = .{ .fr = contents_file.?.reader(io, &.{}) }; + break :contents_r &contents_impl.fr.interface; + }, }; switch (cmd) { else => { @@ -237,6 +280,10 @@ fn runTest( _ = try contents_r.streamRemaining(stderr); try stderr.writeAll("\n}\n"); }, + .skip => if (!allow_skip) + return error.NonInitialSkipCommand // #skip must appear first + else if (std.mem.eql(u8, target, maybe_arg orelse return error.MissingArg)) + return error.SkipZigTest, .write => { const file = try src_dir.createFile(io, maybe_arg orelse return error.MissingArg, .{}); defer file.close(io); @@ -262,6 +309,7 @@ fn runTest( }, } std.debug.assert(try contents_r.discardRemaining() == 0); + if (cmd != .skip) allow_skip = false; } else |err| switch (err) { else => |e| return e, error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, diff --git a/test/tests.zig b/test/tests.zig index db2973e413b4e1babb231a79435a6cd1b8f3cd07..cd5166f48e149830f910a6f837acd83b2122512d 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -2214,9 +2214,9 @@ const IncrementalTarget = struct { /// These are passed to `incr-check` as `-` strings. /// -/// If only one specific test is failing on a target, instead of entirely disabling the target here, -/// you can skip the target for that specific test only by adding a line like this to the manifest: -/// #skip_target=x86_64-linux-selfhosted +/// If only one specific test is failing on a target, instead of entirely disabling the target here, you +/// can skip the target for that specific test only by adding a line like this to top of the manifest: +/// #skip x86_64-linux-selfhosted const incremental_targets = &[_]IncrementalTarget{ // Avoid adding more CBE or LLVM targets without good reason: they're a lot slower than others // to run due to the output (C source code or LLVM IR) being built non-incrementally (by Clang diff --git a/tools/runner-check.zig b/tools/runner-check.zig index 0291d93814f99242322930cf3722f4d10af3c418..429e1e3d42ba22399348298cac7ae17509695900 100644 --- a/tools/runner-check.zig +++ b/tools/runner-check.zig @@ -5,6 +5,8 @@ pub fn main(init: std.process.Init) !void { 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); @@ -18,6 +20,13 @@ pub fn main(init: std.process.Init) !void { lib_path_arg = arg_it.next() orelse fail("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 fail("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 { @@ -72,45 +81,47 @@ pub fn main(init: std.process.Init) !void { .out = &stdin_writer.interface, }; { + var args: std.ArrayList(u8) = .empty; + defer args.deinit(init.gpa); const Arg = std.zig.Client.Message.Arg; - var args: std.ArrayList(u8) = .initBuffer(try arena.alloc(u8, 0 + - @sizeOf(Arg) + @sizeOf(std.Io.File.Handle) + - @sizeOf(Arg) + "--zig=\x00".len + - @sizeOf(Arg) + @sizeOf(std.Io.File.Handle) + - @sizeOf(Arg) + "--lib=\x00".len + - @sizeOf(Arg) + @sizeOf(std.Io.Dir.Handle) + - @sizeOf(Arg) + "--src=\x00".len + - @sizeOf(Arg) + @sizeOf(std.Io.Dir.Handle))); - args.appendAssumeCapacity(@backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { + try args.append(init.gpa, @backingInt(@as(Arg, switch ((try test_file.stat(init.io)).kind) { else => unreachable, .file => .input_file, .directory => .input_dir, }))); - args.appendSliceAssumeCapacity(@ptrCast(&test_file.handle)); + try args.appendSlice(init.gpa, @ptrCast(&test_file.handle)); - args.appendAssumeCapacity(@backingInt(Arg.prefix)); - args.appendSliceAssumeCapacity("--zig="); - args.appendAssumeCapacity(0); + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--zig="); + try args.append(init.gpa, 0); - args.appendAssumeCapacity(@backingInt(Arg.input_file)); - args.appendSliceAssumeCapacity(@ptrCast(&zig_file.handle)); + try args.append(init.gpa, @backingInt(Arg.input_file)); + try args.appendSlice(init.gpa, @ptrCast(&zig_file.handle)); - args.appendAssumeCapacity(@backingInt(Arg.prefix)); - args.appendSliceAssumeCapacity("--lib="); - args.appendAssumeCapacity(0); + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--lib="); + try args.append(init.gpa, 0); - args.appendAssumeCapacity(@backingInt(Arg.input_dir)); - args.appendSliceAssumeCapacity(@ptrCast(&lib_dir.handle)); + try args.append(init.gpa, @backingInt(Arg.input_dir)); + try args.appendSlice(init.gpa, @ptrCast(&lib_dir.handle)); - args.appendAssumeCapacity(@backingInt(Arg.prefix)); - args.appendSliceAssumeCapacity("--src="); - args.appendAssumeCapacity(0); + try args.append(init.gpa, @backingInt(Arg.prefix)); + try args.appendSlice(init.gpa, "--src="); + try args.append(init.gpa, 0); - args.appendAssumeCapacity(@backingInt(Arg.output_dir)); - args.appendSliceAssumeCapacity(@ptrCast(&src_dir.handle)); + try args.append(init.gpa, @backingInt(Arg.output_dir)); + try args.appendSlice(init.gpa, @ptrCast(&src_dir.handle)); - std.debug.assert(args.unusedCapacitySlice().len == 0); + 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, @@ -118,6 +129,16 @@ pub fn main(init: std.process.Init) !void { }); try client.out.writeAll(args.items); + var metadata: struct { + index: u32, + len: u32, + + fn next(m: *@This()) ?u32 { + if (m.len - m.index == 0) return null; + defer m.index += 1; + return m.index; + } + } = .{ .index = 0, .len = 0 }; try client.serveBodylessMessage(.query_test_metadata); while (true) { const hdr = try client.receiveMessage(); @@ -132,12 +153,17 @@ pub fn main(init: std.process.Init) !void { ); }, .test_metadata => { - try client.in.discardAll(hdr.bytes_len); - try client.serveRunTest(0); + const tm_hdr = + try client.in.takeStruct(std.zig.Server.Message.TestMetadata, .little); + try client.in.discardAll( + hdr.bytes_len - @sizeOf(std.zig.Server.Message.TestMetadata), + ); + metadata = .{ .index = 0, .len = tm_hdr.tests_len }; + try client.serveRunTest(metadata.next() orelse break); }, .test_results => { try client.in.discardAll(hdr.bytes_len); - break; + try client.serveRunTest(metadata.next() orelse break); }, } } -- 2.54.0 From 8a6475c4f79374975be61ac7be13e164956d1a5c Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 14 Sep 2026 03:24:28 -0400 Subject: [PATCH 04/23] test-new-incremental: combine targets into a single Run step Since the test protocol Client supports crashing step recovery, we can just model this as multiple tests in one binary. --- test/tests.zig | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/test/tests.zig b/test/tests.zig index cd5166f48e149830f910a6f837acd83b2122512d..eb24a266e2b19ed3a9498c1555043c018711a9d1 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3456,6 +3456,19 @@ pub fn addNewIncrementalTests( .directory => {}, } + const run = b.addRunArtifact(runner); + run.setName(@"test".name); + run.enableProtocolMode(); + + switch (@"test".kind) { + else => continue, + .file => run.addFileArg(test_path), + .directory => run.addDirectoryArg(test_path), + } + run.addPrefixedFileArg("--zig=", .zig_exe); + run.addPrefixedDirectoryArg("--lib=", .zig_lib); + _ = run.addPrefixedOutputDirectoryArg("--src=", "src"); + for (incremental_targets) |test_target| { const resolved_target = b.resolveTargetQuery(test_target.target); @@ -3486,30 +3499,16 @@ pub fn addNewIncrementalTests( } else continue; } - const run = b.addRunArtifact(runner); - run.setName(@"test".name); - run.enableProtocolMode(); - - switch (@"test".kind) { - else => continue, - .file => run.addFileArg(test_path), - .directory => run.addDirectoryArg(test_path), - } - run.addPrefixedFileArg("--zig=", .zig_exe); - run.addPrefixedDirectoryArg("--lib=", .zig_lib); - _ = run.addPrefixedOutputDirectoryArg("--src=", "src"); run.addArgs(&.{ "--target", target_str }); - - run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc - - run.addThirdPartyEnabledArgDarling(.{ .enabled = "-fdarling" }); - run.addThirdPartyEnabledArgQemu(.{ .enabled = "-fqemu" }); - run.addThirdPartyEnabledArgRosetta(.{ .enabled = "-frosetta" }); - run.addThirdPartyEnabledArgWasmtime(.{ .enabled = "-fwasmtime" }); - run.addThirdPartyEnabledArgWine(.{ .enabled = "-fwine" }); - - tests_step.dependOn(&run.step); } + + run.addThirdPartyEnabledArgDarling(.{ .enabled = "-fdarling" }); + run.addThirdPartyEnabledArgQemu(.{ .enabled = "-fqemu" }); + run.addThirdPartyEnabledArgRosetta(.{ .enabled = "-frosetta" }); + run.addThirdPartyEnabledArgWasmtime(.{ .enabled = "-fwasmtime" }); + run.addThirdPartyEnabledArgWine(.{ .enabled = "-fwine" }); + + tests_step.dependOn(&run.step); } return tests_step; -- 2.54.0 From 488c15320b6e2848dd61245303e75f7577313ff2 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 17 Sep 2026 04:17:54 -0400 Subject: [PATCH 05/23] runner: spawn compiler process --- lib/compiler/Maker/Step/Run.zig | 40 +- lib/std/Io.zig | 18 - lib/std/Io/Threaded.zig | 166 ++++---- lib/std/c.zig | 6 + lib/std/process.zig | 52 ++- test/src/runner.zig | 654 ++++++++++++++++++++++++++++++-- test/standalone/build.zig | 1 - test/tests.zig | 2 +- tools/runner-check.zig | 177 --------- 9 files changed, 762 insertions(+), 354 deletions(-) delete mode 100644 tools/runner-check.zig diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 6b0c40dcd9e014ce741591ccc0eeca5ba7ed2c83..8c4d7b5572999568fd7799d208fedcbf1291d33f 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -231,6 +231,7 @@ pub fn make( argv_list.appendAssumeCapacity("--listen=-"); }, .protocol => { + argv_list.appendAssumeCapacity("--listen=-"); try input_dirs.append(gpa, if (conf_run.cwd.value) |lazy_cwd| try maker.resolveLazyPathIndex(arena, lazy_cwd, run_index) else @@ -742,7 +743,8 @@ fn waitZigTest( run_index: Configuration.Step.Index, maker: *Maker, child: *process.Child, - progress_node: std.Progress.Node, + tests_prog_node: std.Progress.Node, + test_prog_node: std.Progress.Node, protocol_args: []const u8, input_dirs: []const Cache.Path, man: ?*Cache.Manifest, @@ -765,9 +767,6 @@ fn waitZigTest( const io = graph.io; const step = maker.stepByIndex(run_index); - var sub_prog_node: ?std.Progress.Node = null; - defer if (sub_prog_node) |n| n.end(); - const stdout = multi_reader.reader(0); const stderr = multi_reader.reader(1); @@ -788,7 +787,7 @@ fn waitZigTest( if (opt_metadata.*) |*md| { // Previous unit test process died or was killed; we're continuing where it left off - requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, md, test_prog_node) catch |err| return .{ .write_failed = err }; } else { // Running unit tests normally run.fuzz_tests.clearRetainingCapacity(); @@ -859,27 +858,27 @@ fn waitZigTest( const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; - progress_node.setEstimatedTotalItems(names.len); + tests_prog_node.setEstimatedTotalItems(names.len); opt_metadata.* = .{ .string_bytes = try arena.dupe(u8, string_bytes), .ns_per_test = try arena.alloc(u64, results.test_count), .names = names, .expected_panic_msgs = expected_panic_msgs, .next_index = 0, - .prog_node = progress_node, }; @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); active_test_index = null; last_update = .now(io, .awake); - requestNextTest(&client, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, &opt_metadata.*.?, test_prog_node) catch |err| return .{ .write_failed = err }; }, .test_started => { active_test_index = opt_metadata.*.?.next_index - 1; last_update = .now(io, .awake); }, .test_results => { + tests_prog_node.completeOne(); const md = &opt_metadata.*.?; const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; @@ -924,7 +923,7 @@ fn waitZigTest( md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); last_update = now; - requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + requestNextTest(&client, md, test_prog_node) catch |err| return .{ .write_failed = err }; }, .discovered_inputs => while (body_r.takeEnum( std.zig.Server.Message.InputDir, @@ -1549,6 +1548,7 @@ fn evalZigTest( maker, &child, progress_node, + spawn_options.progress_node, protocol_args, input_dirs, man, @@ -1681,7 +1681,6 @@ const TestMetadata = struct { expected_panic_msgs: []const u32, string_bytes: []const u8, next_index: u32, - prog_node: std.Progress.Node, fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { return .{ @@ -1704,16 +1703,14 @@ pub const CachedTestMetadata = struct { } }; -fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { +fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, test_prog_node: std.Progress.Node) !void { while (metadata.next_index < metadata.names.len) { const i = metadata.next_index; metadata.next_index += 1; if (metadata.expected_panic_msgs[i] != 0) continue; - const name = metadata.testName(i); - if (sub_prog_node.*) |n| n.end(); - sub_prog_node.* = metadata.prog_node.start(name, 0); + test_prog_node.setName(metadata.testName(i)); try client.serveRunTest(i); return; @@ -2664,9 +2661,9 @@ fn spawnChildAndCollect( .cwd = child_cwd, .environ_map = environ_map, .request_resource_usage_statistics = true, - .stdin = if (conf_run.stdin.u != .none) s: { + .stdin = if (conf_run.stdin.u != .none) stdin: { assert(conf_run.flags.stdio != .inherit); - break :s .pipe; + break :stdin .pipe; } else switch (conf_run.flags.stdio) { .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore, .inherit => .inherit, @@ -2691,10 +2688,10 @@ fn spawnChildAndCollect( if (maker.protocol_server != null) { if (spawn_options.stdin == .inherit) { - return step.fail(maker, "Cannot inherit stdin when running through over the build system protocol", .{}); + return step.fail(maker, "Cannot inherit stdin when running over the build system protocol", .{}); } if (spawn_options.stdout == .inherit) { - return step.fail(maker, "Cannot inherit stdout when running through over the build system protocol", .{}); + return step.fail(maker, "Cannot inherit stdout when running over the build system protocol", .{}); } assert(spawn_options.stderr != .inherit); } @@ -2720,7 +2717,12 @@ fn spawnChildAndCollect( step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); return try result; }, - .zig_test, .protocol => { + .zig_test, .protocol => |stdio| { + spawn_options.progress_node = if (conf_run.flags.disable_zig_progress or fuzz_context != null) + .none + else + progress_node.start(@tagName(stdio), 0); + defer spawn_options.progress_node.end(); try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?); const started: Io.Clock.Timestamp = .now(io, .awake); const result = evalZigTest( diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b1fefc704ab6a21cfa03af9c6fefcb235b22db7b..c7f94cbb9652fd9e98f6b419d629c6fc55b292e2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -213,9 +213,7 @@ pub const VTable = struct { processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void, processSetCurrentPath: *const fn (?*anyopaque, []const u8) std.process.SetCurrentPathError!void, processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError, - processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError, processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, - processSpawnPath: *const fn (?*anyopaque, Dir, std.process.SpawnOptions) std.process.SpawnError!std.process.Child, childWait: *const fn (?*anyopaque, *std.process.Child) std.process.Child.WaitError!std.process.Child.Term, childKill: *const fn (?*anyopaque, *std.process.Child) void, @@ -2797,9 +2795,7 @@ pub const failing: std.Io = .{ .processSetCurrentDir = failingProcessSetCurrentDir, .processSetCurrentPath = failingProcessSetCurrentPath, .processReplace = failingProcessReplace, - .processReplacePath = failingProcessReplacePath, .processSpawn = failingProcessSpawn, - .processSpawnPath = failingProcessSpawnPath, .childWait = unreachableChildWait, .childKill = unreachableChildKill, @@ -3451,26 +3447,12 @@ pub fn failingProcessReplace(userdata: ?*anyopaque, options: std.process.Replace return error.OperationUnsupported; } -pub fn failingProcessReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError { - _ = userdata; - _ = dir; - _ = options; - return error.OperationUnsupported; -} - pub fn failingProcessSpawn(userdata: ?*anyopaque, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { _ = userdata; _ = options; return error.OperationUnsupported; } -pub fn failingProcessSpawnPath(userdata: ?*anyopaque, dir: Dir, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { - _ = userdata; - _ = dir; - _ = options; - return error.OperationUnsupported; -} - pub fn unreachableChildWait(userdata: ?*anyopaque, child: *std.process.Child) std.process.Child.WaitError!std.process.Child.Term { _ = userdata; _ = child; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 58216a3497264988026f05449f5df4450f11e49f..50d9e464483921faa8d33179363c145c9b673bc4 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1901,9 +1901,7 @@ pub fn io(t: *Threaded) Io { .processSetCurrentDir = processSetCurrentDir, .processSetCurrentPath = processSetCurrentPath, .processReplace = processReplace, - .processReplacePath = processReplacePath, .processSpawn = processSpawn, - .processSpawnPath = processSpawnPath, .childWait = childWait, .childKill = childKill, @@ -15133,23 +15131,7 @@ fn processReplace(userdata: ?*anyopaque, options: process.ReplaceOptions) proces }); }; - return posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); -} - -fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: process.ReplaceOptions) process.ReplaceError { - if (!process.can_replace) return error.OperationUnsupported; - _ = userdata; - _ = dir; - _ = options; - @panic("TODO processReplacePath"); -} - -fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: process.SpawnOptions) process.SpawnError!process.Child { - if (!process.can_spawn) return error.OperationUnsupported; - _ = userdata; - _ = dir; - _ = options; - @panic("TODO processSpawnPath"); + return posixExec(options.exe, argv_buf.ptr, options.expand_arg0, env_block, PATH); } const processSpawn = switch (native_os) { @@ -15263,15 +15245,15 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked; const ep1 = err_pipe[1]; - // Must happen befor clobbering fds below. + // Must happen before clobbering fds below. switch (options.cwd) { .inherit => {}, .dir => |cwd| fchdir(cwd.handle) catch |err| forkBail(ep1, err), .path => |cwd| chdir(cwd) catch |err| forkBail(ep1, err), } - for (options.inherit_dirs) |dir| switch (posix.errno( - posix.system.fcntl(dir.handle, posix.F.SETFD, @as(u32, 0)), + for (options.inherit_dirs) |inherit_dir| switch (posix.errno( + posix.system.fcntl(inherit_dir.handle, posix.F.SETFD, @as(u32, 0)), )) { .SUCCESS => {}, else => |err| forkBail(ep1, posix.unexpectedErrno(err)), @@ -15331,14 +15313,14 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp } if (options.start_suspended) { - switch (posix.errno(posix.system.kill(0, .STOP))) { + switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) { .SUCCESS => {}, .PERM => forkBail(ep1, error.PermissionDenied), else => forkBail(ep1, error.Unexpected), } } - const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, env_block, PATH); + const err = posixExec(options.exe, argv_buf.ptr, options.expand_arg0, env_block, PATH); forkBail(ep1, err); } @@ -17042,61 +17024,109 @@ fn testArgvToCommandLineWindows(argv: []const []const u8, expected_cmd_line: []c try std.testing.expectEqualStrings(expected_cmd_line, cmd_line); } -fn posixExecv( - arg0_expand: process.ArgExpansion, - file: [*:0]const u8, - child_argv: [*:null]?[*:0]const u8, +fn posixExec( + exe: process.ReplaceOptions.Exe, + argv: [*:null]?[*:0]const u8, + expand_arg0: process.ArgExpansion, env_block: process.Environ.PosixBlock, PATH: []const u8, ) process.ReplaceError { - const file_slice = std.mem.sliceTo(file, 0); - if (std.mem.findScalar(u8, file_slice, '/') != null) return posixExecvPath(file, child_argv, env_block); + const arg0_slice = std.mem.span(argv[0].?); + exe: switch (exe) { + .detect => continue :exe if (std.mem.findScalar(u8, arg0_slice, '/')) |_| .{ + .path = .cwd(), + } else .search, + .search => { + // Use of PATH_MAX here is valid as the path_buf will be passed + // directly to the operating system in posixExecveat. + var path_buf: [posix.PATH_MAX]u8 = undefined; + var it = std.mem.tokenizeScalar(u8, PATH, ':'); + var err: error{ AccessDenied, FileNotFound, NotDir } = error.FileNotFound; - // Use of PATH_MAX here is valid as the path_buf will be passed - // directly to the operating system in posixExecvPath. - var path_buf: [posix.PATH_MAX]u8 = undefined; - var it = std.mem.tokenizeScalar(u8, PATH, ':'); - var seen_eacces = false; - var err: process.ReplaceError = error.FileNotFound; - - // In case of expanding arg0 we must put it back if we return with an error. - const prev_arg0 = child_argv[0]; - defer switch (arg0_expand) { - .expand => child_argv[0] = prev_arg0, - .no_expand => {}, - }; - - while (it.next()) |search_path| { - const path_len = search_path.len + file_slice.len + 1; - if (path_buf.len < path_len + 1) return error.NameTooLong; - @memcpy(path_buf[0..search_path.len], search_path); - path_buf[search_path.len] = '/'; - @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); - path_buf[path_len] = 0; - const full_path = path_buf[0..path_len :0].ptr; - switch (arg0_expand) { - .expand => child_argv[0] = full_path, - .no_expand => {}, - } - err = posixExecvPath(full_path, child_argv, env_block); - switch (err) { - error.AccessDenied => seen_eacces = true, - error.FileNotFound, error.NotDir => {}, - else => |e| return e, - } + while (it.next()) |search_path| { + const path_len = search_path.len + arg0_slice.len + 1; + if (path_buf.len < path_len + 1) return error.NameTooLong; + @memcpy(path_buf[0..search_path.len], search_path); + path_buf[search_path.len] = '/'; + @memcpy(path_buf[search_path.len + 1 ..][0..arg0_slice.len], arg0_slice); + path_buf[path_len] = 0; + const full_path = path_buf[0..path_len :0].ptr; + switch (expand_arg0) { + .expand => argv[0] = full_path, + .no_expand => {}, + } + switch (posixExecveat(posix.AT.FDCWD, full_path, argv, env_block)) { + error.AccessDenied, error.FileNotFound, error.NotDir => |e| switch (err) { + error.AccessDenied => {}, + error.FileNotFound, error.NotDir => err = e, + }, + else => |e| return e, + } + } + return err; + }, + .path => |path| return posixExecveat(path.handle, argv[0], argv, env_block), + .file => |file| return posixExecveat(file.handle, null, argv, env_block), + .explicit => |explicit| { + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const path_posix = try pathToPosix(explicit.path, &path_buffer); + return posixExecveat(explicit.dir.handle, path_posix, argv, env_block); + }, } - if (seen_eacces) return error.AccessDenied; - return err; } /// This function ignores PATH environment variable. -pub fn posixExecvPath( - path: [*:0]const u8, - child_argv: [*:null]const ?[*:0]const u8, +pub fn posixExecveat( + dir: posix.fd_t, + path: ?[*:0]const u8, + argv: [*:null]const ?[*:0]const u8, env_block: process.Environ.PosixBlock, ) process.ReplaceError { try Thread.checkCancel(); - switch (posix.errno(posix.system.execve(path, child_argv, env_block.slice.ptr))) { + switch (err: { + if (path) |p| if (dir == posix.AT.FDCWD or Dir.path.isAbsoluteZ(p)) { + break :err posix.errno(posix.system.execve(p, argv, env_block.slice.ptr)); + }; + if (comptime native_os == .linux and (!builtin.link_libc or + (builtin.target.abi.isGnu() and builtin.target.os.version_range.linux.glibc.order( + .{ .major = 2, .minor = 34, .patch = 0 }, + ).compare(.gte)))) + { + switch (posix.errno(posix.system.execveat(dir, path orelse "", argv, env_block.slice.ptr, .{ + .EMPTY_PATH = path == null, + .SYMLINK_NOFOLLOW = false, + }))) { + else => |err| break :err err, + .NOSYS => {}, + } + } + var path_buffer: [posix.PATH_MAX:0]u8 = undefined; + var path_len = realPathPosix(dir, &path_buffer) catch |err| switch (err) { + else => |e| return e, + error.InputOutput, + error.FileTooBig, + error.NoSpaceLeft, + error.PathAlreadyExists, + error.SymLinkLoop, + error.NetworkNotFound, + error.PipeBusy, + error.AntivirusInterference, + => return error.FileNotFound, + error.DeviceBusy, + error.NoDevice, + error.UnrecognizedVolume, + => return error.FileSystem, + }; + if (path) |p| { + const path_slice = std.mem.span(p); + path_buffer[path_len] = Dir.path.sep; + path_len += 1; + @memcpy(path_buffer[path_len..][0..path_slice.len], path_slice); + path_len += path_slice.len; + } + path_buffer[path_len] = 0; + break :err posix.errno(posix.system.execve(&path_buffer, argv, env_block.slice.ptr)); + }) { .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .@"2BIG" => return error.SystemResources, .MFILE => return error.ProcessFdQuotaExceeded, diff --git a/lib/std/c.zig b/lib/std/c.zig index abadb78976db809b025e6b08e30b5963b9cd95bc..4ec2d6b0ac2db23b2ffa6b846772f2f4e9b18b16 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -8809,6 +8809,11 @@ pub const O = switch (native_os) { else => void, }; +pub const EXECVEAT = switch (native_os) { + .linux => linux.EXECVEAT, + else => void, +}; + pub const MAP = switch (native_os) { .linux => linux.MAP, .emscripten => packed struct(u32) { @@ -11045,6 +11050,7 @@ pub extern "c" fn renameat(olddirfd: fd_t, old: [*:0]const u8, newdirfd: fd_t, n pub extern "c" fn chdir(path: [*:0]const u8) c_int; pub extern "c" fn fchdir(fd: fd_t) c_int; pub extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int; +pub extern "c" fn execveat(dirfd: fd_t, path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8, flags: EXECVEAT) c_int; pub extern "c" fn dup(fd: fd_t) c_int; pub extern "c" fn dup2(old_fd: fd_t, new_fd: fd_t) c_int; pub extern "c" fn dup3(old: c_int, new: c_int, flags: c_uint) c_int; diff --git a/lib/std/process.zig b/lib/std/process.zig index 716c02f6f0875f524f830566ed099bf35dcb013b..43ec3611e0c373a72e60dfdf42c0ba405af5ab4a 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -282,37 +282,40 @@ pub const ReplaceError = error{ } || Allocator.Error || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub const ReplaceOptions = struct { + exe: Exe = .detect, argv: []const []const u8, expand_arg0: ArgExpansion = .no_expand, /// Replaces the environment when provided. The PATH value from here is /// never used to resolve `argv[0]`. environ_map: ?*const Environ.Map = null, + + pub const Exe = union(enum) { + /// `argv[0]` is the name of the program to execute. If it is not already a + /// file path (i.e. it contains '/'), it is resolved into a file path based on + /// PATH from the parent environment. + detect, + /// `argv[0]` is the name of the program to execute, resolved into a file path + /// based on PATH from the parent environment. + search, + /// `argv[0]` is the file path of the program to execute, relative to `Dir` payload. + /// It is *always* treated as a file path, even if it does not contain '/'. + path: Dir, + /// `File` payload is the program to execute. + file: File, + /// `path` is the file path of the program to execute, relative to `dir`. + /// It is *always* treated as a file path, even if it does not contain '/'. + explicit: struct { dir: Dir, path: []const u8 }, + }; }; /// Replaces the current process image with the executed process. If this /// function succeeds, it does not return. /// -/// `argv[0]` is the name of the process to replace the current one with. If it -/// is not already a file path (i.e. it contains '/'), it is resolved into a -/// file path based on PATH from the parent environment. -/// /// It is illegal to call this function in a fork() child. pub fn replace(io: Io, options: ReplaceOptions) ReplaceError { return io.vtable.processReplace(io.userdata, options); } -/// Replaces the current process image with the executed process. If this -/// function succeeds, it does not return. -/// -/// `argv[0]` is the file path of the process to replace the current one with, -/// relative to `dir`. It is *always* treated as a file path, even if it does -/// not contain '/'. -/// -/// It is illegal to call this function in a fork() child. -pub fn replacePath(io: Io, dir: Io.Dir, options: ReplaceOptions) ReplaceError { - return io.vtable.processReplacePath(io.userdata, dir, options); -} - pub const ArgExpansion = enum { expand, no_expand }; /// File name extensions supported natively by `CreateProcess()` on Windows. @@ -368,6 +371,7 @@ pub const SpawnError = error{ } || Io.File.OpenError || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub const SpawnOptions = struct { + exe: ReplaceOptions.Exe = .detect, argv: []const []const u8, /// Set to change the current working directory when spawning the child process. @@ -385,7 +389,7 @@ pub const SpawnOptions = struct { /// /// The child's progress tree will be grafted into the parent's progress tree, /// by substituting this node with the child's root node. - progress_node: std.Progress.Node = std.Progress.Node.none, + progress_node: std.Progress.Node = .none, stdin: StdIo = .inherit, stdout: StdIo = .inherit, @@ -445,27 +449,16 @@ pub const SpawnOptions = struct { }; /// Creates a child process. -/// -/// `argv[0]` is the name of the program to execute. If it is not already a -/// file path (i.e. it contains '/'), it is resolved into a file path based on -/// PATH from the parent environment. pub fn spawn(io: Io, options: SpawnOptions) SpawnError!Child { return io.vtable.processSpawn(io.userdata, options); } -/// Creates a child process. -/// -/// `argv[0]` is the file path of the program to execute, relative to `dir`. It -/// is *always* treated as a file path, even if it does not contain '/'. -pub fn spawnPath(io: Io, dir: Io.Dir, options: SpawnOptions) SpawnError!Child { - return io.vtable.processSpawnPath(io.userdata, dir, options); -} - pub const RunError = error{ StreamTooLong, } || SpawnError || Io.File.MultiReader.UnendingError || Io.Timeout.Error; pub const RunOptions = struct { + exe: ReplaceOptions.Exe = .detect, argv: []const []const u8, stderr_limit: Io.Limit = .unlimited, stdout_limit: Io.Limit = .unlimited, @@ -503,6 +496,7 @@ pub const RunResult = struct { /// If it succeeds, the caller owns result.stdout and result.stderr memory. pub fn run(gpa: Allocator, io: Io, options: RunOptions) RunError!RunResult { var child = try spawn(io, .{ + .exe = options.exe, .argv = options.argv, .cwd = options.cwd, .environ_map = options.environ_map, diff --git a/test/src/runner.zig b/test/src/runner.zig index bc9d65b0d572d0aa8d23e3650cff1d562b7d2989..55e24b73348cb8eead27d3ed0e1e14052ac863f3 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -1,11 +1,218 @@ 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 test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ + .allow_directory = true, + }); + const zig_exe = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); + defer zig_exe.close(init.io); + const lib_dir = try std.Io.Dir.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 std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); + defer { + src_dir.close(init.io); + if (!keep_src) std.Io.Dir.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_file: ?std.Io.File, + zig_exe: ?std.Io.File, lib_dir: ?std.Io.Dir, src_dir: ?std.Io.Dir, targets: std.ArrayList(u32), @@ -23,15 +230,14 @@ const Args = struct { args.target_bytes.appendAssumeCapacity(0); } }; -pub fn main(init: std.process.Init) !void { - const arena = init.arena.allocator(); +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 stderr_buffer: [512]u8 = undefined; - var stdin: std.Io.File.Reader = .initStreaming(.stdin(), init.io, &stdin_buffer); - var stdout: std.Io.File.Writer = .initStreaming(.stdout(), init.io, &stdout_buffer); - var stderr: std.Io.File.Writer = .initStreaming(.stderr(), init.io, &stderr_buffer); + 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, @@ -40,16 +246,18 @@ pub fn main(init: std.process.Init) !void { var args: Args = .{ .@"test" = null, .manifest_file = null, - .zig_file = null, + .zig_exe = null, .lib_dir = null, .src_dir = null, .targets = .empty, .target_bytes = .empty, }; - defer args.deinit(init.gpa); + 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 => { @@ -77,12 +285,12 @@ pub fn main(init: std.process.Init) !void { state = .target; continue; } else if (std.mem.cutPrefix(u8, string, "--target=")) |target| { - try args.addTarget(init.gpa, target); + try args.addTarget(gpa, target); }, .zig => unreachable, .lib => unreachable, .src => unreachable, - .target => try args.addTarget(init.gpa, string), + .target => try args.addTarget(gpa, string), } }, .prefix => { @@ -118,7 +326,7 @@ pub fn main(init: std.process.Init) !void { 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(init.io, manifest_path, .{}); + args.manifest_file = try dir.openFile(io, manifest_path, .{}); try server.serveMessageHeader(.{ .tag = .discovered_inputs, @@ -157,8 +365,8 @@ pub fn main(init: std.process.Init) !void { args.manifest_file = file; }, .zig => { - std.debug.assert(args.zig_file == null); - args.zig_file = file; + std.debug.assert(args.zig_exe == null); + args.zig_exe = file; }, .lib => unreachable, .src => unreachable, @@ -180,16 +388,17 @@ pub fn main(init: std.process.Init) !void { }, .run_test => { const test_index = try server.receiveBody_u32(); - const target = + 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(init.io, &stderr.interface, &server, &args, target); - try stderr.flush(); + 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; }, @@ -205,18 +414,49 @@ pub fn main(init: std.process.Init) !void { } } fn runTest( + gpa: std.mem.Allocator, + arena: std.mem.Allocator, io: std.Io, - stderr: *std.Io.Writer, + prog_node: std.Progress.Node, server: *std.zig.Server, args: *const Args, - target: []const u8, + target_string: []const u8, ) !void { const manifest_file = args.manifest_file orelse return error.MissingManifestArg; const src_dir = args.src_dir orelse return error.MissingSrcDir; + 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| { @@ -229,12 +469,13 @@ fn runTest( continue; } const cmd = std.meta.stringToEnum( - enum { skip, obj, exe, lib, write, delete, update, check }, + enum { 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, @@ -253,7 +494,7 @@ fn runTest( break :contents_path contents_path; }; if (std.mem.eql(u8, contents_path, "{")) { - contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &.{}) }; + contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &contents_buffer) }; skip_delimiter = true; break :contents_r &contents_impl.dr.interface; } @@ -270,43 +511,263 @@ fn runTest( try server.out.flush(); contents_file = try @"test".dir.openFile(io, contents_path, .{}); - contents_impl = .{ .fr = contents_file.?.reader(io, &.{}) }; + contents_impl = .{ .fr = contents_file.?.reader(io, &contents_buffer) }; break :contents_r &contents_impl.fr.interface; }, }; - switch (cmd) { - else => { - try stderr.print("{t} {s} {{\n", .{ cmd, maybe_arg orelse "" }); - _ = try contents_r.streamRemaining(stderr); - try stderr.writeAll("\n}\n"); - }, + 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) { .skip => if (!allow_skip) return error.NonInitialSkipCommand // #skip must appear first - else if (std.mem.eql(u8, target, maybe_arg orelse return error.MissingArg)) + 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, + }); + 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.MissingArg, .{}); + 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.MissingArg), + .delete => try src_dir.deleteFile(io, maybe_arg orelse return error.MissingFileArg), .update => { - var name_buffer: [std.Progress.Node.max_name_len]u8 = undefined; - const name = 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], - }; + 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(); - try stderr.print("update({q})\n", .{name}); + 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 expected = try contents_r.allocRemaining(gpa, .unlimited); + defer gpa.free(expected); + const check = std.meta.stringToEnum( + enum { errors, stdout, exit, lldb }, + maybe_arg orelse return error.MissingCheckArg, + ) orelse return error.UnknownCheck; + + 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) { + .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 => if (error_bundle.errorMessageCount() != 0) { + @panic("TODO"); + } else fatal("expected compile errors", .{}), + .stdout, .exit, .lldb => if (error_bundle.errorMessageCount() != 0) { + try error_bundle.renderToStderr(io, .{}, .auto); + fatal("unexpected compile errors", .{}); + }, + } + comp.state = .idle; + break :cmd; + }, + .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 result_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, &.{ result_dir, bin_name }); + const bin_file = try std.Io.Dir.cwd().openFile(io, bin_path, .{}); + defer bin_file.close(io); + switch (check) { + .errors => unreachable, + .stdout, .exit => { + const result = std.process.run(arena, io, .{ + .exe = .{ .file = bin_file }, + .argv = &.{bin_path}, + }) catch continue; + 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 => {}, + } + }, + 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); if (cmd != .skip) allow_skip = false; @@ -314,6 +775,7 @@ fn runTest( else => |e| return e, error.EndOfStream => if (skip_delimiter) return error.MissingDelimiter, } + if (compiler) |*comp| try comp.exit(io); } const DelimitedReader = struct { const Io = std.Io; @@ -360,3 +822,113 @@ const DelimitedReader = struct { 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 update(comp: *Compiler, io: std.Io) !void { + comp.client.serveBodylessMessage(.update) catch |err| switch (err) { + error.WriteFailed => return comp.fw.err.?, + }; + 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) { + .error_bundle => { + return; + }, + .emit_digest => { + _ = body; + }, + else => {}, // Ignore other messages, + } + } + + const buffered_stderr = comp.mr.reader(1).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", .{}); + } + + 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/standalone/build.zig b/test/standalone/build.zig index 93488b0ad94b3126ae8d76ea73f9f64612e96120..ab4262c8b90686517cf32f90b7b99263c72d3969 100644 --- a/test/standalone/build.zig +++ b/test/standalone/build.zig @@ -44,7 +44,6 @@ pub fn build(b: *std.Build) void { "../../tools/generate_linux_syscalls.zig", "../../tools/process_headers.zig", "../../tools/migrate_langref.zig", - "../../tools/runner-check.zig", "../../tools/unskip_spirv_tests.zig", "../../tools/update-linux-headers.zig", "../../tools/update_clang_options.zig", diff --git a/test/tests.zig b/test/tests.zig index eb24a266e2b19ed3a9498c1555043c018711a9d1..0d41503255b2d3d30fac186eeeb0587c245b46a9 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3488,7 +3488,7 @@ pub fn addNewIncrementalTests( if (options.skip_llvm and test_target.backend == .llvm) continue; - const target_str = b.fmt("{s}-{t}", .{ + const target_str = b.fmt("{s}-incremental-{t}", .{ resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"), test_target.backend, }); diff --git a/tools/runner-check.zig b/tools/runner-check.zig deleted file mode 100644 index 429e1e3d42ba22399348298cac7ae17509695900..0000000000000000000000000000000000000000 --- a/tools/runner-check.zig +++ /dev/null @@ -1,177 +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); - std.debug.assert(arg_it.skip()); - while (arg_it.next()) |arg| { - if (std.mem.eql(u8, arg, "--zig")) { - zig_path_arg = arg_it.next() orelse fail("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 fail("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 fail("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) |_| fail("unknown arg '{s}'", .{arg}); - test_path_arg = arg; - } - } - - const test_path = test_path_arg orelse fail("missing 'path/to/test'", .{}); - const zig_path = zig_path_arg orelse fail("missing '--zig=path/to/zig'", .{}); - const lib_path = lib_path_arg orelse fail("missing '--lib=path/to/lib'", .{}); - - const test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ - .allow_directory = true, - }); - const zig_file = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); - defer zig_file.close(init.io); - const lib_dir = try std.Io.Dir.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 std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); - defer { - src_dir.close(init.io); - if (!keep_src) std.Io.Dir.cwd().deleteTree(init.io, src_dir_path) catch |err| { - std.log.warn("failed to delete tree '{s}': {t}", .{ src_dir_path, err }); - }; - } - - var child = try std.process.spawn(init.io, .{ - .argv = &.{ - zig_path, - "run", - try std.fs.path.resolveAlloc(arena, &.{ lib_path, "..", "test", "src", "runner.zig" }), - }, - .stdin = .pipe, - .stdout = .pipe, - .inherit_dirs = &.{ lib_dir, src_dir }, - .inherit_files = &.{ zig_file, test_file }, - }); - - var stdout_buffer: [512]u8 = undefined; - var stdout_reader = child.stdout.?.readerStreaming(init.io, &stdout_buffer); - var stdin_buffer: [512]u8 = undefined; - var stdin_writer = child.stdin.?.writerStreaming(init.io, &stdin_buffer); - var client: std.zig.Client = .{ - .in = &stdout_reader.interface, - .out = &stdin_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_file.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, - - fn next(m: *@This()) ?u32 { - if (m.len - m.index == 0) return null; - defer m.index += 1; - return m.index; - } - } = .{ .index = 0, .len = 0 }; - 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 fail( - "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); - try client.in.discardAll( - hdr.bytes_len - @sizeOf(std.zig.Server.Message.TestMetadata), - ); - metadata = .{ .index = 0, .len = tm_hdr.tests_len }; - try client.serveRunTest(metadata.next() orelse break); - }, - .test_results => { - try client.in.discardAll(hdr.bytes_len); - try client.serveRunTest(metadata.next() orelse break); - }, - } - } - try client.serveBodylessMessage(.exit); - } - _ = try child.wait(init.io); -} -fn fail(comptime fmt: []const u8, args: anytype) noreturn { - std.log.err(fmt, args); - std.process.exit(1); -} -- 2.54.0 From fbe1bbf662e339dd5f3bc01c83d236d867726854 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 19 Sep 2026 04:50:31 -0400 Subject: [PATCH 06/23] runner: make test runs deterministic --- .../{0-main.zig => 0/main.zig} | 0 .../{1-main.zig => 1/main.zig} | 0 .../{2-main.zig => 2/main.zig} | 0 .../change_panic_handler_explicit/manifest | 6 +- test/src/runner.zig | 110 +++++++----------- 5 files changed, 43 insertions(+), 73 deletions(-) rename test/incremental2/change_panic_handler_explicit/{0-main.zig => 0/main.zig} (100%) rename test/incremental2/change_panic_handler_explicit/{1-main.zig => 1/main.zig} (100%) rename test/incremental2/change_panic_handler_explicit/{2-main.zig => 2/main.zig} (100%) diff --git a/test/incremental2/change_panic_handler_explicit/0-main.zig b/test/incremental2/change_panic_handler_explicit/0/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/0-main.zig rename to test/incremental2/change_panic_handler_explicit/0/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/1-main.zig b/test/incremental2/change_panic_handler_explicit/1/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/1-main.zig rename to test/incremental2/change_panic_handler_explicit/1/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/2-main.zig b/test/incremental2/change_panic_handler_explicit/2/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/2-main.zig rename to test/incremental2/change_panic_handler_explicit/2/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/manifest b/test/incremental2/change_panic_handler_explicit/manifest index 2a873447048a981cefea243c2a33c8e2e323dd7b..3388f912ec3c0f6f111bf0750283c4c94a2b720f 100644 --- a/test/incremental2/change_panic_handler_explicit/manifest +++ b/test/incremental2/change_panic_handler_explicit/manifest @@ -2,20 +2,20 @@ -Mroot=main.zig #} -#write main.zig 0-main.zig +#write main.zig 0/main.zig #update #check stdout { panic message: integer overflow #} -#write main.zig 1-main.zig +#write main.zig 1/main.zig #update { change the panic handler body#} #check stdout { new panic message: integer overflow #} -#write main.zig 2-main.zig +#write main.zig 2/main.zig #update { change the panic handler function value#} #check stdout { diff --git a/test/src/runner.zig b/test/src/runner.zig index 55e24b73348cb8eead27d3ed0e1e14052ac863f3..9b652ee72607e48512fe2accce29d3127a6defc1 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -42,12 +42,13 @@ pub fn main(init: std.process.Init) !void { 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 test_file = try std.Io.Dir.cwd().openFile(init.io, test_path, .{ + const cwd: std.Io.Dir = .cwd(); + const test_file = try cwd.openFile(init.io, test_path, .{ .allow_directory = true, }); - const zig_exe = try std.Io.Dir.cwd().openFile(init.io, zig_path, .{}); + const zig_exe = try cwd.openFile(init.io, zig_path, .{}); defer zig_exe.close(init.io); - const lib_dir = try std.Io.Dir.cwd().openDir(init.io, lib_path, .{}); + 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: { @@ -55,10 +56,10 @@ pub fn main(init: std.process.Init) !void { init.io.random(@ptrCast(&rand_int)); break :rand_int rand_int; }); - var src_dir = try std.Io.Dir.cwd().createDirPathOpen(init.io, src_dir_path, .{}); + var src_dir = try cwd.createDirPathOpen(init.io, src_dir_path, .{}); defer { src_dir.close(init.io); - if (!keep_src) std.Io.Dir.cwd().deleteTree(init.io, src_dir_path) catch |err| { + 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 }); }; } @@ -423,7 +424,8 @@ fn runTest( target_string: []const u8, ) !void { const manifest_file = args.manifest_file orelse return error.MissingManifestArg; - const src_dir = args.src_dir orelse return error.MissingSrcDir; + 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; @@ -544,6 +546,8 @@ fn runTest( }, "-target", target.triple, + "--cache-dir", + ".zig-cache", }); switch (target.mode) { .whole => {}, @@ -663,28 +667,6 @@ fn runTest( }; const body = comp.client.in.take(header.bytes_len) catch unreachable; switch (header.tag) { - .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 => if (error_bundle.errorMessageCount() != 0) { - @panic("TODO"); - } else fatal("expected compile errors", .{}), - .stdout, .exit, .lldb => if (error_bundle.errorMessageCount() != 0) { - try error_bundle.renderToStderr(io, .{}, .auto); - fatal("unexpected compile errors", .{}); - }, - } - comp.state = .idle; - break :cmd; - }, .emit_digest => { var r: std.Io.Reader = .fixed(body); _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch @@ -708,7 +690,7 @@ fn runTest( } const digest = r.takeArray(std.Build.Cache.bin_digest_len) catch unreachable; - const result_dir = ".zig-cache" ++ std.Io.Dir.path.sep_str ++ + 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, @@ -719,16 +701,20 @@ fn runTest( .output_mode = comp.output_mode, }); const bin_path = - try std.Io.Dir.path.join(arena, &.{ result_dir, bin_name }); - const bin_file = try std.Io.Dir.cwd().openFile(io, 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(arena, io, .{ + 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, @@ -752,6 +738,28 @@ fn runTest( .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 => if (error_bundle.errorMessageCount() != 0) { + @panic("TODO"); + } else fatal("expected compile errors", .{}), + .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, } } @@ -861,44 +869,6 @@ const Compiler = struct { }; }; - fn update(comp: *Compiler, io: std.Io) !void { - comp.client.serveBodylessMessage(.update) catch |err| switch (err) { - error.WriteFailed => return comp.fw.err.?, - }; - 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) { - .error_bundle => { - return; - }, - .emit_digest => { - _ = body; - }, - else => {}, // Ignore other messages, - } - } - - const buffered_stderr = comp.mr.reader(1).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", .{}); - } - fn exit(comp: *Compiler, io: std.Io) !void { comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { error.WriteFailed => switch (comp.fw.err.?) { -- 2.54.0 From e4c1598ac8dd984a480fad41e61cc60c41448cac Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 19 Sep 2026 05:15:45 -0400 Subject: [PATCH 07/23] runner: implement error checks --- test/incremental2/bad_import | 48 ++++++++++++++++++++++++++++++++++++ test/src/runner.zig | 11 ++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 test/incremental2/bad_import diff --git a/test/incremental2/bad_import b/test/incremental2/bad_import new file mode 100644 index 0000000000000000000000000000000000000000..d93a9ed6309f963c310a82f0813d0905a564cc10 --- /dev/null +++ b/test/incremental2/bad_import @@ -0,0 +1,48 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig { +pub fn main() !void { + _ = @import("foo.zig"); + try std.Io.File.stdout().writeStreamingAll(io, "success\n"); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); +#} +#write foo.zig { +comptime { + _ = @import("bad.zig"); +} +#} +#update +#check errors { +bad.zig:1:1: error: unable to load "bad.zig": FileNotFound +foo.zig:2:17: note: file imported here +#} + +#write foo.zig { +comptime { + _ = @import("this_is/not_real.zig"); +} +#} +#update { +change bad import#} +#check errors { +this_is/not_real.zig:1:1: error: unable to load "not_real.zig": FileNotFound +foo.zig:2:17: note: file imported here +#} + +#write main.zig { +pub fn main() !void { + //_ = @import("foo.zig"); + try std.Io.File.stdout().writeStreamingAll(io, "success\n"); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); +#} +#update { +remove import of 'foo.zig'#} +#check stdout { +success +#} diff --git a/test/src/runner.zig b/test/src/runner.zig index 9b652ee72607e48512fe2accce29d3127a6defc1..e4d7e9971fc965bb4724bcdec971a5c1ed754156 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -749,9 +749,14 @@ fn runTest( stderr.tossBuffered(); } switch (check) { - .errors => if (error_bundle.errorMessageCount() != 0) { - @panic("TODO"); - } else fatal("expected compile errors", .{}), + .errors => { + var error_aw: std.Io.Writer.Allocating = .init(gpa); + defer error_aw.deinit(); + try error_bundle.renderToWriter(.{ + .include_source_line = false, + }, &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", .{}); -- 2.54.0 From 4e4e8902e5bec12befd191b6ce412d60ced819f4 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 19 Sep 2026 08:20:15 -0400 Subject: [PATCH 08/23] Build: fix UAF of step names --- lib/std/Build/Step.zig | 2 +- lib/std/Build/Step/CheckFile.zig | 2 +- lib/std/Build/Step/Run.zig | 2 +- test/tests.zig | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 78c1e3343c23acec72985f2b9d357e52dc3b977f..a4d6987126ac87113fb73ce63dca3b75c89ffe9b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -95,7 +95,7 @@ pub fn init(options: StepOptions) Step { return .{ .tag = options.tag, - .name = arena.dupe(u8, options.name) catch @panic("OOM"), + .name = options.owner.graph.dupeString(options.name), .owner = options.owner, .dependencies = .empty, .max_rss = options.max_rss, diff --git a/lib/std/Build/Step/CheckFile.zig b/lib/std/Build/Step/CheckFile.zig index 21ebbd4621c51319de1cc69579c0c59a4ec1df0d..44c0494c1e544eb9a92c36bdadfc06b9a75ed45f 100644 --- a/lib/std/Build/Step/CheckFile.zig +++ b/lib/std/Build/Step/CheckFile.zig @@ -41,5 +41,5 @@ pub fn create(owner: *std.Build, file: std.Build.LazyPath, options: Options) *Ch } pub fn setName(check_file: *CheckFile, name: []const u8) void { - check_file.step.name = name; + check_file.step.name = check_file.step.owner.graph.dupeString(name); } diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index e8a26ea36ba163c115df3be63abed8db84ec59d2..8662d5258a3553e349e0f5682c733f2b4678e8c0 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -241,7 +241,7 @@ pub fn create(owner: *std.Build, name: []const u8) *Run { } pub fn setName(run: *Run, name: []const u8) void { - run.step.name = name; + run.step.name = run.step.owner.graph.dupeString(name); run.rename_step_with_output_arg = false; } diff --git a/test/tests.zig b/test/tests.zig index 0d41503255b2d3d30fac186eeeb0587c245b46a9..b782307a3644d54a11ec3da00d150d9650ae4d11 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -2502,7 +2502,7 @@ pub fn addStandaloneTests( .simple_skip_release_small = mem.findScalar(OptimizeMode, optimize_modes, .small) == null, }); const test_cases_dep_step = test_cases_dep.builder.default_step; - test_cases_dep_step.name = b.graph.dupeString(test_cases_dep_name); + test_cases_dep_step.name = test_cases_dep_name; step.dependOn(test_cases_dep.builder.default_step); return step; } -- 2.54.0 From ec261c5e8d569a7bf90a51638d41de6053f98e1a Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 19 Sep 2026 06:52:35 -0400 Subject: [PATCH 09/23] test-incremental: convert tests to new format --- build.zig | 19 +- test/incremental/add_decl | 63 +- test/incremental/add_decl_namespaced | 65 +- ..._field_and_nested_struct_uses_changed_decl | 22 +- test/incremental/add_remove_struct_fields | 93 -- .../add_remove_struct_fields/0/main.zig | 19 + .../add_remove_struct_fields/1/main.zig | 19 + .../add_remove_struct_fields/2/main.zig | 19 + .../add_remove_struct_fields/3/main.zig | 20 + .../add_remove_struct_fields/manifest | 31 + test/incremental/add_remove_toplevel_fields | 96 -- .../add_remove_toplevel_fields/0/main.zig | 20 + .../add_remove_toplevel_fields/1/main.zig | 21 + .../add_remove_toplevel_fields/2/main.zig | 19 + .../add_remove_toplevel_fields/3/main.zig | 20 + .../add_remove_toplevel_fields/manifest | 31 + .../analysis_error_and_syntax_error | 62 +- test/incremental/bad_import | 40 +- test/incremental/change_bitpack_backing_int | 83 +- test/incremental/change_container_kind | 135 ++- test/incremental/change_embed_file | 65 +- test/incremental/change_enum_tag_type | 93 -- .../change_enum_tag_type/0/main.zig | 15 + .../change_enum_tag_type/1/main.zig | 21 + .../change_enum_tag_type/2/main.zig | 16 + .../change_enum_tag_type/3/main.zig | 12 + .../change_enum_tag_type/4/main.zig | 12 + .../incremental/change_enum_tag_type/manifest | 43 + test/incremental/change_exports | 169 --- test/incremental/change_exports/0/main.zig | 17 + test/incremental/change_exports/1/main.zig | 19 + test/incremental/change_exports/2/main.zig | 19 + test/incremental/change_exports/3/main.zig | 22 + test/incremental/change_exports/4/main.zig | 17 + test/incremental/change_exports/5/main.zig | 22 + test/incremental/change_exports/6/main.zig | 22 + test/incremental/change_exports/manifest | 54 + test/incremental/change_fn_type | 33 +- test/incremental/change_generic_line_number | 26 +- test/incremental/change_line_number | 24 +- test/incremental/change_module | 72 +- test/incremental/change_panic_handler | 33 +- .../incremental/change_panic_handler_explicit | 149 --- .../change_panic_handler_explicit/0/main.zig | 0 .../change_panic_handler_explicit/1/main.zig | 0 .../change_panic_handler_explicit/2/main.zig | 0 .../change_panic_handler_explicit/manifest | 0 .../change_reify_struct_field_type | 22 +- test/incremental/change_shift_op | 24 +- test/incremental/change_struct_same_fields | 33 +- test/incremental/change_union_tag_type | 35 +- test/incremental/change_zon_file | 69 +- .../change_zon_file_no_result_type | 26 +- test/incremental/compile_error_then_log | 32 +- test/incremental/compile_log | 38 +- test/incremental/delete_comptime_decls | 29 +- .../dependency_on_type_of_inferred_global | 23 +- test/incremental/do_nothing | 26 +- test/incremental/fix_astgen_failure | 50 +- test/incremental/function_becomes_inline | 34 +- test/incremental/hello | 24 +- test/incremental/increase_array_field_length | 23 +- test/incremental/make_decl_pub | 28 +- test/incremental/modify_inline_fn | 24 +- test/incremental/move_src | 23 +- .../pointer_to_decl_with_astgen_error | 31 +- .../recursive_function_becomes_non_recursive | 23 +- test/incremental/remove_enum_field | 30 +- .../remove_function_with_changed_callee | 22 +- .../remove_invalid_union_backing_enum | 25 +- .../struct_reification_becomes_anon_init | 23 +- test/incremental/tag_name | 32 +- ...porary_analysis_error_in_generic_signature | 35 +- test/incremental/temporary_parse_error | 31 +- test/incremental/type_becomes_comptime_only | 30 +- test/incremental/type_dependency_loop | 62 +- test/incremental/unreferenced_error | 43 +- test/incremental2/bad_import | 48 - test/incremental2/hello | 28 - test/src/runner.zig | 19 +- test/tests.zig | 99 +- tools/incr-check.zig | 1032 ----------------- 82 files changed, 1739 insertions(+), 2309 deletions(-) delete mode 100644 test/incremental/add_remove_struct_fields create mode 100644 test/incremental/add_remove_struct_fields/0/main.zig create mode 100644 test/incremental/add_remove_struct_fields/1/main.zig create mode 100644 test/incremental/add_remove_struct_fields/2/main.zig create mode 100644 test/incremental/add_remove_struct_fields/3/main.zig create mode 100644 test/incremental/add_remove_struct_fields/manifest delete mode 100644 test/incremental/add_remove_toplevel_fields create mode 100644 test/incremental/add_remove_toplevel_fields/0/main.zig create mode 100644 test/incremental/add_remove_toplevel_fields/1/main.zig create mode 100644 test/incremental/add_remove_toplevel_fields/2/main.zig create mode 100644 test/incremental/add_remove_toplevel_fields/3/main.zig create mode 100644 test/incremental/add_remove_toplevel_fields/manifest delete mode 100644 test/incremental/change_enum_tag_type create mode 100644 test/incremental/change_enum_tag_type/0/main.zig create mode 100644 test/incremental/change_enum_tag_type/1/main.zig create mode 100644 test/incremental/change_enum_tag_type/2/main.zig create mode 100644 test/incremental/change_enum_tag_type/3/main.zig create mode 100644 test/incremental/change_enum_tag_type/4/main.zig create mode 100644 test/incremental/change_enum_tag_type/manifest delete mode 100644 test/incremental/change_exports create mode 100644 test/incremental/change_exports/0/main.zig create mode 100644 test/incremental/change_exports/1/main.zig create mode 100644 test/incremental/change_exports/2/main.zig create mode 100644 test/incremental/change_exports/3/main.zig create mode 100644 test/incremental/change_exports/4/main.zig create mode 100644 test/incremental/change_exports/5/main.zig create mode 100644 test/incremental/change_exports/6/main.zig create mode 100644 test/incremental/change_exports/manifest delete mode 100644 test/incremental/change_panic_handler_explicit rename test/{incremental2 => incremental}/change_panic_handler_explicit/0/main.zig (100%) rename test/{incremental2 => incremental}/change_panic_handler_explicit/1/main.zig (100%) rename test/{incremental2 => incremental}/change_panic_handler_explicit/2/main.zig (100%) rename test/{incremental2 => incremental}/change_panic_handler_explicit/manifest (100%) delete mode 100644 test/incremental2/bad_import delete mode 100644 test/incremental2/hello delete mode 100644 tools/incr-check.zig diff --git a/build.zig b/build.zig index df065484e061352b3f5a33eebcc3c969d7d87b93..bc09ce6393c0598245d43f16e07bd5fbf72d72b7 100644 --- a/build.zig +++ b/build.zig @@ -125,7 +125,6 @@ pub fn build(b: *std.Build) !void { const skip_darwin = b.option(bool, "skip-darwin", "Main test suite skips targets with darwin OSs") orelse false; const skip_linux = b.option(bool, "skip-linux", "Main test suite skips targets with linux OS") orelse false; const skip_llvm = b.option(bool, "skip-llvm", "Main test suite skips targets that use LLVM backend") orelse false; - const skip_test_incremental = b.option(bool, "skip-test-incremental", "Main test step omits dependency on test-incremental step") orelse false; const only_install_lib_files = b.option(bool, "lib-files-only", "Only install library files") orelse false; @@ -781,22 +780,6 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(check_oracle_step); } - const test_incremental_step = b.step("test-incremental", "Run the incremental compilation test cases"); - try tests.addIncrementalTests(b, test_incremental_step, .{ - .test_filters = test_filters, - .test_target_filters = test_target_filters, - .skip_non_native = skip_non_native, - .skip_wasm = skip_wasm, - .skip_freebsd = skip_freebsd, - .skip_netbsd = skip_netbsd, - .skip_openbsd = skip_openbsd, - .skip_windows = skip_windows, - .skip_darwin = skip_darwin, - .skip_linux = skip_linux, - .skip_llvm = skip_llvm, - }); - if (!skip_test_incremental) test_step.dependOn(test_incremental_step); - if (tests.addLibcTestNszTests(b, .{ .optimize_modes = optimize_modes, .test_filters = test_filters, @@ -812,7 +795,7 @@ pub fn build(b: *std.Build) !void { .target = b.graph.host, }), }); - test_step.dependOn(try tests.addNewIncrementalTests(b, runner, .{ + test_step.dependOn(try tests.addIncrementalTests(b, runner, .{ .test_filters = test_filters, .test_target_filters = test_target_filters, .skip_non_native = skip_non_native, diff --git a/test/incremental/add_decl b/test/incremental/add_decl index 50d961da7b88f00b78b2895ebfeae18e76219d6a..5832015217c571a79a2bc5ca0ab45abf91d7a156 100644 --- a/test/incremental/add_decl +++ b/test/incremental/add_decl @@ -1,15 +1,21 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, foo); } const foo = "good morning\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good morning\n" +#} +#update +#check stdout { +good morning +#} -#update=add new declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, foo); @@ -17,10 +23,14 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good morning\n" +#} +#update { +add new declaration#} +#check stdout { +good morning +#} -#update=reference new declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, bar); @@ -28,10 +38,14 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good evening\n" +#} +#update { +reference new declaration#} +#check stdout { +good evening +#} -#update=reference missing declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, qux); @@ -39,10 +53,14 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:3:52: error: use of undeclared identifier 'qux' +#} +#update { +reference missing declaration#} +#check errors { +main.zig:3:52: error: use of undeclared identifier 'qux' +#} -#update=add missing declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, qux); @@ -51,14 +69,23 @@ const foo = "good morning\n"; const bar = "good evening\n"; const qux = "good night\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good night\n" +#} +#update { +add missing declaration#} +#check stdout { +good night +#} -#update=remove unused declarations -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, qux); } const qux = "good night\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good night\n" +#} +#update { +remove unused declarations#} +#check stdout { +good night +#} diff --git a/test/incremental/add_decl_namespaced b/test/incremental/add_decl_namespaced index e1fa74a3ad7ecd6bb70b481f73d6db0702c27d22..30bf6a81e9abb3449ad1575a20022b7d6f0d279f 100644 --- a/test/incremental/add_decl_namespaced +++ b/test/incremental/add_decl_namespaced @@ -1,15 +1,21 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().foo); } const foo = "good morning\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good morning\n" +#} +#update +#check stdout { +good morning +#} -#update=add new declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().foo); @@ -17,10 +23,14 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good morning\n" +#} +#update { +add new declaration#} +#check stdout { +good morning +#} -#update=reference new declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().bar); @@ -28,10 +38,14 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good evening\n" +#} +#update { +reference new declaration#} +#check stdout { +good evening +#} -#update=reference missing declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().qux); @@ -39,11 +53,15 @@ pub fn main() !void { const foo = "good morning\n"; const bar = "good evening\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:3:59: error: root source file struct 'main' has no member named 'qux' -#expect_error=main.zig:1:1: note: struct declared here +#} +#update { +reference missing declaration#} +#check errors { +main.zig:3:59: error: root source file struct 'main' has no member named 'qux' +main.zig:1:1: note: struct declared here +#} -#update=add missing declaration -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().qux); @@ -52,14 +70,23 @@ const foo = "good morning\n"; const bar = "good evening\n"; const qux = "good night\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good night\n" +#} +#update { +add missing declaration#} +#check stdout { +good night +#} -#update=remove unused declarations -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @This().qux); } const qux = "good night\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="good night\n" +#} +#update { +remove unused declarations#} +#check stdout { +good night +#} diff --git a/test/incremental/add_field_and_nested_struct_uses_changed_decl b/test/incremental/add_field_and_nested_struct_uses_changed_decl index 35882c10a3fcda7d06a2ca968226e3c545d29c12..731d34837bc82b9b3ffac774943f53ffad77d468 100644 --- a/test/incremental/add_field_and_nested_struct_uses_changed_decl +++ b/test/incremental/add_field_and_nested_struct_uses_changed_decl @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() void { _ = @as(S, undefined); } @@ -9,9 +12,12 @@ const SomeType = u8; const S = struct { foo: struct { inner: SomeType }, }; -#expect_stdout="" -#update=add field to outer struct, change decl used by inner struct -#file=main.zig +#} +#update +#check stdout { +#} + +#write main.zig { pub fn main() void { _ = @as(S, undefined); } @@ -22,4 +28,8 @@ const S = struct { foo: struct { inner: SomeType }, bar: u32, }; -#expect_stdout="" +#} +#update { +add field to outer struct, change decl used by inner struct#} +#check stdout { +#} diff --git a/test/incremental/add_remove_struct_fields b/test/incremental/add_remove_struct_fields deleted file mode 100644 index f5e62fad787f9c2e96a2cdcf476173732cfe32f6..0000000000000000000000000000000000000000 --- a/test/incremental/add_remove_struct_fields +++ /dev/null @@ -1,93 +0,0 @@ -#update=initial version -#file=main.zig -const S = struct { x: u8 }; -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="1 100\n" - -#update=add a field -#file=main.zig -const S = struct { x: u8, y: u16 = 200 }; -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="2 100\n" - -#update=remove all fields -#file=main.zig -const S = struct {}; -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_error=main.zig:15:24: error: no field named 'x' in struct 'main.S' -#expect_error=main.zig:1:11: note: struct declared here - -#update=remove reference to non-existent field -#file=main.zig -const S = struct {}; -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - //const val: S = .{ .x = 100 }; - //try w.print("{d}\n", .{val.x}); - try w.writeAll("\n"); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="0 \n" diff --git a/test/incremental/add_remove_struct_fields/0/main.zig b/test/incremental/add_remove_struct_fields/0/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..0481e7418f5889de45d38cb7033e498831d8305a --- /dev/null +++ b/test/incremental/add_remove_struct_fields/0/main.zig @@ -0,0 +1,19 @@ +const S = struct { x: u8 }; +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_struct_fields/1/main.zig b/test/incremental/add_remove_struct_fields/1/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..82305cdf3a00d0d56681a196b94e8cfff72710bd --- /dev/null +++ b/test/incremental/add_remove_struct_fields/1/main.zig @@ -0,0 +1,19 @@ +const S = struct { x: u8, y: u16 = 200 }; +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_struct_fields/2/main.zig b/test/incremental/add_remove_struct_fields/2/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..b490c06b2315fce7bbbd8c9c24729adcb6e12084 --- /dev/null +++ b/test/incremental/add_remove_struct_fields/2/main.zig @@ -0,0 +1,19 @@ +const S = struct {}; +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_struct_fields/3/main.zig b/test/incremental/add_remove_struct_fields/3/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..62ca3461d6ee668ee6dbd046e127b8fafead8f8b --- /dev/null +++ b/test/incremental/add_remove_struct_fields/3/main.zig @@ -0,0 +1,20 @@ +const S = struct {}; +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + //const val: S = .{ .x = 100 }; + //try w.print("{d}\n", .{val.x}); + try w.writeAll("\n"); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_struct_fields/manifest b/test/incremental/add_remove_struct_fields/manifest new file mode 100644 index 0000000000000000000000000000000000000000..2099e73cef2d89db7446cfe1c9f3a82057332248 --- /dev/null +++ b/test/incremental/add_remove_struct_fields/manifest @@ -0,0 +1,31 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig 0/main.zig +#update +#check stdout { +1 100 +#} + +#write main.zig 1/main.zig +#update { +add a field#} +#check stdout { +2 100 +#} + +#write main.zig 2/main.zig +#update { +remove all fields#} +#check errors { +main.zig:15:24: error: no field named 'x' in struct 'main.S' +main.zig:1:11: note: struct declared here +#} + +#write main.zig 3/main.zig +#update { +remove reference to non-existent field#} +#check stdout { +0 +#} diff --git a/test/incremental/add_remove_toplevel_fields b/test/incremental/add_remove_toplevel_fields deleted file mode 100644 index e7ba9a2e892d343b08f90016a545252d04b04d5c..0000000000000000000000000000000000000000 --- a/test/incremental/add_remove_toplevel_fields +++ /dev/null @@ -1,96 +0,0 @@ -#update=initial version -#file=main.zig -const S = @This(); -x: u8, -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="1 100\n" - -#update=add a field -#file=main.zig -const S = @This(); -x: u8, -y: u16 = 200, -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="2 100\n" - -#update=remove all fields -#file=main.zig -const S = @This(); -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - const val: S = .{ .x = 100 }; - try w.print("{d}\n", .{val.x}); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_error=main.zig:15:24: error: no field named 'x' in struct 'main' -#expect_error=main.zig:1:1: note: struct declared here - -#update=remove reference to non-existent field -#file=main.zig -const S = @This(); -pub fn main(init: std.process.Init) !void { - var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); - printFieldCount(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; - printOneField(&stdout_writer.interface) catch |err| switch (err) { - error.WriteFailed => return stdout_writer.err.?, - }; -} -fn printFieldCount(w: *Writer) Writer.Error!void { - try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); -} -fn printOneField(w: *Writer) Writer.Error!void { - //const val: S = .{ .x = 100 }; - //try w.print("{d}\n", .{val.x}); - try w.writeAll("\n"); -} -const std = @import("std"); -const Writer = std.Io.Writer; -#expect_stdout="0 \n" diff --git a/test/incremental/add_remove_toplevel_fields/0/main.zig b/test/incremental/add_remove_toplevel_fields/0/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..72823f713cab4244b581f51ca442a7ae57ece762 --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields/0/main.zig @@ -0,0 +1,20 @@ +const S = @This(); +x: u8, +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_toplevel_fields/1/main.zig b/test/incremental/add_remove_toplevel_fields/1/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..7fecd0bb563d9962f8cbc4830f714259e34d393b --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields/1/main.zig @@ -0,0 +1,21 @@ +const S = @This(); +x: u8, +y: u16 = 200, +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_toplevel_fields/2/main.zig b/test/incremental/add_remove_toplevel_fields/2/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..cdba934cc767e1ab7e9ed7e5ff9a4a43db164981 --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields/2/main.zig @@ -0,0 +1,19 @@ +const S = @This(); +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + const val: S = .{ .x = 100 }; + try w.print("{d}\n", .{val.x}); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_toplevel_fields/3/main.zig b/test/incremental/add_remove_toplevel_fields/3/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..495c87c2b1aeeefe9628072bb2e2f325f8c82325 --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields/3/main.zig @@ -0,0 +1,20 @@ +const S = @This(); +pub fn main(init: std.process.Init) !void { + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); + printFieldCount(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; + printOneField(&stdout_writer.interface) catch |err| switch (err) { + error.WriteFailed => return stdout_writer.err.?, + }; +} +fn printFieldCount(w: *Writer) Writer.Error!void { + try w.print("{d} ", .{@typeInfo(S).@"struct".field_names.len}); +} +fn printOneField(w: *Writer) Writer.Error!void { + //const val: S = .{ .x = 100 }; + //try w.print("{d}\n", .{val.x}); + try w.writeAll("\n"); +} +const std = @import("std"); +const Writer = std.Io.Writer; diff --git a/test/incremental/add_remove_toplevel_fields/manifest b/test/incremental/add_remove_toplevel_fields/manifest new file mode 100644 index 0000000000000000000000000000000000000000..569ee7527b51e75d6a214d260b4206b1c5dd2974 --- /dev/null +++ b/test/incremental/add_remove_toplevel_fields/manifest @@ -0,0 +1,31 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig 0/main.zig +#update +#check stdout { +1 100 +#} + +#write main.zig 1/main.zig +#update { +add a field#} +#check stdout { +2 100 +#} + +#write main.zig 2/main.zig +#update { +remove all fields#} +#check errors { +main.zig:15:24: error: no field named 'x' in struct 'main' +main.zig:1:1: note: struct declared here +#} + +#write main.zig 3/main.zig +#update { +remove reference to non-existent field#} +#check stdout { +0 +#} diff --git a/test/incremental/analysis_error_and_syntax_error b/test/incremental/analysis_error_and_syntax_error index 6c703db7545fd51153794b18ab696e25851bf8a5..5a2246ee8a8cbc2da6512e70830d8927a676d75b 100644 --- a/test/incremental/analysis_error_and_syntax_error +++ b/test/incremental/analysis_error_and_syntax_error @@ -1,38 +1,64 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { @compileError("uh oh"); } -#expect_error=main.zig:2:5: error: uh oh +#} +#update +#check errors { +main.zig:2:5: error: uh oh +#} -#update=add parse error -#file=main.zig +#write main.zig { pub fn main() !void { @compileError("uh oh"); -#expect_error=main.zig:3:1: error: expected statement, found 'EOF' +#} +#update { +add parse error#} +#check errors { +main.zig:3:1: error: expected statement, found 'EOF' +#} -#update=fix parse error -#file=main.zig +#write main.zig { pub fn main() !void { @compileError("uh oh"); } -#expect_error=main.zig:2:5: error: uh oh +#} +#update { +fix parse error#} +#check errors { +main.zig:2:5: error: uh oh +#} -#update=add parse error again -#file=main.zig +#write main.zig { pub fn main() !void { @compileError("uh oh"); -#expect_error=main.zig:3:1: error: expected statement, found 'EOF' +#} +#update { +add parse error again#} +#check errors { +main.zig:3:1: error: expected statement, found 'EOF' +#} -#update=comment @compileError call -#file=main.zig +#write main.zig { pub fn main() !void { //@compileError("uh oh"); -#expect_error=main.zig:3:1: error: expected statement, found 'EOF' +#} +#update { +comment @compileError call#} +#check errors { +main.zig:3:1: error: expected statement, found 'EOF' +#} -#update=fix parse error again -#file=main.zig +#write main.zig { pub fn main() !void { //@compileError("uh oh"); } -#expect_stdout="" +#} +#update { +fix parse error again#} +#check stdout { +#} diff --git a/test/incremental/bad_import b/test/incremental/bad_import index fd91fa66050e2ae3cb1407382d5f173bdb2fe884..d93a9ed6309f963c310a82f0813d0905a564cc10 100644 --- a/test/incremental/bad_import +++ b/test/incremental/bad_import @@ -1,32 +1,48 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { _ = @import("foo.zig"); try std.Io.File.stdout().writeStreamingAll(io, "success\n"); } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#file=foo.zig +#} +#write foo.zig { comptime { _ = @import("bad.zig"); } -#expect_error=bad.zig:1:1: error: unable to load "bad.zig": FileNotFound -#expect_error=foo.zig:2:17: note: file imported here +#} +#update +#check errors { +bad.zig:1:1: error: unable to load "bad.zig": FileNotFound +foo.zig:2:17: note: file imported here +#} -#update=change bad import -#file=foo.zig +#write foo.zig { comptime { _ = @import("this_is/not_real.zig"); } -#expect_error=this_is/not_real.zig:1:1: error: unable to load "not_real.zig": FileNotFound -#expect_error=foo.zig:2:17: note: file imported here +#} +#update { +change bad import#} +#check errors { +this_is/not_real.zig:1:1: error: unable to load "not_real.zig": FileNotFound +foo.zig:2:17: note: file imported here +#} -#update=remove import of 'foo.zig' -#file=main.zig +#write main.zig { pub fn main() !void { //_ = @import("foo.zig"); try std.Io.File.stdout().writeStreamingAll(io, "success\n"); } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="success\n" +#} +#update { +remove import of 'foo.zig'#} +#check stdout { +success +#} diff --git a/test/incremental/change_bitpack_backing_int b/test/incremental/change_bitpack_backing_int index 0af16aa43e092554bc22c09053b22b54e87a32e4..a5a2b81ce3c3df699043da2fdbd06e7718627ec8 100644 --- a/test/incremental/change_bitpack_backing_int +++ b/test/incremental/change_bitpack_backing_int @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const Foo = packed struct(u8) { a: u4, b: i4 }; const Bar = packed union(u10) { a: u10, b: i10 }; pub fn main() void {} @@ -11,13 +14,18 @@ comptime { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:5:5: error: found compile log statement -#expect_error=main.zig:8:5: note: also here -#expect_compile_log=@as(type, u8) -#expect_compile_log=@as(comptime_int, 10) +#} +#update +#check errors { +main.zig:5:5: error: found compile log statement +main.zig:8:5: note: also here -#update=make backing types signed -#file=main.zig +Compile Log Output: +@as(type, u8) +@as(comptime_int, 10) +#} + +#write main.zig { const Foo = packed struct(i8) { a: u4, b: i4 }; const Bar = packed union(i10) { a: u10, b: i10 }; pub fn main() void {} @@ -29,13 +37,19 @@ comptime { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:5:5: error: found compile log statement -#expect_error=main.zig:8:5: note: also here -#expect_compile_log=@as(type, i8) -#expect_compile_log=@as(comptime_int, 10) +#} +#update { +make backing types signed#} +#check errors { +main.zig:5:5: error: found compile log statement +main.zig:8:5: note: also here -#update=make backing types too small -#file=main.zig +Compile Log Output: +@as(type, i8) +@as(comptime_int, 10) +#} + +#write main.zig { const Foo = packed struct(i5) { a: u4, b: i4 }; const Bar = packed union(i8) { a: u10, b: i10 }; pub fn main() void {} @@ -47,16 +61,20 @@ comptime { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:1:20: error: backing integer bit width does not match total bit width of fields -#expect_error=main.zig:1:27: note: backing integer 'i5' has bit width '5' -#expect_error=main.zig:1:20: note: struct fields have total bit width '8' -#expect_error=main.zig:2:35: error: field bit width does not match backing integer -#expect_error=main.zig:2:35: note: field type 'u10' has bit width '10' -#expect_error=main.zig:2:26: note: backing integer 'i8' has bit width '8' -#expect_error=main.zig:2:35: note: all fields in a packed union must have the same bit width +#} +#update { +make backing types too small#} +#check errors { +main.zig:1:20: error: backing integer bit width does not match total bit width of fields +main.zig:1:27: note: backing integer 'i5' has bit width '5' +main.zig:1:20: note: struct fields have total bit width '8' +main.zig:2:35: error: field bit width does not match backing integer +main.zig:2:35: note: field type 'u10' has bit width '10' +main.zig:2:26: note: backing integer 'i8' has bit width '8' +main.zig:2:35: note: all fields in a packed union must have the same bit width +#} -#update=make backing types too big -#file=main.zig +#write main.zig { const Foo = packed struct(u10) { a: u4, b: i4 }; const Bar = packed union(u32) { a: u10, b: i10 }; pub fn main() void {} @@ -68,10 +86,15 @@ comptime { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:1:20: error: backing integer bit width does not match total bit width of fields -#expect_error=main.zig:1:27: note: backing integer 'u10' has bit width '10' -#expect_error=main.zig:1:20: note: struct fields have total bit width '8' -#expect_error=main.zig:2:36: error: field bit width does not match backing integer -#expect_error=main.zig:2:36: note: field type 'u10' has bit width '10' -#expect_error=main.zig:2:26: note: backing integer 'u32' has bit width '32' -#expect_error=main.zig:2:36: note: all fields in a packed union must have the same bit width +#} +#update { +make backing types too big#} +#check errors { +main.zig:1:20: error: backing integer bit width does not match total bit width of fields +main.zig:1:27: note: backing integer 'u10' has bit width '10' +main.zig:1:20: note: struct fields have total bit width '8' +main.zig:2:36: error: field bit width does not match backing integer +main.zig:2:36: note: field type 'u10' has bit width '10' +main.zig:2:26: note: backing integer 'u32' has bit width '32' +main.zig:2:36: note: all fields in a packed union must have the same bit width +#} diff --git a/test/incremental/change_container_kind b/test/incremental/change_container_kind index cf6b5a3689cdb51f037db505ad5fdd9ebebdb48b..b83abc6ed79414180e28bf393af94cb708bb377e 100644 --- a/test/incremental/change_container_kind +++ b/test/incremental/change_container_kind @@ -1,61 +1,114 @@ -#update=initial version, T is struct -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const T = @import("foo.zig").T; pub fn main() void { @compileLog(T); @compileLog(@tagName(@typeInfo(T))); } -#file=foo.zig +#} +#write foo.zig { pub const T = struct { x: u8 }; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [6:0]u8, "struct") +#} +#update { +T is struct#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is union -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [6:0]u8, "struct") +#} + +#write foo.zig { pub const T = union {}; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [5:0]u8, "union") +#} +#update { +T is union#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is enum -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [5:0]u8, "union") +#} + +#write foo.zig { pub const T = enum {}; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [4:0]u8, "enum") +#} +#update { +T is enum#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is opaque -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [4:0]u8, "enum") +#} + +#write foo.zig { pub const T = opaque {}; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [6:0]u8, "opaque") +#} +#update { +T is opaque#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is packed struct -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [6:0]u8, "opaque") +#} + +#write foo.zig { pub const T = packed struct {}; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [6:0]u8, "struct") +#} +#update { +T is packed struct#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is packed union -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [6:0]u8, "struct") +#} + +#write foo.zig { pub const T = packed union { x: void }; // packed unions need at least 1 field -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [5:0]u8, "union") +#} +#update { +T is packed union#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is extern struct -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [5:0]u8, "union") +#} + +#write foo.zig { pub const T = extern struct {}; -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [6:0]u8, "struct") +#} +#update { +T is extern struct#} +#check errors { +main.zig:3:5: error: found compile log statement -#update=T is extern union -#file=foo.zig +Compile Log Output: +@as(type, foo.T) +@as(*const [6:0]u8, "struct") +#} + +#write foo.zig { pub const T = extern union { x: void }; // extern unions need at least 1 field -#expect_error=main.zig:3:5: error: found compile log statement -#expect_compile_log=@as(type, foo.T) -#expect_compile_log=@as(*const [5:0]u8, "union") +#} +#update { +T is extern union#} +#check errors { +main.zig:3:5: error: found compile log statement + +Compile Log Output: +@as(type, foo.T) +@as(*const [5:0]u8, "union") +#} diff --git a/test/incremental/change_embed_file b/test/incremental/change_embed_file index 8b515d2d980c9ee68da186c8fa149b5b19c2ed20..d50101dc476973ab1974d4ae82fca3cebffed359 100644 --- a/test/incremental/change_embed_file +++ b/test/incremental/change_embed_file @@ -1,45 +1,72 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const string = @embedFile("string.txt"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, string); } const io = std.Io.Threaded.global_single_threaded.io(); -#file=string.txt +#} +#write string.txt { Hello, World! -#expect_stdout="Hello, World!\n" +#} +#update +#check stdout { +Hello, World! +#} -#update=change file contents -#file=string.txt +#write string.txt { +Hello again, World! +#} +#update { +change file contents#} +#check stdout { Hello again, World! -#expect_stdout="Hello again, World!\n" +#} -#update=delete file -#rm_file=string.txt -#expect_error=main.zig:2:27: error: failed opening "string.txt": FileNotFound +#delete string.txt +#update { +delete file#} +#check errors { +main.zig:2:27: error: failed opening "string.txt": FileNotFound +#} -#update=remove reference to file -#file=main.zig +#write main.zig { const std = @import("std"); const string = @embedFile("string.txt"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "a hardcoded string\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="a hardcoded string\n" +#} +#update { +remove reference to file#} +#check stdout { +a hardcoded string +#} -#update=re-introduce reference to file -#file=main.zig +#write main.zig { const std = @import("std"); const string = @embedFile("string.txt"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, string); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:2:27: error: failed opening "string.txt": FileNotFound +#} +#update { +re-introduce reference to file#} +#check errors { +main.zig:2:27: error: failed opening "string.txt": FileNotFound +#} -#update=recreate file -#file=string.txt +#write string.txt { We're back, World! -#expect_stdout="We're back, World!\n" +#} +#update { +recreate file#} +#check stdout { +We're back, World! +#} diff --git a/test/incremental/change_enum_tag_type b/test/incremental/change_enum_tag_type deleted file mode 100644 index 6ff8a1424d0c695145e7e136b9989c7413fda5a2..0000000000000000000000000000000000000000 --- a/test/incremental/change_enum_tag_type +++ /dev/null @@ -1,93 +0,0 @@ -#update=initial version -#file=main.zig -const Tag = u2; -const Foo = enum(Tag) { - a, - b, - c, - d, -}; -pub fn main() !void { - var val: Foo = undefined; - val = .a; - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="a\n" -#update=too many enum fields -#file=main.zig -const Tag = u2; -const Foo = enum(Tag) { - a, - b, - c, - d, - e, -}; -pub fn main() !void { - var val: Foo = undefined; - val = .a; - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); -} -comptime { - // These can't be true at the same time; analysis should stop as soon as it sees `Foo` - std.debug.assert(@intFromEnum(Foo.e) == 4); - std.debug.assert(@TypeOf(@intFromEnum(Foo.e)) == Tag); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:7:5: error: enum tag value '4' too large for type 'u2' -#update=increase tag size -#file=main.zig -const Tag = u3; -const Foo = enum(Tag) { - a, - b, - c, - d, - e, -}; -pub fn main() !void { - var val: Foo = undefined; - val = .a; - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="a\n" -#update=specify tag directly -#file=main.zig -const Foo = enum(u3) { - a, - b, - c, - d, - e, -}; -pub fn main() !void { - @compileLog(@typeInfo(Foo).@"enum".tag_type); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:9:5: error: found compile log statement -#expect_compile_log=@as(type, u3) -#update=change directly-specified tag type -#file=main.zig -const Foo = enum(u8) { - a, - b, - c, - d, - e, -}; -pub fn main() !void { - @compileLog(@typeInfo(Foo).@"enum".tag_type); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:9:5: error: found compile log statement -#expect_compile_log=@as(type, u8) diff --git a/test/incremental/change_enum_tag_type/0/main.zig b/test/incremental/change_enum_tag_type/0/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..391e354224b13f84fd0e172d9a2c5a07074cdcd8 --- /dev/null +++ b/test/incremental/change_enum_tag_type/0/main.zig @@ -0,0 +1,15 @@ +const Tag = u2; +const Foo = enum(Tag) { + a, + b, + c, + d, +}; +pub fn main() !void { + var val: Foo = undefined; + val = .a; + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_enum_tag_type/1/main.zig b/test/incremental/change_enum_tag_type/1/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..ea4ff6f7a9097e4f4982ae4598a40b0e1f023e80 --- /dev/null +++ b/test/incremental/change_enum_tag_type/1/main.zig @@ -0,0 +1,21 @@ +const Tag = u2; +const Foo = enum(Tag) { + a, + b, + c, + d, + e, +}; +pub fn main() !void { + var val: Foo = undefined; + val = .a; + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); +} +comptime { + // These can't be true at the same time; analysis should stop as soon as it sees `Foo` + std.debug.assert(@backingInt(Foo.e) == 4); + std.debug.assert(@TypeOf(@backingInt(Foo.e)) == Tag); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_enum_tag_type/2/main.zig b/test/incremental/change_enum_tag_type/2/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..2a1eee0ba91d59ed1d5df6adfc5ae2de2d29a5d6 --- /dev/null +++ b/test/incremental/change_enum_tag_type/2/main.zig @@ -0,0 +1,16 @@ +const Tag = u3; +const Foo = enum(Tag) { + a, + b, + c, + d, + e, +}; +pub fn main() !void { + var val: Foo = undefined; + val = .a; + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{s}\n", .{@tagName(val)}); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_enum_tag_type/3/main.zig b/test/incremental/change_enum_tag_type/3/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..c4e213660e09f90523a3c8914d0f09dd5266cdf4 --- /dev/null +++ b/test/incremental/change_enum_tag_type/3/main.zig @@ -0,0 +1,12 @@ +const Foo = enum(u3) { + a, + b, + c, + d, + e, +}; +pub fn main() !void { + @compileLog(@typeInfo(Foo).@"enum".tag_type); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_enum_tag_type/4/main.zig b/test/incremental/change_enum_tag_type/4/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..729f2501dd4e1ff7260c7234a1b204052fe5f92c --- /dev/null +++ b/test/incremental/change_enum_tag_type/4/main.zig @@ -0,0 +1,12 @@ +const Foo = enum(u8) { + a, + b, + c, + d, + e, +}; +pub fn main() !void { + @compileLog(@typeInfo(Foo).@"enum".tag_type); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_enum_tag_type/manifest b/test/incremental/change_enum_tag_type/manifest new file mode 100644 index 0000000000000000000000000000000000000000..682c83a67ed4a3d9ced7ba413b3c8e2ddfab4559 --- /dev/null +++ b/test/incremental/change_enum_tag_type/manifest @@ -0,0 +1,43 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig 0/main.zig +#update +#check stdout { +a +#} + +#write main.zig 1/main.zig +#update { +too many enum fields#} +#check errors { +main.zig:7:5: error: enum tag value '4' too large for type 'u2' +#} + +#write main.zig 2/main.zig +#update { +increase tag size#} +#check stdout { +a +#} + +#write main.zig 3/main.zig +#update { +specify tag directly#} +#check errors { +main.zig:9:5: error: found compile log statement + +Compile Log Output: +@as(type, u3) +#} + +#write main.zig 4/main.zig +#update { +change directly-specified tag type#} +#check errors { +main.zig:9:5: error: found compile log statement + +Compile Log Output: +@as(type, u8) +#} diff --git a/test/incremental/change_exports b/test/incremental/change_exports deleted file mode 100644 index 64aec0288cbbf82420b8eaf5345245bea34d72b0..0000000000000000000000000000000000000000 --- a/test/incremental/change_exports +++ /dev/null @@ -1,169 +0,0 @@ -#update=initial version -#file=main.zig -export fn foo() void {} -const bar: u32 = 123; -const other: u32 = 456; -comptime { - @export(&bar, .{ .name = "bar" }); -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{}\n", .{S.bar}); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123\n" - -#update=add conflict -#file=main.zig -export fn foo() void {} -const bar: u32 = 123; -const other: u32 = 456; -comptime { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "foo" }); -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - extern const other: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:6:5: error: exported symbol collision: foo -#expect_error=main.zig:1:1: note: other symbol here - -#update=resolve conflict -#file=main.zig -export fn foo() void {} -const bar: u32 = 123; -const other: u32 = 456; -comptime { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "other" }); -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - extern const other: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123 456\n" - -#update=put exports in decl -#file=main.zig -export fn foo() void {} -const bar: u32 = 123; -const other: u32 = 456; -const does_exports = { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "other" }); -}; -comptime { - _ = does_exports; -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - extern const other: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123 456\n" - -#update=remove reference to exporting decl -#file=main.zig -export fn foo() void {} -const bar: u32 = 123; -const other: u32 = 456; -const does_exports = { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "other" }); -}; -comptime { - //_ = does_exports; -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - }; - S.foo(); -} -const std = @import("std"); -#expect_stdout="" - -#update=mark consts as export -#file=main.zig -export fn foo() void {} -export const bar: u32 = 123; -export const other: u32 = 456; -const does_exports = { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "other" }); -}; -comptime { - //_ = does_exports; -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - extern const other: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123 456\n" - -#update=reintroduce reference to exporting decl, introducing conflict -#file=main.zig -export fn foo() void {} -export const bar: u32 = 123; -export const other: u32 = 456; -const does_exports = { - @export(&bar, .{ .name = "bar" }); - @export(&other, .{ .name = "other" }); -}; -comptime { - _ = does_exports; -} -pub fn main() !void { - const S = struct { - extern fn foo() void; - extern const bar: u32; - extern const other: u32; - }; - S.foo(); - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:5:5: error: exported symbol collision: bar -#expect_error=main.zig:2:1: note: other symbol here -#expect_error=main.zig:6:5: error: exported symbol collision: other -#expect_error=main.zig:3:1: note: other symbol here diff --git a/test/incremental/change_exports/0/main.zig b/test/incremental/change_exports/0/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..79f3265c558830ea94304c71feb5eeb39b96de33 --- /dev/null +++ b/test/incremental/change_exports/0/main.zig @@ -0,0 +1,17 @@ +export fn foo() void {} +const bar: u32 = 123; +const other: u32 = 456; +comptime { + @export(&bar, .{ .name = "bar" }); +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{}\n", .{S.bar}); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/1/main.zig b/test/incremental/change_exports/1/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..af1dd07028368824ec0bc5a81d0c7b43e99ad521 --- /dev/null +++ b/test/incremental/change_exports/1/main.zig @@ -0,0 +1,19 @@ +export fn foo() void {} +const bar: u32 = 123; +const other: u32 = 456; +comptime { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "foo" }); +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + extern const other: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/2/main.zig b/test/incremental/change_exports/2/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..e2737aea7e73d589b7525d06085a79f0b8a11f0f --- /dev/null +++ b/test/incremental/change_exports/2/main.zig @@ -0,0 +1,19 @@ +export fn foo() void {} +const bar: u32 = 123; +const other: u32 = 456; +comptime { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "other" }); +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + extern const other: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/3/main.zig b/test/incremental/change_exports/3/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..20662c64435cc85c232ec1a18ffec0982b31fc58 --- /dev/null +++ b/test/incremental/change_exports/3/main.zig @@ -0,0 +1,22 @@ +export fn foo() void {} +const bar: u32 = 123; +const other: u32 = 456; +const does_exports = { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "other" }); +}; +comptime { + _ = does_exports; +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + extern const other: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/4/main.zig b/test/incremental/change_exports/4/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..f53a371119987978b64e3cac638dc3630b179a0a --- /dev/null +++ b/test/incremental/change_exports/4/main.zig @@ -0,0 +1,17 @@ +export fn foo() void {} +const bar: u32 = 123; +const other: u32 = 456; +const does_exports = { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "other" }); +}; +comptime { + //_ = does_exports; +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + }; + S.foo(); +} +const std = @import("std"); diff --git a/test/incremental/change_exports/5/main.zig b/test/incremental/change_exports/5/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..ad9ccddf989a962083c351a2badc6fd7a5777b92 --- /dev/null +++ b/test/incremental/change_exports/5/main.zig @@ -0,0 +1,22 @@ +export fn foo() void {} +export const bar: u32 = 123; +export const other: u32 = 456; +const does_exports = { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "other" }); +}; +comptime { + //_ = does_exports; +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + extern const other: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/6/main.zig b/test/incremental/change_exports/6/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..2019185e6046252901faf087291720f93895d514 --- /dev/null +++ b/test/incremental/change_exports/6/main.zig @@ -0,0 +1,22 @@ +export fn foo() void {} +export const bar: u32 = 123; +export const other: u32 = 456; +const does_exports = { + @export(&bar, .{ .name = "bar" }); + @export(&other, .{ .name = "other" }); +}; +comptime { + _ = does_exports; +} +pub fn main() !void { + const S = struct { + extern fn foo() void; + extern const bar: u32; + extern const other: u32; + }; + S.foo(); + var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); + try stdout_writer.interface.print("{} {}\n", .{ S.bar, S.other }); +} +const std = @import("std"); +const io = std.Io.Threaded.global_single_threaded.io(); diff --git a/test/incremental/change_exports/manifest b/test/incremental/change_exports/manifest new file mode 100644 index 0000000000000000000000000000000000000000..41716cd32cadf13dca51971fbab9703bc1530ade --- /dev/null +++ b/test/incremental/change_exports/manifest @@ -0,0 +1,54 @@ +#exe { +-Mroot=main.zig +#} + +#write main.zig 0/main.zig +#update +#check stdout { +123 +#} + +#write main.zig 1/main.zig +#update { +add conflict#} +#check errors { +main.zig:6:5: error: exported symbol collision: foo +main.zig:1:1: note: other symbol here +#} + +#write main.zig 2/main.zig +#update { +resolve conflict#} +#check stdout { +123 456 +#} + +#write main.zig 3/main.zig +#update { +put exports in decl#} +#check stdout { +123 456 +#} + +#write main.zig 4/main.zig +#update { +remove reference to exporting decl#} +#check stdout { +#} + +#write main.zig 5/main.zig +#update { +mark consts as export#} +#check stdout { +123 456 +#} + +#write main.zig 6/main.zig +#update { +reintroduce reference to exporting decl, introducing conflict#} +#check errors { +main.zig:5:5: error: exported symbol collision: bar +main.zig:2:1: note: other symbol here +main.zig:6:5: error: exported symbol collision: other +main.zig:3:1: note: other symbol here +#} diff --git a/test/incremental/change_fn_type b/test/incremental/change_fn_type index ca0667d8041e05c363a9c975b865c6e1a7640904..b16811fbc937aa183fd80bfe7b0eb59690e089b7 100644 --- a/test/incremental/change_fn_type +++ b/test/incremental/change_fn_type @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { try foo(123); } @@ -9,10 +12,13 @@ fn foo(x: u8) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123\n" +#} +#update +#check stdout { +123 +#} -#update=change function type -#file=main.zig +#write main.zig { pub fn main() !void { try foo(123); } @@ -22,10 +28,14 @@ fn foo(x: i64) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="123\n" +#} +#update { +change function type#} +#check stdout { +123 +#} -#update=change function argument -#file=main.zig +#write main.zig { pub fn main() !void { try foo(-42); } @@ -35,4 +45,9 @@ fn foo(x: i64) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="-42\n" +#} +#update { +change function argument#} +#check stdout { +-42 +#} diff --git a/test/incremental/change_generic_line_number b/test/incremental/change_generic_line_number index 23b3f2c0190a47c9c5e8a124af324b308fc100a3..cda09859f9c849649b5b40c66d9d16e94101cf34 100644 --- a/test/incremental/change_generic_line_number +++ b/test/incremental/change_generic_line_number @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); fn Printer(message: []const u8) type { @@ -13,9 +16,14 @@ pub fn main() !void { try Printer("foo\n").print(); try Printer("bar\n").print(); } -#expect_stdout="foo\nbar\n" -#update=change line number -#file=main.zig +#} +#update +#check stdout { +foo +bar +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); @@ -30,4 +38,10 @@ pub fn main() !void { try Printer("foo\n").print(); try Printer("bar\n").print(); } -#expect_stdout="foo\nbar\n" +#} +#update { +change line number#} +#check stdout { +foo +bar +#} diff --git a/test/incremental/change_line_number b/test/incremental/change_line_number index 90a1edb95c924143792363c8b14c6414f0fbac13..ed8e24b009356b452dd71d17d7ca510e0df285d2 100644 --- a/test/incremental/change_line_number +++ b/test/incremental/change_line_number @@ -1,17 +1,29 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "foo\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="foo\n" -#update=change line number -#file=main.zig +#} +#update +#check stdout { +foo +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "foo\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="foo\n" +#} +#update { +change line number#} +#check stdout { +foo +#} diff --git a/test/incremental/change_module b/test/incremental/change_module index 73129f9eec6db02e446ff663c5600ffac2345994..4c24df6e4dd2c3f410d2d0a555e5a05a2d94d592 100644 --- a/test/incremental/change_module +++ b/test/incremental/change_module @@ -1,61 +1,87 @@ -#module=foo=foo.zig +#exe { +--dep foo -Mroot=main.zig +-Mfoo=foo.zig +#} -#update=initial version -#file=main.zig +#write main.zig { pub fn main() void { _ = @import("foo"); //_ = @import("other.zig"); } -#file=foo.zig +#} +#write foo.zig { comptime { _ = @import("other.zig"); } -#file=other.zig +#} +#write other.zig { fn f() void { @compileLog(@src().module); } comptime { f(); } -#expect_error=other.zig:2:5: error: found compile log statement -#expect_compile_log=@as([:0]const u8, "foo"[0..3]) +#} +#update +#check errors { +other.zig:2:5: error: found compile log statement -#update=change module of other.zig -#file=main.zig +Compile Log Output: +@as([:0]const u8, "foo"[0..3]) +#} + +#write main.zig { pub fn main() void { _ = @import("foo"); _ = @import("other.zig"); } -#file=foo.zig +#} +#write foo.zig { comptime { //_ = @import("other.zig"); } -#expect_error=other.zig:2:5: error: found compile log statement -#expect_compile_log=@as([:0]const u8, "root"[0..4]) +#} +#update { +change module of other.zig#} +#check errors { +other.zig:2:5: error: found compile log statement + +Compile Log Output: +@as([:0]const u8, "root"[0..4]) +#} -#update=put other.zig in both modules -#file=main.zig +#write main.zig { pub fn main() void { _ = @import("foo"); _ = @import("other.zig"); } -#file=foo.zig +#} +#write foo.zig { comptime { _ = @import("other.zig"); } -#expect_error=other.zig:1:1: error: file exists in modules 'root' and 'foo' -#expect_error=other.zig:1:1: note: files must belong to only one module -#expect_error=main.zig:3:17: note: file is imported here by the root of module 'root' -#expect_error=foo.zig:2:17: note: file is imported here by the root of module 'foo' +#} +#update { +put other.zig in both modules#} +#check errors { +other.zig:1:1: error: file exists in modules 'root' and 'foo' +other.zig:1:1: note: files must belong to only one module +main.zig:3:17: note: file is imported here by the root of module 'root' +foo.zig:2:17: note: file is imported here by the root of module 'foo' +#} -#update=put other.zig in no modules -#file=main.zig +#write main.zig { pub fn main() void { _ = @import("foo"); //_ = @import("other.zig"); } -#file=foo.zig +#} +#write foo.zig { comptime { //_ = @import("other.zig"); } -#expect_stdout="" +#} +#update { +put other.zig in no modules#} +#check stdout { +#} diff --git a/test/incremental/change_panic_handler b/test/incremental/change_panic_handler index b250c9867985a683a209279835f366703f2f5161..3407de9bade926e20542b9342c91e2d223a019da 100644 --- a/test/incremental/change_panic_handler +++ b/test/incremental/change_panic_handler @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !u8 { var a: u8 = undefined; a = 255; @@ -14,10 +17,13 @@ fn myPanic(msg: []const u8, _: ?usize) noreturn { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="panic message: integer overflow\n" +#} +#update +#check stdout { +panic message: integer overflow +#} -#update=change the panic handler body -#file=main.zig +#write main.zig { pub fn main() !u8 { var a: u8 = undefined; a = 255; @@ -32,10 +38,14 @@ fn myPanic(msg: []const u8, _: ?usize) noreturn { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="new panic message: integer overflow\n" +#} +#update { +change the panic handler body#} +#check stdout { +new panic message: integer overflow +#} -#update=change the panic handler function value -#file=main.zig +#write main.zig { pub fn main() !u8 { var a: u8 = undefined; a = 255; @@ -50,4 +60,9 @@ fn myPanicNew(msg: []const u8, _: ?usize) noreturn { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="third panic message: integer overflow\n" +#} +#update { +change the panic handler function value#} +#check stdout { +third panic message: integer overflow +#} diff --git a/test/incremental/change_panic_handler_explicit b/test/incremental/change_panic_handler_explicit deleted file mode 100644 index 3735cfd86c0116ce130481c56f60d6816174d00f..0000000000000000000000000000000000000000 --- a/test/incremental/change_panic_handler_explicit +++ /dev/null @@ -1,149 +0,0 @@ -#update=initial version -#file=main.zig -pub fn main() !u8 { - var a: u8 = undefined; - a = 255; - _ = a + 1; - return 1; -} -const no_panic = std.debug.no_panic; -pub const panic = struct { - pub const call = myPanic; - pub fn integerOverflow() noreturn { - @panic("integer overflow"); - } - pub const sentinelMismatch = no_panic.sentinelMismatch; - pub const unwrapError = no_panic.unwrapError; - pub const outOfBounds = no_panic.outOfBounds; - pub const startGreaterThanEnd = no_panic.startGreaterThanEnd; - pub const inactiveUnionField = no_panic.inactiveUnionField; - pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; - pub const reachedUnreachable = no_panic.reachedUnreachable; - pub const unwrapNull = no_panic.unwrapNull; - pub const castToNull = no_panic.castToNull; - pub const incorrectAlignment = no_panic.incorrectAlignment; - pub const invalidErrorCode = no_panic.invalidErrorCode; - pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; - pub const integerOutOfBounds = no_panic.integerOutOfBounds; - pub const shlOverflow = no_panic.shlOverflow; - pub const shrOverflow = no_panic.shrOverflow; - pub const divideByZero = no_panic.divideByZero; - pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; - pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; - pub const corruptSwitch = no_panic.corruptSwitch; - pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; - pub const invalidEnumValue = no_panic.invalidEnumValue; - pub const forLenMismatch = no_panic.forLenMismatch; - pub const copyLenMismatch = no_panic.copyLenMismatch; - pub const memcpyAlias = no_panic.memcpyAlias; - pub const noreturnReturned = no_panic.noreturnReturned; - pub const loadUninstantiableType = no_panic.loadUninstantiableType; -}; -fn myPanic(msg: []const u8, _: ?usize) noreturn { - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - stdout_writer.interface.print("panic message: {s}\n", .{msg}) catch {}; - std.process.exit(0); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="panic message: integer overflow\n" - -#update=change the panic handler body -#file=main.zig -pub fn main() !u8 { - var a: u8 = undefined; - a = 255; - _ = a + 1; - return 1; -} -const no_panic = std.debug.no_panic; -pub const panic = struct { - pub const call = myPanic; - pub fn integerOverflow() noreturn { - @panic("integer overflow"); - } - pub const sentinelMismatch = no_panic.sentinelMismatch; - pub const unwrapError = no_panic.unwrapError; - pub const outOfBounds = no_panic.outOfBounds; - pub const startGreaterThanEnd = no_panic.startGreaterThanEnd; - pub const inactiveUnionField = no_panic.inactiveUnionField; - pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; - pub const reachedUnreachable = no_panic.reachedUnreachable; - pub const unwrapNull = no_panic.unwrapNull; - pub const castToNull = no_panic.castToNull; - pub const incorrectAlignment = no_panic.incorrectAlignment; - pub const invalidErrorCode = no_panic.invalidErrorCode; - pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; - pub const integerOutOfBounds = no_panic.integerOutOfBounds; - pub const shlOverflow = no_panic.shlOverflow; - pub const shrOverflow = no_panic.shrOverflow; - pub const divideByZero = no_panic.divideByZero; - pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; - pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; - pub const corruptSwitch = no_panic.corruptSwitch; - pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; - pub const invalidEnumValue = no_panic.invalidEnumValue; - pub const forLenMismatch = no_panic.forLenMismatch; - pub const copyLenMismatch = no_panic.copyLenMismatch; - pub const memcpyAlias = no_panic.memcpyAlias; - pub const noreturnReturned = no_panic.noreturnReturned; - pub const loadUninstantiableType = no_panic.loadUninstantiableType; -}; -fn myPanic(msg: []const u8, _: ?usize) noreturn { - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - stdout_writer.interface.print("new panic message: {s}\n", .{msg}) catch {}; - std.process.exit(0); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="new panic message: integer overflow\n" - -#update=change the panic handler function value -#file=main.zig -pub fn main() !u8 { - var a: u8 = undefined; - a = 255; - _ = a + 1; - return 1; -} -const no_panic = std.debug.no_panic; -pub const panic = struct { - pub const call = myPanicNew; - pub fn integerOverflow() noreturn { - @panic("integer overflow"); - } - pub const sentinelMismatch = std.debug.no_panic.sentinelMismatch; - pub const unwrapError = std.debug.no_panic.unwrapError; - pub const outOfBounds = std.debug.no_panic.outOfBounds; - pub const startGreaterThanEnd = std.debug.no_panic.startGreaterThanEnd; - pub const inactiveUnionField = std.debug.no_panic.inactiveUnionField; - pub const sliceCastLenRemainder = no_panic.sliceCastLenRemainder; - pub const reachedUnreachable = no_panic.reachedUnreachable; - pub const unwrapNull = no_panic.unwrapNull; - pub const castToNull = no_panic.castToNull; - pub const incorrectAlignment = no_panic.incorrectAlignment; - pub const invalidErrorCode = no_panic.invalidErrorCode; - pub const unexpectedErrorCode = no_panic.unexpectedErrorCode; - pub const integerOutOfBounds = no_panic.integerOutOfBounds; - pub const shlOverflow = no_panic.shlOverflow; - pub const shrOverflow = no_panic.shrOverflow; - pub const divideByZero = no_panic.divideByZero; - pub const exactDivisionRemainder = no_panic.exactDivisionRemainder; - pub const integerPartOutOfBounds = no_panic.integerPartOutOfBounds; - pub const corruptSwitch = no_panic.corruptSwitch; - pub const shiftRhsTooBig = no_panic.shiftRhsTooBig; - pub const invalidEnumValue = no_panic.invalidEnumValue; - pub const forLenMismatch = no_panic.forLenMismatch; - pub const copyLenMismatch = no_panic.copyLenMismatch; - pub const memcpyAlias = no_panic.memcpyAlias; - pub const noreturnReturned = no_panic.noreturnReturned; - pub const loadUninstantiableType = no_panic.loadUninstantiableType; -}; -fn myPanicNew(msg: []const u8, _: ?usize) noreturn { - var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - stdout_writer.interface.print("third panic message: {s}\n", .{msg}) catch {}; - std.process.exit(0); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="third panic message: integer overflow\n" diff --git a/test/incremental2/change_panic_handler_explicit/0/main.zig b/test/incremental/change_panic_handler_explicit/0/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/0/main.zig rename to test/incremental/change_panic_handler_explicit/0/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/1/main.zig b/test/incremental/change_panic_handler_explicit/1/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/1/main.zig rename to test/incremental/change_panic_handler_explicit/1/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/2/main.zig b/test/incremental/change_panic_handler_explicit/2/main.zig similarity index 100% rename from test/incremental2/change_panic_handler_explicit/2/main.zig rename to test/incremental/change_panic_handler_explicit/2/main.zig diff --git a/test/incremental2/change_panic_handler_explicit/manifest b/test/incremental/change_panic_handler_explicit/manifest similarity index 100% rename from test/incremental2/change_panic_handler_explicit/manifest rename to test/incremental/change_panic_handler_explicit/manifest diff --git a/test/incremental/change_reify_struct_field_type b/test/incremental/change_reify_struct_field_type index bc157bd68364af5eda2d806a2d83b24479bdb843..92a41faee42808ef4b2338a1bdac241507e659fe 100644 --- a/test/incremental/change_reify_struct_field_type +++ b/test/incremental/change_reify_struct_field_type @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { fn getEnum(s: @Struct(.auto, null, &.{"field"}, &.{struct { tag: Enum }}, &.{.{}})) Enum { return s.field.tag; } @@ -8,9 +11,12 @@ pub fn main(init: std.process.Init) !void { } const Enum = enum { foo, bar }; const std = @import("std"); -#expect_stdout="foo" -#update=change field type -#file=main.zig +#} +#update +#check stdout { +foo#} + +#write main.zig { fn getEnum(s: @Struct(.auto, null, &.{"field"}, &.{Enum}, &.{.{}})) Enum { return s.field; } @@ -19,4 +25,8 @@ pub fn main(init: std.process.Init) !void { } const Enum = enum { foo, bar }; const std = @import("std"); -#expect_stdout="bar" +#} +#update { +change field type#} +#check stdout { +bar#} diff --git a/test/incremental/change_shift_op b/test/incremental/change_shift_op index d1fa6e747e125af9506bdff9766bbb5fa0dcd368..bcd0ff1d2f61b4aeb305a350bc64a8b64b76fbed 100644 --- a/test/incremental/change_shift_op +++ b/test/incremental/change_shift_op @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { try foo(0x1300); } @@ -9,9 +12,13 @@ fn foo(x: u16) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="0x3000\n" -#update=change to right shift -#file=main.zig +#} +#update +#check stdout { +0x3000 +#} + +#write main.zig { pub fn main() !void { try foo(0x1300); } @@ -21,4 +28,9 @@ fn foo(x: u16) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="0x130\n" +#} +#update { +change to right shift#} +#check stdout { +0x130 +#} diff --git a/test/incremental/change_struct_same_fields b/test/incremental/change_struct_same_fields index b94f03778b21467cf255a13c48c95cce59f99329..348a443dd555223482c96f6ae0ccd7ed5a48c459 100644 --- a/test/incremental/change_struct_same_fields +++ b/test/incremental/change_struct_same_fields @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const S = extern struct { x: u8, y: u8 }; pub fn main() !void { const val: S = .{ .x = 100, .y = 200 }; @@ -14,10 +17,13 @@ fn foo(val: *const S) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="100 200\n" +#} +#update +#check stdout { +100 200 +#} -#update=change struct layout -#file=main.zig +#write main.zig { const S = extern struct { x: u32, y: u32 }; pub fn main() !void { const val: S = .{ .x = 100, .y = 200 }; @@ -32,10 +38,14 @@ fn foo(val: *const S) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="100 200\n" +#} +#update { +change struct layout#} +#check stdout { +100 200 +#} -#update=change values -#file=main.zig +#write main.zig { const S = extern struct { x: u32, y: u32 }; pub fn main() !void { const val: S = .{ .x = 1234, .y = 5678 }; @@ -50,4 +60,9 @@ fn foo(val: *const S) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="1234 5678\n" +#} +#update { +change values#} +#check stdout { +1234 5678 +#} diff --git a/test/incremental/change_union_tag_type b/test/incremental/change_union_tag_type index 5f754de42ab27224a99c30cff9c645c60fbbf1cb..dbbbc94c1e9bebbfdd1b98743de8a85763e725a5 100644 --- a/test/incremental/change_union_tag_type +++ b/test/incremental/change_union_tag_type @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const A = enum(u8) { a }; const B = enum(u8) { b }; const Foo = union(A) { a: u8 }; @@ -9,10 +12,13 @@ pub fn main(init: std.process.Init) !void { try stdout_writer.interface.print("{s}\n", .{field_name}); } const std = @import("std"); -#expect_stdout="a\n" +#} +#update +#check stdout { +a +#} -#update=change tag type -#file=main.zig +#write main.zig { const A = enum(u8) { a }; const B = enum(u8) { b }; const Foo = union(B) { a: u8 }; @@ -22,11 +28,15 @@ pub fn main(init: std.process.Init) !void { try stdout_writer.interface.print("{s}\n", .{field_name}); } const std = @import("std"); -#expect_error=main.zig:3:24: error: no field named 'a' in enum 'main.B' -#expect_error=main.zig:2:11: note: enum declared here +#} +#update { +change tag type#} +#check errors { +main.zig:3:24: error: no field named 'a' in enum 'main.B' +main.zig:2:11: note: enum declared here +#} -#update=change field name to match new tag type -#file=main.zig +#write main.zig { const A = enum(u8) { a }; const B = enum(u8) { b }; const Foo = union(B) { b: u8 }; @@ -36,4 +46,9 @@ pub fn main(init: std.process.Init) !void { try stdout_writer.interface.print("{s}\n", .{field_name}); } const std = @import("std"); -#expect_stdout="b\n" +#} +#update { +change field name to match new tag type#} +#check stdout { +b +#} diff --git a/test/incremental/change_zon_file b/test/incremental/change_zon_file index 7616c53ea80094d746102df13d51e387a3d1bc40..53b7c82c84703475b71d4495a716041858d27cee 100644 --- a/test/incremental/change_zon_file +++ b/test/incremental/change_zon_file @@ -1,47 +1,74 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const message: []const u8 = @import("message.zon"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, message); } const io = std.Io.Threaded.global_single_threaded.io(); -#file=message.zon +#} +#write message.zon { "Hello, World!\n" -#expect_stdout="Hello, World!\n" +#} +#update +#check stdout { +Hello, World! +#} -#update=change ZON file contents -#file=message.zon +#write message.zon { "Hello again, World!\n" -#expect_stdout="Hello again, World!\n" +#} +#update { +change ZON file contents#} +#check stdout { +Hello again, World! +#} -#update=delete file -#rm_file=message.zon -#expect_error=message.zon:1:1: error: unable to load "message.zon": FileNotFound -#expect_error=main.zig:2:37: note: file imported here +#delete message.zon +#update { +delete file#} +#check errors { +message.zon:1:1: error: unable to load "message.zon": FileNotFound +main.zig:2:37: note: file imported here +#} -#update=remove reference to ZON file -#file=main.zig +#write main.zig { const std = @import("std"); const message: []const u8 = @import("message.zon"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "a hardcoded string\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=message.zon:1:1: error: unable to load "message.zon": FileNotFound -#expect_error=main.zig:2:37: note: file imported here +#} +#update { +remove reference to ZON file#} +#check errors { +message.zon:1:1: error: unable to load "message.zon": FileNotFound +main.zig:2:37: note: file imported here +#} -#update=recreate ZON file -#file=message.zon +#write message.zon { "We're back, World!\n" -#expect_stdout="a hardcoded string\n" +#} +#update { +recreate ZON file#} +#check stdout { +a hardcoded string +#} -#update=re-introduce reference to ZON file -#file=main.zig +#write main.zig { const std = @import("std"); const message: []const u8 = @import("message.zon"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, message); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="We're back, World!\n" +#} +#update { +re-introduce reference to ZON file#} +#check stdout { +We're back, World! +#} diff --git a/test/incremental/change_zon_file_no_result_type b/test/incremental/change_zon_file_no_result_type index 5deca8c7fda5e77995ec385bea0ed4e58839dd2e..dd63226cbaa21d113d2ff5f7f43484d616a5d956 100644 --- a/test/incremental/change_zon_file_no_result_type +++ b/test/incremental/change_zon_file_no_result_type @@ -1,21 +1,33 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @import("foo.zon").message); } -#file=foo.zon +#} +#write foo.zon { .{ .message = "Hello, World!\n", .a_number = 0, } -#expect_stdout="Hello, World!\n" +#} +#update +#check stdout { +Hello, World! +#} -#update=change ZON file -#file=foo.zon +#write foo.zon { .{ .message = "Hello again, World!\n", .b_number = 0, } -#expect_stdout="Hello again, World!\n" +#} +#update { +change ZON file#} +#check stdout { +Hello again, World! +#} diff --git a/test/incremental/compile_error_then_log b/test/incremental/compile_error_then_log index a19430f7cd2a2e45a7fadb4c6b8b57a3a9927608..44dd98a1734dc9fdc2c5618fa58d8fdcceea8ce9 100644 --- a/test/incremental/compile_error_then_log +++ b/test/incremental/compile_error_then_log @@ -1,5 +1,8 @@ -#update=initial version with compile error -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() void {} comptime { @compileError("this is an error"); @@ -7,11 +10,17 @@ comptime { comptime { @compileLog("this is a log"); } -#expect_error=main.zig:3:5: error: this is an error -#expect_compile_log=@as(*const [13:0]u8, "this is a log") +#} +#update { +compile error#} +#check errors { +main.zig:3:5: error: this is an error -#update=remove the compile error -#file=main.zig +Compile Log Output: +@as(*const [13:0]u8, "this is a log") +#} + +#write main.zig { pub fn main() void {} comptime { //@compileError("this is an error"); @@ -19,5 +28,12 @@ comptime { comptime { @compileLog("this is a log"); } -#expect_error=main.zig:6:5: error: found compile log statement -#expect_compile_log=@as(*const [13:0]u8, "this is a log") +#} +#update { +remove the compile error#} +#check errors { +main.zig:6:5: error: found compile log statement + +Compile Log Output: +@as(*const [13:0]u8, "this is a log") +#} diff --git a/test/incremental/compile_log b/test/incremental/compile_log index cc4c6368391979358ac215f6b9c73a080cd39ff0..d4445f3ca80aaac2262e70b94ec2f8acaf751603 100644 --- a/test/incremental/compile_log +++ b/test/incremental/compile_log @@ -1,28 +1,46 @@ -#update=initial version with no compile log -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update { +no compile log#} +#check stdout { +Hello, World! +#} -#update=add compile log -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); @compileLog("this is a log"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:4:5: error: found compile log statement -#expect_compile_log=@as(*const [13:0]u8, "this is a log") +#} +#update { +add compile log#} +#check errors { +main.zig:4:5: error: found compile log statement -#update=remove compile log -#file=main.zig +Compile Log Output: +@as(*const [13:0]u8, "this is a log") +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update { +remove compile log#} +#check stdout { +Hello, World! +#} diff --git a/test/incremental/delete_comptime_decls b/test/incremental/delete_comptime_decls index 3d653d6d7dea5296437618569edb701d43bbf3ba..ffae08f3d8d7a26173aefdb4c398895eb26d420a 100644 --- a/test/incremental/delete_comptime_decls +++ b/test/incremental/delete_comptime_decls @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() void {} comptime { var array = [_:0]u8{ 1, 2, 3, 4 }; @@ -22,13 +25,16 @@ comptime { const slice = array[3..2]; _ = slice; } -#expect_error=main.zig:5:32: error: end index 6 out of bounds for slice of length 4 +1 (sentinel) -#expect_error=main.zig:10:28: error: end index 6 out of bounds for array of length 4 +1 (sentinel) -#expect_error=main.zig:15:28: error: end index 5 out of bounds for array of length 4 -#expect_error=main.zig:20:25: error: start index 3 is larger than end index 2 +#} +#update +#check errors { +main.zig:5:32: error: end index 6 out of bounds for slice of length 4 +1 (sentinel) +main.zig:10:28: error: end index 6 out of bounds for array of length 4 +1 (sentinel) +main.zig:15:28: error: end index 5 out of bounds for array of length 4 +main.zig:20:25: error: start index 3 is larger than end index 2 +#} -#update=delete and modify comptime decls -#file=main.zig +#write main.zig { pub fn main() void {} comptime { const x: [*c]u8 = null; @@ -37,4 +43,9 @@ comptime { const y = x[0..runtime_len]; _ = y; } -#expect_error=main.zig:6:16: error: slice of null pointer +#} +#update { +delete and modify comptime decls#} +#check errors { +main.zig:6:16: error: slice of null pointer +#} diff --git a/test/incremental/dependency_on_type_of_inferred_global b/test/incremental/dependency_on_type_of_inferred_global index 66e52d58394bec7274dec9b0e1767a72338109ac..73d36a7254759c0f98720617aaf3848cc8703070 100644 --- a/test/incremental/dependency_on_type_of_inferred_global +++ b/test/incremental/dependency_on_type_of_inferred_global @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const foo = @as(u8, 123); comptime { // depends on value of `foo` @@ -10,9 +13,12 @@ comptime { if (@TypeOf(&foo) != *const u8) unreachable; } pub fn main() void {} -#expect_stdout="" -#update=change the type -#file=main.zig +#} +#update +#check stdout { +#} + +#write main.zig { const foo = @as(u16, 123); comptime { // depends on value of `foo` @@ -23,4 +29,9 @@ comptime { if (@TypeOf(&foo) != *const u8) unreachable; } pub fn main() void {} -#expect_error=main.zig:8:37: error: reached unreachable code +#} +#update { +change the type#} +#check errors { +main.zig:8:37: error: reached unreachable code +#} diff --git a/test/incremental/do_nothing b/test/incremental/do_nothing index bc50b4bc03836fe9fd93317c60c3b703c7368c0b..213f7f1892d8fa96dbb9313054d862513956ce66 100644 --- a/test/incremental/do_nothing +++ b/test/incremental/do_nothing @@ -1,6 +1,11 @@ -// TODO: it'd be great if we could actually check that no analysis happened! -#update=initial version -#file=main.zig +#todo { +it'd be great if we could actually check that no analysis happened! +#} +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() void { const ptr: *const O = @ptrFromInt(0x1000); _ = ptr; @@ -18,9 +23,12 @@ const O = opaque { _ = wrapper; } }; -#expect_stdout="" -#update=do literally nothing -#file=main.zig +#} +#update +#check stdout { +#} + +#write main.zig { pub fn main() void { const ptr: *const O = @ptrFromInt(0x1000); _ = ptr; @@ -38,4 +46,8 @@ const O = opaque { _ = wrapper; } }; -#expect_stdout="" +#} +#update { +do literally nothing#} +#check stdout { +#} diff --git a/test/incremental/fix_astgen_failure b/test/incremental/fix_astgen_failure index adddc9ac054c2d95709467018ef70af6a71d2a8a..feb078359a513becff249d9e5e89a88a2e243352 100644 --- a/test/incremental/fix_astgen_failure +++ b/test/incremental/fix_astgen_failure @@ -1,35 +1,59 @@ -#update=initial version with error -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { try @import("foo.zig").hello(); } -#file=foo.zig +#} +#write foo.zig { pub fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } -#expect_error=foo.zig:2:9: error: use of undeclared identifier 'std' -#update=fix the error -#file=foo.zig +#} +#update { +error#} +#check errors { +foo.zig:2:9: error: use of undeclared identifier 'std' +#} + +#write foo.zig { const std = @import("std"); pub fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" -#update=add new error -#file=foo.zig +#} +#update { +fix the error#} +#check stdout { +Hello, World! +#} + +#write foo.zig { const std = @import("std"); pub fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, hello_str); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=foo.zig:3:52: error: use of undeclared identifier 'hello_str' -#update=fix the new error -#file=foo.zig +#} +#update { +add new error#} +#check errors { +foo.zig:3:52: error: use of undeclared identifier 'hello_str' +#} + +#write foo.zig { const std = @import("std"); const hello_str = "Hello, World! Again!\n"; pub fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, hello_str); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World! Again!\n" +#} +#update { +fix the new error#} +#check stdout { +Hello, World! Again! +#} diff --git a/test/incremental/function_becomes_inline b/test/incremental/function_becomes_inline index 91afe196ab4d1c28fb041a5f9dd8f0b6956ea463..7d64be329579442c284c149e6342bcc0d3105c36 100644 --- a/test/incremental/function_becomes_inline +++ b/test/incremental/function_becomes_inline @@ -1,5 +1,8 @@ -#update=non-inline version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { try foo(); } @@ -8,10 +11,14 @@ fn foo() !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update { +non-inline version#} +#check stdout { +Hello, World! +#} -#update=make function inline -#file=main.zig +#write main.zig { pub fn main() !void { try foo(); } @@ -20,10 +27,14 @@ inline fn foo() !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update { +make function inline#} +#check stdout { +Hello, World! +#} -#update=change string -#file=main.zig +#write main.zig { pub fn main() !void { try foo(); } @@ -32,4 +43,9 @@ inline fn foo() !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, `inline` World!\n" +#} +#update { +change string#} +#check stdout { +Hello, `inline` World! +#} diff --git a/test/incremental/hello b/test/incremental/hello index 6988323d3dbb504cad158ad134fccc4ed4023087..74a45d2a8b81afebe334968569949b6c82d4c397 100644 --- a/test/incremental/hello +++ b/test/incremental/hello @@ -1,16 +1,28 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "good morning\n"); } -#expect_stdout="good morning\n" -#update=change the string -#file=main.zig +#} +#update +#check stdout { +good morning +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, "おはようございます\n"); } -#expect_stdout="おはようございます\n" +#} +#update { +change the string#} +#check stdout { +おはようございます +#} diff --git a/test/incremental/increase_array_field_length b/test/incremental/increase_array_field_length index 714473dd8302d57b0b2644a90f73442d47e9db2d..7b0eb9c165ff1a1b43206624569303b820d658ac 100644 --- a/test/incremental/increase_array_field_length +++ b/test/incremental/increase_array_field_length @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const Big = struct { arr: [10]u8 }; pub fn main(init: std.process.Init) !void { var big: Big = undefined; @@ -12,10 +15,13 @@ pub fn main(init: std.process.Init) !void { }; } const std = @import("std"); -#expect_stdout="arr[9] = 234\n" +#} +#update +#check stdout { +arr[9] = 234 +#} -#update=increase array length -#file=main.zig +#write main.zig { const Big = struct { arr: [1000]u8 }; pub fn main(init: std.process.Init) !void { var big: Big = undefined; @@ -28,4 +34,9 @@ pub fn main(init: std.process.Init) !void { }; } const std = @import("std"); -#expect_stdout="arr[999] = 234\n" +#} +#update { +increase array length#} +#check stdout { +arr[999] = 234 +#} diff --git a/test/incremental/make_decl_pub b/test/incremental/make_decl_pub index cdb057a7dc1fbc0f72d5ed935d47f47c91855f9e..59a54ece02b3850083fdd6c86c63d901cd346157 100644 --- a/test/incremental/make_decl_pub +++ b/test/incremental/make_decl_pub @@ -1,23 +1,35 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const foo = @import("foo.zig"); pub fn main() !void { try foo.hello(); } -#file=foo.zig +#} +#write foo.zig { const std = @import("std"); fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:3:12: error: 'hello' is not marked 'pub' -#expect_error=foo.zig:2:1: note: declared here +#} +#update +#check errors { +main.zig:3:12: error: 'hello' is not marked 'pub' +foo.zig:2:1: note: declared here +#} -#update=make hello pub -#file=foo.zig +#write foo.zig { const std = @import("std"); pub fn hello() !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update { +make hello pub#} +#check stdout { +Hello, World! +#} diff --git a/test/incremental/modify_inline_fn b/test/incremental/modify_inline_fn index aa7223f6ace49b27eaadfbc92bebffbe6affcc44..956a47a6da82a984edfb3cc9ebf788759c0c61a5 100644 --- a/test/incremental/modify_inline_fn +++ b/test/incremental/modify_inline_fn @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { const str = getStr(); @@ -9,9 +12,13 @@ inline fn getStr() []const u8 { return "foo\n"; } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="foo\n" -#update=change the string -#file=main.zig +#} +#update +#check stdout { +foo +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { const str = getStr(); @@ -21,4 +28,9 @@ inline fn getStr() []const u8 { return "bar\n"; } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="bar\n" +#} +#update { +change the string#} +#check stdout { +bar +#} diff --git a/test/incremental/move_src b/test/incremental/move_src index 20134267d1244fefb659ffc3d204f55c5db347d3..b385a312dbb5d55b6f7605aee556abbda5f9ade4 100644 --- a/test/incremental/move_src +++ b/test/incremental/move_src @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); @@ -12,10 +15,13 @@ fn bar() u32 { return 123; } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="7 123\n" +#} +#update +#check stdout { +7 123 +#} -#update=add newline -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); @@ -29,4 +35,9 @@ fn bar() u32 { return 123; } const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="8 123\n" +#} +#update { +add newline#} +#check stdout { +8 123 +#} diff --git a/test/incremental/pointer_to_decl_with_astgen_error b/test/incremental/pointer_to_decl_with_astgen_error index 6c9359209b7fa7e83de855a3e4680f31c566fa5c..87d07ca1abe476a8bc0be5a2abfa2b127736127f 100644 --- a/test/incremental/pointer_to_decl_with_astgen_error +++ b/test/incremental/pointer_to_decl_with_astgen_error @@ -1,23 +1,36 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const foo = {}; pub fn main() void { _ = &foo; } -#expect_stdout="" +#} +#update +#check stdout { +#} -#update=introduce use of undeclared identifier -#file=main.zig +#write main.zig { const foo = something; pub fn main() void { _ = &foo; } -#expect_error=main.zig:1:13: error: use of undeclared identifier 'something' +#} +#update { +introduce use of undeclared identifier#} +#check errors { +main.zig:1:13: error: use of undeclared identifier 'something' +#} -#update=revert use of undeclared identifier -#file=main.zig +#write main.zig { const foo = {}; pub fn main() void { _ = &foo; } -#expect_stdout="" +#} +#update { +revert use of undeclared identifier#} +#check stdout { +#} diff --git a/test/incremental/recursive_function_becomes_non_recursive b/test/incremental/recursive_function_becomes_non_recursive index e789f3860de557247e84240918773ada0362feaa..442748a079c7bc0ca8a97fc726a661e86546acd1 100644 --- a/test/incremental/recursive_function_becomes_non_recursive +++ b/test/incremental/recursive_function_becomes_non_recursive @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub fn main() !void { try foo(false); } @@ -10,10 +13,13 @@ fn foo(recurse: bool) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="non-recursive path\n" +#} +#update +#check stdout { +non-recursive path +#} -#update=eliminate recursion and change argument -#file=main.zig +#write main.zig { pub fn main() !void { try foo(true); } @@ -24,4 +30,9 @@ fn foo(recurse: bool) !void { } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="x==1\n" +#} +#update { +eliminate recursion and change argument#} +#check stdout { +x==1 +#} diff --git a/test/incremental/remove_enum_field b/test/incremental/remove_enum_field index f478ea7f5528410cb20c367cb01aa0157a34fc12..67b0db55b9d597270216eee728189fbd0a638833 100644 --- a/test/incremental/remove_enum_field +++ b/test/incremental/remove_enum_field @@ -1,27 +1,39 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const MyEnum = enum(u8) { foo = 1, bar = 2, }; pub fn main() !void { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{}\n", .{@intFromEnum(MyEnum.foo)}); + try stdout_writer.interface.print("{}\n", .{@backingInt(MyEnum.foo)}); } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="1\n" -#update=remove enum field -#file=main.zig +#} +#update +#check stdout { +1 +#} + +#write main.zig { const MyEnum = enum(u8) { //foo = 1, bar = 2, }; pub fn main() !void { var stdout_writer = std.Io.File.stdout().writerStreaming(io, &.{}); - try stdout_writer.interface.print("{}\n", .{@intFromEnum(MyEnum.foo)}); + try stdout_writer.interface.print("{}\n", .{@backingInt(MyEnum.foo)}); } const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:7:69: error: enum 'main.MyEnum' has no member named 'foo' -#expect_error=main.zig:1:16: note: enum declared here +#} +#update { +remove enum field#} +#check errors { +main.zig:7:68: error: enum 'main.MyEnum' has no member named 'foo' +main.zig:1:16: note: enum declared here +#} diff --git a/test/incremental/remove_function_with_changed_callee b/test/incremental/remove_function_with_changed_callee index 31e962124dfb10374fd9b0dd70bc743edde143bd..d0a04678922d9ef841393ad2903469baf6ed7ca6 100644 --- a/test/incremental/remove_function_with_changed_callee +++ b/test/incremental/remove_function_with_changed_callee @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); const io = std.Io.Threaded.global_single_threaded.io(); @@ -23,9 +26,12 @@ fn bar(a: A) !void { pub fn main() !void { try foo(); } -#expect_stdout="0" -#update=remove foo and change bar wasm function type -#file=main.zig +#} +#update +#check stdout { +0#} + +#write main.zig { //! This update must preserve `bar` `InternPool.Index` value, while changing its wasm function type. //! If `foo` code is preserved in the binary without any changes, wasm type checking will fail. const std = @import("std"); @@ -47,4 +53,8 @@ fn bar(a: A) !void { pub fn main() !void { try bar(.one); } -#expect_stdout="1" +#} +#update { +remove foo and change bar wasm function type#} +#check stdout { +1#} diff --git a/test/incremental/remove_invalid_union_backing_enum b/test/incremental/remove_invalid_union_backing_enum index 04970380fb621c4e2e9cb2d3c5069d6448c5b989..8a2099afb4ebe92697ff76f773b53abb4eb8b871 100644 --- a/test/incremental/remove_invalid_union_backing_enum +++ b/test/incremental/remove_invalid_union_backing_enum @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const E = enum { a, b, c }; const U = union(E) { a: i32, @@ -11,10 +14,14 @@ pub fn main() void { const u: U = .{ .a = 123 }; _ = u; } -#expect_error=main.zig:6:5: error: no field named 'd' in enum 'main.E' -#expect_error=main.zig:1:11: note: enum declared here -#update=remove invalid backing enum -#file=main.zig +#} +#update +#check errors { +main.zig:6:5: error: no field named 'd' in enum 'main.E' +main.zig:1:11: note: enum declared here +#} + +#write main.zig { const U = union { a: i32, b: f64, @@ -25,4 +32,8 @@ pub fn main() void { const u: U = .{ .a = 123 }; _ = u; } -#expect_stdout="" +#} +#update { +remove invalid backing enum#} +#check stdout { +#} diff --git a/test/incremental/struct_reification_becomes_anon_init b/test/incremental/struct_reification_becomes_anon_init index 7099685ba2c2e464607039c5eb461fa500422d69..bd790cdeb9445aab9834c64f0a5fa683974b6d95 100644 --- a/test/incremental/struct_reification_becomes_anon_init +++ b/test/incremental/struct_reification_becomes_anon_init @@ -1,19 +1,30 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const Foo = @Struct(.auto, null, &.{"reified_field"}, &.{void}, &.{.{}}); pub fn main(init: std.process.Init) !void { const field_name = @typeInfo(Foo).@"struct".field_names[0]; try std.Io.File.stdout().writeStreamingAll(init.io, field_name ++ "\n"); } const std = @import("std"); -#expect_stdout="reified_field\n" +#} +#update +#check stdout { +reified_field +#} -#update=change to `@TypeOf` anonymous init -#file=main.zig +#write main.zig { const Foo = @TypeOf(.{ .anon_field = 123 }); pub fn main(init: std.process.Init) !void { const field_name = @typeInfo(Foo).@"struct".field_names[0]; try std.Io.File.stdout().writeStreamingAll(init.io, field_name ++ "\n"); } const std = @import("std"); -#expect_stdout="anon_field\n" +#} +#update { +change to `@TypeOf` anonymous init#} +#check stdout { +anon_field +#} diff --git a/test/incremental/tag_name b/test/incremental/tag_name index 17954435c15786ddf98bbd576c1a2ae346ab14aa..05991657cdbfd920ee24d094bb8db93ada706d17 100644 --- a/test/incremental/tag_name +++ b/test/incremental/tag_name @@ -1,27 +1,41 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); var some_enum: enum { first, second } = .first; const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @tagName(some_enum)); } -#expect_stdout="first" -#update=no change -#file=main.zig +#} +#update +#check stdout { +first#} + +#write main.zig { const std = @import("std"); var some_enum: enum { first, second } = .first; const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @tagName(some_enum)); } -#expect_stdout="first" -#update=swap fields -#file=main.zig +#} +#update { +no change#} +#check stdout { +first#} + +#write main.zig { const std = @import("std"); var some_enum: enum { second, first } = .first; const io = std.Io.Threaded.global_single_threaded.io(); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, @tagName(some_enum)); } -#expect_stdout="first" +#} +#update { +swap fields#} +#check stdout { +first#} diff --git a/test/incremental/temporary_analysis_error_in_generic_signature b/test/incremental/temporary_analysis_error_in_generic_signature index d8db72050b3af1164a312e1e7a1ed1f89bfc3dae..dbb3b6407eba276b9654775979353b53d9cea452 100644 --- a/test/incremental/temporary_analysis_error_in_generic_signature +++ b/test/incremental/temporary_analysis_error_in_generic_signature @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { //! The original repro here depends on re-analysis order, which depends on //! declaration order, so this exact declaration order must be used. const Foo = struct { x: u8 }; @@ -11,10 +14,13 @@ fn bar(comptime x: u8) @This().Foo { return .{ .x = x }; } const std = @import("std"); -#expect_stdout="Z\n" +#} +#update +#check stdout { +Z +#} -#update=change generic signature to use non-existent member -#file=main.zig +#write main.zig { //! The original repro here depends on re-analysis order, which depends on //! declaration order, so this exact declaration order must be used. const Foo = struct { x: u8 }; @@ -26,11 +32,15 @@ fn bar(comptime x: u8) @This().FooAlias { return .{ .x = x }; } const std = @import("std"); -#expect_error=main.zig:8:31: error: root source file struct 'main' has no member named 'FooAlias' -#expect_error=main.zig:1:1: note: struct declared here +#} +#update { +change generic signature to use non-existent member#} +#check errors { +main.zig:8:31: error: root source file struct 'main' has no member named 'FooAlias' +main.zig:1:1: note: struct declared here +#} -#update=add that member, fixing the error -#file=main.zig +#write main.zig { //! The original repro here depends on re-analysis order, which depends on //! declaration order, so this exact declaration order must be used. const Foo = struct { x: u8 }; @@ -43,4 +53,9 @@ fn bar(comptime x: u8) @This().FooAlias { return .{ .x = x }; } const std = @import("std"); -#expect_stdout="Z\n" +#} +#update { +add that member, fixing the error#} +#check stdout { +Z +#} diff --git a/test/incremental/temporary_parse_error b/test/incremental/temporary_parse_error index b19636e4455a8c19a028372276b96560c5c523da..9687ac4844b79133391f85addd09e71a44c8f6cd 100644 --- a/test/incremental/temporary_parse_error +++ b/test/incremental/temporary_parse_error @@ -1,15 +1,28 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void {} -#expect_stdout="" +#} +#update +#check stdout { +#} -#update=introduce parse error -#file=main.zig +#write main.zig { pub fn main() !void { -#expect_error=main.zig:2:1: error: expected statement, found 'EOF' +#} +#update { +introduce parse error#} +#check errors { +main.zig:2:1: error: expected statement, found 'EOF' +#} -#update=fix parse error -#file=main.zig +#write main.zig { pub fn main() !void {} -#expect_stdout="" +#} +#update { +fix parse error#} +#check stdout { +#} diff --git a/test/incremental/type_becomes_comptime_only b/test/incremental/type_becomes_comptime_only index f5b0f9b5786a684d7e2dc72f78438d18ffad84e5..f174faacd788e3e5d4720c9badb1cb2e5f91c9ea 100644 --- a/test/incremental/type_becomes_comptime_only +++ b/test/incremental/type_becomes_comptime_only @@ -1,5 +1,8 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const SomeType = u32; const S = struct { x: SomeType, @@ -9,10 +12,12 @@ pub fn main() void { const s: S = .{ .x = 456 }; s.foo(); } -#expect_stdout="" +#} +#update +#check stdout { +#} -#update=make S comptime-only -#file=main.zig +#write main.zig { const SomeType = comptime_int; const S = struct { x: SomeType, @@ -22,10 +27,13 @@ pub fn main() void { const s: S = .{ .x = 456 }; s.foo(); } -#expect_stdout="" +#} +#update { +make S comptime-only#} +#check stdout { +#} -#update=make S runtime again -#file=main.zig +#write main.zig { const SomeType = u16; const S = struct { x: SomeType, @@ -35,4 +43,8 @@ pub fn main() void { const s: S = .{ .x = 456 }; s.foo(); } -#expect_stdout="" +#} +#update { +make S runtime again#} +#check stdout { +#} diff --git a/test/incremental/type_dependency_loop b/test/incremental/type_dependency_loop index b645d4266215f712826dee9291dc3150c397108f..85de8ae8391015b85194357423522046466cc042 100644 --- a/test/incremental/type_dependency_loop +++ b/test/incremental/type_dependency_loop @@ -1,50 +1,70 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { pub const A = struct { b: B }; pub const B = struct { a: A }; pub fn main() void { _ = @as(B, undefined); } -#expect_error=:error: dependency loop with length 2 -#expect_error=main.zig:2:27: note: type 'main.B' depends on type 'main.A' for field declared here -#expect_error=main.zig:1:27: note: type 'main.A' depends on type 'main.B' for field declared here -#expect_error=:note: eliminate any one of these dependencies to break the loop +#} +#update +#check errors { +error: dependency loop with length 2 + main.zig:2:27: note: type 'main.B' depends on type 'main.A' for field declared here + main.zig:1:27: note: type 'main.A' depends on type 'main.B' for field declared here + note: eliminate any one of these dependencies to break the loop +#} -#update=remove reference to dependency loop -#file=main.zig +#write main.zig { pub const A = struct { b: B }; pub const B = struct { a: A }; pub fn main() void { _ = B; } -#expect_stdout="" +#} +#update { +remove reference to dependency loop#} +#check stdout { +#} -#update=change dependency loop without fixing it -#file=main.zig +#write main.zig { pub const A = struct { b: B }; pub const B = struct { a: *align(@alignOf(A)) A }; pub fn main() void { _ = B; } -#expect_stdout="" +#} +#update { +change dependency loop without fixing it#} +#check stdout { +#} -#update=reference dependency loop again -#file=main.zig +#write main.zig { pub const A = struct { b: B }; pub const B = struct { a: *align(@alignOf(A)) A }; pub fn main() void { _ = @as(B, undefined); } -#expect_error=:error: dependency loop with length 2 -#expect_error=main.zig:2:43: note: type 'main.B' depends on type 'main.A' for alignment query here -#expect_error=main.zig:1:27: note: type 'main.A' depends on type 'main.B' for field declared here -#expect_error=:note: eliminate any one of these dependencies to break the loop +#} +#update { +reference dependency loop again#} +#check errors { +error: dependency loop with length 2 + main.zig:2:43: note: type 'main.B' depends on type 'main.A' for alignment query here + main.zig:1:27: note: type 'main.A' depends on type 'main.B' for field declared here + note: eliminate any one of these dependencies to break the loop +#} -#update=fix dependency loop -#file=main.zig +#write main.zig { pub const A = struct { b: B }; pub const B = struct { a: *A }; pub fn main() void { _ = @as(B, undefined); } -#expect_stdout="" +#} +#update { +fix dependency loop#} +#check stdout { +#} diff --git a/test/incremental/unreferenced_error b/test/incremental/unreferenced_error index f0ab8062594912c7b990472104877bd61a3d6680..0f5f65feab5c98985d105d06db1e2086cd6f0470 100644 --- a/test/incremental/unreferenced_error +++ b/test/incremental/unreferenced_error @@ -1,25 +1,35 @@ -#update=initial version -#file=main.zig +#exe { +-Mroot=main.zig +#} + +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, a); } const a = "Hello, World!\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hello, World!\n" +#} +#update +#check stdout { +Hello, World! +#} -#update=introduce compile error -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, a); } const a = @compileError("bad a"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_error=main.zig:5:11: error: bad a +#} +#update { +introduce compile error#} +#check errors { +main.zig:5:11: error: bad a +#} -#update=remove error reference -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, b); @@ -27,10 +37,14 @@ pub fn main() !void { const a = @compileError("bad a"); const b = "Hi there!\n"; const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Hi there!\n" +#} +#update { +remove error reference#} +#check stdout { +Hi there! +#} -#update=introduce and remove reference to error -#file=main.zig +#write main.zig { const std = @import("std"); pub fn main() !void { try std.Io.File.stdout().writeStreamingAll(io, a); @@ -38,4 +52,9 @@ pub fn main() !void { const a = "Back to a\n"; const b = @compileError("bad b"); const io = std.Io.Threaded.global_single_threaded.io(); -#expect_stdout="Back to a\n" +#} +#update { +introduce and remove reference to error#} +#check stdout { +Back to a +#} diff --git a/test/incremental2/bad_import b/test/incremental2/bad_import deleted file mode 100644 index d93a9ed6309f963c310a82f0813d0905a564cc10..0000000000000000000000000000000000000000 --- a/test/incremental2/bad_import +++ /dev/null @@ -1,48 +0,0 @@ -#exe { --Mroot=main.zig -#} - -#write main.zig { -pub fn main() !void { - _ = @import("foo.zig"); - try std.Io.File.stdout().writeStreamingAll(io, "success\n"); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#} -#write foo.zig { -comptime { - _ = @import("bad.zig"); -} -#} -#update -#check errors { -bad.zig:1:1: error: unable to load "bad.zig": FileNotFound -foo.zig:2:17: note: file imported here -#} - -#write foo.zig { -comptime { - _ = @import("this_is/not_real.zig"); -} -#} -#update { -change bad import#} -#check errors { -this_is/not_real.zig:1:1: error: unable to load "not_real.zig": FileNotFound -foo.zig:2:17: note: file imported here -#} - -#write main.zig { -pub fn main() !void { - //_ = @import("foo.zig"); - try std.Io.File.stdout().writeStreamingAll(io, "success\n"); -} -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -#} -#update { -remove import of 'foo.zig'#} -#check stdout { -success -#} diff --git a/test/incremental2/hello b/test/incremental2/hello deleted file mode 100644 index 74a45d2a8b81afebe334968569949b6c82d4c397..0000000000000000000000000000000000000000 --- a/test/incremental2/hello +++ /dev/null @@ -1,28 +0,0 @@ -#exe { --Mroot=main.zig -#} - -#write main.zig { -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -pub fn main() !void { - try std.Io.File.stdout().writeStreamingAll(io, "good morning\n"); -} -#} -#update -#check stdout { -good morning -#} - -#write main.zig { -const std = @import("std"); -const io = std.Io.Threaded.global_single_threaded.io(); -pub fn main() !void { - try std.Io.File.stdout().writeStreamingAll(io, "おはようございます\n"); -} -#} -#update { -change the string#} -#check stdout { -おはようございます -#} diff --git a/test/src/runner.zig b/test/src/runner.zig index e4d7e9971fc965bb4724bcdec971a5c1ed754156..9e37cec171b5d5134486a21c9459e508b31e3025 100644 --- a/test/src/runner.zig +++ b/test/src/runner.zig @@ -471,7 +471,7 @@ fn runTest( continue; } const cmd = std.meta.stringToEnum( - enum { skip, exe, lib, obj, write, delete, update, check }, + enum { todo, skip, exe, lib, obj, write, delete, update, check }, cmd_str, ) orelse return error.UnknownCommand; var maybe_arg = line_it.next(); @@ -525,6 +525,7 @@ fn runTest( }; 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)) @@ -646,12 +647,12 @@ fn runTest( update_num += 1; }, .check => { - const expected = try contents_r.allocRemaining(gpa, .unlimited); - defer gpa.free(expected); 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; @@ -753,7 +754,9 @@ fn runTest( 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()); }, @@ -783,12 +786,18 @@ fn runTest( }, } std.debug.assert(try contents_r.discardRemaining() == 0); - if (cmd != .skip) allow_skip = false; + 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| try comp.exit(io); + if (compiler) |*comp| { + if (comp.state != .idle) return error.MissingCheck; + try comp.exit(io); + } } const DelimitedReader = struct { const Io = std.Io; diff --git a/test/tests.zig b/test/tests.zig index b782307a3644d54a11ec3da00d150d9650ae4d11..d92863cc88bded40c15102941b11d3ec5f748178 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3343,109 +3343,16 @@ const IncrementalTestOptions = struct { }; pub fn addIncrementalTests( - b: *std.Build, - test_step: *Step, - options: IncrementalTestOptions, -) !void { - const io = b.graph.io; - - const incr_check = b.addExecutable(.{ - .name = "incr-check", - .root_module = b.createModule(.{ - .root_source_file = b.path("tools/incr-check.zig"), - .target = b.graph.host, - .optimize = .debug, - }), - }); - - b.dependOnDirectoryContents(b.path("test/incremental")); - - var dir = try b.root.openDir(io, "test/incremental", .{ .iterate = true }); - defer dir.close(io); - - var it = try dir.walk(b.graph.arena); - while (try it.next(io)) |entry| { - if (isEditorFileName(entry.basename)) continue; - - for (options.test_filters) |test_filter| { - if (std.mem.find(u8, entry.path, test_filter)) |_| break; - } else if (options.test_filters.len > 0) continue; - - switch (entry.kind) { - .file => {}, - .directory => { - b.dependOnDirectoryContents(b.path(b.pathJoin(&.{ "test", "incremental", entry.path }))); - }, - else => continue, - } - b.dependOnFileContents(b.path(b.pathJoin(&.{ "test", "incremental", entry.path }))); - - for (incremental_targets) |test_target| { - const resolved_target = b.resolveTargetQuery(test_target.target); - - if (options.skip_non_native and !isNative(&resolved_target, &b.graph.host.result)) - continue; - - const target = &resolved_target.result; - - if (options.skip_wasm and target.cpu.arch.isWasm()) continue; - - if (options.skip_freebsd and target.os.tag == .freebsd) continue; - if (options.skip_netbsd and target.os.tag == .netbsd) continue; - if (options.skip_openbsd and target.os.tag == .openbsd) continue; - if (options.skip_windows and target.os.tag == .windows) continue; - if (options.skip_darwin and target.os.tag.isDarwin()) continue; - if (options.skip_linux and target.os.tag == .linux) continue; - - if (options.skip_llvm and test_target.backend == .llvm) continue; - - const target_str = b.fmt("{s}-{t}", .{ - resolved_target.query.zigTriple(b.allocator) catch @panic("OOM"), - 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; - } - - const run = b.addRunArtifact(incr_check); - run.setName(b.fmt("incr-check {s} '{s}'", .{ target_str, entry.basename })); - - run.addArg(b.graph.zig_exe); - run.addFileArg(b.path("test/incremental/").path(b, entry.path)); - - run.addArg("--zig-lib-dir"); - run.addDirectoryArg(.zig_lib); - - run.addArgs(&.{ "--target", target_str }); - - run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc - - run.addThirdPartyEnabledArgDarling(.{ .enabled = "-fdarling" }); - run.addThirdPartyEnabledArgQemu(.{ .enabled = "-fqemu" }); - run.addThirdPartyEnabledArgRosetta(.{ .enabled = "-frosetta" }); - run.addThirdPartyEnabledArgWasmtime(.{ .enabled = "-fwasmtime" }); - run.addThirdPartyEnabledArgWine(.{ .enabled = "-fwine" }); - - run.addCheck(.{ .expect_term = .{ .exited = 0 } }); - test_step.dependOn(&run.step); - } - } -} - -pub fn addNewIncrementalTests( b: *std.Build, runner: *std.Build.Step.Compile, options: IncrementalTestOptions, ) !*Step { - const tests_step = b.step("test-new-incremental", "Run the new incremental compilation test cases"); + const tests_step = b.step("test-incremental", "Run the new incremental compilation test cases"); - const tests_path = b.path("test/incremental2"); + const tests_path = b.path("test/incremental"); b.dependOnDirectoryContents(tests_path); - var tests_dir = try b.root.openDir(b.graph.io, "test/incremental2", .{ .iterate = true }); + var tests_dir = try b.root.openDir(b.graph.io, "test/incremental", .{ .iterate = true }); defer tests_dir.close(b.graph.io); var test_it = tests_dir.iterate(); while (try test_it.next(b.graph.io)) |@"test"| { diff --git a/tools/incr-check.zig b/tools/incr-check.zig deleted file mode 100644 index c8034260dbc2af7a256151c7c43f4429531e390b..0000000000000000000000000000000000000000 --- a/tools/incr-check.zig +++ /dev/null @@ -1,1032 +0,0 @@ -const std = @import("std"); -const Io = std.Io; -const Dir = std.Io.Dir; -const Allocator = std.mem.Allocator; -const Cache = std.Build.Cache; - -const usage = - \\Usage: incr-check [options] - \\Options: - \\ --target triple-backend - \\ --quiet - \\ --zig-lib-dir /path/to/zig/lib - \\ --zig-cc-binary /path/to/zig - \\ -fqemu - \\ -fwine - \\ -fwasmtime - \\Debug Options: - \\ --preserve-tmp - \\ --debug-log foo - \\ --debug-link-snapshot -; - -pub const std_options: std.Options = .{ - .logFn = logImpl, -}; -var log_cur_update: ?*const Case.Update = null; -fn logImpl( - comptime level: std.log.Level, - comptime scope: @EnumLiteral(), - comptime format: []const u8, - args: anytype, -) void { - const update = log_cur_update orelse { - return std.log.defaultLog(level, scope, format, args); - }; - std.log.defaultLog( - level, - scope, - "['{s}'] " ++ format, - .{update.name} ++ args, - ); -} - -pub fn main(init: std.process.Init) !void { - const gpa = init.gpa; - const arena = init.arena.allocator(); - const io = init.io; - const environ_map = init.environ_map; - const cwd_path = try std.process.currentPathAlloc(io, arena); - - var opt_zig_exe: ?[]const u8 = null; - var opt_input_file_name: ?[]const u8 = null; - var opt_lib_dir: ?[]const u8 = null; - var opt_cc_zig: ?[]const u8 = null; - var opt_target: ?struct { std.Target.Query, Backend } = null; - var preserve_tmp = false; - var enable_qemu: bool = false; - var enable_wine: bool = false; - var enable_wasmtime: bool = false; - var enable_darling: bool = false; - var quiet: bool = false; - - var debug_log_args: std.ArrayList([]const u8) = .empty; - var debug_link_snapshot = false; - - var arg_it = try init.minimal.args.iterateAllocator(arena); - _ = arg_it.skip(); - while (arg_it.next()) |arg| { - if (arg.len > 0 and arg[0] == '-') { - if (std.mem.eql(u8, arg, "--zig-lib-dir")) { - opt_lib_dir = arg_it.next() orelse badUsage("expected arg after --zig-lib-dir", .{}); - } else if (std.mem.eql(u8, arg, "--target")) { - const str = arg_it.next() orelse badUsage("expected arg after --zig-cc-binary", .{}); - opt_target = parseTargetQueryAndBackend(str, ""); - } else if (std.mem.eql(u8, arg, "--quiet")) { - quiet = true; - } else if (std.mem.eql(u8, arg, "--debug-log")) { - try debug_log_args.append( - arena, - arg_it.next() orelse badUsage("expected arg after --debug-log", .{}), - ); - } else if (std.mem.eql(u8, arg, "--debug-link-snapshot")) { - debug_link_snapshot = true; - } else if (std.mem.eql(u8, arg, "--preserve-tmp")) { - preserve_tmp = 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 (std.mem.eql(u8, arg, "--zig-cc-binary")) { - opt_cc_zig = arg_it.next() orelse badUsage("expected arg after --zig-cc-binary", .{}); - } else { - badUsage("unknown option '{s}'", .{arg}); - } - continue; - } - if (opt_zig_exe == null) { - opt_zig_exe = arg; - } else if (opt_input_file_name == null) { - opt_input_file_name = arg; - } else { - badUsage("unknown argument '{s}'\n{s}", .{ arg, usage }); - } - } - const zig_exe = opt_zig_exe orelse badUsage("missing path to zig", .{}); - const input_file_name = opt_input_file_name orelse badUsage("missing input file", .{}); - const target_query, const backend = opt_target orelse badUsage("missing required option '--target'", .{}); - - if (backend == .cbe and opt_lib_dir == null) { - std.process.fatal("'--zig-lib-dir' required when using backend 'cbe'", .{}); - } - - const input_file_bytes = try Dir.cwd().readFileAlloc(io, input_file_name, arena, .limited(std.math.maxInt(u32))); - const case: Case = try .parse(arena, input_file_bytes); - - for (case.skip_targets) |skip| { - if (target_query.eql(skip.query) and backend == skip.backend) { - if (!quiet) std.log.warn("skipping test because of a 'skip_target' match", .{}); - return; - } - } - - const target = try std.zig.system.resolveTargetQuery(io, target_query); - - const prog_node = std.Progress.start(io, .{}); - defer prog_node.end(); - - const rand_int = rand64(io); - const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int); - var tmp_dir = try Dir.cwd().createDirPathOpen(io, tmp_dir_path, .{}); - defer { - tmp_dir.close(io); - if (!preserve_tmp) { - Dir.cwd().deleteTree(io, tmp_dir_path) catch |err| { - std.log.warn("failed to delete tree '{s}': {t}", .{ tmp_dir_path, err }); - }; - } - } - - // Convert paths to be relative to the cwd of the subprocess. - const resolved_zig_exe = try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, zig_exe); - const opt_resolved_lib_dir = if (opt_lib_dir) |lib_dir| - try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, lib_dir) - else - null; - - const host = try std.zig.system.resolveTargetQuery(io, .{}); - - var child_args: std.ArrayList([]const u8) = .empty; - try child_args.appendSlice(arena, &.{ - resolved_zig_exe, "build-exe", - "-fincremental", "-fno-ubsan-rt", - "-target", try target_query.zigTriple(arena), - "--cache-dir", ".local-cache", - "--global-cache-dir", ".global-cache", - }); - - if (opt_resolved_lib_dir) |resolved_lib_dir| { - try child_args.appendSlice(arena, &.{ "--zig-lib-dir", resolved_lib_dir }); - } - switch (backend) { - .sema => try child_args.append(arena, "-fno-emit-bin"), - .selfhosted => try child_args.appendSlice(arena, &.{ "-fno-llvm", "-fno-lld" }), - .llvm => try child_args.appendSlice(arena, &.{ "-fllvm", "-flld" }), - .cbe => try child_args.appendSlice(arena, &.{ "-ofmt=c", "-lc" }), - } - for (debug_log_args.items) |arg| { - try child_args.appendSlice(arena, &.{ "--debug-log", arg }); - } - if (debug_link_snapshot) { - try child_args.append(arena, "--debug-link-snapshot"); - } - for (case.modules) |mod| { - try child_args.appendSlice(arena, &.{ "--dep", mod.name }); - } - try child_args.append(arena, try std.fmt.allocPrint(arena, "-Mroot={s}", .{case.root_source_file})); - for (case.modules) |mod| { - try child_args.append(arena, try std.fmt.allocPrint(arena, "-M{s}={s}", .{ mod.name, mod.file })); - } - try child_args.append(arena, "--listen=-"); - - const zig_prog_node = prog_node.start("zig", 0); - defer zig_prog_node.end(); - - var cc_child_args: std.ArrayList([]const u8) = .empty; - if (backend == .cbe) { - const resolved_cc_zig_exe = if (opt_cc_zig) |cc_zig_exe| - try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, cc_zig_exe) - else - resolved_zig_exe; - - try cc_child_args.appendSlice(arena, &.{ - resolved_cc_zig_exe, "build-exe", - "-target", try target_query.zigTriple(arena), - "--cache-dir", ".local-cache", - "--global-cache-dir", ".global-cache", - "-I", opt_resolved_lib_dir.?, // verified earlier - "-lc", - }); - } - - const allow_compiler_stderr = debug_log_args.items.len != 0; - - if (allow_compiler_stderr) { - const cmd: std.zig.SubprocessCommand = .{ - .argv = child_args.items, - .cwd = tmp_dir_path, - }; - std.log.scoped(.spawn).info("{f}", .{cmd}); - } - - var child = try std.process.spawn(io, .{ - .argv = child_args.items, - .stdin = .pipe, - .stdout = .pipe, - .stderr = .pipe, - .progress_node = zig_prog_node, - .cwd = .{ .path = tmp_dir_path }, - }); - defer child.kill(io); - - const updates_prog_node = prog_node.start("updates", case.updates.len); - defer updates_prog_node.end(); - - var eval: Eval = .{ - .arena = arena, - .io = io, - .case = case, - .host = host, - .target = target, - .backend = backend, - .tmp_dir = tmp_dir, - .tmp_dir_path = tmp_dir_path, - .child = &child, - .allow_compiler_stderr = allow_compiler_stderr, - .quiet = quiet, - .preserve_tmp_on_fatal = preserve_tmp, - .cc_child_args = &cc_child_args, - .enable_qemu = enable_qemu, - .enable_wine = enable_wine, - .enable_wasmtime = enable_wasmtime, - .enable_darling = enable_darling, - }; - - var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; - var multi_reader: Io.File.MultiReader = undefined; - multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); - defer multi_reader.deinit(); - - var update_mtime = Io.Clock.real.now(io); - for (case.updates) |update| { - var update_prog_node = updates_prog_node.start(update.name, 0); - defer update_prog_node.end(); - - if (debug_log_args.items.len != 0) { - // Print a line separating the debug logs from the compiler in the stderr output. - std.log.scoped(.status).info("update: '{s}'", .{update.name}); - } - - log_cur_update = &update; - defer log_cur_update = null; - - eval.write(update, update_mtime); - update_mtime = update_mtime.addDuration(.fromSeconds(5)); - try eval.requestUpdate(); - try eval.check(&multi_reader, update, update_prog_node); - } - - try eval.end(&multi_reader); - - waitChild(&child, &eval); -} - -const Eval = struct { - arena: Allocator, - io: Io, - case: Case, - host: std.Target, - target: std.Target, - backend: Backend, - tmp_dir: Dir, - tmp_dir_path: []const u8, - child: *std.process.Child, - allow_compiler_stderr: bool, - quiet: bool, - preserve_tmp_on_fatal: bool, - /// When `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.ArrayList([]const u8), - - enable_qemu: bool, - enable_wine: bool, - enable_wasmtime: bool, - enable_darling: bool, - - /// Currently this function assumes the previous updates have already been written. - fn write(eval: *Eval, update: Case.Update, mtime: Io.Timestamp) void { - const io = eval.io; - for (update.changes) |full_contents| { - var update_file = eval.tmp_dir.createFile(io, full_contents.name, .{}) catch |err| { - eval.fatal("failed to create update '{s}': {t}", .{ full_contents.name, err }); - }; - defer update_file.close(io); - update_file.writeStreamingAll(io, full_contents.bytes) catch |err| { - eval.fatal("failed to write update '{s}': {t}", .{ full_contents.name, err }); - }; - update_file.setTimestamps(io, .{ .modify_timestamp = .{ .new = mtime } }) catch |err| { - eval.fatal("failed to set mtime for '{s}': {t}", .{ full_contents.name, err }); - }; - } - for (update.deletes) |doomed_name| { - eval.tmp_dir.deleteFile(io, doomed_name) catch |err| { - eval.fatal("failed to delete '{s}': {t}", .{ doomed_name, err }); - }; - } - } - - fn check(eval: *Eval, mr: *Io.File.MultiReader, update: Case.Update, prog_node: std.Progress.Node) !void { - const arena = eval.arena; - const stdout = mr.reader(0); - const stderr = mr.reader(1); - - var client: std.zig.Client = .{ - .in = stdout, - .out = undefined, - }; - - while (true) { - const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) { - error.Timeout => unreachable, - error.EndOfStream => break, - else => |e| return e, - }; - const body = client.in.take(header.bytes_len) catch unreachable; - - switch (header.tag) { - .error_bundle => { - const result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body); - if (stderr.bufferedLen() > 0) { - if (eval.allow_compiler_stderr) { - std.log.info("error_bundle stderr:\n{s}", .{stderr.buffered()}); - } else { - eval.fatal("error_bundle unexpected stderr:\n{s}", .{stderr.buffered()}); - } - stderr.tossBuffered(); - } - if (result_error_bundle.errorMessageCount() != 0) { - try eval.checkErrorOutcome(update, result_error_bundle); - } - // This message indicates the end of the update. - return; - }, - .emit_digest => { - var r: std.Io.Reader = .fixed(body); - _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable; - - if (stderr.bufferedLen() > 0) { - if (eval.allow_compiler_stderr) { - std.log.info("emit_digest stderr:\n{s}", .{stderr.buffered()}); - } else { - eval.fatal("emit_digest unexpected stderr:\n{s}", .{stderr.buffered()}); - } - stderr.tossBuffered(); - } - if (eval.backend == .sema) { - try eval.checkSuccessOutcome(update, null, prog_node); - continue; - } - - const digest = r.takeArray(Cache.bin_digest_len) catch unreachable; - const result_dir = ".local-cache" ++ Dir.path.sep_str ++ "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*); - - const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{ - .root_name = "root", // corresponds to the module name "root" - .cpu_arch = eval.target.cpu.arch, - .os_tag = eval.target.os.tag, - .ofmt = eval.target.ofmt, - .abi = eval.target.abi, - .output_mode = .Exe, - }); - const bin_path = try Dir.path.join(arena, &.{ result_dir, bin_name }); - - try eval.checkSuccessOutcome(update, bin_path, prog_node); - }, - else => { - // Ignore other messages. - }, - } - } - - const buffered_stderr = stderr.buffered(); - if (buffered_stderr.len > 0) { - if (eval.allow_compiler_stderr) { - std.log.info("stderr:\n{s}", .{buffered_stderr}); - } else { - eval.fatal("unexpected stderr:\n{s}", .{buffered_stderr}); - } - } - - waitChild(eval.child, eval); - eval.fatal("compiler failed to send terminating error_bundle", .{}); - } - - fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void { - const io = eval.io; - const expected = switch (update.outcome) { - .unknown => return, - .compile_errors => |ce| ce, - .stdout, .exit_code => { - try error_bundle.renderToStderr(io, .{}, .auto); - eval.fatal("unexpected compile errors", .{}); - }, - }; - - var expected_idx: usize = 0; - - for (error_bundle.getMessages()) |err_idx| { - if (expected_idx == expected.errors.len) { - try error_bundle.renderToStderr(io, .{}, .auto); - eval.fatal("more errors than expected", .{}); - } - try eval.checkOneError(error_bundle, expected.errors[expected_idx], false, err_idx); - expected_idx += 1; - - for (error_bundle.getNotes(err_idx)) |note_idx| { - if (expected_idx == expected.errors.len) { - try error_bundle.renderToStderr(io, .{}, .auto); - eval.fatal("more error notes than expected", .{}); - } - try eval.checkOneError(error_bundle, expected.errors[expected_idx], true, note_idx); - expected_idx += 1; - } - } - - if (!std.mem.eql(u8, error_bundle.getCompileLogOutput(), expected.compile_log_output)) { - try error_bundle.renderToStderr(io, .{}, .auto); - eval.fatal("unexpected compile log output", .{}); - } - } - - fn checkOneError( - eval: *Eval, - eb: std.zig.ErrorBundle, - expected: Case.ExpectedError, - is_note: bool, - err_idx: std.zig.ErrorBundle.MessageIndex, - ) Allocator.Error!void { - const io = eval.io; - const err = eb.getErrorMessage(err_idx); - if (err.count != 1) @panic("TODO error message with count>1"); - const msg = eb.nullTerminatedString(err.msg); - const matches = matches: { - if (expected.is_note != is_note) break :matches false; - if (!std.mem.eql(u8, expected.msg, msg)) break :matches false; - if (err.src_loc == .none) { - break :matches expected.src == null; - } - const expected_src = expected.src orelse break :matches false; - const src = eb.getSourceLocation(err.src_loc); - const raw_filename = eb.nullTerminatedString(src.src_path); - // We need to replace backslashes for consistency between platforms. - const filename = name: { - if (std.mem.findScalar(u8, raw_filename, '\\') == null) break :name raw_filename; - const copied = try eval.arena.dupe(u8, raw_filename); - std.mem.replaceScalar(u8, copied, '\\', '/'); - break :name copied; - }; - if (!std.mem.eql(u8, expected_src.filename, filename)) break :matches false; - if (expected_src.line != src.line + 1) break :matches false; - if (expected_src.column != src.column + 1) break :matches false; - break :matches true; - }; - if (!matches) { - eb.renderToStderr(io, .{}, .auto) catch {}; - eval.fatal("compile error did not match expected error", .{}); - } - } - - 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 => eval.fatal("expected compile errors but compilation incorrectly succeeded", .{}), - .stdout, .exit_code => {}, - } - const emitted_path = opt_emitted_path orelse { - std.debug.assert(eval.backend == .sema); - return; - }; - const io = eval.io; - - const binary_path = switch (eval.backend) { - .sema => unreachable, - .selfhosted, .llvm => emitted_path, - .cbe => bin: { - const rand_int = rand64(io); - const out_bin_name = "./out_" ++ std.fmt.hex(rand_int); - try eval.buildCOutput(emitted_path, out_bin_name, prog_node); - break :bin out_bin_name; - }, - }; - - var argv_buf: [2][]const u8 = undefined; - const argv: []const []const u8, const is_foreign: bool = sw: switch (std.zig.system.getExternalExecutor( - io, - &eval.target, - .{ - .link_libc = eval.backend == .cbe, - .host_cpu_arch = eval.host.cpu.arch, - .host_os_tag = eval.host.os.tag, - }, - )) { - .bad_dl, .bad_os_or_cpu => { - // This binary cannot be executed on this host. - if (!eval.quiet) { - std.log.warn("skipping execution because host '{s}' cannot execute binaries for foreign target '{s}'", .{ - try eval.host.zigTriple(eval.arena), - try eval.target.zigTriple(eval.arena), - }); - } - return; - }, - .native, .rosetta => argv: { - argv_buf[0] = binary_path; - break :argv .{ argv_buf[0..1], false }; - }, - .qemu => |executor_cmd| argv: { - if (eval.enable_qemu) { - argv_buf[0] = executor_cmd; - argv_buf[1] = binary_path; - break :argv .{ argv_buf[0..2], true }; - } else { - continue :sw .bad_os_or_cpu; - } - }, - .wine => |executor_cmd| argv: { - if (eval.enable_wine) { - argv_buf[0] = executor_cmd; - argv_buf[1] = binary_path; - break :argv .{ argv_buf[0..2], true }; - } else { - continue :sw .bad_os_or_cpu; - } - }, - .wasmtime => |executor_cmd| argv: { - if (eval.enable_wasmtime) { - argv_buf[0] = executor_cmd; - argv_buf[1] = binary_path; - break :argv .{ argv_buf[0..2], true }; - } else { - continue :sw .bad_os_or_cpu; - } - }, - .darling => |executor_cmd| argv: { - if (eval.enable_darling) { - argv_buf[0] = executor_cmd; - argv_buf[1] = binary_path; - break :argv .{ argv_buf[0..2], true }; - } else { - continue :sw .bad_os_or_cpu; - } - }, - }; - - const run_prog_node = prog_node.start("run generated executable", 0); - defer run_prog_node.end(); - - const result = std.process.run(eval.arena, io, .{ - .argv = argv, - .cwd = .{ .path = eval.tmp_dir_path }, - }) catch |err| { - if (is_foreign) { - // Chances are the foreign executor isn't available. Skip this evaluation. - if (!eval.quiet) { - std.log.warn("skipping execution of '{s}' via executor for foreign target '{s}': {t}", .{ - binary_path, - try eval.target.zigTriple(eval.arena), - err, - }); - } - return; - } - eval.fatal("failed to run the generated executable '{s}': {t}", .{ binary_path, err }); - }; - - // 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("generated executable '{s}' had unexpected stderr:\n{s}", .{ - binary_path, result.stderr, - }); - } - - switch (result.term) { - .exited => |code| switch (update.outcome) { - .unknown, .compile_errors => unreachable, - .stdout => |expected_stdout| { - if (code != 0) { - eval.fatal("generated executable '{s}' failed with code {d}", .{ binary_path, code }); - } - try std.testing.expectEqualStrings(expected_stdout, result.stdout); - }, - .exit_code => |expected_code| try std.testing.expectEqual(expected_code, code), - }, - .signal => |sig| { - eval.fatal("generated executable '{s}' terminated with signal {t}", .{ binary_path, sig }); - }, - .stopped => |sig| { - eval.fatal("generated executable '{s}' stopped with signal {t}", .{ binary_path, sig }); - }, - .unknown => { - eval.fatal("generated executable '{s}' terminated unexpectedly", .{binary_path}); - }, - } - - if (!is_foreign and result.stderr.len != 0) std.process.exit(1); - } - - fn requestUpdate(eval: *Eval) !void { - const io = eval.io; - - var w = eval.child.stdin.?.writerStreaming(io, &.{}); - var client: std.zig.Client = .{ - .in = undefined, - .out = &w.interface, - }; - client.serveBodylessMessage(.update) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - } - - fn end(eval: *Eval, mr: *Io.File.MultiReader) !void { - requestExit(eval.child, eval); - - var client: std.zig.Client = .{ - .in = mr.reader(0), - .out = undefined, - }; - - while (true) { - const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) { - error.Timeout => unreachable, - error.EndOfStream => |e| { - if (client.in.bufferedLen() == 0) break; - return e; - }, - else => |e| return e, - }; - try client.in.discardAll(header.bytes_len); - } - - const stderr = mr.reader(1).buffered(); - if (stderr.len > 0) eval.fatal("unexpected stderr:\n{s}", .{stderr}); - } - - fn buildCOutput(eval: *Eval, c_path: []const u8, out_path: []const u8, prog_node: std.Progress.Node) !void { - std.debug.assert(eval.cc_child_args.items.len > 0); - - const child_prog_node = prog_node.start("build cbe output", 0); - defer child_prog_node.end(); - - try eval.cc_child_args.append(eval.arena, try std.fmt.allocPrint(eval.arena, "-femit-bin={s}", .{out_path})); - try eval.cc_child_args.append(eval.arena, c_path); - defer eval.cc_child_args.items.len -= 2; - - if (eval.allow_compiler_stderr) { - const cmd: std.zig.SubprocessCommand = .{ - .argv = eval.cc_child_args.items, - .cwd = eval.tmp_dir_path, - }; - std.log.scoped(.spawn).info("{f}", .{cmd}); - } - - const result = std.process.run(eval.arena, eval.io, .{ - .argv = eval.cc_child_args.items, - .cwd = .{ .path = eval.tmp_dir_path }, - .progress_node = child_prog_node, - }) catch |err| { - eval.fatal("failed to spawn zig cc for '{s}': {t}", .{ c_path, err }); - }; - - if (result.term == .exited and result.term.exited == 0) return; - - if (result.stderr.len != 0) { - std.log.err("zig cc stderr:\n{s}", .{result.stderr}); - } - switch (result.term) { - .exited => |code| eval.fatal("zig cc for '{s}' failed with code {d}", .{ c_path, code }), - .signal => |sig| eval.fatal("zig cc for '{s}' terminated unexpectedly with signal {t}", .{ c_path, sig }), - .stopped => |sig| eval.fatal("zig cc for '{s}' stopped unexpectedly with signal {t}", .{ c_path, sig }), - .unknown => eval.fatal("zig cc for '{s}' terminated unexpectedly", .{c_path}), - } - } - - fn fatal(eval: *Eval, comptime fmt: []const u8, args: anytype) noreturn { - const io = eval.io; - eval.tmp_dir.close(io); - if (!eval.preserve_tmp_on_fatal) { - // Kill the child since it holds an open handle to its CWD which is the tmp dir path - eval.child.kill(io); - Dir.cwd().deleteTree(io, eval.tmp_dir_path) catch |err| { - std.log.warn("failed to delete tree '{s}': {t}", .{ eval.tmp_dir_path, err }); - }; - } - std.process.fatal(fmt, args); - } -}; - -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, -}; - -const Case = struct { - updates: []Update, - root_source_file: []const u8, - skip_targets: []const SkipTarget, - modules: []const Module, - - const SkipTarget = struct { - query: std.Target.Query, - backend: Backend, - }; - - const Module = struct { - name: []const u8, - file: []const u8, - }; - - const Update = struct { - name: []const u8, - outcome: Outcome, - changes: []const FullContents = &.{}, - deletes: []const []const u8 = &.{}, - }; - - const FullContents = struct { - name: []const u8, - bytes: []const u8, - }; - - const Outcome = union(enum) { - unknown, - compile_errors: struct { - errors: []const ExpectedError, - compile_log_output: []const u8, - }, - stdout: []const u8, - exit_code: u8, - }; - - const ExpectedError = struct { - is_note: bool, - msg: []const u8, - src: ?struct { - filename: []const u8, - line: u32, - column: u32, - }, - }; - - fn parse(arena: Allocator, bytes: []const u8) !Case { - const fatal = std.process.fatal; - - var skip_targets: std.ArrayList(SkipTarget) = .empty; - var modules: std.ArrayList(Module) = .empty; - var updates: std.ArrayList(Update) = .empty; - var changes: std.ArrayList(FullContents) = .empty; - var deletes: std.ArrayList([]const u8) = .empty; - var it = std.mem.splitScalar(u8, bytes, '\n'); - var line_n: usize = 1; - var root_source_file: ?[]const u8 = null; - while (it.next()) |line| : (line_n += 1) { - if (std.mem.startsWith(u8, line, "#")) { - var line_it = std.mem.splitScalar(u8, line, '='); - const key = line_it.first()[1..]; - const val = std.mem.trimEnd(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, "skip_target")) { - const query, const backend = parseTargetQueryAndBackend( - val, - try std.fmt.allocPrint(arena, "line {d}: ", .{line_n}), - ); - try skip_targets.append(arena, .{ - .query = query, - .backend = backend, - }); - } else if (std.mem.eql(u8, key, "module")) { - const split_idx = std.mem.findScalar(u8, val, '=') orelse - fatal("line {d}: module does not include file", .{line_n}); - const name = val[0..split_idx]; - const file = val[split_idx + 1 ..]; - try modules.append(arena, .{ - .name = name, - .file = file, - }); - } else if (std.mem.eql(u8, key, "update")) { - if (updates.items.len > 0) { - const last_update = &updates.items[updates.items.len - 1]; - last_update.changes = try changes.toOwnedSlice(arena); - last_update.deletes = try deletes.toOwnedSlice(arena); - } - try updates.append(arena, .{ - .name = val, - .outcome = .unknown, - }); - } else if (std.mem.eql(u8, key, "file")) { - if (updates.items.len == 0) fatal("line {d}: file directive before update", .{line_n}); - - if (root_source_file == null) - root_source_file = val; - - // Because Windows is so excellent, we need to convert CRLF to LF, so - // can't just slice into the input here. How delightful! - var src: std.ArrayList(u8) = .empty; - - while (true) { - const next_line_raw = it.peek() orelse fatal("line {d}: unexpected EOF", .{line_n}); - const next_line = std.mem.trimEnd(u8, next_line_raw, "\r"); - if (std.mem.startsWith(u8, next_line, "#")) break; - - _ = it.next(); - line_n += 1; - - try src.ensureUnusedCapacity(arena, next_line.len + 1); - src.appendSliceAssumeCapacity(next_line); - src.appendAssumeCapacity('\n'); - } - - try changes.append(arena, .{ - .name = val, - .bytes = src.items, - }); - } else if (std.mem.eql(u8, key, "rm_file")) { - if (updates.items.len == 0) fatal("line {d}: rm_file directive before update", .{line_n}); - try deletes.append(arena, val); - } else if (std.mem.eql(u8, key, "expect_stdout")) { - if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n}); - const last_update = &updates.items[updates.items.len - 1]; - if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n}); - last_update.outcome = .{ - .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| { - fatal("line {d}: bad string literal: {t}", .{ line_n, err }); - }, - }; - } else if (std.mem.eql(u8, key, "expect_error")) { - if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n}); - const last_update = &updates.items[updates.items.len - 1]; - if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n}); - - var errors: std.ArrayList(ExpectedError) = .empty; - try errors.append(arena, parseExpectedError(val, line_n)); - while (true) { - const next_line = it.peek() orelse break; - if (!std.mem.startsWith(u8, next_line, "#")) break; - var new_line_it = std.mem.splitScalar(u8, next_line, '='); - const new_key = new_line_it.first()[1..]; - const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r"); - if (new_val.len == 0) break; - if (!std.mem.eql(u8, new_key, "expect_error")) break; - - _ = it.next(); - line_n += 1; - try errors.append(arena, parseExpectedError(new_val, line_n)); - } - - var compile_log_output: std.ArrayList(u8) = .empty; - while (true) { - const next_line = it.peek() orelse break; - if (!std.mem.startsWith(u8, next_line, "#")) break; - var new_line_it = std.mem.splitScalar(u8, next_line, '='); - const new_key = new_line_it.first()[1..]; - const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r"); - if (new_val.len == 0) break; - if (!std.mem.eql(u8, new_key, "expect_compile_log")) break; - - _ = it.next(); - line_n += 1; - try compile_log_output.ensureUnusedCapacity(arena, new_val.len + 1); - compile_log_output.appendSliceAssumeCapacity(new_val); - compile_log_output.appendAssumeCapacity('\n'); - } - - last_update.outcome = .{ .compile_errors = .{ - .errors = errors.items, - .compile_log_output = compile_log_output.items, - } }; - } else if (std.mem.eql(u8, key, "expect_compile_log")) { - fatal("line {d}: 'expect_compile_log' must immediately follow 'expect_error'", .{line_n}); - } else { - fatal("line {d}: unrecognized key '{s}'", .{ line_n, key }); - } - } - } - - if (changes.items.len > 0) { - const last_update = &updates.items[updates.items.len - 1]; - last_update.changes = changes.items; // arena so no need for toOwnedSlice - last_update.deletes = deletes.items; - } - - return .{ - .updates = updates.items, - .root_source_file = root_source_file orelse fatal("missing root source file", .{}), - .skip_targets = skip_targets.items, // arena so no need for toOwnedSlice - .modules = modules.items, - }; - } -}; - -fn requestExit(child: *std.process.Child, eval: *Eval) void { - if (child.stdin == null) return; - const io = eval.io; - - var w = eval.child.stdin.?.writerStreaming(io, &.{}); - var client: std.zig.Client = .{ - .in = undefined, - .out = &w.interface, - }; - client.serveBodylessMessage(.exit) catch |err| switch (err) { - error.WriteFailed => switch (w.err.?) { - error.BrokenPipe => {}, - else => |e| eval.fatal("failed to send exit: {t}", .{e}), - }, - }; - - // Send EOF to stdin. - child.stdin.?.close(io); - child.stdin = null; -} - -fn waitChild(child: *std.process.Child, eval: *Eval) void { - const io = eval.io; - requestExit(child, eval); - const term = child.wait(io) catch |err| eval.fatal("child process failed: {t}", .{err}); - switch (term) { - .exited => |code| if (code != 0) eval.fatal("compiler failed with code {d}", .{code}), - .signal => |sig| eval.fatal("compiler terminated with signal {t}", .{sig}), - .stopped => |sig| eval.fatal("compiler stopped unexpectedly with signal {t}", .{sig}), - .unknown => eval.fatal("compiler terminated unexpectedly", .{}), - } -} - -fn parseExpectedError(str: []const u8, l: usize) Case.ExpectedError { - // #expect_error=foo.zig:1:2: error: the error message - // #expect_error=foo.zig:1:2: note: and a note - - const fatal = std.process.fatal; - - var it = std.mem.splitScalar(u8, str, ':'); - const filename = it.first(); - const line_str, const column_str = if (filename.len > 0) .{ - it.next() orelse fatal("line {d}: incomplete error specification", .{l}), - it.next() orelse fatal("line {d}: incomplete error specification", .{l}), - } else .{ undefined, undefined }; - const error_or_note_str = std.mem.trim( - u8, - it.next() orelse fatal("line {d}: incomplete error specification", .{l}), - " ", - ); - - const is_note = if (std.mem.eql(u8, error_or_note_str, "error")) - false - else if (std.mem.eql(u8, error_or_note_str, "note")) - true - else - fatal("line {d}: expeted 'error' or 'note', found '{s}'", .{ l, error_or_note_str }); - - const message = std.mem.trim(u8, it.rest(), " "); - if (message.len == 0) fatal("line {d}: empty error message", .{l}); - - return .{ - .is_note = is_note, - .msg = message, - .src = if (filename.len == 0) null else .{ - .filename = filename, - .line = std.fmt.parseInt(u32, line_str, 10) catch - fatal("line {d}: invalid line number '{s}'", .{ l, line_str }), - .column = std.fmt.parseInt(u32, column_str, 10) catch - fatal("line {d}: invalid column number '{s}'", .{ l, column_str }), - }, - }; -} - -fn rand64(io: Io) u64 { - var x: u64 = undefined; - io.random(@ptrCast(&x)); - return x; -} - -/// Calls `std.process.fatal` on error. The error messages are prefixed with `err_prefix`. -fn parseTargetQueryAndBackend(input_str: []const u8, err_prefix: []const u8) struct { std.Target.Query, Backend } { - const fatal = std.process.fatal; - - const split_idx = std.mem.findScalarLast(u8, input_str, '-') orelse - fatal("{s}target does not include backend", .{err_prefix}); - - const query = input_str[0..split_idx]; - - const backend_str = input_str[split_idx + 1 ..]; - const backend: Backend = std.meta.stringToEnum(Backend, backend_str) orelse - fatal("{s}invalid backend '{s}'", .{ err_prefix, backend_str }); - - const parsed_query = std.Build.parseTargetQuery(.{ - .arch_os_abi = query, - .object_format = switch (backend) { - .sema, .selfhosted, .llvm => null, - .cbe => "c", - }, - }) catch fatal("{s}invalid target query '{s}'", .{ err_prefix, query }); - - return .{ parsed_query, backend }; -} - -fn badUsage(comptime fmt: []const u8, args: anytype) noreturn { - std.log.err(fmt ++ "\n{s}", args ++ .{usage}); - std.process.exit(1); -} -- 2.54.0 From b6f68ca4ba477213ea89bc0ac6c9422f78116692 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Tue, 22 Sep 2026 13:57:40 -0400 Subject: [PATCH 10/23] 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 From 146d0529ca5c0f0044ea28c0450250273862df2a Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 24 Sep 2026 11:26:06 -0400 Subject: [PATCH 11/23] Maker.Step.Run: watch even in the face of skip/failure --- lib/compiler/Maker/Step/Run.zig | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index c33cdeb2936e18c0e0031a2d78da9634e6f0a81e..58bf2c7b8818fec5f1473a648b92c5b2af2a656f 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -571,8 +571,18 @@ pub fn make( .args = protocol_args.items, } }, }); - try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, output_dir_path, null); - if (!has_side_effects) try step.finalizeManifestAndWatch(maker, &man); + runCommand(arena, run, run_index, maker, progress_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, output_dir_path, null) catch |err| switch (err) { + else => |e| return e, + error.MakeFailed, error.MakeSkipped => |e| { + try step.setWatchInputsFromManifest(maker, &man); + return e; + }, + }; + if (has_side_effects) { + try step.setWatchInputsFromManifest(maker, &man); + } else { + try step.finalizeManifestAndWatch(maker, &man); + } return; } @@ -591,7 +601,7 @@ pub fn make( .args = protocol_args.items, } }, }); - try runCommand( + runCommand( arena, run, run_index, @@ -606,7 +616,13 @@ pub fn make( has_side_effects, tmp_dir_path, null, - ); + ) catch |err| switch (err) { + else => |e| return e, + error.MakeFailed, error.MakeSkipped => |e| { + try step.setWatchInputsFromManifest(maker, &man); + return e; + }, + }; for (output_placeholders.items) |placeholder| { const arg = placeholder.arg_index.get(conf); @@ -681,7 +697,11 @@ pub fn make( }; } - if (!has_side_effects) try step.finalizeManifestAndWatch(maker, &man); + if (has_side_effects) { + try step.setWatchInputsFromManifest(maker, &man); + } else { + try step.finalizeManifestAndWatch(maker, &man); + } try populateGeneratedStdIo(maker, &conf_run, &digest); try populateGeneratedPaths(maker, output_placeholders.items, &digest); -- 2.54.0 From 00b59eb18ae796ab2d5b20e91025811fdaae0b13 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Wed, 23 Sep 2026 21:01:42 -0400 Subject: [PATCH 12/23] Runner: implement error reporting --- lib/compiler/Maker.zig | 6 +- lib/compiler/Maker/Fetch.zig | 2 +- lib/compiler/Maker/Step/Run.zig | 9 +- lib/compiler/resinator/main.zig | 9 +- lib/compiler/util.zig | 3 +- lib/std/Io/Reader.zig | 15 +- lib/std/zig.zig | 3 +- lib/std/zig/ErrorBundle.zig | 11 +- src/Compilation.zig | 9 +- src/Sema.zig | 3 +- src/fmt.zig | 12 +- src/link.zig | 3 +- src/main.zig | 12 +- test/src/Runner.zig | 1567 ++++++++++++++++++++----------- tools/gen_spirv_spec.zig | 3 +- 15 files changed, 1050 insertions(+), 617 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 739ddd0bb8183c8d9040f73fc11c1341054ff828..61b99159cecbfc9603b04e716d97ba448a431248 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -4125,8 +4125,7 @@ const Fork = struct { fork.arena_allocator = .init(gpa); const arena = fork.arena_allocator.allocator(); - var error_bundle: std.zig.ErrorBundle.Wip = undefined; - try error_bundle.init(gpa); + var error_bundle: std.zig.ErrorBundle.Wip = try .init(gpa); defer error_bundle.deinit(); const manifest_path = try fork.path.join(arena, Package.Manifest.basename); @@ -4227,8 +4226,7 @@ fn loadManifest( errdefer manifest.deinit(gpa); if (manifest.errors.len > 0) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); const src_path = try wip_errors.addString(Package.Manifest.basename); diff --git a/lib/compiler/Maker/Fetch.zig b/lib/compiler/Maker/Fetch.zig index 5d3e80e25ee3becf1b308aa4917fe6a5c92781b2..21b7b2597c3e33cf7f10e31c82c82bbf052d2785 100644 --- a/lib/compiler/Maker/Fetch.zig +++ b/lib/compiler/Maker/Fetch.zig @@ -527,7 +527,7 @@ pub fn run(f: *Fetch) RunError!void { const arena = f.arena.allocator(); const gpa = f.arena.child_allocator; - try eb.init(gpa); + eb.* = try .init(gpa); // Check the global zig package cache to see if the hash already exists. If // so, load, parse, and validate the build.zig.zon file therein, and skip diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 58bf2c7b8818fec5f1473a648b92c5b2af2a656f..784f84eef95e7aeb912c097a4b0781def6a2ddcf 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -861,6 +861,7 @@ fn waitZigTest( .{ builtin.zig_version_string, body }, ); }, + .error_bundle => step.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body), .test_metadata => { // `metadata` would only be populated if we'd already seen a `test_metadata`, but we // only request it once (and importantly, we don't re-request it if we kill and @@ -920,10 +921,12 @@ fn waitZigTest( const name = md.testName(tr_hdr.index); const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); stderr.tossBuffered(); - if (stderr_bytes.len == 0) { - try step.addError(maker, "'{s}' failed without output", .{name}); - } else { + if (stderr_bytes.len > 0) { try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes }); + } else if (step.result_error_bundle.errorMessageCount() > 0) { + try step.addError(maker, "'{s}' errored", .{name}); + } else { + try step.addError(maker, "'{s}' failed without output", .{name}); } } else if (leak_count > 0) { const name = md.testName(tr_hdr.index); diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index 7ba7f7445cdb930783a3aa2bac9307b0367c4b6d..1331c68aa8ae193b6bfab3e20c3ce88228e3a525 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -790,8 +790,7 @@ fn cliDiagnosticsToErrorBundle( ) !ErrorBundle { @branchHint(.cold); - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(gpa); + var bundle: ErrorBundle.Wip = try .init(gpa); errdefer bundle.deinit(); try bundle.addRootErrorMessage(.{ @@ -837,8 +836,7 @@ fn diagnosticsToErrorBundle( ) !ErrorBundle { @branchHint(.cold); - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(gpa); + var bundle: ErrorBundle.Wip = try .init(gpa); errdefer bundle.deinit(); var msg_buf: std.Io.Writer.Allocating = .init(gpa); @@ -919,8 +917,7 @@ fn diagnosticsToErrorBundle( fn errorStringToErrorBundle(allocator: Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { @branchHint(.cold); - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(allocator); + var bundle: ErrorBundle.Wip = try .init(allocator); errdefer bundle.deinit(); try bundle.addRootErrorMessage(.{ .msg = try bundle.printString(format, args), diff --git a/lib/compiler/util.zig b/lib/compiler/util.zig index 05570abc3bfd9cfc1c9c3afaf602ee0b615bb78b..9e17e996a47fe572f121788a7c0c6f4868e8192f 100644 --- a/lib/compiler/util.zig +++ b/lib/compiler/util.zig @@ -10,8 +10,7 @@ pub fn aroDiagnosticsToErrorBundle( ) !ErrorBundle { @branchHint(.cold); - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(gpa); + var bundle: ErrorBundle.Wip = try .init(gpa); errdefer bundle.deinit(); if (fail_msg) |msg| { diff --git a/lib/std/Io/Reader.zig b/lib/std/Io/Reader.zig index c8797f26c68bdc41778f6360d75e591c78e10396..06b5b523a8fefcc0427b3900fad4af0bd34ac891 100644 --- a/lib/std/Io/Reader.zig +++ b/lib/std/Io/Reader.zig @@ -849,27 +849,14 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { } while (true) { const content_len = r.end - r.seek; - if (r.buffer.len - content_len == 0) break; try fillMore(r); + if (r.buffer.len - content_len == 0) return error.StreamTooLong; const seek = r.seek; const contents = r.buffer[0..r.end]; if (std.mem.findScalarPos(u8, contents, seek + content_len, delimiter)) |end| { return contents[seek .. end + 1]; } } - // It might or might not be end of stream. There is no more buffer space - // left to disambiguate. If `StreamTooLong` was added to `RebaseError` then - // this logic could be replaced by removing the exit condition from the - // above while loop. That error code would represent when `buffer` capacity - // is too small for an operation, replacing the current use of asserts. - var failing_writer = Writer.failing; - while (r.vtable.stream(r, &failing_writer, .limited(1))) |n| { - assert(n == 0); - } else |err| switch (err) { - error.WriteFailed => return error.StreamTooLong, - error.ReadFailed => |e| return e, - error.EndOfStream => |e| return e, - } } /// Returns a slice of the next bytes of buffered data from the stream until diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 286654f1bd123b8fc6732cc70a5017e970d00f68..5c6bfa52f700385cf2be4894b3f870d9b34cff81 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -727,8 +727,7 @@ pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![ } pub fn printAstErrorsToStderr(gpa: Allocator, io: Io, tree: Ast, path: []const u8, color: Color) !void { - var wip_errors: ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try putAstErrorsIntoBundle(gpa, tree, path, &wip_errors); diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index d6fde276f1e9fb328d262b90a91f0112c45afb77..4ce1af8fdee40685a819298333d4248ddb9abfa0 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -360,22 +360,25 @@ pub const Wip = struct { extra: std.ArrayList(u32), root_list: std.ArrayList(MessageIndex), - pub fn init(wip: *Wip, gpa: Allocator) !void { - wip.* = .{ + pub fn init(gpa: Allocator) !Wip { + var wip: Wip = .{ .gpa = gpa, .string_bytes = .empty, .extra = .empty, .root_list = .empty, }; + errdefer wip.deinit(); // So that 0 can be used to indicate a null string. try wip.string_bytes.append(gpa, 0); - assert(0 == try addExtra(wip, ErrorMessageList{ + assert(0 == try wip.addExtra(ErrorMessageList{ .len = 0, .start = 0, .compile_log_text = 0, })); + + return wip; } pub fn deinit(wip: *Wip) void { @@ -773,7 +776,7 @@ pub const Wip = struct { u32 => @field(extra, field_name), MessageIndex => @backingInt(@field(extra, field_name)), SourceLocationIndex => @backingInt(@field(extra, field_name)), - else => @compileError("bad field type"), + else => @compileError("bad field type: " ++ @typeName(field_type)), }; i += 1; } diff --git a/src/Compilation.zig b/src/Compilation.zig index a2e962fde90f0b6873335c12a2e566f8b9980bdb..4fd5ef2fe7bb6b4e66a2acbd4a65783c5ff94c2a 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -3838,8 +3838,7 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { const gpa = comp.gpa; const io = comp.io; - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(gpa); + var bundle: ErrorBundle.Wip = try .init(gpa); defer bundle.deinit(); for (comp.failed_c_objects.values()) |diag_bundle| { @@ -5442,8 +5441,7 @@ fn reportRetryableWin32ResourceError( win32_resource.status = .failure_retryable; - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(comp.gpa); + var bundle: ErrorBundle.Wip = try .init(comp.gpa); errdefer bundle.deinit(); try bundle.addRootErrorMessage(.{ .msg = try bundle.printString("{s}", .{@errorName(err)}), @@ -6830,8 +6828,7 @@ fn failCObjWithOwnedDiagBundle( fn failWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, comptime format: []const u8, args: anytype) error{ OutOfMemory, AlreadyReported } { @branchHint(.cold); - var bundle: ErrorBundle.Wip = undefined; - try bundle.init(comp.gpa); + var bundle: ErrorBundle.Wip = try .init(comp.gpa); errdefer bundle.deinit(); try bundle.addRootErrorMessage(.{ .msg = try bundle.printString(format, args), diff --git a/src/Sema.zig b/src/Sema.zig index a1d30147758234ed15ba84cbe71a85494ac574aa..e209f453cae9ceb95b5d222fb281d5e3bff7da2e 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -2705,8 +2705,7 @@ pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg assert(sema.err == null); if (build_options.enable_debug_extensions and comp.debug_compile_errors) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - wip_errors.init(gpa) catch @panic("out of memory"); + var wip_errors = std.zig.ErrorBundle.Wip.init(gpa) catch @panic("out of memory"); Compilation.addModuleErrorMsg(zcu, &wip_errors, err_msg.*, false) catch @panic("out of memory"); std.debug.print("compile error during Sema:\n", .{}); var error_bundle = wip_errors.toOwnedBundle("") catch @panic("out of memory"); diff --git a/src/fmt.zig b/src/fmt.zig index 769e9ab3fc1e007b70d413868f103d386f4e0419..8bb5582cbea0e506e66a6b1c52f381b81222bc2e 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -130,8 +130,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! defer zir.deinit(gpa); if (zir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try wip_errors.addZirErrorMessages(zir, tree, source_code, ""); var error_bundle = try wip_errors.toOwnedBundle(""); @@ -144,8 +143,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) ! defer zoir.deinit(gpa); if (zoir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try wip_errors.addZoirErrorMessages(zoir, tree, source_code, ""); var error_bundle = try wip_errors.toOwnedBundle(""); @@ -333,8 +331,7 @@ fn fmtPathFile( defer zir.deinit(gpa); if (zir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try wip_errors.addZirErrorMessages(zir, tree, source_code, file_path); var error_bundle = try wip_errors.toOwnedBundle(""); @@ -348,8 +345,7 @@ fn fmtPathFile( defer zoir.deinit(gpa); if (zoir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try wip_errors.addZoirErrorMessages(zoir, tree, source_code, file_path); var error_bundle = try wip_errors.toOwnedBundle(""); diff --git a/src/link.zig b/src/link.zig index 6b2d2eecdf7f644ddaa94b71a701de983173fda5..e0e32c3a8326d7adeaf21e7507f833b7036d247a 100644 --- a/src/link.zig +++ b/src/link.zig @@ -2586,8 +2586,7 @@ fn resolvePathInputLib( const ld_script_result = LdScript.parse(gpa, &diags, test_path, ld_script_bytes.items); if (diags.hasErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(gpa); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(gpa); defer wip_errors.deinit(); try diags.addMessagesToBundle(&wip_errors, null); diff --git a/src/main.zig b/src/main.zig index f09b81e0ff7ecaaa19d5cec8ed7a852dca376f46..39df0ee67a6f2c79aaebce2ac8de1d22dfbe7f55 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5659,8 +5659,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8, environ_map: const zir = try AstGen.generate(arena, tree); if (zir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(arena); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(arena); try wip_errors.addZirErrorMessages(zir, tree, source, display_path); var error_bundle = try wip_errors.toOwnedBundle(""); try error_bundle.renderToStderr(io, .{}, color); @@ -5730,8 +5729,7 @@ fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8, environ_map: .zon => { const zoir = try ZonGen.generate(arena, tree, .{}); if (zoir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(arena); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(arena); try wip_errors.addZoirErrorMessages(zoir, tree, source, display_path); var error_bundle = try wip_errors.toOwnedBundle(""); error_bundle.renderToStderr(io, .{}, color) catch {}; @@ -5824,8 +5822,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8, environ_map const old_zir = try AstGen.generate(arena, old_tree); if (old_zir.loweringFailed()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(arena); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(arena); try wip_errors.addZirErrorMessages(old_zir, old_tree, old_source, old_source_path); var error_bundle = try wip_errors.toOwnedBundle(""); error_bundle.renderToStderr(io, .{}, color) catch {}; @@ -5836,8 +5833,7 @@ fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8, environ_map const new_zir = try AstGen.generate(arena, new_tree); if (new_zir.loweringFailed()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(arena); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(arena); try wip_errors.addZirErrorMessages(new_zir, new_tree, new_source, new_source_path); var error_bundle = try wip_errors.toOwnedBundle(""); error_bundle.renderToStderr(io, .{}, color) catch {}; diff --git a/test/src/Runner.zig b/test/src/Runner.zig index 599f7b001aaf38e642c1e8bf5e231f0dd0b522b1..158d657ad37ba2b5f73183d9e709eae0be431cbb 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -1,274 +1,5 @@ +const assert = std.debug.assert; 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, @@ -278,6 +9,7 @@ prog_node: std.Progress.Node, args: Args, host: std.Target, server: std.zig.Server, +eb_wip: std.zig.ErrorBundle.Wip, const Args = struct { @"test": ?struct { @@ -312,24 +44,34 @@ const Args = struct { fn deinit(runner: *Runner) void { runner.prog_node.end(); runner.args.deinit(runner.gpa); + runner.eb_wip.deinit(); runner.* = undefined; } -pub fn protocol(runner: *Runner) !void { +const Fallible = error{AlreadyReported} || std.mem.Allocator.Error || std.Io.Cancelable; +fn fail(runner: *Runner, comptime fmt: []const u8, args: anytype) Fallible { + return runner.failString(try runner.eb_wip.printString(fmt, args)); +} +fn failString(runner: *Runner, msg: std.zig.ErrorBundle.String) Fallible { + try runner.eb_wip.addRootErrorMessage(.{ + .msg = msg, + }); + return error.AlreadyReported; +} + +pub const ProtocolError = Fallible || std.Io.Reader.Error || std.Io.Writer.Error; +pub fn runServer(runner: *Runner) ProtocolError { 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); - }, + else => |tag| return runner.fail("unsupported message: {t}", .{tag}), .exit => std.process.exit(0), .args => { const args_body = try arena.alloc(u8, hdr.bytes_len); - try runner.server.in.readSliceAll(args_body); + runner.server.in.readSliceAll(args_body) catch unreachable; var state: enum { positional, zig, lib, src, target } = .positional; var input_dir: std.zig.Server.Message.InputDir = .cwd; var args_body_offset: usize = 0; @@ -392,32 +134,30 @@ pub fn protocol(runner: *Runner) !void { }; switch (state) { .positional => { - std.debug.assert(runner.args.@"test" == null); + assert(runner.args.@"test" == null); runner.args.@"test" = .{ .input_dir = input_dir, .dir = dir }; - std.debug.assert(runner.args.manifest_file == null); + 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(); + runner.args.manifest_file = dir.openFile( + runner.io, + manifest_path, + .{}, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail( + "unable to open manifest file: {t}", + .{e}, + ), + }; + try runner.discoverInput(input_dir, manifest_path); }, .zig => unreachable, .lib => { - std.debug.assert(runner.args.lib_dir == null); + assert(runner.args.lib_dir == null); runner.args.lib_dir = dir; }, .src => { - std.debug.assert(runner.args.src_dir == null); + assert(runner.args.src_dir == null); runner.args.src_dir = dir; }, .target => unreachable, @@ -434,11 +174,11 @@ pub fn protocol(runner: *Runner) !void { }; switch (state) { .positional => { - std.debug.assert(runner.args.manifest_file == null); + assert(runner.args.manifest_file == null); runner.args.manifest_file = file; }, .zig => { - std.debug.assert(runner.args.zig_exe == null); + assert(runner.args.zig_exe == null); runner.args.zig_exe = file; }, .lib => unreachable, @@ -460,29 +200,199 @@ pub fn protocol(runner: *Runner) !void { }); }, .run_test => { - const test_index = try runner.server.receiveBody_u32(); + const test_index = runner.server.receiveBody_u32() catch unreachable; 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); + .status = if (runner.testOne(test_index)) + .pass + else |err| status: switch (err) { + error.Canceled, + error.OutOfMemory, + error.ReadFailed, + error.WriteFailed, + => |e| return e, + error.AlreadyReported => { + var eb = try runner.eb_wip.toOwnedBundle(""); + defer eb.deinit(runner.gpa); + assert(eb.errorMessageCount() > 0); // already reported what? + try runner.server.serveErrorBundle(.error_bundle, eb); + const eb_wip: std.zig.ErrorBundle.Wip = try .init(runner.gpa); + runner.eb_wip.deinit(); + runner.eb_wip = eb_wip; break :status .fail; }, - error.SkipZigTest => .skip, + error.SkipTest => .skip, }, .fuzz = false, .log_err_count = 0, .leak_count = 0, }, }); + assert(runner.eb_wip.root_list.items.len == 0); // failed to report }, } } } -const DelimitedReader = struct { + +fn discoverInput( + runner: *Runner, + dir: std.zig.Server.Message.InputDir, + sub_path: []const u8, +) std.Io.Writer.Error!void { + try runner.server.serveMessageHeader(.{ + .tag = .discovered_inputs, + .bytes_len = @intCast(@sizeOf(std.zig.Server.Message.InputDir) + + sub_path.len + 1), + }); + try runner.server.out.writeInt(u32, @backingInt(dir), .little); + try runner.server.out.writeAll(sub_path); + try runner.server.out.writeByte(0); + try runner.server.out.flush(); +} + +pub const SourceReader = struct { + const Io = std.Io; + + backing: *std.Io.Reader, + line_aw: std.Io.Writer.Allocating, + interface: std.Io.Reader, + line: u32, + eb_wip: *std.zig.ErrorBundle.Wip, + src_path: std.zig.ErrorBundle.String, + + pub fn init( + reader: *Io.Reader, + gpa: std.mem.Allocator, + eb_wip: *std.zig.ErrorBundle.Wip, + src_path: std.zig.ErrorBundle.String, + ) SourceReader { + return .{ + .backing = reader, + .line_aw = .init(gpa), + .interface = .{ + .vtable = &.{ + .stream = stream, + .discard = discard, + .readVec = readVec, + .rebase = rebase, + }, + .buffer = &.{}, + .seek = 0, + .end = 0, + }, + .line = 0, + .eb_wip = eb_wip, + .src_path = src_path, + }; + } + fn deinit(s: *SourceReader) void { + s.line_aw.deinit(); + } + fn stream(r: *Io.Reader, _: *Io.Writer, _: Io.Limit) Io.Reader.StreamError!usize { + const s: *SourceReader = @fieldParentPtr("interface", r); + try s.bufferLine(); + return 0; + } + fn discard(r: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { + const s: *SourceReader = @fieldParentPtr("interface", r); + const remaining = r.buffered(); + if (remaining.len > 0) { + const n = limit.minInt(remaining.len); + r.toss(n); + return n; + } + try s.bufferLine(); + return 0; + } + fn readVec(r: *Io.Reader, _: [][]u8) Io.Reader.Error!usize { + const s: *SourceReader = @fieldParentPtr("interface", r); + try s.bufferLine(); + return 0; + } + fn rebase(r: *Io.Reader, capacity: usize) Io.Reader.RebaseError!void { + const s: *SourceReader = @fieldParentPtr("interface", r); + defer s.updateBuffer(); + s.line_aw.ensureUnusedCapacity(capacity) catch |err| switch (err) { + error.OutOfMemory => return error.ReadFailed, + }; + } + fn updateBuffer(s: *SourceReader) void { + s.interface.buffer = s.line_aw.writer.buffer; + s.interface.end = s.line_aw.writer.end; + } + fn bufferLine(s: *SourceReader) Io.Reader.Error!void { + defer s.updateBuffer(); + { + const written = s.line_aw.written(); + const start = if (std.mem.findScalarLast(u8, written[0..s.interface.seek], '\n')) |newline| + newline + 1 + else + 0; + s.line += @intCast(std.mem.countScalar(u8, written[0..start], '\n')); + const preserve = written[start..]; + @memmove(written[0..preserve.len], preserve); + s.line_aw.shrinkRetainingCapacity(preserve.len); + s.interface.seek -= start; + } + const n = s.backing.streamDelimiterEnding(&s.line_aw.writer, '\n') catch |err| switch (err) { + error.ReadFailed => |e| return e, + error.WriteFailed => return error.ReadFailed, + }; + if (std.mem.endsWith(u8, s.line_aw.written(), "\r")) s.line_aw.writer.end -= 1; + const buffered = s.backing.buffered(); + if (n == 0 and buffered.len == 0) return error.EndOfStream; + if (std.mem.startsWith(u8, buffered, "\n")) { + s.line_aw.writer.writeByte('\n') catch |err| switch (err) { + error.WriteFailed => return error.ReadFailed, + }; + s.backing.toss(1); + } + } + fn fail( + s: *SourceReader, + src_loc: []const u8, + comptime fmt: []const u8, + args: anytype, + ) Fallible { + return s.failString(src_loc, try s.eb_wip.printString(fmt, args)); + } + fn failString( + s: *SourceReader, + src_loc: []const u8, + msg: std.zig.ErrorBundle.String, + ) Fallible { + const line: u32, const column: u32, const source_line = src_loc: { + const pos = src_loc.ptr - s.interface.buffer.ptr; + const end = std.mem.findScalarPos(u8, s.interface.buffer, pos, '\n') orelse + s.interface.buffer.len; + const start = if (std.mem.findScalarLast(u8, s.interface.buffer[0..end], '\n')) |newline| + newline + 1 + else + 0; + break :src_loc .{ + @intCast(s.line + std.mem.countScalar(u8, s.interface.buffer[0..start], '\n')), + @intCast(pos - start), + s.interface.buffer[start..end], + }; + }; + try s.eb_wip.addRootErrorMessage(.{ + .msg = msg, + .src_loc = try s.eb_wip.addSourceLocation(.{ + .src_path = s.src_path, + .line = line, + .column = column, + .span_start = column, + .span_main = column, + .span_end = @intCast(column + src_loc.len), + .source_line = try s.eb_wip.addString(source_line), + }), + }); + return error.AlreadyReported; + } +}; +pub const DelimitedReader = struct { const Io = std.Io; unlimited: *Io.Reader, @@ -528,251 +438,376 @@ const DelimitedReader = struct { } }; -fn testOne(runner: *const Runner, test_index: u32) !void { +const Update = struct { + target_query: []const u8, + manifest_sr: SourceReader, + compiler: ?Compiler, + src_dir: std.Io.Dir, + prog_node: std.Progress.Node, + mtime: std.Io.Timestamp, + num: usize, + allow_skip: bool, +}; + +const TestError = error{ + SkipTest, +} || Fallible || std.Io.Reader.ShortError || std.Io.Writer.Error; +fn testOne(runner: *Runner, test_index: u32) TestError!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; + const manifest_file = runner.args.manifest_file orelse + return runner.fail("missing \"path/to/test\" arg", .{}); var manifest_buffer: [512]u8 = undefined; var manifest_fr = manifest_file.reader(runner.io, &manifest_buffer); - var allow_skip = true; + var update: Update = .{ + .target_query = target_query, + .manifest_sr = .init(&manifest_fr.interface, runner.gpa, &runner.eb_wip, src_path_string: { + var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; + const src_path = src_path: { + break :src_path path_buffer[0 .. std.Io.Dir.readLinkAbsolute( + runner.io, + try runner.arena.allocator().print("/proc/self/fd/{d}", .{manifest_file.handle}), + &path_buffer, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => break :src_path "manifest", + }]; + }; + break :src_path_string try runner.eb_wip.addString(src_path); + }), + .compiler = null, + .src_dir = (runner.args.src_dir orelse + return runner.fail("missing \"--src=path/to/src\" arg", .{})).createDirPathOpen( + runner.io, + target_query, + .{}, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to create {q}: {t}", .{ target_query, e }), + }, + .prog_node = .none, + .mtime = .now(runner.io, .real), + .num = 0, + .allow_skip = true, + }; + defer { + update.manifest_sr.deinit(); + update.src_dir.close(runner.io); + update.prog_node.end(); + } 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"); + while (update.manifest_sr.interface.takeSentinel('\n')) |line| { var line_it = std.mem.tokenizeScalar(u8, line, ' '); - const cmd_str = std.mem.cutPrefix(u8, line_it.next() orelse continue, "#").?; + const cmd_src_loc = line_it.next() orelse continue; if (skip_delimiter) { - std.debug.assert(std.mem.eql(u8, cmd_str, "}")); + assert(std.mem.eql(u8, cmd_src_loc, "#}")); skip_delimiter = false; + const unexpected = line_it.rest(); + if (unexpected.len > 0) + return update.manifest_sr.fail(unexpected, "unexpected argument {q}", .{unexpected}); 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(); + const cmd = std.meta.stringToEnum(Command, std.mem.cutPrefix(u8, cmd_src_loc, "#") orelse + return update.manifest_sr.fail(cmd_src_loc, "expected command", .{})) orelse + return update.manifest_sr.fail(cmd_src_loc, "unknown command", .{}); + const arg = switch (cmd) { + .todo, .exe, .lib, .obj, .update => null, + .skip, .write, .delete, .check => line_it.next() orelse + return update.manifest_sr.fail(line_it.rest()[0..0], "missing argument", .{}), + }; 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 { + var contents_impl: union(enum) { + ending, dr: DelimitedReader, fr: std.Io.File.Reader, - } = undefined; + } = .ending; const contents_r: *std.Io.Reader = contents_r: switch (cmd) { .skip, .delete => { - if (line_it.next()) |_| return error.UnexpectedArg; + const unexpected = line_it.rest(); + if (unexpected.len > 0) return update.manifest_sr.fail( + unexpected, + "unexpected argument {q}", + .{unexpected}, + ); 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; - }; + const contents_path = line_it.rest(); + if (contents_path.len == 0) break :contents_r .ending; + if (std.mem.eql(u8, contents_path, "{")) { - contents_impl = .{ .dr = .init(&manifest_fr.interface, "#}", &contents_buffer) }; + contents_impl = .{ + .dr = .init(&update.manifest_sr.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(); + const args_test = runner.args.@"test" orelse return update.manifest_sr.fail( + contents_path, + "contents file requires directory test", + .{}, + ); + try runner.discoverInput(args_test.input_dir, contents_path); - contents_file = try args_test.dir.openFile(runner.io, contents_path, .{}); + contents_file = + args_test.dir.openFile(runner.io, contents_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail( + contents_path, + "unable to open contents file: {t}", + .{e}, + ), + }; 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 } }); + runner.handleCommand(&update, cmd_src_loc, cmd, arg, contents_r) catch |err| switch (err) { + error.Canceled, + error.OutOfMemory, + error.AlreadyReported, + error.SkipTest, + error.WriteFailed, + => |e| return e, + error.ReadFailed => switch (contents_impl) { + .ending => unreachable, + .dr => return runner.fail("unable to read manifest: {t}", .{manifest_fr.err.?}), + .fr => |fr| return runner.fail("unable to read contents file: {t}", .{fr.err.?}), }, - .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, + error.ReadFailed => |e| return e, + error.StreamTooLong => unreachable, + error.EndOfStream => if (skip_delimiter) + return runner.fail("manifest missing \"#}}\" delimiter", .{}), } - if (compiler) |*comp| { - if (comp.state != .idle) return error.MissingCheck; + if (update.compiler) |*comp| { + if (comp.state != .idle) return runner.fail("missing \"#check\"", .{}); try comp.exit(); } } +const Command = enum { todo, skip, exe, lib, obj, write, delete, update, check }; +fn handleCommand( + runner: *Runner, + update: *Update, + cmd_src_loc: []const u8, + cmd: Command, + arg: ?[]const u8, + contents_r: *std.Io.Reader, +) TestError!void { + 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}{s}", .{ + cmd, if (arg) |_| " " else "", arg orelse "", + }) catch &name_buffer, 0); + defer cmd_prog_node.end(); + + cmd: switch (cmd) { + .todo => _ = contents_r.discardRemaining() catch |err| switch (err) { + error.ReadFailed => {}, + }, + .skip => if (!update.allow_skip) return update.manifest_sr.fail( + cmd_src_loc, + "\"#skip\" must appear before other commands", + .{}, + ) else if (std.mem.eql(u8, update.target_query, arg.?)) + return error.SkipTest, + .exe, .lib, .obj => try runner.spawnCompiler(&update.compiler, update.src_dir, switch (cmd) { + else => unreachable, + .exe => .Exe, + .lib => .Lib, + .obj => .Obj, + }, contents_r, target: { + const backend_split = std.mem.findScalarLast(u8, update.target_query, '-') orelse + return runner.fail("target {q} missing query", .{update.target_query}); + const mode_split = + std.mem.findScalarLast(u8, update.target_query[0..backend_split], '-') orelse + return runner.fail("target {q} missing mode", .{update.target_query}); + const triple = update.target_query[0..mode_split]; + const backend_str = update.target_query[backend_split + 1 ..]; + const backend = std.meta.stringToEnum(Compiler.Target.Backend, backend_str) orelse + return runner.fail("target {q} unknown backend {q}", .{ + update.target_query, backend_str, + }); + const mode_str = update.target_query[mode_split + 1 .. backend_split]; + const mode = std.meta.stringToEnum(Compiler.Target.Mode, mode_str) orelse + return runner.fail("target {q} unknown mode {q}", .{ update.target_query, mode_str }); + break :target .{ + .triple = triple, + .resolved = std.zig.system.resolveTargetQuery( + runner.io, + std.Build.parseTargetQuery(.{ + .arch_os_abi = triple, + .object_format = switch (backend) { + .sema, .selfhosted, .llvm => null, + .cbe => "c", + }, + }) catch |err| switch (err) { + error.ParseFailed => return runner.fail("unable to parse triple {q}", .{ + triple, + }), + }, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to resolve target {q}: {t}", .{ triple, e }), + }, + .mode = mode, + .backend = backend, + }; + }), + .write => { + const file = update.src_dir.createFile(runner.io, arg.?, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail(arg.?, "unable to create file: {t}", .{e}), + }; + defer file.close(runner.io); + var fw = file.writer(runner.io, &.{}); + _ = try contents_r.streamRemaining(&fw.interface); + file.setTimestamps(runner.io, .{ .modify_timestamp = .{ + .new = update.mtime, + } }) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail( + arg.?, + "unable to update file timestamps: {t}", + .{e}, + ), + }; + }, + .delete => update.src_dir.deleteFile(runner.io, arg.?) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail(arg.?, "unable to delete file: {t}", .{e}), + }, + .update => { + const comp = &(update.compiler orelse return update.manifest_sr.fail( + cmd_src_loc, + "missing \"#exe\", \"#lib\", or \"#obj\"", + .{}, + )); + 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 update.manifest_sr.fail(cmd_src_loc, "missing \"#check\"", .{}); + 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, arg.?) orelse + return update.manifest_sr.fail(arg.?, "unknown check", .{}); + const expected = + contents_r.allocRemaining(runner.gpa, .unlimited) catch |err| switch (err) { + error.OutOfMemory, error.ReadFailed => |e| return e, + error.StreamTooLong => unreachable, // .unlimited + }; + defer runner.gpa.free(expected); + + const comp = &(update.compiler orelse return update.manifest_sr.fail( + cmd_src_loc, + "missing \"#exe\", \"#lib\", or \"#obj\"", + .{}, + )); + if (comp.state != .update) + return update.manifest_sr.fail(cmd_src_loc, "missing \"#update\"", .{}); + const stderr = comp.mr.reader(1); + while (try comp.receiveMessage()) |message| switch (message.tag) { + else => {}, // Ignore other messages, + .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, update.src_dir, digest); + }, + .error_bundle => { + const eb = std.zig.Server.allocErrorBundle( + runner.arena.allocator(), + message.body, + ) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.EndOfStream => return runner.fail("error bundle truncated", .{}), + }; + switch (check) { + .errors => { + var error_aw: std.Io.Writer.Allocating = .init(runner.gpa); + defer error_aw.deinit(); + try eb.renderToWriter(.{ + .include_reference_trace = false, + .include_source_line = false, + .include_log_text = true, + }, &error_aw.writer); + std.testing.expectEqualStrings( + expected, + error_aw.written(), + ) catch |err| switch (err) { + error.TestExpectedEqual => return runner.fail( + "errors did not match expected", + .{}, + ), + }; + }, + .stdout, .exit, .lldb => if (eb.errorMessageCount() > 0) { + eb.renderToStderr(runner.io, .{}, .auto) catch |err| switch (err) { + error.Canceled => {}, + else => {}, + }; + return runner.fail("unexpected compile errors", .{}); + }, + } + comp.state = .idle; + break :cmd; + }, + }; + + const buffered_stderr = stderr.buffered(); + if (buffered_stderr.len > 0) { + if (comp.allow_compiler_stderr) { + std.log.info("stderr:\n{s}", .{buffered_stderr}); + } else { + return runner.fail("unexpected stderr:\n{s}", .{buffered_stderr}); + } + } + + try comp.exit(); + return runner.fail("compiler failed to send terminating error_bundle", .{}); + }, + } + assert(try contents_r.discardRemaining() == 0); + switch (cmd) { + .todo, .skip => {}, + else => update.allow_skip = false, + } +} fn spawnCompiler( - runner: *const Runner, + runner: *Runner, compiler: *?Compiler, src_dir: std.Io.Dir, output_mode: std.lang.OutputMode, args_r: *std.Io.Reader, target: Compiler.Target, -) !void { +) (Fallible || std.Io.Reader.ShortError)!void { const gpa = runner.gpa; const arena = runner.arena.allocator(); if (compiler.*) |*comp| { @@ -804,18 +839,20 @@ fn spawnCompiler( .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( + } else try argv.appendSlice(gpa, &.{ "-I", path_buffer[0 .. std.Io.Dir.readLinkAbsolute( runner.io, - try arena.print("/proc/self/fd/{d}", .{ - (runner.args.lib_dir orelse return error.MissingLibDir).handle, - }), + try arena.print("/proc/self/fd/{d}", .{(runner.args.lib_dir orelse + return runner.fail("\"--lib=path/to/lib\" arg required with cbe", .{})).handle}), &path_buffer, - )] }); + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to get lib path: {t}", .{e}), + }] }); 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.ReadFailed => |e| return e, error.EndOfStream => true, }; const buffered = args_r.buffered(); @@ -854,15 +891,19 @@ fn spawnCompiler( }) 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 }, + const child = std.process.spawn(runner.io, .{ + .exe = .{ .file = runner.args.zig_exe orelse + return runner.fail("missing \"--zig=path/to/zig\" arg", .{}) }, .argv = argv.items, .cwd = .{ .dir = src_dir }, .progress_node = comp_prog_node, .stdin = .pipe, .stdout = .pipe, .stderr = .pipe, - }); + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| return runner.fail("unable to spawn compiler: {t}", .{e}), + }; compiler.* = .{ .runner = runner, .prog_node = comp_prog_node, @@ -886,7 +927,7 @@ fn spawnCompiler( comp.client = .{ .in = comp.mr.reader(0), .out = &comp.fw.interface }; } const Compiler = struct { - runner: *const Runner, + runner: *Runner, prog_node: std.Progress.Node, allow_compiler_stderr: bool, output_mode: std.lang.OutputMode, @@ -927,7 +968,7 @@ const Compiler = struct { }; }; - fn receiveMessage(comp: *Compiler) !?struct { + fn receiveMessage(comp: *Compiler) Fallible!?struct { tag: std.zig.Server.Message.Tag, body: []const u8, } { @@ -935,9 +976,10 @@ const Compiler = struct { &comp.mr, .none, ) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, error.Timeout => unreachable, error.EndOfStream => return null, - else => |e| return e, + else => |e| return comp.runner.fail("unable to receive message: {t}", .{e}), }; const body = comp.client.in.take(header.bytes_len) catch unreachable; const stderr = comp.mr.reader(1); @@ -945,7 +987,9 @@ const Compiler = struct { 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() }); + return comp.runner.fail("{t} unexpected stderr:\n{s}", .{ + header.tag, stderr.buffered(), + }); } stderr.tossBuffered(); } @@ -959,7 +1003,7 @@ const Compiler = struct { expected: []const u8, src_dir: std.Io.Dir, digest: *const std.Build.Cache.BinDigest, - ) !void { + ) Fallible!void { const runner = comp.runner; const gpa = runner.gpa; const arena = runner.arena.allocator(); @@ -1021,7 +1065,10 @@ const Compiler = struct { }) catch &name_buffer, 0); defer run_prog_node.end(); - const bin_file = try src_dir.openFile(io, bin_path, .{}); + const bin_file = src_dir.openFile(io, bin_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to open {q}: {t}", .{ bin_path, e }), + }; defer bin_file.close(io); switch (check) { .errors => unreachable, @@ -1033,13 +1080,14 @@ const Compiler = struct { .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, - }); + 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 }); + } else return runner.fail("unable to run the generated executable '{s}': {t}", .{ + bin_path, err, + }); defer { gpa.free(result.stdout); gpa.free(result.stderr); @@ -1048,20 +1096,46 @@ const Compiler = struct { .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); + if (code != 0) return runner.fail( + "generated executable '{s}' failed with code {d}", + .{ bin_path, code }, + ); + std.testing.expectEqualStrings( + expected, + result.stdout, + ) catch |err| switch (err) { + error.TestExpectedEqual => return runner.fail( + "stdout did not match expected", + .{}, + ), + }; }, .exit => { - var actual_code_buffer: [std.fmt.count("{d}", .{std.math.maxInt(u8)})]u8 = undefined; - try std.testing.expectEqualStrings( + var actual_code_buffer: [std.fmt.count("{d}", .{std.math.maxInt(u8)})]u8 = + undefined; + std.testing.expectEqualStrings( expected, std.mem.print(&actual_code_buffer, "{d}", .{code}) catch unreachable, - ); + ) catch |err| switch (err) { + error.TestExpectedEqual => return runner.fail( + "exit code did not match expected", + .{}, + ), + }; }, }, - .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}), + .signal => |sig| return runner.fail( + "generated executable '{s}' terminated with signal {t}", + .{ bin_path, sig }, + ), + .stopped => |sig| return runner.fail( + "generated executable '{s}' stopped with signal {t}", + .{ bin_path, sig }, + ), + .unknown => return runner.fail( + "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}", .{ @@ -1073,12 +1147,12 @@ const Compiler = struct { } } - fn compileC(parent_comp: *Compiler, src_dir: std.Io.Dir, c_path: []const u8) ![]const u8 { + fn compileC(parent_comp: *Compiler, src_dir: std.Io.Dir, c_path: []const u8) Fallible![]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, .{ + 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; @@ -1087,12 +1161,18 @@ const Compiler = struct { }, .mode = .whole, .backend = null, - }); + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory, error.AlreadyReported => |e| return e, + error.ReadFailed => unreachable, // .fixed + }; const comp = &compiler.?; - try comp.client.serveBodylessMessage(.update); + comp.client.serveBodylessMessage(.update) catch |err| switch (err) { + error.WriteFailed => return runner.fail("unable to send message: {t}", .{comp.fw.err.?}), + }; comp.state = .update; var out_path: ?[]const u8 = null; while (try comp.receiveMessage()) |message| switch (message.tag) { + else => {}, // Ignore other messages, .config => { var body_r: std.Io.Reader = .fixed(message.body); comp.config = @@ -1108,40 +1188,421 @@ const Compiler = struct { 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", .{}); + const eb = std.zig.Server.allocErrorBundle( + runner.arena.allocator(), + message.body, + ) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.EndOfStream => return runner.fail("error bundle truncated", .{}), + }; + if (eb.errorMessageCount() > 0) { + try runner.eb_wip.addBundleAsRoots(eb); + return runner.fail("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; + fn exit(comp: *Compiler) Fallible!void { + const runner = comp.runner; comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { error.WriteFailed => switch (comp.fw.err.?) { + error.Canceled => |e| return e, error.BrokenPipe => {}, - else => |e| fatal("failed to send exit: {t}", .{e}), + else => |e| return comp.runner.fail("unable to send exit: {t}", .{e}), }, }; - comp.child.stdin.?.close(io); + comp.child.stdin.?.close(runner.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}); + if (comp.client.in.bufferedLen() > 0) return runner.fail("unable to receive messages", .{}); + const term = comp.child.wait(runner.io) catch |err| + return runner.fail("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", .{}), + .exited => |code| if (code != 0) + return runner.fail("compiler failed with code {d}", .{code}), + .signal => |sig| return runner.fail("compiler terminated with signal {t}", .{sig}), + .stopped => |sig| return runner.fail("compiler stopped unexpectedly with signal {t}", .{ + sig, + }), + .unknown => return runner.fail("compiler terminated unexpectedly", .{}), } } }; + +const ClientArgs = struct { + test_file: std.Io.File, + test_kind: std.Io.File.Kind, + zig_exe: std.Io.File, + lib_dir: std.Io.Dir, + src_dir: std.Io.Dir, + targets: std.ArrayList([]const u8), + keep_src: bool, + enable_qemu: bool, + enable_wine: bool, + enable_wasmtime: bool, + enable_darling: bool, +}; +pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelable)!u8 { + const fatal = struct { + fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); + } + }.fatal; + 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 args: ClientArgs = .{ + .test_file = undefined, + .test_kind = undefined, + .zig_exe = undefined, + .lib_dir = undefined, + .src_dir = undefined, + .targets = .empty, + .keep_src = false, + .enable_qemu = false, + .enable_wine = false, + .enable_wasmtime = false, + .enable_darling = false, + }; + defer args.targets.deinit(init.gpa); + + 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 = std.zig.system.resolveTargetQuery(init.io, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to resolve host target: {t}", .{e}), + }, + .server = .{ + .in = &stdin.interface, + .out = &stdout.interface, + }, + .eb_wip = try .init(init.gpa), + }; + defer runner.deinit(); + switch (runner.runServer()) { + error.Canceled, error.OutOfMemory => |e| return e, + error.ReadFailed => fatal("unable to receive message: {t}", .{stdin.err.?}), + error.EndOfStream => fatal("no more messages", .{}), + error.WriteFailed => fatal("unable to send message: {t}", .{stdout.err.?}), + error.AlreadyReported => { + var eb = try runner.eb_wip.toOwnedBundle(""); + defer eb.deinit(init.gpa); + eb.renderToStderr(runner.io, .{}, .auto) catch |err| switch (err) { + error.Canceled => |e| return e, + else => {}, + }; + std.process.exit(1); + }, + } + } 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 args.targets.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 args.targets.append(init.gpa, try arena.dupe(u8, target)); + } else if (std.mem.eql(u8, arg, "--keep-src")) { + args.keep_src = true; + } else if (std.mem.eql(u8, arg, "-fqemu")) { + args.enable_qemu = true; + } else if (std.mem.eql(u8, arg, "-fwine")) { + args.enable_wine = true; + } else if (std.mem.eql(u8, arg, "-fwasmtime")) { + args.enable_wasmtime = true; + } else if (std.mem.eql(u8, arg, "-fdarling")) { + args.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\" arg", .{}); + const zig_path = zig_path_arg orelse fatal("missing \"--zig=path/to/zig\" arg", .{}); + const lib_path = lib_path_arg orelse fatal("missing \"--lib=path/to/lib\" arg", .{}); + + const cwd: std.Io.Dir = .cwd(); + args.test_file = cwd.openFile(init.io, test_path, .{ + .allow_directory = true, + }) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to open {q}: {t}", .{ test_path, e }), + }; + defer args.test_file.close(init.io); + args.test_kind = (args.test_file.stat(init.io) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to stat {q}: {t}", .{ test_path, e }), + }).kind; + args.zig_exe = cwd.openFile(init.io, zig_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to open {q}: {t}", .{ zig_path, e }), + }; + defer args.zig_exe.close(init.io); + args.lib_dir = cwd.openDir(init.io, lib_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to open {q}: {t}", .{ lib_path, e }), + }; + defer args.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; + }); + args.src_dir = cwd.createDirPathOpen(init.io, src_dir_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to create {q}: {t}", .{ src_dir_path, e }), + }; + defer { + args.src_dir.close(init.io); + if (!args.keep_src) cwd.deleteTree(init.io, src_dir_path) catch |err| { + std.log.warn("unable to delete tree {q}: {t}", .{ src_dir_path, err }); + }; + } + + var child = std.process.spawn(init.io, .{ + .argv = &.{ self, "--listen=-" }, + .stdin = .pipe, + .stdout = .pipe, + .inherit_dirs = &.{ args.lib_dir, args.src_dir }, + .inherit_files = &.{ args.zig_exe, args.test_file }, + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| fatal("unable to spawn runner: {t}", .{e}), + }; + + 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, + }; + const success = runClient(init.gpa, arena, init.io, &client, &args) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.ReadFailed => fatal("unable to receive message: {t}", .{child_reader.err.?}), + error.EndOfStream => fatal("no more messages", .{}), + error.WriteFailed => fatal("unable to send message: {t}", .{child_writer.err.?}), + }; + _ = child.wait(init.io) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to wait for runner: {t}", .{e}), + }; + return if (success) 0 else 1; +} +fn runClient( + gpa: std.mem.Allocator, + arena: std.mem.Allocator, + io: std.Io, + client: *std.zig.Client, + args: *const ClientArgs, +) (std.mem.Allocator.Error || std.Io.Reader.Error || std.Io.Writer.Error)!bool { + var stdout_buffer: [512]u8 = undefined; + var stdout_fw: std.Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buffer); + const stdout = &stdout_fw.interface; + var stderr_buffer: [512]u8 = undefined; + var stderr_fw: std.Io.File.Writer = .initStreaming(.stderr(), io, &stderr_buffer); + const stderr = &stderr_fw.interface; + + var argv: std.ArrayList(u8) = .empty; + defer argv.deinit(gpa); + const Arg = std.zig.Client.Message.Arg; + + try argv.append(gpa, @backingInt(@as(Arg, switch (args.test_kind) { + else => unreachable, + .file => .input_file, + .directory => .input_dir, + }))); + try argv.appendSlice(gpa, @ptrCast(&args.test_file.handle)); + + try argv.append(gpa, @backingInt(Arg.prefix)); + try argv.appendSlice(gpa, "--zig="); + try argv.append(gpa, 0); + + try argv.append(gpa, @backingInt(Arg.input_file)); + try argv.appendSlice(gpa, @ptrCast(&args.zig_exe.handle)); + + try argv.append(gpa, @backingInt(Arg.prefix)); + try argv.appendSlice(gpa, "--lib="); + try argv.append(gpa, 0); + + try argv.append(gpa, @backingInt(Arg.input_dir)); + try argv.appendSlice(gpa, @ptrCast(&args.lib_dir.handle)); + + try argv.append(gpa, @backingInt(Arg.prefix)); + try argv.appendSlice(gpa, "--src="); + try argv.append(gpa, 0); + + try argv.append(gpa, @backingInt(Arg.output_dir)); + try argv.appendSlice(gpa, @ptrCast(&args.src_dir.handle)); + + for (args.targets.items) |target| { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "--target"); + try argv.append(gpa, 0); + + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, target); + try argv.append(gpa, 0); + } + + if (args.enable_qemu) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-fqemu"); + try argv.append(gpa, 0); + } + if (args.enable_wine) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-fwine"); + try argv.append(gpa, 0); + } + if (args.enable_wasmtime) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-fwasmtime"); + try argv.append(gpa, 0); + } + if (args.enable_darling) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-fdarling"); + try argv.append(gpa, 0); + } + + try client.serveMessageHeader(.{ + .tag = .args, + .bytes_len = @intCast(argv.items.len), + }); + try client.out.writeAll(argv.items); + + var success = true; + 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 expected = @import("builtin").zig_version_string; + const actual = try client.in.take(hdr.bytes_len); + if (!std.mem.eql(u8, expected, actual)) { + try stderr.print( + "error: zig version mismatch compiler test runner vs compiler: {q} vs {q}\n", + .{ expected, actual }, + ); + try stderr.flush(); + std.process.exit(1); + } + }, + .error_bundle => { + const body = try arena.alloc(u8, hdr.bytes_len); + try client.in.readSliceAll(body); + const eb = std.zig.Server.allocErrorBundle(arena, body) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.EndOfStream => { + try stderr.writeAll("error bundle truncated\n"); + try stderr.flush(); + std.process.exit(1); + }, + }; + success = false; + eb.renderToStderr(io, .{}, .auto) catch {}; + }, + .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); + switch (tr.flags.status) { + .pass, .skip => {}, + .fail => success = false, + } + 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); + return success; +} diff --git a/tools/gen_spirv_spec.zig b/tools/gen_spirv_spec.zig index b9967d378570ede07fa26cfc2aa7870ba9d6fed7..769f09936e66547da537be43125564991a803e04 100644 --- a/tools/gen_spirv_spec.zig +++ b/tools/gen_spirv_spec.zig @@ -86,8 +86,7 @@ pub fn main(init: std.process.Init) !void { var zir = try std.zig.AstGen.generate(arena, tree); if (zir.hasCompileErrors()) { - var wip_errors: std.zig.ErrorBundle.Wip = undefined; - try wip_errors.init(arena); + var wip_errors: std.zig.ErrorBundle.Wip = try .init(arena); defer wip_errors.deinit(); try wip_errors.addZirErrorMessages(zir, tree, output, ""); var error_bundle = try wip_errors.toOwnedBundle(""); -- 2.54.0 From 9fe0ebe1087ba824dac7a32042b476a9ce8d770e Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 24 Sep 2026 16:19:32 -0400 Subject: [PATCH 13/23] Threaded: implement process spawning for macOS --- lib/std/Io/Threaded.zig | 447 +++++++++++++++++++++++++++++++++++++++- lib/std/c.zig | 2 + lib/std/c/darwin.zig | 4 +- lib/std/process.zig | 12 +- test/src/Runner.zig | 55 +++-- test/tests.zig | 2 + 6 files changed, 492 insertions(+), 30 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 50d9e464483921faa8d33179363c145c9b673bc4..2223d3e0659e50bea6a75595bb781596721c37c4 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7028,6 +7028,11 @@ fn fileRealPathPosix(userdata: ?*anyopaque, file: File, out_buffer: []u8) File.R } fn realPathPosix(fd: posix.fd_t, out_buffer: []u8) File.RealPathError!usize { + if (fd == posix.AT.FDCWD) return processCurrentPathPosix(out_buffer) catch |err| switch (err) { + else => |e| return e, + error.CurrentDirUnlinked => return error.FileNotFound, + error.PathExceedsLimit => return error.NameTooLong, + }; switch (native_os) { .dragonfly, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => { var sufficient_buffer: [posix.PATH_MAX]u8 = undefined; @@ -14143,6 +14148,7 @@ fn unlockStderr(userdata: ?*anyopaque) void { fn processCurrentPath(userdata: ?*anyopaque, buffer: []u8) process.CurrentPathError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; + if (is_windows) { var wtf16le_buf: [windows.PATH_MAX_WIDE:0]u16 = undefined; const n = windows.ntdll.RtlGetCurrentDirectory_U(wtf16le_buf.len * 2 + 2, &wtf16le_buf) / 2; @@ -14158,12 +14164,18 @@ fn processCurrentPath(userdata: ?*anyopaque, buffer: []u8) process.CurrentPathEr end_index += std.unicode.wtf8Encode(codepoint, buffer[end_index..]) catch unreachable; } return end_index; - } else if (native_os == .wasi and !builtin.link_libc) { + } + + if (native_os == .wasi and !builtin.link_libc) { if (buffer.len == 0) return error.NameTooLong; buffer[0] = '.'; return 1; } + return processCurrentPathPosix(buffer); +} + +fn processCurrentPathPosix(buffer: []u8) process.CurrentPathError!usize { const err: posix.E = if (builtin.link_libc) err: { const c_err = if (std.c.getcwd(buffer.ptr, buffer.len)) |_| 0 else std.c._errno().*; break :err @fromBackingInt(@intCast(c_err)); @@ -15131,12 +15143,54 @@ fn processReplace(userdata: ?*anyopaque, options: process.ReplaceOptions) proces }); }; + if (is_darwin) if (t.spawnDarwin( + .replace, + options.exe, + argv_buf.ptr, + options.cwd, + env_block, + options.expand_arg0, + .none, + .inherit, + .inherit, + .inherit, + options.inherit_dirs, + options.inherit_files, + null, + options.start_suspended, + PATH, + )) |_| unreachable else |err| switch (err) { + else => |e| return e, + error.WouldBlock => return error.SystemResources, + error.FileTooBig, + error.NoSpaceLeft, + error.PathAlreadyExists, + error.SymLinkLoop, + error.NetworkNotFound, + error.PipeBusy, + error.AntivirusInterference, + => return error.FileNotFound, + error.DeviceBusy, + error.NoDevice, + error.UnrecognizedVolume, + error.ReadOnlyFileSystem, + => return error.FileSystem, + error.ResourceLimitReached => return error.SystemResources, + error.FileLocksUnsupported => unreachable, // lock not requested + error.InvalidWtf8 => unreachable, // windows only + error.InvalidBatchScriptArg => unreachable, // windows only + error.InvalidUserId => unreachable, // not supported + error.InvalidProcessGroupId => unreachable, // passed null + error.InvalidName => unreachable, // windows only + error.ProcessAlreadyExec => unreachable, // linux only + }; return posixExec(options.exe, argv_buf.ptr, options.expand_arg0, env_block, PATH); } const processSpawn = switch (native_os) { .wasi, .emscripten, .ios, .tvos, .visionos, .watchos => processSpawnUnsupported, .windows => processSpawnWindows, + .driverkit, .maccatalyst, .macos => processSpawnDarwin, else => processSpawnPosix, }; @@ -15743,6 +15797,56 @@ fn destroyPipe(pipe: [2]posix.fd_t) void { if (pipe[0] != pipe[1]) closeFd(pipe[1]); } +fn processSpawnDarwin(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + t.scanEnviron(); // for PATH + const PATH = t.environ.string.PATH orelse default_PATH; + + var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + const argv_buf = try arena.allocSentinel(?[*:0]const u8, options.argv.len, null); + for (options.argv, 0..) |arg, i| argv_buf[i] = (try arena.dupeSentinel(u8, arg, 0)).ptr; + + const env_block = env_block: { + const prog_fd: i32 = -1; + if (options.environ_map) |environ_map| break :env_block try environ_map.createPosixBlock(arena, .{ + .zig_progress_fd = prog_fd, + }); + break :env_block try t.environ.process_environ.createPosixBlock(arena, .{ + .zig_progress_fd = prog_fd, + }); + }; + + const spawned = try t.spawnDarwin( + .spawn, + options.exe, + argv_buf.ptr, + options.cwd, + env_block, + options.expand_arg0, + options.progress_node, + options.stdin, + options.stdout, + options.stderr, + options.inherit_dirs, + options.inherit_files, + options.pgid, + options.start_suspended, + PATH, + ); + return .{ + .id = spawned.pid, + .thread_handle = {}, + .stdin = spawned.stdin, + .stdout = spawned.stdout, + .stderr = spawned.stderr, + .request_resource_usage_statistics = options.request_resource_usage_statistics, + }; +} + fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -17037,6 +17141,7 @@ fn posixExec( .path = .cwd(), } else .search, .search => { + if (Dir.path.isAbsolute(arg0_slice)) continue :exe .{ .path = .cwd() }; // Use of PATH_MAX here is valid as the path_buf will be passed // directly to the operating system in posixExecveat. var path_buf: [posix.PATH_MAX]u8 = undefined; @@ -17047,7 +17152,7 @@ fn posixExec( const path_len = search_path.len + arg0_slice.len + 1; if (path_buf.len < path_len + 1) return error.NameTooLong; @memcpy(path_buf[0..search_path.len], search_path); - path_buf[search_path.len] = '/'; + path_buf[search_path.len] = Dir.path.sep; @memcpy(path_buf[search_path.len + 1 ..][0..arg0_slice.len], arg0_slice); path_buf[path_len] = 0; const full_path = path_buf[0..path_len :0].ptr; @@ -17065,7 +17170,7 @@ fn posixExec( } return err; }, - .path => |path| return posixExecveat(path.handle, argv[0], argv, env_block), + .path => |path| return posixExecveat(path.handle, arg0_slice, argv, env_block), .file => |file| return posixExecveat(file.handle, null, argv, env_block), .explicit => |explicit| { var path_buffer: [posix.PATH_MAX]u8 = undefined; @@ -17100,7 +17205,7 @@ pub fn posixExecveat( .NOSYS => {}, } } - var path_buffer: [posix.PATH_MAX:0]u8 = undefined; + var path_buffer: [posix.PATH_MAX]u8 = undefined; var path_len = realPathPosix(dir, &path_buffer) catch |err| switch (err) { else => |e| return e, error.InputOutput, @@ -17125,7 +17230,9 @@ pub fn posixExecveat( path_len += path_slice.len; } path_buffer[path_len] = 0; - break :err posix.errno(posix.system.execve(&path_buffer, argv, env_block.slice.ptr)); + break :err posix.errno( + posix.system.execve(path_buffer[0..path_len :0], argv, env_block.slice.ptr), + ); }) { .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .@"2BIG" => return error.SystemResources, @@ -17158,6 +17265,334 @@ pub fn posixExecveat( } } +fn spawnDarwin( + t: *Threaded, + mode: enum { spawn, replace }, + exe: process.ReplaceOptions.Exe, + argv: [*:null]?[*:0]const u8, + cwd: process.Child.Cwd, + env_block: process.Environ.PosixBlock, + expand_arg0: process.ArgExpansion, + progress_node: std.Progress.Node, + stdin: process.SpawnOptions.StdIo, + stdout: process.SpawnOptions.StdIo, + stderr: process.SpawnOptions.StdIo, + inherit_dirs: []const Dir, + inherit_files: []const File, + pgid: ?posix.pid_t, + start_suspended: bool, + PATH: []const u8, +) (process.SpawnError || process.ReplaceError)!Spawned { + var fa: std.c.posix_spawn_file_actions_t = undefined; + switch (@as(std.c.E, @fromBackingInt(@intCast(std.c.posix_spawn_file_actions_init(&fa))))) { + .SUCCESS => {}, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + defer switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_destroy(&fa), + )))) { + .SUCCESS => {}, + .INVAL => recoverableOsBugDetected(), + else => recoverableOsBugDetected(), + }; + + var attr: std.c.posix_spawnattr_t = undefined; + switch (@as(std.c.E, @fromBackingInt(@intCast(std.c.posix_spawnattr_init(&attr))))) { + .SUCCESS => {}, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + defer switch (@as(std.c.E, @fromBackingInt(@intCast(std.c.posix_spawnattr_destroy(&attr))))) { + .SUCCESS => {}, + .INVAL => recoverableOsBugDetected(), + else => recoverableOsBugDetected(), + }; + + // The child process does need to access (one end of) these pipes. However, + // we must initially set CLOEXEC to avoid a race condition. If another thread + // is racing to spawn a different child process, we don't want it to inherit + // these FDs in any scenario; that would mean that, for instance, calls to + // `poll` from the parent would not report the child's stdout as closing when + // expected, since the other child may retain a reference to the write end of + // the pipe. So, we create the pipes with CLOEXEC initially. After fork, we + // need to do something in the new child to make sure we preserve the reference + // we want. We could use `fcntl` to remove CLOEXEC from the FD, but as it + // turns out, we `dup2` everything anyway, so there's no need! + const pipe_flags: posix.O = .{ .CLOEXEC = true }; + + const stdin_pipe = if (stdin == .pipe) try pipe2(pipe_flags) else undefined; + errdefer if (stdin == .pipe) { + destroyPipe(stdin_pipe); + }; + + const stdout_pipe = if (stdout == .pipe) try pipe2(pipe_flags) else undefined; + errdefer if (stdout == .pipe) { + destroyPipe(stdout_pipe); + }; + + const stderr_pipe = if (stderr == .pipe) try pipe2(pipe_flags) else undefined; + errdefer if (stderr == .pipe) { + destroyPipe(stderr_pipe); + }; + + const any_ignore = (stdin == .ignore or stdout == .ignore or stderr == .ignore); + const dev_null_fd = if (any_ignore) try t.getDevNullFd() else undefined; + + const prog_pipe: [2]posix.fd_t = if (progress_node.index != .none) pipe: { + // We use CLOEXEC for the same reason as in `pipe_flags`. + break :pipe try pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); + } else .{ -1, -1 }; + errdefer destroyPipe(prog_pipe); + + var cwd_buffer: [posix.PATH_MAX]u8 = undefined; + switch (cwd) { + .inherit => {}, + .dir => |dir| switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_addfchdir_np(&fa, dir.handle), + )))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }, + .path => |path| switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_addchdir_np(&fa, try pathToPosix(path, &cwd_buffer)), + )))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }, + } + + for (inherit_dirs) |inherit_dir| switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_addinherit_np(&fa, inherit_dir.handle), + )))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }; + for (inherit_files) |inherit_file| switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_addinherit_np(&fa, inherit_file.handle), + )))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }; + if (prog_pipe[1] != -1) switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn_file_actions_addinherit_np(&fa, prog_pipe[1]), + )))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }; + for ( + [_]process.SpawnOptions.StdIo{ stdin, stdout, stderr }, + [_]posix.fd_t{ stdin_pipe[0], stdout_pipe[1], stderr_pipe[1] }, + [_]posix.fd_t{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }, + ) |stdio, pipe_fd, std_fd| switch (@as(std.c.E, @fromBackingInt(@intCast(switch (stdio) { + .pipe => std.c.posix_spawn_file_actions_adddup2(&fa, pipe_fd, std_fd), + .close => std.c.posix_spawn_file_actions_addclose(&fa, std_fd), + .inherit => std.c.posix_spawn_file_actions_addinherit_np(&fa, std_fd), + .ignore => std.c.posix_spawn_file_actions_adddup2(&fa, dev_null_fd, std_fd), + .file => |file| std.c.posix_spawn_file_actions_adddup2(&fa, file.handle, std_fd), + })))) { + .SUCCESS => {}, + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }; + + if (pgid) |pid| switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawnattr_setpgroup(&attr, pid), + )))) { + .SUCCESS => {}, + .INVAL => return error.InvalidProcessGroupId, + else => |err| return posix.unexpectedErrno(err), + }; + + switch (@as(std.c.E, @fromBackingInt(@intCast(std.c.posix_spawnattr_setflags(&attr, .{ + .SETPGROUP = pgid != null, + .SETEXEC = switch (mode) { + .spawn => false, + .replace => true, + }, + .START_SUSPENDED = start_suspended, + }))))) { + .SUCCESS => {}, + .INVAL => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + + var pid: posix.pid_t = undefined; + const arg0_slice = std.mem.span(argv[0].?); + var path_buffer: [posix.PATH_MAX]u8 = undefined; + switch (err: { + break :err @as(std.c.E, @fromBackingInt(@intCast(exe: switch (exe) { + .detect => if (std.mem.findScalar(u8, arg0_slice, '/')) |_| + std.c.posix_spawn(&pid, arg0_slice, &fa, &attr, argv, env_block.slice.ptr) + else + continue :exe .search, + .search => if (!Dir.path.isAbsolute(arg0_slice)) { + // Use of PATH_MAX here is valid as the path_buf will be passed + // directly to the operating system in posixExecveat. + var path_buf: [posix.PATH_MAX]u8 = undefined; + var it = std.mem.tokenizeScalar(u8, PATH, ':'); + var err: error{ AccessDenied, FileNotFound, NotDir } = error.FileNotFound; + + while (it.next()) |search_path| { + const path_len = search_path.len + arg0_slice.len + 1; + if (path_buf.len < path_len + 1) return error.NameTooLong; + @memcpy(path_buf[0..search_path.len], search_path); + path_buf[search_path.len] = Dir.path.sep; + @memcpy(path_buf[search_path.len + 1 ..][0..arg0_slice.len], arg0_slice); + path_buf[path_len] = 0; + const full_path = path_buf[0..path_len :0].ptr; + switch (expand_arg0) { + .expand => argv[0] = full_path, + .no_expand => {}, + } + switch (@as(std.c.E, @fromBackingInt(@intCast( + std.c.posix_spawn(&pid, full_path, &fa, &attr, argv, env_block.slice.ptr), + )))) { + else => |e| break :err e, + .ACCES, .NOENT, .NOTDIR => |e| switch (err) { + error.AccessDenied => {}, + error.FileNotFound, error.NotDir => err = switch (e) { + else => unreachable, + .ACCES => error.AccessDenied, + .NOENT => error.FileNotFound, + .NOTDIR => error.NotDir, + }, + }, + } + } + return err; + } else std.c.posix_spawn(&pid, arg0_slice, &fa, &attr, argv, env_block.slice.ptr), + .path => |path| if (path.handle != posix.AT.FDCWD and !Dir.path.isAbsolute(arg0_slice)) { + var path_len = realPathPosix(path.handle, &path_buffer) catch |err| switch (err) { + else => |e| return e, + error.InputOutput => return error.FileNotFound, + }; + path_buffer[path_len] = Dir.path.sep; + path_len += 1; + @memcpy(path_buffer[path_len..][0..arg0_slice.len], arg0_slice); + path_len += arg0_slice.len; + path_buffer[path_len] = 0; + break :exe std.c.posix_spawn( + &pid, + path_buffer[0..path_len :0], + &fa, + &attr, + argv, + env_block.slice.ptr, + ); + } else std.c.posix_spawn(&pid, arg0_slice, &fa, &attr, argv, env_block.slice.ptr), + .file => |file| { + const path_len = realPathPosix(file.handle, &path_buffer) catch |err| switch (err) { + else => |e| return e, + error.InputOutput => return error.FileNotFound, + }; + path_buffer[path_len] = 0; + break :exe std.c.posix_spawn( + &pid, + path_buffer[0..path_len :0], + &fa, + &attr, + argv, + env_block.slice.ptr, + ); + }, + .explicit => |explicit| if (explicit.dir.handle != posix.AT.FDCWD and + !Dir.path.isAbsolute(explicit.path)) + { + var path_len = realPathPosix( + explicit.dir.handle, + &path_buffer, + ) catch |err| switch (err) { + else => |e| return e, + error.InputOutput => return error.FileNotFound, + }; + path_buffer[path_len] = Dir.path.sep; + path_len += 1; + @memcpy(path_buffer[path_len..][0..explicit.path.len], explicit.path); + path_len += explicit.path.len; + path_buffer[path_len] = 0; + break :exe std.c.posix_spawn( + &pid, + path_buffer[0..path_len :0], + &fa, + &attr, + argv, + env_block.slice.ptr, + ); + } else std.c.posix_spawn( + &pid, + try pathToPosix(explicit.path, &path_buffer), + &fa, + &attr, + argv, + env_block.slice.ptr, + ), + }))); + }) { + .SUCCESS => {}, + .AGAIN => return error.SystemResources, + .INVAL => |err| return errnoBug(err), + .@"2BIG" => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| return errnoBug(err), + .IO => return error.FileSystem, + .LOOP => return error.FileSystem, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOEXEC => return error.InvalidExe, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .TXTBSY => return error.FileBusy, + .BADARCH => return error.InvalidExe, + .BADF => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + + errdefer comptime unreachable; // The child is spawned; we must not error from now on + + if (stdin == .pipe) closeFd(stdin_pipe[0]); + if (stdout == .pipe) closeFd(stdout_pipe[1]); + if (stderr == .pipe) closeFd(stderr_pipe[1]); + + if (prog_pipe[1] != -1) closeFd(prog_pipe[1]); + progress_node.setIpcFile(t, .{ .handle = prog_pipe[0], .flags = .{ .nonblocking = true } }); + + return .{ + .pid = pid, + .err_fd = -1, + .stdin = switch (stdin) { + .pipe => .{ .handle = stdin_pipe[1], .flags = .{ .nonblocking = false } }, + else => null, + }, + .stdout = switch (stdout) { + .pipe => .{ .handle = stdout_pipe[0], .flags = .{ .nonblocking = false } }, + else => null, + }, + .stderr = switch (stderr) { + .pipe => .{ .handle = stderr_pipe[0], .flags = .{ .nonblocking = false } }, + else => null, + }, + }; +} + pub const CreatePipeOptions = struct { server: End, client: End, @@ -18469,7 +18904,7 @@ const CreateFileMapError = error{ fn createFileMap( file: File, - protection: std.process.MemoryProtection, + protection: process.MemoryProtection, offset: u64, populate: bool, len: usize, diff --git a/lib/std/c.zig b/lib/std/c.zig index 4ec2d6b0ac2db23b2ffa6b846772f2f4e9b18b16..51c6656d4f21dc23717f757faddc76f109049e2a 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -11785,8 +11785,10 @@ pub const posix_spawn_file_actions_init = darwin.posix_spawn_file_actions_init; pub const posix_spawn_file_actions_t = darwin.posix_spawn_file_actions_t; pub const posix_spawnattr_destroy = darwin.posix_spawnattr_destroy; pub const posix_spawnattr_getflags = darwin.posix_spawnattr_getflags; +pub const posix_spawnattr_getpgroup = darwin.posix_spawnattr_getpgroup; pub const posix_spawnattr_init = darwin.posix_spawnattr_init; pub const posix_spawnattr_setflags = darwin.posix_spawnattr_setflags; +pub const posix_spawnattr_setpgroup = darwin.posix_spawnattr_setpgroup; pub const posix_spawnattr_t = darwin.posix_spawnattr_t; pub const posix_spawnp = darwin.posix_spawnp; pub const pthread_attr_get_qos_class_np = darwin.pthread_attr_get_qos_class_np; diff --git a/lib/std/c/darwin.zig b/lib/std/c/darwin.zig index 19ff7897d8a4297dedcd565d4f01b5f903f60667..fa8c1901cd74954ece2856487cf4eb6afcd90beb 100644 --- a/lib/std/c/darwin.zig +++ b/lib/std/c/darwin.zig @@ -1253,8 +1253,10 @@ pub const posix_spawnattr_t = *opaque {}; pub const posix_spawn_file_actions_t = *opaque {}; pub extern "c" fn posix_spawnattr_init(attr: *posix_spawnattr_t) c_int; pub extern "c" fn posix_spawnattr_destroy(attr: *posix_spawnattr_t) c_int; -pub extern "c" fn posix_spawnattr_setflags(attr: *posix_spawnattr_t, flags: POSIX_SPAWN) c_int; pub extern "c" fn posix_spawnattr_getflags(attr: *const posix_spawnattr_t, flags: *POSIX_SPAWN) c_int; +pub extern "c" fn posix_spawnattr_getpgroup(attr: *const posix_spawnattr_t, pgroup: *pid_t) c_int; +pub extern "c" fn posix_spawnattr_setflags(attr: *posix_spawnattr_t, flags: POSIX_SPAWN) c_int; +pub extern "c" fn posix_spawnattr_setpgroup(attr: *posix_spawnattr_t, pgroup: pid_t) c_int; pub extern "c" fn posix_spawn_file_actions_init(actions: *posix_spawn_file_actions_t) c_int; pub extern "c" fn posix_spawn_file_actions_destroy(actions: *posix_spawn_file_actions_t) c_int; pub extern "c" fn posix_spawn_file_actions_addclose(actions: *posix_spawn_file_actions_t, filedes: fd_t) c_int; diff --git a/lib/std/process.zig b/lib/std/process.zig index 43ec3611e0c373a72e60dfdf42c0ba405af5ab4a..ed4dceabdde0abee586683e90daddf144d78fff9 100644 --- a/lib/std/process.zig +++ b/lib/std/process.zig @@ -284,10 +284,20 @@ pub const ReplaceError = error{ pub const ReplaceOptions = struct { exe: Exe = .detect, argv: []const []const u8, - expand_arg0: ArgExpansion = .no_expand, + + /// Set to change the current working directory when spawning the child process. + cwd: Child.Cwd = .inherit, /// Replaces the environment when provided. The PATH value from here is /// never used to resolve `argv[0]`. environ_map: ?*const Environ.Map = null, + expand_arg0: ArgExpansion = .no_expand, + + inherit_dirs: []const Dir = &.{}, + inherit_files: []const File = &.{}, + + /// Start child process in suspended state. + /// For Posix systems it's started as if SIGSTOP was sent. + start_suspended: bool = false, pub const Exe = union(enum) { /// `argv[0]` is the name of the program to execute. If it is not already a diff --git a/test/src/Runner.zig b/test/src/Runner.zig index 158d657ad37ba2b5f73183d9e709eae0be431cbb..c985ff8410fea3e0181a74d41d19be62d36767d0 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -26,6 +26,7 @@ const Args = struct { enable_wine: bool, enable_wasmtime: bool, enable_darling: bool, + quiet: bool, fn deinit(args: *Args, gpa: std.mem.Allocator) void { args.targets.deinit(gpa); @@ -98,6 +99,8 @@ pub fn runServer(runner: *Runner) ProtocolError { runner.args.enable_wasmtime = true; } else if (std.mem.eql(u8, string, "-fdarling")) { runner.args.enable_darling = true; + } else if (std.mem.eql(u8, string, "--quiet")) { + runner.args.quiet = true; }, .zig => unreachable, .lib => unreachable, @@ -463,10 +466,10 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { .target_query = target_query, .manifest_sr = .init(&manifest_fr.interface, runner.gpa, &runner.eb_wip, src_path_string: { var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; + const src_path = src_path: { - break :src_path path_buffer[0 .. std.Io.Dir.readLinkAbsolute( + break :src_path path_buffer[0 .. manifest_file.realPath( runner.io, - try runner.arena.allocator().print("/proc/self/fd/{d}", .{manifest_file.handle}), &path_buffer, ) catch |err| switch (err) { error.Canceled => |e| return e, @@ -839,12 +842,9 @@ fn spawnCompiler( .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 .. std.Io.Dir.readLinkAbsolute( - runner.io, - try arena.print("/proc/self/fd/{d}", .{(runner.args.lib_dir orelse - return runner.fail("\"--lib=path/to/lib\" arg required with cbe", .{})).handle}), - &path_buffer, - ) catch |err| switch (err) { + } else try argv.appendSlice(gpa, &.{ "-I", path_buffer[0 .. (runner.args.lib_dir orelse + return runner.fail("\"--lib=path/to/lib\" arg required with cbe", .{})) + .realPath(runner.io, &path_buffer) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| return runner.fail("unable to get lib path: {t}", .{e}), }] }); @@ -1034,8 +1034,8 @@ const Compiler = struct { )) { .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}'", .{ + if (!runner.args.quiet) std.log.warn("skipping execution because host {q} cannot " ++ + "execute binaries for foreign target {q}", .{ try runner.host.zigTriple(arena), comp.target.triple, }); return; @@ -1080,12 +1080,12 @@ const Compiler = struct { .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}", + if (!runner.args.quiet) std.log.warn( + "skipping execution of {q} via executor for foreign target {q}: {t}", .{ bin_path, comp.target.triple, err }, ); return; - } else return runner.fail("unable to run the generated executable '{s}': {t}", .{ + } else return runner.fail("unable to run the generated executable {q}: {t}", .{ bin_path, err, }); defer { @@ -1097,7 +1097,7 @@ const Compiler = struct { .errors, .lldb => unreachable, .stdout => { if (code != 0) return runner.fail( - "generated executable '{s}' failed with code {d}", + "generated executable {q} failed with code {d}", .{ bin_path, code }, ); std.testing.expectEqualStrings( @@ -1125,20 +1125,20 @@ const Compiler = struct { }, }, .signal => |sig| return runner.fail( - "generated executable '{s}' terminated with signal {t}", + "generated executable {q} terminated with signal {t}", .{ bin_path, sig }, ), .stopped => |sig| return runner.fail( - "generated executable '{s}' stopped with signal {t}", + "generated executable {q} stopped with signal {t}", .{ bin_path, sig }, ), .unknown => return runner.fail( - "generated executable '{s}' terminated unexpectedly", + "generated executable {q} terminated unexpectedly", .{bin_path}, ), } if (executor == null and result.stderr.len > 0) { - std.log.err("generated executable '{s}' had unexpected stderr:\n{s}", .{ + std.log.err("generated executable {q} had unexpected stderr:\n{s}", .{ bin_path, result.stderr, }); } @@ -1247,6 +1247,7 @@ const ClientArgs = struct { enable_wine: bool, enable_wasmtime: bool, enable_darling: bool, + quiet: bool, }; pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelable)!u8 { const fatal = struct { @@ -1272,6 +1273,7 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl .enable_wine = false, .enable_wasmtime = false, .enable_darling = false, + .quiet = false, }; defer args.targets.deinit(init.gpa); @@ -1300,6 +1302,7 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl .enable_wine = false, .enable_wasmtime = false, .enable_darling = false, + .quiet = false, }, .host = std.zig.system.resolveTargetQuery(init.io, .{}) catch |err| switch (err) { error.Canceled => |e| return e, @@ -1328,17 +1331,17 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl }, } } else if (std.mem.eql(u8, arg, "--zig")) { - zig_path_arg = arg_it.next() orelse fatal("missing arg after '{s}'", .{arg}); + zig_path_arg = arg_it.next() orelse fatal("missing arg after {q}", .{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}); + lib_path_arg = arg_it.next() orelse fatal("missing arg after {q}", .{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 args.targets.append( init.gpa, - try arena.dupe(u8, arg_it.next() orelse fatal("missing arg after '{s}'", .{arg})), + try arena.dupe(u8, arg_it.next() orelse fatal("missing arg after {q}", .{arg})), ); } else if (std.mem.cutPrefix(u8, arg, "--target=")) |target| { try args.targets.append(init.gpa, try arena.dupe(u8, target)); @@ -1352,8 +1355,10 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl args.enable_wasmtime = true; } else if (std.mem.eql(u8, arg, "-fdarling")) { args.enable_darling = true; + } else if (std.mem.eql(u8, arg, "--quiet")) { + args.quiet = true; } else { - if (test_path_arg) |_| fatal("unknown arg '{s}'", .{arg}); + if (test_path_arg) |_| fatal("unknown arg {q}", .{arg}); test_path_arg = arg; } } @@ -1509,6 +1514,12 @@ fn runClient( try argv.append(gpa, 0); } + if (args.quiet) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "--quiet"); + try argv.append(gpa, 0); + } + try client.serveMessageHeader(.{ .tag = .args, .bytes_len = @intCast(argv.items.len), diff --git a/test/tests.zig b/test/tests.zig index ac6cca3ab78d3325daac58214975254b7ef3da9f..3da8ef96442b31b91217fadc6334d5102dcaa76d 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3417,6 +3417,8 @@ pub fn addIncrementalTests( run.addThirdPartyEnabledArgWasmtime(.{ .enabled = "-fwasmtime" }); run.addThirdPartyEnabledArgWine(.{ .enabled = "-fwine" }); + run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc + tests_step.dependOn(&run.step); } -- 2.54.0 From ee0bd6c69ee01caf076e5c9f7461fe09fa8cf9ba Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 10:00:39 -0400 Subject: [PATCH 14/23] Io: avoid leaking inherited descriptors --- lib/std/Io.zig | 17 +++++++++++++++++ lib/std/Io/Threaded.zig | 40 ++++++++++++++++++++++++++++++++++++++++ test/src/Runner.zig | 24 +++++++++++++++++++----- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index c7f94cbb9652fd9e98f6b419d629c6fc55b292e2..759855179a6e99e770afc2be497774b9e5b21cd2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -218,6 +218,8 @@ pub const VTable = struct { childKill: *const fn (?*anyopaque, *std.process.Child) void, progressParentFile: *const fn (?*anyopaque) std.Progress.ParentFileError!File, + inheritParentDir: *const fn (?*anyopaque, handle: Dir.Handle) (Cancelable || UnexpectedError)!Dir, + inheritParentFile: *const fn (?*anyopaque, handle: File.Handle, flags: File.Flags) (Cancelable || UnexpectedError)!File, now: *const fn (?*anyopaque, Clock) Timestamp, clockResolution: *const fn (?*anyopaque, Clock) Clock.ResolutionError!Duration, @@ -2800,6 +2802,8 @@ pub const failing: std.Io = .{ .childKill = unreachableChildKill, .progressParentFile = failingProgressParentFile, + .inheritParentDir = unreachableInheritParentDir, + .inheritParentFile = unreachableInheritParentFile, .random = noRandom, .randomSecure = failingRandomSecure, @@ -3470,6 +3474,19 @@ pub fn failingProgressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileE return error.UnsupportedOperation; } +pub fn unreachableInheritParentDir(userdata: ?*anyopaque, handle: Dir.Handle) (Cancelable || UnexpectedError)!Dir { + _ = userdata; + _ = handle; + unreachable; +} + +pub fn unreachableInheritParentFile(userdata: ?*anyopaque, handle: File.Handle, flags: File.Flags) (Cancelable || UnexpectedError)!File { + _ = userdata; + _ = handle; + _ = flags; + unreachable; +} + pub fn noRandom(userdata: ?*anyopaque, buffer: []u8) void { _ = userdata; @memset(buffer, 0); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 2223d3e0659e50bea6a75595bb781596721c37c4..00f29608098c8942e621ba96d6e81e906b463d0f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1906,6 +1906,8 @@ pub fn io(t: *Threaded) Io { .childKill = childKill, .progressParentFile = progressParentFile, + .inheritParentDir = inheritParentDir, + .inheritParentFile = inheritParentFile, .now = now, .clockResolution = clockResolution, @@ -17694,6 +17696,44 @@ fn progressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileError!File { return t.environ.zig_progress_file; } +fn inheritParentHandle(handle: posix.fd_t) Io.InheritParentHandleError!void { + switch (native_os) { + .linux, + .dragonfly, + .freebsd, + .netbsd, + .openbsd, + .illumos, + .driverkit, + .ios, + .maccatalyst, + .macos, + .tvos, + .visionos, + .watchos, + => return setCloexec(handle), + else => return error.UnsupportedOperation, + } +} + +fn inheritParentDir(userdata: ?*anyopaque, handle: Dir.Handle) Io.InheritParentHandleError!Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + try inheritParentHandle(handle); + return .{ .handle = handle }; +} + +fn inheritParentFile( + userdata: ?*anyopaque, + handle: File.Handle, + flags: File.Flags, +) Io.InheritParentHandleError!File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + try inheritParentHandle(handle); + return .{ .handle = handle, .flags = flags }; +} + pub fn environString(t: *Threaded, comptime name: []const u8) ?[:0]const u8 { t.scanEnviron(); return @field(t.environ.string, name); diff --git a/test/src/Runner.zig b/test/src/Runner.zig index c985ff8410fea3e0181a74d41d19be62d36767d0..8a7b3832a41b014de1d86626fe9b8bdd686ec15e 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -132,8 +132,15 @@ pub fn runServer(runner: *Runner) ProtocolError { 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.*, + const dir = runner.io.vtable.inheritParentDir( + runner.io.userdata, + dir_handle.*, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Unexpected => |e| return runner.fail( + "unable to inherit parent dir: {t}", + .{e}, + ), }; switch (state) { .positional => { @@ -171,9 +178,16 @@ pub fn runServer(runner: *Runner) ProtocolError { 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 }, + const file = runner.io.vtable.inheritParentFile( + runner.io.userdata, + file_handle.*, + .{ .nonblocking = false }, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Unexpected => |e| return runner.fail( + "unable to inherit parent file: {t}", + .{e}, + ), }; switch (state) { .positional => { -- 2.54.0 From 4dd645fdddec2c56f8eaabce9f358f95b54f8eec Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 12:20:18 -0400 Subject: [PATCH 15/23] Runner: add --libc-runtimes support --- lib/compiler/Maker.zig | 34 +++- lib/compiler/Maker/Step.zig | 10 +- lib/compiler/Maker/Step/Run.zig | 32 +++- lib/std/Build.zig | 5 +- lib/std/Build/Configuration.zig | 1 + lib/std/Io.zig | 20 ++- lib/std/Io/Threaded.zig | 6 +- test/src/Runner.zig | 288 +++++++++++++++++++++++--------- test/tests.zig | 1 + 9 files changed, 285 insertions(+), 112 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 61b99159cecbfc9603b04e716d97ba448a431248..12df472dea24af6477265bbd9e922542db8a8757 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -3631,7 +3631,7 @@ pub fn resolveLazyPath( const c = &maker.scanned_config.configuration; return switch (lazy_path) { .source_path => |sp| try packagePath(maker, arena, sp.owner, sp.sub_path.slice(c)), - .relative => |relative| relativePath(maker, arena, relative), + .relative => |relative| try relativePath(maker, arena, relative, stepByIndex(maker, asking_step_index)), .generated => |gen| { const base = generatedPath(maker, gen.index); var file_path = base; @@ -3768,7 +3768,12 @@ pub fn packagePath( }; } -pub fn relativePath(maker: *const Maker, arena: Allocator, relative: Configuration.LazyPath.Relative) Allocator.Error!Path { +pub fn relativePath( + maker: *const Maker, + arena: Allocator, + relative: Configuration.LazyPath.Relative, + asking_step: *Step, +) error{ OutOfMemory, MakeFailed }!Path { const graph = maker.graph; const c = &maker.scanned_config.configuration; const sub_path = relative.sub_path.slice(c); @@ -3789,12 +3794,12 @@ pub fn relativePath(maker: *const Maker, arena: Allocator, relative: Configurati .root_dir = graph.build_root_directory, .sub_path = sub_path, }, - .zig_exe => .{ - .root_dir = .cwd(), - .sub_path = if (sub_path.len == 0) - graph.zig_exe - else - try Io.Dir.path.join(arena, &.{ graph.zig_exe, sub_path }), + .zig_exe => { + assert(sub_path.len == 0); + return .{ + .root_dir = .cwd(), + .sub_path = graph.zig_exe, + }; }, .zig_lib => .{ .root_dir = graph.zig_lib_directory, @@ -3804,6 +3809,10 @@ pub fn relativePath(maker: *const Maker, arena: Allocator, relative: Configurati .install_lib => try maker.install_paths.lib.join(arena, sub_path), .install_bin => try maker.install_paths.bin.join(arena, sub_path), .install_include => try maker.install_paths.include.join(arena, sub_path), + .libc_runtimes => if (maker.graph.libc_runtimes_dir) |libc_runtimes_dir| .{ + .root_dir = .cwd(), + .sub_path = try Dir.path.join(arena, &.{ libc_runtimes_dir, sub_path }), + } else asking_step.fail(maker, "unknown LazyPath: \"--libc-runtimes\" not specified", .{}), }; } @@ -4391,15 +4400,22 @@ fn confPathDepToCachePath( else => |index| try Dir.path.join(arena, &.{ index.get(c).?.root_path.slice(c), sub_path }), }, }, + .zig_exe => { + assert(sub_path.len == 0); + return .{ + .root_dir = .cwd(), + .sub_path = graph.zig_exe, + }; + }, .zig_lib => .{ .root_dir = graph.zig_lib_directory, .sub_path = sub_path, }, - .zig_exe => @panic("TODO"), .install_prefix => @panic("TODO"), .install_lib => @panic("TODO"), .install_bin => @panic("TODO"), .install_include => @panic("TODO"), + .libc_runtimes => @panic("TODO"), }; } diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig index ebf2a4672892d24cf96db6947d9a04e07da1e69e..37212902592f2dfd5d2ea0d760b861d1e7fe03e2 100644 --- a/lib/compiler/Maker/Step.zig +++ b/lib/compiler/Maker/Step.zig @@ -857,7 +857,7 @@ pub fn setWatchInputsFromManifestFiles( } /// For steps that have a single input that never changes when re-running `make`. -pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) Allocator.Error!void { +pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) FailError!void { if (!step.inputs.populated()) try step.addWatchInput(maker, arena, lazy_path); } @@ -866,7 +866,7 @@ pub fn clearWatchInputs(step: *Step, maker: *Maker) void { } /// Places a *file* dependency on the path. -pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) Allocator.Error!void { +pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) FailError!void { const conf = &maker.scanned_config.configuration; switch (lazy_file) { .source_path => |source_path| { @@ -875,7 +875,7 @@ pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: La try addWatchInputPath(step, maker, pkg_path); }, .relative => |relative| { - const resolved_path = try maker.relativePath(arena, relative); + const resolved_path = try maker.relativePath(arena, relative, step); try addWatchInputPath(step, maker, resolved_path); }, // Nothing to watch because this dependency edge is modeled instead via `dependants`. @@ -890,7 +890,7 @@ pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: La /// Paths derived from this directory should also be manually added via /// `addDirectoryWatchInputFromPath` if and only if this function returns /// `true`. -pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool { +pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) FailError!bool { const graph = maker.graph; const arena = graph.arena; // TODO don't leak into the process arena switch (lazy_directory) { @@ -901,7 +901,7 @@ pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPa try addDirectoryWatchInputFromPath(step, maker, pkg_path); }, .relative => |relative| { - const resolved_path = try maker.relativePath(arena, relative); + const resolved_path = try maker.relativePath(arena, relative, step); try addDirectoryWatchInputFromPath(step, maker, resolved_path); }, // Nothing to watch because this dependency edge is modeled instead via `dependants`. diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 784f84eef95e7aeb912c097a4b0781def6a2ddcf..8ce0a66837e0ed9fb9cf27e39a170bf7845a9204 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -250,9 +250,18 @@ pub fn make( protocol_args.appendAssumeCapacity(0); }, .path_file => { + const lazy_path = arg.path.value.?.get(conf); + switch (lazy_path) { + else => {}, + .relative => |relative| switch (relative.flags.base) { + else => {}, + .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, + }, + } + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + const file_path = try maker.resolveLazyPath(arena, lazy_path, run_index); try owned_files.resize(gpa, owned_files.bit_length + 1, true); try inherit_files.ensureUnusedCapacity(gpa, 1); @@ -283,9 +292,18 @@ pub fn make( } }, .path_directory => { + const lazy_path = arg.path.value.?.get(conf); + switch (lazy_path) { + else => {}, + .relative => |relative| switch (relative.flags.base) { + else => {}, + .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, + }, + } + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const dir_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + const dir_path = try maker.resolveLazyPath(arena, lazy_path, run_index); try input_dirs.append(gpa, dir_path); const has_sub_path = dir_path.sub_path.len > 0; @@ -2242,12 +2260,8 @@ fn runCommand( else => builtin.target, }; - const need_cross_libc = config.flags.link_libc and root_target.os.tag == .linux and - switch (producer.flags2.linkage) { - .static => false, - .dynamic => true, - .default => root_target.isGnuLibC(), - }; + const need_cross_libc = root_target.os.tag == .linux and + config.flags.link_libc and config.flags.link_mode == .dynamic; switch (std.zig.system.getExternalExecutor(io, &root_target, .{ .host_cpu_arch = host.cpu.arch, .host_os_tag = host.os.tag, @@ -2316,7 +2330,7 @@ fn runCommand( interp_argv.appendAssumeCapacity(bin_name); interp_argv.appendAssumeCapacity("--dir=."); for (conf_run.preopens.slice) |preopen| { - const path = try maker.resolveLazyPath(arena, preopen.path.get(conf), run_index); + const path = try maker.resolveLazyPathIndex(arena, preopen.path, run_index); path.root_dir.handle.createDirPath(io, path.subPathOrDot()) catch |e| return step.fail(maker, "failed creating directory {f}: {t}", .{ path, e }); interp_argv.appendAssumeCapacity(try arena.print("--dir={f}::{s}", .{ path, preopen.name.slice(conf) })); diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 730c6bcba58f9c216929acd6157f30c497508a6b..36b48d62cf8075024dddc8dc21b339b929d34c5e 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -2693,12 +2693,15 @@ fn validateConfigureDependency(lazy_path: LazyPath) void { .src_path, .cwd_relative, .dependency => {}, // OK .generated => @panic("configure phase cannot depend on files generated during make phase"), .relative => |relative| switch (relative.base) { - .cwd, .build_root, .local_cache, .global_cache, .zig_exe, .zig_lib => {}, // OK + .cwd, .build_root, .local_cache, .global_cache, .zig_lib => {}, // OK + .zig_exe => if (relative.sub_path.len > 0) @panic("file base cannot have a sub path"), .install_prefix, .install_lib, .install_bin, .install_include, => @panic("configure phase cannot depend on files installed during make phase"), + .libc_runtimes, + => @panic("configure phase cannot depend on directory known only during make phase"), }, } } diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig index f0c3ff44d92678e6fbdf945e9d12883b75f95049..53bd11500b0d70d4448bd461cfcd924fec6482a2 100644 --- a/lib/std/Build/Configuration.zig +++ b/lib/std/Build/Configuration.zig @@ -1575,6 +1575,7 @@ pub const LazyPath = union(@This().Tag) { install_lib, install_bin, install_include, + libc_runtimes, }; }; }; diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 759855179a6e99e770afc2be497774b9e5b21cd2..1f26d2bf7ea49c898b81d7921e5520a6d6d033e0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -218,8 +218,8 @@ pub const VTable = struct { childKill: *const fn (?*anyopaque, *std.process.Child) void, progressParentFile: *const fn (?*anyopaque) std.Progress.ParentFileError!File, - inheritParentDir: *const fn (?*anyopaque, handle: Dir.Handle) (Cancelable || UnexpectedError)!Dir, - inheritParentFile: *const fn (?*anyopaque, handle: File.Handle, flags: File.Flags) (Cancelable || UnexpectedError)!File, + inheritParentDir: *const fn (?*anyopaque, handle: Dir.Handle) InheritParentHandleError!Dir, + inheritParentFile: *const fn (?*anyopaque, handle: File.Handle, flags: File.Flags) InheritParentHandleError!File, now: *const fn (?*anyopaque, Clock) Timestamp, clockResolution: *const fn (?*anyopaque, Clock) Clock.ResolutionError!Duration, @@ -827,6 +827,10 @@ pub const UnexpectedError = error{ Unexpected, }; +pub const InheritParentHandleError = error{ + UnsupportedOperation, +} || Cancelable || UnexpectedError; + pub const Clock = enum { /// A settable system-wide clock that measures real (i.e. wall-clock) /// time. This clock is affected by discontinuous jumps in the system @@ -2802,8 +2806,8 @@ pub const failing: std.Io = .{ .childKill = unreachableChildKill, .progressParentFile = failingProgressParentFile, - .inheritParentDir = unreachableInheritParentDir, - .inheritParentFile = unreachableInheritParentFile, + .inheritParentDir = failingInheritParentDir, + .inheritParentFile = failingInheritParentFile, .random = noRandom, .randomSecure = failingRandomSecure, @@ -3474,17 +3478,17 @@ pub fn failingProgressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileE return error.UnsupportedOperation; } -pub fn unreachableInheritParentDir(userdata: ?*anyopaque, handle: Dir.Handle) (Cancelable || UnexpectedError)!Dir { +pub fn failingInheritParentDir(userdata: ?*anyopaque, handle: Dir.Handle) InheritParentHandleError!Dir { _ = userdata; _ = handle; - unreachable; + return error.UnsupportedOperation; } -pub fn unreachableInheritParentFile(userdata: ?*anyopaque, handle: File.Handle, flags: File.Flags) (Cancelable || UnexpectedError)!File { +pub fn failingInheritParentFile(userdata: ?*anyopaque, handle: File.Handle, flags: File.Flags) InheritParentHandleError!File { _ = userdata; _ = handle; _ = flags; - unreachable; + return error.UnsupportedOperation; } pub fn noRandom(userdata: ?*anyopaque, buffer: []u8) void { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 00f29608098c8942e621ba96d6e81e906b463d0f..30b924f59d835a85aeb2a1420d504a4a7d443b01 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1975,6 +1975,10 @@ const have_copy_file_range = switch (native_os) { .linux, .freebsd => true, else => false, }; +const have_fcntl = switch (native_os) { + .linux, .dragonfly, .freebsd, .netbsd, .openbsd, .illumos, .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => true, + else => false, +}; const have_fcopyfile = is_darwin; const have_fchmodat2 = native_os == .linux and (builtin.os.isAtLeast(.linux, .{ .major = 6, .minor = 6, .patch = 0 }) orelse true) and @@ -17195,7 +17199,7 @@ pub fn posixExecveat( break :err posix.errno(posix.system.execve(p, argv, env_block.slice.ptr)); }; if (comptime native_os == .linux and (!builtin.link_libc or - (builtin.target.abi.isGnu() and builtin.target.os.version_range.linux.glibc.order( + (builtin.target.isGnuLibC() and builtin.target.os.version_range.linux.glibc.order( .{ .major = 2, .minor = 34, .patch = 0 }, ).compare(.gte)))) { diff --git a/test/src/Runner.zig b/test/src/Runner.zig index 8a7b3832a41b014de1d86626fe9b8bdd686ec15e..22f1962f0d6739e0a8b3ca09cef45bd610455cef 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -22,10 +22,12 @@ const Args = struct { src_dir: ?std.Io.Dir, targets: std.ArrayList(u32), target_bytes: std.ArrayList(u8), + libc_runtimes_dir: ?std.Io.Dir, + enable_darling: bool, enable_qemu: bool, - enable_wine: bool, + enable_rosetta: bool, enable_wasmtime: bool, - enable_darling: bool, + enable_wine: bool, quiet: bool, fn deinit(args: *Args, gpa: std.mem.Allocator) void { @@ -73,7 +75,28 @@ pub fn runServer(runner: *Runner) ProtocolError { .args => { const args_body = try arena.alloc(u8, hdr.bytes_len); runner.server.in.readSliceAll(args_body) catch unreachable; - var state: enum { positional, zig, lib, src, target } = .positional; + const State = enum { + positional, + @"--zig", + @"--lib", + @"--src", + @"--target", + @"--libc-runtimes", + }; + var state: State = .positional; + const state_expected: std.enums.EnumArray(State, enum { + path, + dir, + file, + string, + }) = .init(.{ + .positional = .path, + .@"--zig" = .file, + .@"--lib" = .dir, + .@"--src" = .dir, + .@"--target" = .string, + .@"--libc-runtimes" = .dir, + }); var input_dir: std.zig.Server.Message.InputDir = .cwd; var args_body_offset: usize = 0; while (args_body.len - args_body_offset > 0) { @@ -87,42 +110,56 @@ pub fn runServer(runner: *Runner) ProtocolError { args_body_offset = end + 1; switch (state) { .positional => if (std.mem.eql(u8, string, "--target")) { - state = .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, "--libc-runtimes")) { + state = .@"--libc-runtimes"; + continue; + } else if (std.mem.eql(u8, string, "-fdarling")) { + runner.args.enable_darling = true; } 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, "-frosetta")) { + runner.args.enable_rosetta = 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; + } else if (std.mem.eql(u8, string, "-fwine")) { + runner.args.enable_wine = true; } else if (std.mem.eql(u8, string, "--quiet")) { runner.args.quiet = true; }, - .zig => unreachable, - .lib => unreachable, - .src => unreachable, - .target => try runner.args.addTarget(runner.gpa, string), + .@"--target" => try runner.args.addTarget(runner.gpa, string), + else => return runner.fail( + "\"{t}\" expected {t}, got {t}", + .{ state, state_expected.get(state), arg }, + ), } }, .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; + assert(state == .positional); state = if (std.mem.eql(u8, string, "--zig=")) - .zig + .@"--zig" else if (std.mem.eql(u8, string, "--lib=")) - .lib + .@"--lib" else if (std.mem.eql(u8, string, "--src=")) - .src + .@"--src" + else if (std.mem.eql(u8, string, "--libc-runtimes=")) + .@"--libc-runtimes" else - unreachable; + return runner.fail("unsupported arg prefix: {q}", .{string}); continue; }, - .suffix => unreachable, + .suffix => { + 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; + return runner.fail("unsupported arg suffix: {q}", .{string}); + }, .input_dir => { input_dir = @fromBackingInt(@backingInt(input_dir) + 1); continue :arg .output_dir; @@ -137,16 +174,19 @@ pub fn runServer(runner: *Runner) ProtocolError { dir_handle.*, ) catch |err| switch (err) { error.Canceled => |e| return e, - error.Unexpected => |e| return runner.fail( + else => |e| return runner.fail( "unable to inherit parent dir: {t}", .{e}, ), }; switch (state) { .positional => { - assert(runner.args.@"test" == null); + if (runner.args.@"test" != null or + runner.args.manifest_file != null) return runner.fail( + "{t} specified multiple times", + .{state}, + ); runner.args.@"test" = .{ .input_dir = input_dir, .dir = dir }; - assert(runner.args.manifest_file == null); const manifest_path = "manifest"; runner.args.manifest_file = dir.openFile( runner.io, @@ -161,16 +201,31 @@ pub fn runServer(runner: *Runner) ProtocolError { }; try runner.discoverInput(input_dir, manifest_path); }, - .zig => unreachable, - .lib => { - assert(runner.args.lib_dir == null); + .@"--lib" => { + if (runner.args.lib_dir != null) return runner.fail( + "\"{t}\" specified multiple times", + .{state}, + ); runner.args.lib_dir = dir; }, - .src => { - assert(runner.args.src_dir == null); + .@"--src" => { + if (runner.args.src_dir != null) return runner.fail( + "\"{t}\" specified multiple times", + .{state}, + ); runner.args.src_dir = dir; }, - .target => unreachable, + .@"--libc-runtimes" => { + if (runner.args.libc_runtimes_dir != null) return runner.fail( + "\"{t}\" specified multiple times", + .{state}, + ); + runner.args.libc_runtimes_dir = dir; + }, + else => return runner.fail( + "\"{t}\" expected {t}, got {t}", + .{ state, state_expected.get(state), arg }, + ), } }, .input_file, .input_file_content, .output_file => { @@ -184,23 +239,31 @@ pub fn runServer(runner: *Runner) ProtocolError { .{ .nonblocking = false }, ) catch |err| switch (err) { error.Canceled => |e| return e, - error.Unexpected => |e| return runner.fail( + else => |e| return runner.fail( "unable to inherit parent file: {t}", .{e}, ), }; switch (state) { .positional => { - assert(runner.args.manifest_file == null); + if (runner.args.@"test" != null or + runner.args.manifest_file != null) return runner.fail( + "{t} specified multiple times", + .{state}, + ); runner.args.manifest_file = file; }, - .zig => { - assert(runner.args.zig_exe == null); + .@"--zig" => { + if (runner.args.zig_exe != null) return runner.fail( + "\"{t}\" specified multiple times", + .{state}, + ); runner.args.zig_exe = file; }, - .lib => unreachable, - .src => unreachable, - .target => unreachable, + else => return runner.fail( + "\"{t}\" expected {t}, got {t}", + .{ state, state_expected.get(state), arg }, + ), } }, } @@ -480,7 +543,6 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { .target_query = target_query, .manifest_sr = .init(&manifest_fr.interface, runner.gpa, &runner.eb_wip, src_path_string: { var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; - const src_path = src_path: { break :src_path path_buffer[0 .. manifest_file.realPath( runner.io, @@ -1022,6 +1084,7 @@ const Compiler = struct { const gpa = runner.gpa; const arena = runner.arena.allocator(); const io = runner.io; + const target = &comp.target.resolved; 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.*); @@ -1036,16 +1099,18 @@ const Compiler = struct { .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, - }, - )) { + var argv: std.ArrayList([]const u8) = .initBuffer(try arena.alloc([]const u8, 4)); + var environ_map: std.process.Environ.Map = .init(gpa); + defer environ_map.deinit(); + const need_cross_libc = + target.os.tag == .linux and config.flags.link_libc and config.flags.link_mode == .dynamic; + const use_executor = use_executor: switch (std.zig.system.getExternalExecutor(io, target, .{ + .host_cpu_arch = runner.host.cpu.arch, + .host_os_tag = runner.host.os.tag, + .qemu_fixes_dl = need_cross_libc and runner.args.libc_runtimes_dir != null, + .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. if (!runner.args.quiet) std.log.warn("skipping execution because host {q} cannot " ++ @@ -1054,24 +1119,57 @@ const Compiler = struct { }); return; }, - .native, .rosetta => null, - .qemu => |executor| if (runner.args.enable_qemu) - executor + .native => false, + .darling => |executor| if (runner.args.enable_darling) { + argv.appendAssumeCapacity(executor); + break :use_executor true; + } else continue :use_executor .bad_os_or_cpu, + .qemu => |executor| if (runner.args.enable_qemu) { + argv.appendAssumeCapacity(executor); + if (need_cross_libc) { + const libc_runtimes_dir = runner.args.libc_runtimes_dir orelse + continue :use_executor .bad_os_or_cpu; + var path_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; + const libc_runtimes_path = path_buffer[0 .. libc_runtimes_dir.realPath( + runner.io, + &path_buffer, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to get libc runtimes path: {t}", .{e}), + }]; + argv.appendSliceAssumeCapacity(&.{ "-L", try std.Io.Dir.path.join(arena, &.{ + libc_runtimes_path, + try if (target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( + arena, + target.cpu.arch, + target.os.tag, + target.abi, + ) else if (target.isMuslLibC()) std.zig.target.muslRuntimeTriple( + arena, + target.cpu.arch, + target.abi, + ) else unreachable, + }) }); + } + break :use_executor true; + } else continue :use_executor .bad_os_or_cpu, + .rosetta => if (runner.args.enable_rosetta) + true 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, + continue :use_executor .bad_os_or_cpu, + .wine => |executor| if (runner.args.enable_wine) { + if (!environ_map.contains("WINEDEBUG")) try environ_map.put("WINEDEBUG", "-all"); + argv.appendAssumeCapacity(executor); + break :use_executor true; + } else continue :use_executor .bad_os_or_cpu, + .wasmtime => |executor| if (runner.args.enable_wasmtime) { + if (!environ_map.contains("WASMTIME_BACKTRACE_DETAILS")) + try environ_map.put("WASMTIME_BACKTRACE_DETAILS", "1"); + argv.appendSliceAssumeCapacity(&.{ executor, "--dir=.", "-Sinherit-env" }); + break :use_executor true; + } else continue :use_executor .bad_os_or_cpu, }; + argv.appendAssumeCapacity(bin_path); 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}", .{ @@ -1088,11 +1186,12 @@ const Compiler = struct { .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}, + .exe = if (use_executor) .search else .{ .file = bin_file }, + .argv = argv.items, .cwd = .{ .dir = src_dir }, + .environ_map = &environ_map, .progress_node = run_prog_node, - }) catch |err| if (executor) |_| { + }) catch |err| if (use_executor) { // Chances are the foreign executor isn't available. Skip this evaluation. if (!runner.args.quiet) std.log.warn( "skipping execution of {q} via executor for foreign target {q}: {t}", @@ -1151,7 +1250,7 @@ const Compiler = struct { .{bin_path}, ), } - if (executor == null and result.stderr.len > 0) { + if (!use_executor and result.stderr.len > 0) { std.log.err("generated executable {q} had unexpected stderr:\n{s}", .{ bin_path, result.stderr, }); @@ -1257,10 +1356,12 @@ const ClientArgs = struct { src_dir: std.Io.Dir, targets: std.ArrayList([]const u8), keep_src: bool, + libc_runtimes_dir: ?std.Io.Dir, + enable_darling: bool, enable_qemu: bool, + enable_rosetta: bool, enable_wine: bool, enable_wasmtime: bool, - enable_darling: bool, quiet: bool, }; pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelable)!u8 { @@ -1275,6 +1376,7 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl var test_path_arg: ?[]const u8 = null; var zig_path_arg: ?[]const u8 = null; var lib_path_arg: ?[]const u8 = null; + var libc_runtimes_path_arg: ?[]const u8 = null; var args: ClientArgs = .{ .test_file = undefined, .test_kind = undefined, @@ -1283,10 +1385,12 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl .src_dir = undefined, .targets = .empty, .keep_src = false, + .libc_runtimes_dir = null, + .enable_darling = false, .enable_qemu = false, + .enable_rosetta = false, .enable_wine = false, .enable_wasmtime = false, - .enable_darling = false, .quiet = false, }; defer args.targets.deinit(init.gpa); @@ -1312,10 +1416,12 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl .src_dir = null, .targets = .empty, .target_bytes = .empty, + .libc_runtimes_dir = null, + .enable_darling = false, .enable_qemu = false, - .enable_wine = false, + .enable_rosetta = false, .enable_wasmtime = false, - .enable_darling = false, + .enable_wine = false, .quiet = false, }, .host = std.zig.system.resolveTargetQuery(init.io, .{}) catch |err| switch (err) { @@ -1361,18 +1467,24 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl try args.targets.append(init.gpa, try arena.dupe(u8, target)); } else if (std.mem.eql(u8, arg, "--keep-src")) { args.keep_src = true; + } else if (std.mem.eql(u8, arg, "--libc-runtimes")) { + libc_runtimes_path_arg = arg_it.next() orelse fatal("missing arg after {q}", .{arg}); + } else if (std.mem.cutPrefix(u8, arg, "--libc-runtimes=")) |libc_runtimes_path| { + libc_runtimes_path_arg = libc_runtimes_path; + } else if (std.mem.eql(u8, arg, "-fdarling")) { + args.enable_darling = true; } else if (std.mem.eql(u8, arg, "-fqemu")) { args.enable_qemu = true; + } else if (std.mem.eql(u8, arg, "-frosetta")) { + args.enable_rosetta = true; } else if (std.mem.eql(u8, arg, "-fwine")) { args.enable_wine = true; } else if (std.mem.eql(u8, arg, "-fwasmtime")) { args.enable_wasmtime = true; - } else if (std.mem.eql(u8, arg, "-fdarling")) { - args.enable_darling = true; } else if (std.mem.eql(u8, arg, "--quiet")) { args.quiet = true; } else { - if (test_path_arg) |_| fatal("unknown arg {q}", .{arg}); + if (test_path_arg) |_| fatal("unexpected arg {q}", .{arg}); test_path_arg = arg; } } @@ -1403,6 +1515,15 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl else => |e| fatal("unable to open {q}: {t}", .{ lib_path, e }), }; defer args.lib_dir.close(init.io); + if (libc_runtimes_path_arg) |libc_runtimes_path| args.libc_runtimes_dir = cwd.openDir( + init.io, + libc_runtimes_path, + .{}, + ) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| fatal("unable to open {q}: {t}", .{ lib_path, e }), + }; + defer if (args.libc_runtimes_dir) |libc_runtimes_dir| libc_runtimes_dir.close(init.io); const src_dir_path = "src_" ++ std.fmt.hex(rand_int: { var rand_int: u64 = undefined; @@ -1479,21 +1600,18 @@ fn runClient( try argv.append(gpa, @backingInt(Arg.prefix)); try argv.appendSlice(gpa, "--zig="); try argv.append(gpa, 0); - try argv.append(gpa, @backingInt(Arg.input_file)); try argv.appendSlice(gpa, @ptrCast(&args.zig_exe.handle)); try argv.append(gpa, @backingInt(Arg.prefix)); try argv.appendSlice(gpa, "--lib="); try argv.append(gpa, 0); - try argv.append(gpa, @backingInt(Arg.input_dir)); try argv.appendSlice(gpa, @ptrCast(&args.lib_dir.handle)); try argv.append(gpa, @backingInt(Arg.prefix)); try argv.appendSlice(gpa, "--src="); try argv.append(gpa, 0); - try argv.append(gpa, @backingInt(Arg.output_dir)); try argv.appendSlice(gpa, @ptrCast(&args.src_dir.handle)); @@ -1507,11 +1625,28 @@ fn runClient( try argv.append(gpa, 0); } + if (args.libc_runtimes_dir) |libc_runtimes_dir| { + try argv.append(gpa, @backingInt(Arg.prefix)); + try argv.appendSlice(gpa, "--libc-runtimes="); + try argv.append(gpa, 0); + try argv.append(gpa, @backingInt(Arg.input_dir)); + try argv.appendSlice(gpa, @ptrCast(&libc_runtimes_dir.handle)); + } + if (args.enable_darling) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-fdarling"); + try argv.append(gpa, 0); + } if (args.enable_qemu) { try argv.append(gpa, @backingInt(Arg.string)); try argv.appendSlice(gpa, "-fqemu"); try argv.append(gpa, 0); } + if (args.enable_rosetta) { + try argv.append(gpa, @backingInt(Arg.string)); + try argv.appendSlice(gpa, "-frosetta"); + try argv.append(gpa, 0); + } if (args.enable_wine) { try argv.append(gpa, @backingInt(Arg.string)); try argv.appendSlice(gpa, "-fwine"); @@ -1522,11 +1657,6 @@ fn runClient( try argv.appendSlice(gpa, "-fwasmtime"); try argv.append(gpa, 0); } - if (args.enable_darling) { - try argv.append(gpa, @backingInt(Arg.string)); - try argv.appendSlice(gpa, "-fdarling"); - try argv.append(gpa, 0); - } if (args.quiet) { try argv.append(gpa, @backingInt(Arg.string)); diff --git a/test/tests.zig b/test/tests.zig index 3da8ef96442b31b91217fadc6334d5102dcaa76d..307a061ac885678ee2868821135896409e01e817 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3411,6 +3411,7 @@ pub fn addIncrementalTests( run.addArgs(&.{ "--target", target_str }); } + run.addPrefixedDirectoryArg("--libc-runtimes=", .{ .relative = .{ .base = .libc_runtimes } }); run.addThirdPartyEnabledArgDarling(.{ .enabled = "-fdarling" }); run.addThirdPartyEnabledArgQemu(.{ .enabled = "-fqemu" }); run.addThirdPartyEnabledArgRosetta(.{ .enabled = "-frosetta" }); -- 2.54.0 From 2cdf284af96ea016a5553ae9f6516b9b2c8f3f8e Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 17:40:22 -0400 Subject: [PATCH 16/23] Threaded: implement new process spawning API for windows --- lib/std/Io/Threaded.zig | 297 ++++++++++++++++++++----------- lib/std/fs/path.zig | 64 +++---- lib/std/heap/debug_allocator.zig | 8 +- lib/std/os/windows.zig | 14 ++ lib/std/os/windows/ntdll.zig | 7 + 5 files changed, 249 insertions(+), 141 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 30b924f59d835a85aeb2a1420d504a4a7d443b01..82f26e74930a87132f70e681982a91dfacde139f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -85,6 +85,8 @@ csprng: Csprng = .uninitialized, system_basic_information: SystemBasicInformation = .{}, +process_spawn_mutex: if (is_windows) Io.Mutex else void = if (is_windows) .init else {}, + const SystemBasicInformation = if (!is_windows) struct {} else struct { buffer: windows.SYSTEM.BASIC_INFORMATION = undefined, initialized: std.atomic.Value(bool) = .{ .raw = false }, @@ -1706,7 +1708,7 @@ var global_single_threaded_instance: Threaded = .init_single_threaded; pub const global_single_threaded: *Threaded = &global_single_threaded_instance; pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void { - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); t.async_limit = new_limit; } @@ -1727,7 +1729,7 @@ pub fn deinit(t: *Threaded) void { fn join(t: *Threaded) void { if (builtin.single_threaded) return; { - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); t.join_requested = true; } @@ -1789,7 +1791,7 @@ fn worker(t: *Threaded) void { defer t.wait_group.finish(); - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); while (true) { @@ -1798,7 +1800,7 @@ fn worker(t: *Threaded) void { thread.cancel_protection = .unblocked; const runnable: *Runnable = @fieldParentPtr("node", runnable_node); runnable.startFn(runnable, &thread, t); - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); t.busy_count -= 1; } if (t.join_requested) break; @@ -2088,7 +2090,7 @@ fn async( }, }; - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); const busy_count = t.busy_count; @@ -2140,7 +2142,7 @@ fn concurrent( }; errdefer future.destroy(gpa); - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); const busy_count = t.busy_count; @@ -2185,7 +2187,7 @@ fn groupAsync( error.OutOfMemory => return groupAsyncEager(start, context.ptr), }; - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); const busy_count = t.busy_count; @@ -2248,7 +2250,7 @@ fn groupConcurrent( }; errdefer task.destroy(gpa); - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); const busy_count = t.busy_count; @@ -4118,7 +4120,7 @@ fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { fn systemBasicInformation(t: *Threaded) ?*const windows.SYSTEM.BASIC_INFORMATION { if (!t.system_basic_information.initialized.load(.acquire)) { - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); switch (windows.ntdll.NtQuerySystemInformation( @@ -10011,7 +10013,7 @@ fn ntReadFileResult(io_status_block: *const windows.IO_STATUS_BLOCK) !usize { .PENDING => unreachable, .CANCELLED => unreachable, .SUCCESS => return io_status_block.Information, - .END_OF_FILE, .PIPE_BROKEN => return error.EndOfStream, + .END_OF_FILE, .PIPE_BROKEN, .PIPE_CLOSING => return error.EndOfStream, .INVALID_HANDLE => return error.NotOpenForReading, .INVALID_DEVICE_REQUEST => return error.IsDir, .FILE_LOCK_CONFLICT => return error.LockViolation, @@ -10028,7 +10030,7 @@ fn ntWriteFileResult(io_status_block: *const windows.IO_STATUS_BLOCK) !usize { .INVALID_USER_BUFFER => return error.SystemResources, .NO_MEMORY => return error.SystemResources, .QUOTA_EXCEEDED => return error.SystemResources, - .PIPE_BROKEN => return error.BrokenPipe, + .PIPE_BROKEN, .PIPE_CLOSING => return error.BrokenPipe, .INVALID_HANDLE => return error.NotOpenForWriting, .FILE_LOCK_CONFLICT => return error.LockViolation, .ACCESS_DENIED => return error.AccessDenied, @@ -14091,7 +14093,7 @@ fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Io.Terminal.Mode) Io.Cancel const current_thread_id = Thread.currentId(); if (@atomicLoad(std.Thread.Id, &t.stderr_mutex_locker, .unordered) != current_thread_id) { - mutexLock(&t.stderr_mutex); + try mutexLock(&t.stderr_mutex); assert(t.stderr_mutex_lock_count == 0); @atomicStore(std.Thread.Id, &t.stderr_mutex_locker, current_thread_id, .unordered); } @@ -15117,7 +15119,7 @@ const WindowsEnvironStrings = struct { }; fn scanEnviron(t: *Threaded) void { - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); if (t.environ_initialized) return; t.environ.scan(t.allocator); @@ -15416,7 +15418,7 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp fn getDevNullFd(t: *Threaded) !posix.fd_t { { - mutexLock(&t.mutex); + try mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); if (t.null_file.fd != -1) return t.null_file.fd; } @@ -15428,7 +15430,7 @@ fn getDevNullFd(t: *Threaded) !posix.fd_t { .SUCCESS => { syscall.finish(); const fresh_fd: posix.fd_t = @intCast(rc); - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.null_file.fd != -1) { closeFd(fresh_fd); @@ -15856,6 +15858,84 @@ fn processSpawnDarwin(userdata: ?*anyopaque, options: process.SpawnOptions) proc fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child { const t: *Threaded = @ptrCast(@alignCast(userdata)); + var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + const cwd_w = cwd_w: { + switch (options.cwd) { + .inherit => break :cwd_w null, + .dir => |cwd_dir| { + var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1); + const dir_path = try GetFinalPathNameByHandle( + cwd_dir.handle, + .{}, + dir_path_buffer[0..windows.PATH_MAX_WIDE], + ); + dir_path_buffer[dir_path.len] = 0; + // Shrink the allocation down to just the path buffer + sentinel + dir_path_buffer = try arena.realloc(dir_path_buffer, dir_path.len + 1); + break :cwd_w dir_path_buffer[0..dir_path.len :0]; + }, + .path => |cwd| { + break :cwd_w try std.unicode.wtf8ToWtf16LeAllocZ(arena, cwd); + }, + } + }; + const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; + + // If the app name has more than just a filename, then we need to separate + // that into the basename and dirname and use the dirname as an addition to + // the cwd path. This is because NtQueryDirectoryFile cannot accept + // FileName params with path separators. + var app_w_buf: [windows.PATH_MAX_WIDE]u16 = undefined; + // If the app name is absolute, then the cwd will already have the app's dirname in it, + // so only populate app_dir if app name is a relative path with > 0 path separators. + const app_path_w, const app_dir_w, const app_name_w = app: { + const path = switch (options.exe) { + .detect, .search, .path => options.argv[0], + .file => |file| { + const path_w = try GetFinalPathNameByHandle(file.handle, .{}, &app_w_buf); + var path_it: Dir.path.ComponentIterator(.windows, u16) = .init(path_w); + const base = path_it.last().?; + break :app .{ + null, + if (path_it.previous()) |dir| dir.path else path_it.root(), + base.name, + }; + }, + .explicit => |explicit| explicit.path, + }; + break :app .{ + if (Dir.path.isAbsolute(path)) null else switch (options.exe) { + // The cwd provided by options is in effect when choosing the executable + // path to match POSIX semantics. + .detect, .search => cwd_w, + .path => |dir| try GetFinalPathNameByHandle(dir.handle, .{}, &app_w_buf), + .file => unreachable, + .explicit => |explicit| try GetFinalPathNameByHandle( + explicit.dir.handle, + .{}, + &app_w_buf, + ), + }, + if (Dir.path.dirname(path)) |app_dir| + try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_dir) + else + null, + try std.unicode.wtf8ToWtf16LeAllocZ(arena, Dir.path.basename(path)), + }; + }; + + const flags: windows.CreateProcessFlags = .{ + .create_suspended = options.start_suspended, + .create_unicode_environment = true, + .create_no_window = options.create_no_window, + }; + + try mutexLock(&t.process_spawn_mutex); + defer mutexUnlock(&t.process_spawn_mutex); + const any_ignore = options.stdin == .ignore or options.stdout == .ignore or @@ -15897,6 +15977,43 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }) else undefined; errdefer if (options.progress_node.index != .none) for (prog_pipe) |handle| windows.CloseHandle(handle); + defer for (options.inherit_dirs) |inherit_dir| switch (windows.ntdll.NtSetInformationObject( + inherit_dir.handle, + .HandleFlag, + &windows.OBJECT.HANDLE_FLAG{ .INHERIT = false }, + @sizeOf(windows.OBJECT.HANDLE_FLAG), + )) { + .SUCCESS => {}, + else => |status| windows.unexpectedStatus(status) catch {}, + }; + for (options.inherit_dirs) |inherit_dir| switch (windows.ntdll.NtSetInformationObject( + inherit_dir.handle, + .HandleFlag, + &windows.OBJECT.HANDLE_FLAG{ .INHERIT = true }, + @sizeOf(windows.OBJECT.HANDLE_FLAG), + )) { + .SUCCESS => {}, + else => |status| return windows.unexpectedStatus(status), + }; + defer for (options.inherit_files) |inherit_file| switch (windows.ntdll.NtSetInformationObject( + inherit_file.handle, + .HandleFlag, + &windows.OBJECT.HANDLE_FLAG{ .INHERIT = false }, + @sizeOf(windows.OBJECT.HANDLE_FLAG), + )) { + .SUCCESS => {}, + else => |status| windows.unexpectedStatus(status) catch {}, + }; + for (options.inherit_files) |inherit_file| switch (windows.ntdll.NtSetInformationObject( + inherit_file.handle, + .HandleFlag, + &windows.OBJECT.HANDLE_FLAG{ .INHERIT = true }, + @sizeOf(windows.OBJECT.HANDLE_FLAG), + )) { + .SUCCESS => {}, + else => |status| return windows.unexpectedStatus(status), + }; + var siStartInfo: windows.STARTUPINFOW = .{ .cb = @sizeOf(windows.STARTUPINFOW), .dwFlags = windows.STARTF_USESTDHANDLES, @@ -15974,32 +16091,6 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }; var piProcInfo: windows.PROCESS.INFORMATION = undefined; - var arena_allocator = std.heap.ArenaAllocator.init(t.allocator); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - - const cwd_w = cwd_w: { - switch (options.cwd) { - .inherit => break :cwd_w null, - .dir => |cwd_dir| { - var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1); - const dir_path = try GetFinalPathNameByHandle( - cwd_dir.handle, - .{}, - dir_path_buffer[0..windows.PATH_MAX_WIDE], - ); - dir_path_buffer[dir_path.len] = 0; - // Shrink the allocation down to just the path buffer + sentinel - dir_path_buffer = try arena.realloc(dir_path_buffer, dir_path.len + 1); - break :cwd_w dir_path_buffer[0..dir_path.len :0]; - }, - .path => |cwd| { - break :cwd_w try std.unicode.wtf8ToWtf16LeAllocZ(arena, cwd); - }, - } - }; - const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; - const env_block = env_block: { const prog_handle = if (options.progress_node.index != .none) prog_pipe[1] @@ -16013,45 +16104,6 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }); }; - const app_name_wtf8 = options.argv[0]; - const app_name_is_absolute = Dir.path.isAbsolute(app_name_wtf8); - - // The cwd provided by options is in effect when choosing the executable - // path to match POSIX semantics. - const cwd_path_w = x: { - // If the app name is absolute, then we need to use its dirname as the cwd - if (app_name_is_absolute) { - const dir = Dir.path.dirname(app_name_wtf8).?; - break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, dir); - } else if (cwd_w) |cwd| { - break :x cwd; - } else { - break :x &[_:0]u16{}; // empty for cwd - } - }; - - // If the app name has more than just a filename, then we need to separate - // that into the basename and dirname and use the dirname as an addition to - // the cwd path. This is because NtQueryDirectoryFile cannot accept - // FileName params with path separators. - const app_basename_wtf8 = Dir.path.basename(app_name_wtf8); - // If the app name is absolute, then the cwd will already have the app's dirname in it, - // so only populate app_dirname if app name is a relative path with > 0 path separators. - const maybe_app_dirname_wtf8 = if (!app_name_is_absolute) Dir.path.dirname(app_name_wtf8) else null; - const app_dirname_w: ?[:0]u16 = x: { - if (maybe_app_dirname_wtf8) |app_dirname_wtf8| { - break :x try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_dirname_wtf8); - } - break :x null; - }; - const app_name_w = try std.unicode.wtf8ToWtf16LeAllocZ(arena, app_basename_wtf8); - - const flags: windows.CreateProcessFlags = .{ - .create_suspended = options.start_suspended, - .create_unicode_environment = true, - .create_no_window = options.create_no_window, - }; - run: { // We have to scan each time because the PEB environment pointer is not stable. const env_strings: WindowsEnvironStrings = .scan(); @@ -16072,10 +16124,8 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro var dir_buf: std.ArrayList(u16) = .empty; - if (cwd_path_w.len > 0) { - try dir_buf.appendSlice(arena, cwd_path_w); - } - if (app_dirname_w) |app_dir| { + if (app_path_w) |app_path| try dir_buf.appendSlice(arena, app_path); + if (app_dir_w) |app_dir| { if (dir_buf.items.len > 0) try dir_buf.append(arena, Dir.path.sep); try dir_buf.appendSlice(arena, app_dir); } @@ -16100,15 +16150,18 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro else => |e| return e, }; - // If the app name had path separators, that disallows PATH searching, - // and there's no need to search the PATH if the app name is absolute. // We still search the path if the cwd is absolute because of the // "cwd provided by options is in effect when choosing the executable path // to match posix semantics" behavior--we don't want to skip searching // the PATH just because we were trying to set the cwd of the child process. - if (app_dirname_w != null or app_name_is_absolute) { - return original_err; - } + if (switch (options.exe) { + // If the app name had path separators, that disallows PATH searching. + .detect => app_dir_w != null, + // There's no need to search the PATH if the app name is absolute. + .search => app_path_w == null, + // PATH searching is disallowed. + .path, .file, .explicit => true, + }) return original_err; var it = std.mem.tokenizeScalar(u16, PATH, ';'); while (it.next()) |search_path| { @@ -16186,11 +16239,9 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro }; } -fn inheritFile() windows.HANDLE {} - fn getCngDevice(t: *Threaded) Io.RandomSecureError!windows.HANDLE { { - mutexLock(&t.mutex); + try mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); if (t.random_file.handle) |handle| return handle; } @@ -16213,7 +16264,7 @@ fn getCngDevice(t: *Threaded) Io.RandomSecureError!windows.HANDLE { )) { .SUCCESS => { syscall.finish(); - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.random_file.handle) |prev_handle| { windows.CloseHandle(fresh_handle); @@ -16234,7 +16285,7 @@ fn getCngDevice(t: *Threaded) Io.RandomSecureError!windows.HANDLE { fn getNulDevice(t: *Threaded) !windows.HANDLE { { - mutexLock(&t.mutex); + try mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); if (t.null_file.handle) |handle| return handle; } @@ -16260,7 +16311,7 @@ fn getNulDevice(t: *Threaded) !windows.HANDLE { )) { .SUCCESS => { syscall.finish(); - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.null_file.handle) |prev_handle| { windows.CloseHandle(fresh_handle); @@ -16293,7 +16344,7 @@ fn getNulDevice(t: *Threaded) !windows.HANDLE { fn getNamedPipeDevice(t: *Threaded) !windows.HANDLE { { - mutexLock(&t.mutex); + try mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); if (t.pipe_file.handle) |handle| return handle; } @@ -16315,7 +16366,7 @@ fn getNamedPipeDevice(t: *Threaded) !windows.HANDLE { )) { .SUCCESS => { syscall.finish(); - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.pipe_file.handle) |prev_handle| { windows.CloseHandle(fresh_handle); @@ -16600,7 +16651,15 @@ fn windowsCreateProcessPathExt( else full_app_name; - if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, env_block, cwd_ptr, flags, lpStartupInfo, lpProcessInformation)) |_| { + if (windowsCreateProcess( + app_name_w.ptr, + cmd_line_w.ptr, + env_block, + cwd_ptr, + flags, + lpStartupInfo, + lpProcessInformation, + )) |_| { return; } else |err| switch (err) { error.FileNotFound => continue, @@ -17716,6 +17775,15 @@ fn inheritParentHandle(handle: posix.fd_t) Io.InheritParentHandleError!void { .visionos, .watchos, => return setCloexec(handle), + .windows => switch (windows.ntdll.NtSetInformationObject( + handle, + .HandleFlag, + &windows.OBJECT.HANDLE_FLAG{ .INHERIT = false }, + @sizeOf(windows.OBJECT.HANDLE_FLAG), + )) { + .SUCCESS => return, + else => |status| return windows.unexpectedStatus(status), + }, else => return error.UnsupportedOperation, } } @@ -17756,7 +17824,7 @@ fn random(userdata: ?*anyopaque, buffer: []u8) void { } fn randomMainThread(t: *Threaded, buffer: []u8) void { - mutexLock(&t.mutex); + mutexLockUncancelable(&t.mutex); defer mutexUnlock(&t.mutex); if (!t.csprng.isInitialized()) { @@ -17764,7 +17832,7 @@ fn randomMainThread(t: *Threaded, buffer: []u8) void { var seed: [Csprng.seed_len]u8 = undefined; { mutexUnlock(&t.mutex); - defer mutexLock(&t.mutex); + defer mutexLockUncancelable(&t.mutex); const prev = swapCancelProtection(t, .blocked); defer _ = swapCancelProtection(t, prev); @@ -17951,7 +18019,7 @@ fn randomSecure(userdata: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void { fn getRandomFd(t: *Threaded) Io.RandomSecureError!posix.fd_t { { - mutexLock(&t.mutex); + try mutexLock(&t.mutex); defer mutexUnlock(&t.mutex); if (t.random_file.fd == -2) return error.EntropyUnavailable; @@ -17992,7 +18060,7 @@ fn getRandomFd(t: *Threaded) Io.RandomSecureError!posix.fd_t { .SUCCESS => { syscall.finish(); if (!statx.mask.TYPE) return error.EntropyUnavailable; - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.random_file.fd >= 0) { closeFd(fd); @@ -18020,7 +18088,7 @@ fn getRandomFd(t: *Threaded) Io.RandomSecureError!posix.fd_t { switch (posix.errno(fstat_sym(fd, &stat))) { .SUCCESS => { syscall.finish(); - mutexLock(&t.mutex); // Another thread might have won the race. + mutexLockUncancelable(&t.mutex); // Another thread might have won the race. defer mutexUnlock(&t.mutex); if (t.random_file.fd >= 0) { closeFd(fd); @@ -19576,7 +19644,7 @@ fn condWait(cond: *Io.Condition, mutex: *Io.Mutex) void { } mutexUnlock(mutex); - defer mutexLock(mutex); + defer mutexLockUncancelable(mutex); while (true) { Thread.futexWaitUncancelable(&cond.epoch.raw, epoch, null); @@ -19596,8 +19664,27 @@ fn condWait(cond: *Io.Condition, mutex: *Io.Mutex) void { } } +/// Same as `Io.Mutex.lock` but avoids the VTable. +pub fn mutexLock(m: *Io.Mutex) Io.Cancelable!void { + const initial_state = m.state.cmpxchgStrong( + .unlocked, + .locked_once, + .acquire, + .monotonic, + ) orelse { + @branchHint(.likely); + return; + }; + if (initial_state == .contended) { + try Thread.futexWait(@ptrCast(&m.state.raw), @backingInt(Io.Mutex.State.contended), null); + } + while (m.state.swap(.contended, .acquire) != .unlocked) { + try Thread.futexWait(@ptrCast(&m.state.raw), @backingInt(Io.Mutex.State.contended), null); + } +} + /// Same as `Io.Mutex.lockUncancelable` but avoids the VTable. -pub fn mutexLock(m: *Io.Mutex) void { +pub fn mutexLockUncancelable(m: *Io.Mutex) void { const initial_state = m.state.cmpxchgStrong( .unlocked, .locked_once, diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index 01d6f0236c1c2b9ade0034cf6d0f35435dd19f73..58813c48753f92b63b4571161ae172c0f525e70b 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -1438,7 +1438,7 @@ pub fn dirnamePosix(path: []const u8) ?[]const u8 { } fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 { - var it = ComponentIterator(path_type, u8).init(path); + var it: ComponentIterator(path_type, u8) = .init(path); _ = it.last() orelse return null; const up = it.previous() orelse return it.root(); return up.path; @@ -1539,7 +1539,7 @@ pub fn basenameWindows(path: []const u8) []const u8 { } fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 { - var it = ComponentIterator(path_type, u8).init(path); + var it: ComponentIterator(path_type, u8) = .init(path); const last = it.last() orelse return &[_]u8{}; return last.name; } @@ -2338,14 +2338,14 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) { }, u8); pub fn componentIterator(path: []const u8) NativeComponentIterator { - return NativeComponentIterator.init(path); + return .init(path); } test "ComponentIterator posix" { const PosixComponentIterator = ComponentIterator(.posix, u8); { const path = "a/b/c/"; - var it = PosixComponentIterator.init(path); + var it: PosixComponentIterator = .init(path); try std.testing.expectEqual(0, it.root_len); try std.testing.expectEqual(0, it.root_end_index); try std.testing.expect(null == it.root()); @@ -2393,7 +2393,7 @@ test "ComponentIterator posix" { { const path = "/a/b/c/"; - var it = PosixComponentIterator.init(path); + var it: PosixComponentIterator = .init(path); try std.testing.expectEqual(1, it.root_len); try std.testing.expectEqual(1, it.root_end_index); try std.testing.expectEqualStrings("/", it.root().?); @@ -2441,7 +2441,7 @@ test "ComponentIterator posix" { { const path = "////a///b///c////"; - var it = PosixComponentIterator.init(path); + var it: PosixComponentIterator = .init(path); try std.testing.expectEqual(1, it.root_len); try std.testing.expectEqual(4, it.root_end_index); try std.testing.expectEqualStrings("/", it.root().?); @@ -2489,7 +2489,7 @@ test "ComponentIterator posix" { { const path = "/"; - var it = PosixComponentIterator.init(path); + var it: PosixComponentIterator = .init(path); try std.testing.expectEqual(1, it.root_len); try std.testing.expectEqual(1, it.root_end_index); try std.testing.expectEqualStrings("/", it.root().?); @@ -2507,7 +2507,7 @@ test "ComponentIterator posix" { { const path = ""; - var it = PosixComponentIterator.init(path); + var it: PosixComponentIterator = .init(path); try std.testing.expectEqual(0, it.root_len); try std.testing.expectEqual(0, it.root_end_index); try std.testing.expect(null == it.root()); @@ -2528,7 +2528,7 @@ test "ComponentIterator windows" { const WindowsComponentIterator = ComponentIterator(.windows, u8); { const path = "a/b\\c//"; - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(0, it.root_len); try std.testing.expectEqual(0, it.root_end_index); try std.testing.expect(null == it.root()); @@ -2576,7 +2576,7 @@ test "ComponentIterator windows" { { const path = "C:\\a/b/c/"; - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(3, it.root_len); try std.testing.expectEqual(3, it.root_end_index); try std.testing.expectEqualStrings("C:\\", it.root().?); @@ -2614,7 +2614,7 @@ test "ComponentIterator windows" { { const path = "C:\\\\//a/\\/\\b///c////"; - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(3, it.root_len); try std.testing.expectEqual(6, it.root_end_index); try std.testing.expectEqualStrings("C:\\", it.root().?); @@ -2652,7 +2652,7 @@ test "ComponentIterator windows" { { const path = "/"; - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(1, it.root_len); try std.testing.expectEqual(1, it.root_end_index); try std.testing.expectEqualStrings("/", it.root().?); @@ -2670,7 +2670,7 @@ test "ComponentIterator windows" { { const path = ""; - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(0, it.root_len); try std.testing.expectEqual(0, it.root_end_index); try std.testing.expect(null == it.root()); @@ -2692,7 +2692,7 @@ test "ComponentIterator windows WTF-16" { const L = std.unicode.utf8ToUtf16LeStringLiteral; const path = L("C:\\a/b/c/"); - var it = WindowsComponentIterator.init(path); + var it: WindowsComponentIterator = .init(path); try std.testing.expectEqual(3, it.root_len); try std.testing.expectEqual(3, it.root_end_index); try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?); @@ -2731,59 +2731,59 @@ test "ComponentIterator windows WTF-16" { test "ComponentIterator roots" { // UEFI { - var it = ComponentIterator(.uefi, u8).init("\\\\a"); + var it: ComponentIterator(.uefi, u8) = .init("\\\\a"); try std.testing.expectEqualStrings("\\", it.root().?); - it = ComponentIterator(.uefi, u8).init("//a"); + it = .init("//a"); try std.testing.expect(null == it.root()); } // POSIX { - var it = ComponentIterator(.posix, u8).init("//a"); + var it: ComponentIterator(.posix, u8) = .init("//a"); try std.testing.expectEqualStrings("/", it.root().?); - it = ComponentIterator(.posix, u8).init("\\\\a"); + it = .init("\\\\a"); try std.testing.expect(null == it.root()); } // Windows { // Drive relative - var it = ComponentIterator(.windows, u8).init("C:a"); + var it: ComponentIterator(.windows, u8) = .init("C:a"); try std.testing.expectEqualStrings("C:", it.root().?); // Drive absolute - it = ComponentIterator(.windows, u8).init("C:/a"); + it = .init("C:/a"); try std.testing.expectEqualStrings("C:/", it.root().?); - it = ComponentIterator(.windows, u8).init("C:\\a"); + it = .init("C:\\a"); try std.testing.expectEqualStrings("C:\\", it.root().?); - it = ComponentIterator(.windows, u8).init("C:///a"); + it = .init("C:///a"); try std.testing.expectEqualStrings("C:/", it.root().?); // Rooted - it = ComponentIterator(.windows, u8).init("\\a"); + it = .init("\\a"); try std.testing.expectEqualStrings("\\", it.root().?); - it = ComponentIterator(.windows, u8).init("/a"); + it = .init("/a"); try std.testing.expectEqualStrings("/", it.root().?); // Root local device - it = ComponentIterator(.windows, u8).init("\\\\."); + it = .init("\\\\."); try std.testing.expectEqualStrings("\\\\.", it.root().?); - it = ComponentIterator(.windows, u8).init("//?"); + it = .init("//?"); try std.testing.expectEqualStrings("//?", it.root().?); // UNC absolute - it = ComponentIterator(.windows, u8).init("//"); + it = .init("//"); try std.testing.expectEqualStrings("//", it.root().?); - it = ComponentIterator(.windows, u8).init("\\\\a"); + it = .init("\\\\a"); try std.testing.expectEqualStrings("\\\\a", it.root().?); - it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c"); + it = .init("\\\\a\\b\\\\c"); try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?); - it = ComponentIterator(.windows, u8).init("//a"); + it = .init("//a"); try std.testing.expectEqualStrings("//a", it.root().?); - it = ComponentIterator(.windows, u8).init("//a/b//c"); + it = .init("//a/b//c"); try std.testing.expectEqualStrings("//a/b/", it.root().?); // Malformed UNC path with empty server name - it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c"); + it = .init("\\\\\\a\\b\\c"); try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?); } } diff --git a/lib/std/heap/debug_allocator.zig b/lib/std/heap/debug_allocator.zig index 5d410fe92647d389304da9655537ae86911981ea..ccba352bb55e0e12bfd596d3a1f49458407a1c98 100644 --- a/lib/std/heap/debug_allocator.zig +++ b/lib/std/heap/debug_allocator.zig @@ -718,7 +718,7 @@ pub fn DebugAllocator(comptime config: Config) type { fn alloc(context: *anyopaque, len: usize, alignment: mem.Alignment, ret_addr: usize) ?[*]u8 { const self: *Self = @ptrCast(@alignCast(context)); - if (have_mutex) std.Io.Threaded.mutexLock(&self.mutex); + if (have_mutex) std.Io.Threaded.mutexLockUncancelable(&self.mutex); defer if (have_mutex) std.Io.Threaded.mutexUnlock(&self.mutex); if (config.enable_memory_limit) { @@ -831,7 +831,7 @@ pub fn DebugAllocator(comptime config: Config) type { return_address: usize, ) bool { const self: *Self = @ptrCast(@alignCast(context)); - if (have_mutex) std.Io.Threaded.mutexLock(&self.mutex); + if (have_mutex) std.Io.Threaded.mutexLockUncancelable(&self.mutex); defer if (have_mutex) std.Io.Threaded.mutexUnlock(&self.mutex); const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(memory.len - 1), @backingInt(alignment)); @@ -850,7 +850,7 @@ pub fn DebugAllocator(comptime config: Config) type { return_address: usize, ) ?[*]u8 { const self: *Self = @ptrCast(@alignCast(context)); - if (have_mutex) std.Io.Threaded.mutexLock(&self.mutex); + if (have_mutex) std.Io.Threaded.mutexLockUncancelable(&self.mutex); defer if (have_mutex) std.Io.Threaded.mutexUnlock(&self.mutex); const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(memory.len - 1), @backingInt(alignment)); @@ -868,7 +868,7 @@ pub fn DebugAllocator(comptime config: Config) type { return_address: usize, ) void { const self: *Self = @ptrCast(@alignCast(context)); - if (have_mutex) std.Io.Threaded.mutexLock(&self.mutex); + if (have_mutex) std.Io.Threaded.mutexLockUncancelable(&self.mutex); defer if (have_mutex) std.Io.Threaded.mutexUnlock(&self.mutex); const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(old_memory.len - 1), @backingInt(alignment)); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 0f6eaae0122b2dc59d2dcc1d0cd40a262686938b..5476c17147677692bbe2422a99f21456f11c88a1 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -147,9 +147,23 @@ pub const OBJECT = struct { pub const Max: @typeInfo(@This()).@"enum".tag_type = @typeInfo(@This()).@"enum".field_names.len; }; + pub const BASIC_INFORMATION = extern struct { + Attributes: ATTRIBUTES.Flags, + GrantedAccess: ACCESS_MASK, + HandleCount: ULONG, + PointerCount: ULONG, + Reserved: [10]ULONG, + }; + pub const NAME_INFORMATION = extern struct { Name: UNICODE_STRING, }; + + pub const HANDLE_FLAG = packed struct(USHORT) { + INHERIT: bool = false, + PROTECT_FROM_CLOSE: bool = false, + Reserved1: u14 = 0, + }; }; pub const FILE = struct { diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 1f6151308ce389b7eed1686892a01cb5b668a69d..16b3f57f21a7f55f2ddb3188d180673022e7bee7 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -461,6 +461,13 @@ pub extern "ntdll" fn LdrUnregisterDllNotification( Cookie: LDR.DLL_NOTIFICATION.COOKIE, ) callconv(.winapi) NTSTATUS; +pub extern "ntdll" fn NtSetInformationObject( + Handle: HANDLE, + ObjectInformationClass: OBJECT.INFORMATION_CLASS, + ObjectInformation: *const anyopaque, + ObjectInformationLength: ULONG, +) callconv(.winapi) NTSTATUS; + pub extern "ntdll" fn NtQueryAttributesFile( ObjectAttributes: *const OBJECT.ATTRIBUTES, FileAttributes: *FILE.BASIC_INFORMATION, -- 2.54.0 From e0beb0222312a5c4737ccfe76e75e414f594bd9a Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 19:56:44 -0400 Subject: [PATCH 17/23] Runner: fix crashes caused by noop mutations of read-only data `std.Io.Reader.ending` is pretty much unusable, so just make my own on the stack. --- test/src/Runner.zig | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/test/src/Runner.zig b/test/src/Runner.zig index 22f1962f0d6739e0a8b3ca09cef45bd610455cef..0cd77d82842f48aa12c38efea80589b524206473 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -598,10 +598,10 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { defer if (contents_file) |file| file.close(runner.io); var contents_buffer: [512]u8 = undefined; var contents_impl: union(enum) { - ending, + ending: std.Io.Reader, dr: DelimitedReader, fr: std.Io.File.Reader, - } = .ending; + } = undefined; const contents_r: *std.Io.Reader = contents_r: switch (cmd) { .skip, .delete => { const unexpected = line_it.rest(); @@ -610,11 +610,15 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { "unexpected argument {q}", .{unexpected}, ); - break :contents_r .ending; + contents_impl = .{ .ending = .ending_instance }; + break :contents_r &contents_impl.ending; }, else => { const contents_path = line_it.rest(); - if (contents_path.len == 0) break :contents_r .ending; + if (contents_path.len == 0) { + contents_impl = .{ .ending = .ending_instance }; + break :contents_r &contents_impl.ending; + } if (std.mem.eql(u8, contents_path, "{")) { contents_impl = .{ @@ -743,8 +747,27 @@ fn handleCommand( else => |e| return update.manifest_sr.fail(arg.?, "unable to create file: {t}", .{e}), }; defer file.close(runner.io); - var fw = file.writer(runner.io, &.{}); - _ = try contents_r.streamRemaining(&fw.interface); + var fw_buffer: [512]u8 = undefined; + var fw = file.writer(runner.io, &fw_buffer); + _ = contents_r.streamRemaining(&fw.interface) catch |err| switch (err) { + error.ReadFailed => |e| return e, + error.WriteFailed => switch (fw.err.?) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail( + arg.?, + "unable to write file: {t}", + .{e}, + ), + }, + }; + fw.flush() catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| return update.manifest_sr.fail( + arg.?, + "unable to write file: {t}", + .{e}, + ), + }; file.setTimestamps(runner.io, .{ .modify_timestamp = .{ .new = update.mtime, } }) catch |err| switch (err) { -- 2.54.0 From aa3dd5d5d9a08191a0ebb3af639f35402d58ee87 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 20:21:07 -0400 Subject: [PATCH 18/23] Maker.Step.Run: fix resource deallocation order on windows The inherited file descriptors need to be closed before attempting to rename the output directory. --- lib/compiler/Maker/Step/Run.zig | 1124 ++++++++++++++++--------------- 1 file changed, 565 insertions(+), 559 deletions(-) diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig index 8ce0a66837e0ed9fb9cf27e39a170bf7845a9204..a1fbb989aee08ef463efa44cd8511ec0337a79f4 100644 --- a/lib/compiler/Maker/Step/Run.zig +++ b/lib/compiler/Maker/Step/Run.zig @@ -55,531 +55,564 @@ pub fn make( var man = graph.cache.obtain(); defer man.deinit(); - if (conf_run.environ_map.value) |environ_map_index| { - const environ_map = environ_map_index.get(conf); - for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| { - man.hash.addBytesZ(key.slice(conf)); - man.hash.addBytesZ(value.slice(conf)); + var tmp_dir_path: ["tmp".len + Dir.path.sep_str.len + std.fmt.hex(@as(u64, 0)).len]u8 = undefined; + + const has_side_effects = has_side_effects: { + if (conf_run.environ_map.value) |environ_map_index| { + const environ_map = environ_map_index.get(conf); + for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| { + man.hash.addBytesZ(key.slice(conf)); + man.hash.addBytesZ(value.slice(conf)); + } } - } - for (conf_run.preopens.slice) |preopen| { - man.hash.addBytesZ(preopen.name.slice(conf)); - const cwd_path = try maker.resolveLazyPathIndex(arena, preopen.path, run_index); - man.hash.addBytes(try cwd_path.toString(arena)); - } - - man.hash.add(graph.fuzzing); - man.hash.add(conf_run.flags.color); - man.hash.add(conf_run.flags.disable_zig_progress); - - var any_discovered_inputs = switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .check, .zig_test => false, - .protocol => true, - }; - var any_output_args = false; - var any_cli_positionals = false; - - for (switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .check, .zig_test => conf_run.args.slice, - .protocol => conf_run.args.slice[0..1], // arguments communicated over protocol - }) |arg_index| { - const arg = arg_index.get(conf); - try argv_list.ensureUnusedCapacity(gpa, 1); - switch (arg.flags.tag) { - .string => { - const string = arg.prefix.value.?.slice(conf); - argv_list.appendAssumeCapacity(string); - man.hash.addBytesZ(string); - }, - .path_file => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ - prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, - })); - man.hash.add(arg.flags.make_absolute); - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(suffix); - _ = try man.addInputPath(file_path, .{}); - }, - .path_directory => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - const resolved_arg = try mem.concat(arena, u8, &.{ - prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, - }); - argv_list.appendAssumeCapacity(resolved_arg); - man.hash.add(arg.flags.make_absolute); - man.hash.addBytes(resolved_arg); - }, - .file_content => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - - var result: std.Io.Writer.Allocating = .init(arena); - result.writer.writeAll(prefix) catch return error.OutOfMemory; - - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); - defer file.close(io); - - var file_reader = file.reader(io, &.{}); - _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { - error.ReadFailed => switch (file_reader.err.?) { - error.Canceled => |e| return e, - else => |e| return step.fail(maker, "failed to read from {qf}: {t}", .{ file_path, e }), - }, - error.WriteFailed => return error.OutOfMemory, - }; - result.writer.writeAll(suffix) catch return error.OutOfMemory; - - argv_list.appendAssumeCapacity(result.written()); - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - }, - .artifact => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const producer_index = arg.producer.value.?; - const producer_step = producer_index.ptr(conf); - const producer = producer_step.extended.get(conf.extra).compile; - const producer_make_comp_step = maker.stepByIndex(producer_index); - const producer_make_comp = &producer_make_comp_step.extended.compile; - - const file_path = producer_make_comp.installed_path orelse - maker.generatedPath(producer.generated_bin.value.?); - - argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ - prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, - })); - - man.hash.add(arg.flags.make_absolute); - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(suffix); - _ = try man.addInputPath(file_path, .{}); - }, - .output_file, .output_directory => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const basename = arg.basename.value.?.slice(conf); - - man.hash.add(arg.flags.make_absolute); - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(basename); - man.hash.addBytesZ(suffix); - man.hash.add(arg.flags.dep_file); - - if (arg.flags.dep_file) any_discovered_inputs = true; - any_output_args = true; - - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try output_placeholders.append(gpa, .{ - .index = @intCast(argv_list.items.len), - .offset = 0, - .arg_index = arg_index, - }); - _ = argv_list.addOneAssumeCapacity(); - }, - .passthru => { - any_cli_positionals = true; - if (maker.run_args) |run_args| { - try argv_list.appendSlice(gpa, run_args); - man.hash.addListOfBytes(run_args); - } - }, - .enable_darling => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), - .enable_qemu => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), - .enable_rosetta => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), - .enable_wasmtime => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), - .enable_wine => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + for (conf_run.preopens.slice) |preopen| { + man.hash.addBytesZ(preopen.name.slice(conf)); + const cwd_path = try maker.resolveLazyPathIndex(arena, preopen.path, run_index); + man.hash.addBytes(try cwd_path.toString(arena)); } - } - - var owned_dirs: std.bit_set.Dynamic = .{}; - var inherit_dirs: std.ArrayList(Io.Dir) = .empty; - var owned_files: std.bit_set.Dynamic = .{}; - var inherit_files: std.ArrayList(Io.File) = .empty; - var protocol_args: std.ArrayList(u8) = .empty; - var input_dirs: std.ArrayList(Cache.Path) = .empty; - defer { - for (inherit_dirs.items, 0..) |inherit_dir, inherit_dir_index| - if (owned_dirs.isSet(inherit_dir_index)) inherit_dir.close(io); - for (inherit_files.items, 0..) |inherit_file, inherit_file_index| - if (owned_files.isSet(inherit_file_index)) inherit_file.close(io); - owned_dirs.deinit(gpa); - inherit_dirs.deinit(gpa); - owned_files.deinit(gpa); - inherit_files.deinit(gpa); - protocol_args.deinit(gpa); - input_dirs.deinit(gpa); - } - switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .check => {}, - .zig_test => { - const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); - - try argv_list.ensureUnusedCapacity(gpa, 3); - argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); - argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); - argv_list.appendAssumeCapacity("--listen=-"); - }, - .protocol => { - argv_list.appendAssumeCapacity("--listen=-"); - try input_dirs.append(gpa, if (conf_run.cwd.value) |lazy_cwd| - try maker.resolveLazyPathIndex(arena, lazy_cwd, run_index) - else - .cwd()); - for (conf_run.args.slice[1..]) |arg_index| { - const arg = arg_index.get(conf); - switch (arg.flags.tag) { - .string => { - const string = arg.prefix.value.?.slice(conf); - - man.hash.addBytesZ(string); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + string.len + 1); - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); - protocol_args.appendSliceAssumeCapacity(string); - protocol_args.appendAssumeCapacity(0); - }, - .path_file => { - const lazy_path = arg.path.value.?.get(conf); - switch (lazy_path) { - else => {}, - .relative => |relative| switch (relative.flags.base) { - else => {}, - .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, - }, - } - - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPath(arena, lazy_path, run_index); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .path_directory => { - const lazy_path = arg.path.value.?.get(conf); - switch (lazy_path) { - else => {}, - .relative => |relative| switch (relative.flags.base) { - else => {}, - .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, - }, - } - - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const dir_path = try maker.resolveLazyPath(arena, lazy_path, run_index); - try input_dirs.append(gpa, dir_path); - - const has_sub_path = dir_path.sub_path.len > 0; - try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, has_sub_path); - try inherit_dirs.ensureUnusedCapacity(gpa, 1); - const dir = if (has_sub_path) - dir_path.root_dir.handle.openDir(io, dir_path.sub_path, .{ - .iterate = true, - }) catch |err| return step.fail(maker, "unable to open input dir {qf}: {t}", .{ dir_path, err }) - else - dir_path.root_dir.handle; - inherit_dirs.appendAssumeCapacity(dir); - - man.hash.addBytesZ(prefix); - man.hash.addOptionalBytes(dir_path.root_dir.path); - man.hash.addBytes(dir_path.sub_path); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.Dir.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_dir)); - const dir_handle: *align(1) Io.Dir.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle))); - dir_handle.* = dir.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .file_content => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file_content)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .artifact => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const producer_index = arg.producer.value.?; - const producer_step = producer_index.ptr(conf); - const producer = producer_step.extended.get(conf.extra).compile; - const producer_make_comp_step = maker.stepByIndex(producer_index); - const producer_make_comp = &producer_make_comp_step.extended.compile; - - const file_path = producer_make_comp.installed_path orelse - maker.generatedPath(producer.generated_bin.value.?); - - try owned_files.resize(gpa, owned_files.bit_length + 1, true); - try inherit_files.ensureUnusedCapacity(gpa, 1); - const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| - return step.fail(maker, "unable to open input artifact {qf}: {t}", .{ file_path, err }); - inherit_files.appendAssumeCapacity(file); - - man.hash.addBytesZ(prefix); - _ = try man.addInputPath(file_path, .{}); - man.hash.addBytesZ(suffix); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); - const file_handle: *align(1) Io.File.Handle = - @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); - file_handle.* = file.handle; - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .output_file => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const basename = arg.basename.value.?.slice(conf); - - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(basename); - man.hash.addBytesZ(suffix); - assert(!arg.flags.dep_file); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.File.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_file)); - - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try owned_files.resize(gpa, owned_files.bit_length + 1, false); - try inherit_files.ensureUnusedCapacity(gpa, 1); - try output_placeholders.append(gpa, .{ - .index = @intCast(inherit_files.items.len), - .offset = @intCast(protocol_args.items.len), - .arg_index = arg_index, - }); - _ = inherit_files.addOneAssumeCapacity(); - - _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle)); - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .output_directory => { - const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; - const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; - const basename = arg.basename.value.?.slice(conf); - - man.hash.addBytesZ(prefix); - man.hash.addBytesZ(basename); - man.hash.addBytesZ(suffix); - assert(!arg.flags.dep_file); - - try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + - 1 + @sizeOf(Io.Dir.Handle) + - 1 + suffix.len + 1); - if (prefix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_dir)); - - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, false); - try inherit_dirs.ensureUnusedCapacity(gpa, 1); - try output_placeholders.append(gpa, .{ - .index = @intCast(inherit_dirs.items.len), - .offset = @intCast(protocol_args.items.len), - .arg_index = arg_index, - }); - _ = inherit_dirs.addOneAssumeCapacity(); - _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle)); - if (suffix.len > 0) { - protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); - protocol_args.appendSliceAssumeCapacity(prefix); - protocol_args.appendAssumeCapacity(0); - } - }, - .passthru => if (maker.run_args) |run_args| { + man.hash.add(graph.fuzzing); + man.hash.add(conf_run.flags.color); + man.hash.add(conf_run.flags.disable_zig_progress); + + var any_discovered_inputs = switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => false, + .protocol => true, + }; + var any_output_args = false; + var any_cli_positionals = false; + + for (switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => conf_run.args.slice, + .protocol => conf_run.args.slice[0..1], // arguments communicated over protocol + }) |arg_index| { + const arg = arg_index.get(conf); + try argv_list.ensureUnusedCapacity(gpa, 1); + switch (arg.flags.tag) { + .string => { + const string = arg.prefix.value.?.slice(conf); + argv_list.appendAssumeCapacity(string); + man.hash.addBytesZ(string); + }, + .path_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, + })); + man.hash.add(arg.flags.make_absolute); + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(suffix); + _ = try man.addInputPath(file_path, .{}); + }, + .path_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + const resolved_arg = try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, + }); + argv_list.appendAssumeCapacity(resolved_arg); + man.hash.add(arg.flags.make_absolute); + man.hash.addBytes(resolved_arg); + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + var result: std.Io.Writer.Allocating = .init(arena); + result.writer.writeAll(prefix) catch return error.OutOfMemory; + + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to read from {qf}: {t}", .{ file_path, e }), + }, + error.WriteFailed => return error.OutOfMemory, + }; + result.writer.writeAll(suffix) catch return error.OutOfMemory; + + argv_list.appendAssumeCapacity(result.written()); + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + + const file_path = producer_make_comp.installed_path orelse + maker.generatedPath(producer.generated_bin.value.?); + + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix, + })); + + man.hash.add(arg.flags.make_absolute); + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(suffix); + _ = try man.addInputPath(file_path, .{}); + }, + .output_file, .output_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.add(arg.flags.make_absolute); + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + man.hash.add(arg.flags.dep_file); + + if (arg.flags.dep_file) any_discovered_inputs = true; + any_output_args = true; + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try output_placeholders.append(gpa, .{ + .index = @intCast(argv_list.items.len), + .offset = 0, + .arg_index = arg_index, + }); + _ = argv_list.addOneAssumeCapacity(); + }, + .passthru => { + any_cli_positionals = true; + if (maker.run_args) |run_args| { + try argv_list.appendSlice(gpa, run_args); man.hash.addListOfBytes(run_args); + } + }, + .enable_darling => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), + .enable_qemu => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), + .enable_rosetta => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), + .enable_wasmtime => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), + .enable_wine => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + } + } - for (run_args) |run_arg| { - try protocol_args.ensureUnusedCapacity(gpa, 1 + run_arg.len + 1); + var owned_dirs: std.bit_set.Dynamic = .{}; + var inherit_dirs: std.ArrayList(Io.Dir) = .empty; + var owned_files: std.bit_set.Dynamic = .{}; + var inherit_files: std.ArrayList(Io.File) = .empty; + var protocol_args: std.ArrayList(u8) = .empty; + var input_dirs: std.ArrayList(Cache.Path) = .empty; + defer { + for (inherit_dirs.items, 0..) |inherit_dir, inherit_dir_index| + if (owned_dirs.isSet(inherit_dir_index)) inherit_dir.close(io); + for (inherit_files.items, 0..) |inherit_file, inherit_file_index| + if (owned_files.isSet(inherit_file_index)) inherit_file.close(io); + owned_dirs.deinit(gpa); + inherit_dirs.deinit(gpa); + owned_files.deinit(gpa); + inherit_files.deinit(gpa); + protocol_args.deinit(gpa); + input_dirs.deinit(gpa); + } + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check => {}, + .zig_test => { + const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false); + + try argv_list.ensureUnusedCapacity(gpa, 3); + argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string})); + argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed})); + argv_list.appendAssumeCapacity("--listen=-"); + }, + .protocol => { + argv_list.appendAssumeCapacity("--listen=-"); + try input_dirs.append(gpa, if (conf_run.cwd.value) |lazy_cwd| + try maker.resolveLazyPathIndex(arena, lazy_cwd, run_index) + else + .cwd()); + for (conf_run.args.slice[1..]) |arg_index| { + const arg = arg_index.get(conf); + switch (arg.flags.tag) { + .string => { + const string = arg.prefix.value.?.slice(conf); + + man.hash.addBytesZ(string); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + string.len + 1); protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); - protocol_args.appendSliceAssumeCapacity(run_arg); + protocol_args.appendSliceAssumeCapacity(string); protocol_args.appendAssumeCapacity(0); - } - }, - .enable_darling => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), - .enable_qemu => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), - .enable_rosetta => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), - .enable_wasmtime => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), - .enable_wine => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + }, + .path_file => { + const lazy_path = arg.path.value.?.get(conf); + switch (lazy_path) { + else => {}, + .relative => |relative| switch (relative.flags.base) { + else => {}, + .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, + }, + } + + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPath(arena, lazy_path, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .path_directory => { + const lazy_path = arg.path.value.?.get(conf); + switch (lazy_path) { + else => {}, + .relative => |relative| switch (relative.flags.base) { + else => {}, + .libc_runtimes => if (graph.libc_runtimes_dir == null) continue, + }, + } + + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const dir_path = try maker.resolveLazyPath(arena, lazy_path, run_index); + try input_dirs.append(gpa, dir_path); + + const has_sub_path = dir_path.sub_path.len > 0; + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, has_sub_path); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + const dir = if (has_sub_path) + dir_path.root_dir.handle.openDir(io, dir_path.sub_path, .{ + .iterate = true, + }) catch |err| return step.fail(maker, "unable to open input dir {qf}: {t}", .{ dir_path, err }) + else + dir_path.root_dir.handle; + inherit_dirs.appendAssumeCapacity(dir); + + man.hash.addBytesZ(prefix); + man.hash.addOptionalBytes(dir_path.root_dir.path); + man.hash.addBytes(dir_path.sub_path); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_dir)); + const dir_handle: *align(1) Io.Dir.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle))); + dir_handle.* = dir.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file_content)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + + const file_path = producer_make_comp.installed_path orelse + maker.generatedPath(producer.generated_bin.value.?); + + try owned_files.resize(gpa, owned_files.bit_length + 1, true); + try inherit_files.ensureUnusedCapacity(gpa, 1); + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input artifact {qf}: {t}", .{ file_path, err }); + inherit_files.appendAssumeCapacity(file); + + man.hash.addBytesZ(prefix); + _ = try man.addInputPath(file_path, .{}); + man.hash.addBytesZ(suffix); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.input_file)); + const file_handle: *align(1) Io.File.Handle = + @ptrCast(protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle))); + file_handle.* = file.handle; + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.File.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_file)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_files.resize(gpa, owned_files.bit_length + 1, false); + try inherit_files.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_files.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_files.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.File.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .output_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + assert(!arg.flags.dep_file); + + try protocol_args.ensureUnusedCapacity(gpa, 1 + prefix.len + 1 + + 1 + @sizeOf(Io.Dir.Handle) + + 1 + suffix.len + 1); + if (prefix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.prefix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.output_dir)); + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try owned_dirs.resize(gpa, owned_dirs.bit_length + 1, false); + try inherit_dirs.ensureUnusedCapacity(gpa, 1); + try output_placeholders.append(gpa, .{ + .index = @intCast(inherit_dirs.items.len), + .offset = @intCast(protocol_args.items.len), + .arg_index = arg_index, + }); + _ = inherit_dirs.addOneAssumeCapacity(); + + _ = protocol_args.addManyAsArrayAssumeCapacity(@sizeOf(Io.Dir.Handle)); + if (suffix.len > 0) { + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.suffix)); + protocol_args.appendSliceAssumeCapacity(prefix); + protocol_args.appendAssumeCapacity(0); + } + }, + .passthru => if (maker.run_args) |run_args| { + man.hash.addListOfBytes(run_args); + + for (run_args) |run_arg| { + try protocol_args.ensureUnusedCapacity(gpa, 1 + run_arg.len + 1); + protocol_args.appendAssumeCapacity(@backingInt(std.zig.Client.Message.Arg.string)); + protocol_args.appendSliceAssumeCapacity(run_arg); + protocol_args.appendAssumeCapacity(0); + } + }, + .enable_darling => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value), + .enable_qemu => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value), + .enable_rosetta => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value), + .enable_wasmtime => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value), + .enable_wine => try thirdPartyToggleProtocol(gpa, &man.hash, &protocol_args, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value), + } } - } - }, - } - - switch (conf_run.stdin.u) { - .bytes => |bytes| { - man.hash.addBytes(bytes.slice(conf)); - }, - .lazy_path => |lazy_path| { + }, + } + + switch (conf_run.stdin.u) { + .bytes => |bytes| { + man.hash.addBytes(bytes.slice(conf)); + }, + .lazy_path => |lazy_path| { + const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + _ = try man.addInputPath(file_path, .{}); + }, + .none => {}, + } + + if (conf_run.captured_stdout.value) |captured| { + man.hash.addBytes(captured.basename.slice(conf)); + man.hash.add(conf_run.flags.stdout_trim_whitespace); + } + + if (conf_run.captured_stderr.value) |captured| { + man.hash.addBytes(captured.basename.slice(conf)); + man.hash.add(conf_run.flags.stderr_trim_whitespace); + } + + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .zig_test, .protocol => {}, + .check => { + man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else ""); + man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else ""); + for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); + for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); + man.hash.add(conf_run.flags2.expect_term_status); + man.hash.addOptional(conf_run.expect_term_value.value); + }, + } + + for (conf_run.file_inputs.slice) |lazy_path| { const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); _ = try man.addInputPath(file_path, .{}); - }, - .none => {}, - } - - if (conf_run.captured_stdout.value) |captured| { - man.hash.addBytes(captured.basename.slice(conf)); - man.hash.add(conf_run.flags.stdout_trim_whitespace); - } - - if (conf_run.captured_stderr.value) |captured| { - man.hash.addBytes(captured.basename.slice(conf)); - man.hash.add(conf_run.flags.stderr_trim_whitespace); - } - - switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .zig_test, .protocol => {}, - .check => { - man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else ""); - man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else ""); - for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); - for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); - man.hash.add(conf_run.flags2.expect_term_status); - man.hash.addOptional(conf_run.expect_term_value.value); - }, - } - - for (conf_run.file_inputs.slice) |lazy_path| { - const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); - _ = try man.addInputPath(file_path, .{}); - } - - if (conf_run.cwd.value) |lazy_path| { - const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); - _ = man.hash.addBytes(try cwd_path.toString(arena)); - } - - // Whether the Run step has side effects *other than* updating the output arguments. - // When fuzzing we need to always run the test runner to populate fuzz_tests. - const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or - switch (conf_run.flags.stdio) { - .infer_from_args => !any_output_args and - conf_run.captured_stdout.value == null and - conf_run.captured_stderr.value == null, - .inherit => true, - .check, .zig_test, .protocol => false, - }; - - if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) { - // Cache hit; skip running command. - const digest = man.hitDigestHex(); - try populateGeneratedStdIo(maker, &conf_run, &digest); - try populateGeneratedPaths(maker, output_placeholders.items, &digest); - step.result_cached = true; - return; - } - - if (!any_discovered_inputs) { - // We already know the final output paths; use them directly. - const digest = if (has_side_effects) man.hash.final() else man.missDigestHex(); - const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; - try populateGeneratedStdIo(maker, &conf_run, &digest); - try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { + } + + if (conf_run.cwd.value) |lazy_path| { + const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + _ = man.hash.addBytes(try cwd_path.toString(arena)); + } + + // Whether the Run step has side effects *other than* updating the output arguments. + // When fuzzing we need to always run the test runner to populate fuzz_tests. + const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or + switch (conf_run.flags.stdio) { + .infer_from_args => !any_output_args and + conf_run.captured_stdout.value == null and + conf_run.captured_stderr.value == null, + .inherit => true, + .check, .zig_test, .protocol => false, + }; + + if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) { + // Cache hit; skip running command. + const digest = man.hitDigestHex(); + try populateGeneratedStdIo(maker, &conf_run, &digest); + try populateGeneratedPaths(maker, output_placeholders.items, &digest); + step.result_cached = true; + return; + } + + if (!any_discovered_inputs) { + // We already know the final output paths; use them directly. + const digest = if (has_side_effects) man.hash.final() else man.missDigestHex(); + const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; + try populateGeneratedStdIo(maker, &conf_run, &digest); + try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .check, .zig_test => .{ .argv = argv_list.items }, + .protocol => .{ .protocol = .{ + .owned_dirs = &owned_dirs, + .inherit_dirs = inherit_dirs.items, + .owned_files = &owned_files, + .inherit_files = inherit_files.items, + .args = protocol_args.items, + } }, + }); + runCommand(arena, run, run_index, maker, progress_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, output_dir_path, null) catch |err| switch (err) { + else => |e| return e, + error.MakeFailed, error.MakeSkipped => |e| { + try step.setWatchInputsFromManifest(maker, &man); + return e; + }, + }; + if (has_side_effects) { + try step.setWatchInputsFromManifest(maker, &man); + } else { + try step.finalizeManifestAndWatch(maker, &man); + } + return; + } + + // We do not know the final output paths yet; use temporary directory to run the command. + var rand_int: u64 = undefined; + io.random(@ptrCast(&rand_int)); + tmp_dir_path = ("tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int)).*; + + try populateGeneratedPathsCreateDirs(arena, run_index, maker, &tmp_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { .infer_from_args, .inherit, .check, .zig_test => .{ .argv = argv_list.items }, .protocol => .{ .protocol = .{ .owned_dirs = &owned_dirs, @@ -589,57 +622,30 @@ pub fn make( .args = protocol_args.items, } }, }); - runCommand(arena, run, run_index, maker, progress_node, argv_list.items, &.{}, &.{}, &.{}, &.{}, null, has_side_effects, output_dir_path, null) catch |err| switch (err) { + runCommand( + arena, + run, + run_index, + maker, + progress_node, + argv_list.items, + inherit_dirs.items, + inherit_files.items, + protocol_args.items, + input_dirs.items, + &man, + has_side_effects, + &tmp_dir_path, + null, + ) catch |err| switch (err) { else => |e| return e, error.MakeFailed, error.MakeSkipped => |e| { try step.setWatchInputsFromManifest(maker, &man); return e; }, }; - if (has_side_effects) { - try step.setWatchInputsFromManifest(maker, &man); - } else { - try step.finalizeManifestAndWatch(maker, &man); - } - return; - } - // We do not know the final output paths yet; use temporary directory to run the command. - var rand_int: u64 = undefined; - io.random(@ptrCast(&rand_int)); - const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - - try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, switch (conf_run.flags.stdio) { - .infer_from_args, .inherit, .check, .zig_test => .{ .argv = argv_list.items }, - .protocol => .{ .protocol = .{ - .owned_dirs = &owned_dirs, - .inherit_dirs = inherit_dirs.items, - .owned_files = &owned_files, - .inherit_files = inherit_files.items, - .args = protocol_args.items, - } }, - }); - runCommand( - arena, - run, - run_index, - maker, - progress_node, - argv_list.items, - inherit_dirs.items, - inherit_files.items, - protocol_args.items, - input_dirs.items, - &man, - has_side_effects, - tmp_dir_path, - null, - ) catch |err| switch (err) { - else => |e| return e, - error.MakeFailed, error.MakeSkipped => |e| { - try step.setWatchInputsFromManifest(maker, &man); - return e; - }, + break :has_side_effects has_side_effects; }; for (output_placeholders.items) |placeholder| { @@ -686,7 +692,7 @@ pub fn make( if (any_output) { // Rename into place. - const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = tmp_dir_path }; + const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = &tmp_dir_path }; const dst_path: Path = .{ .root_dir = cache_root, .sub_path = "o" ++ Dir.path.sep_str ++ &digest }; Dir.rename( tmp_path.root_dir.handle, -- 2.54.0 From 47291ae1ac71e595cb1571b1245c58e943aaffd7 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 22:34:06 -0400 Subject: [PATCH 19/23] Runner: ignore path separator differences This seems like a compiler inconsistency. --- test/src/Runner.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/src/Runner.zig b/test/src/Runner.zig index 0cd77d82842f48aa12c38efea80589b524206473..d993f75e657c428b141e58f03a3ebccca12bf5a8 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -859,10 +859,9 @@ fn handleCommand( .include_source_line = false, .include_log_text = true, }, &error_aw.writer); - std.testing.expectEqualStrings( - expected, - error_aw.written(), - ) catch |err| switch (err) { + const actual = error_aw.written(); + std.mem.replaceScalar(u8, actual, '\\', '/'); + std.testing.expectEqualStrings(expected, actual) catch |err| switch (err) { error.TestExpectedEqual => return runner.fail( "errors did not match expected", .{}, -- 2.54.0 From 44f06cb6005d3ca5e0b94fbffd5a7668e6e98bd7 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 25 Sep 2026 23:36:41 -0400 Subject: [PATCH 20/23] Threaded: add retries to directory renaming I think there's a race between a child's exit status being reported and all of its handles being closed, so possibly affects most filesystem operations. --- lib/std/Io/Threaded.zig | 91 ++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 82f26e74930a87132f70e681982a91dfacde139f..c7e68e38615c1fd7061735021b625f0177fa9711 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7685,6 +7685,8 @@ fn dirRenameWindowsInner( defer w.CloseHandle(src_fd); var rc: w.NTSTATUS = undefined; + var attempt: u5 = 0; + // FileRenameInformationEx has varying levels of support: // - FILE_RENAME_INFORMATION_EX requires >= win10_rs1 // (INVALID_INFO_CLASS is returned if not supported) @@ -7708,24 +7710,43 @@ fn dirRenameWindowsInner( }); var io_status_block: w.IO_STATUS_BLOCK = undefined; const rename_info_buf = rename_info.toBuffer(); - rc = w.ntdll.NtSetInformationFile( - src_fd, - &io_status_block, - rename_info_buf.ptr, - @intCast(rename_info_buf.len), - .RenameEx, - ); - switch (rc) { - .SUCCESS => return, - // The filesystem does not support FileDispositionInformationEx - .INVALID_PARAMETER, - // The operating system does not support FileDispositionInformationEx - .INVALID_INFO_CLASS, - // The operating system does not support one of the flags - .NOT_SUPPORTED, - => break :need_fallback true, - // For all other statuses, fall down to the switch below to handle them. - else => break :need_fallback false, + var syscall: Syscall = try .start(); + while (true) { + rc = w.ntdll.NtSetInformationFile( + src_fd, + &io_status_block, + rename_info_buf.ptr, + @intCast(rename_info_buf.len), + .RenameEx, + ); + switch (rc) { + // For all other statuses, fall down to the switch below to handle them. + else => { + syscall.finish(); + break :need_fallback false; + }, + .CANCELLED => try syscall.checkCancel(), + // The filesystem does not support FileDispositionInformationEx + .INVALID_PARAMETER, + // The operating system does not support FileDispositionInformationEx + .INVALID_INFO_CLASS, + // The operating system does not support one of the flags + .NOT_SUPPORTED, + => { + syscall.finish(); + break :need_fallback true; + }, + .ACCESS_DENIED => { + syscall.finish(); + if (max_windows_kernel_bug_retries - attempt == 0) break :need_fallback false; + try parking_sleep.sleep(.{ .duration = .{ + .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1), + .clock = .awake, + } }); + attempt += 1; + syscall = try .start(); + }, + } } }; @@ -7737,13 +7758,33 @@ fn dirRenameWindowsInner( }); var io_status_block: w.IO_STATUS_BLOCK = undefined; const rename_info_buf = rename_info.toBuffer(); - rc = w.ntdll.NtSetInformationFile( - src_fd, - &io_status_block, - rename_info_buf.ptr, - @intCast(rename_info_buf.len), - .Rename, - ); + var syscall: Syscall = try .start(); + while (true) { + rc = w.ntdll.NtSetInformationFile( + src_fd, + &io_status_block, + rename_info_buf.ptr, + @intCast(rename_info_buf.len), + .Rename, + ); + switch (rc) { + else => { + syscall.finish(); + break; + }, + .CANCELLED => try syscall.checkCancel(), + .ACCESS_DENIED => { + syscall.finish(); + if (max_windows_kernel_bug_retries - attempt == 0) break; + try parking_sleep.sleep(.{ .duration = .{ + .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1), + .clock = .awake, + } }); + attempt += 1; + syscall = try .start(); + }, + } + } } switch (rc) { -- 2.54.0 From 44524cbf51a14d6eed0b81f97b2b267783038e72 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 26 Sep 2026 18:56:42 -0400 Subject: [PATCH 21/23] std.Io.Reader: revert interface change This function was broken before and is still broken after, so avoiding the change for now. --- lib/std/Io/Reader.zig | 15 ++++++++++++++- test/src/Runner.zig | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Reader.zig b/lib/std/Io/Reader.zig index 06b5b523a8fefcc0427b3900fad4af0bd34ac891..c8797f26c68bdc41778f6360d75e591c78e10396 100644 --- a/lib/std/Io/Reader.zig +++ b/lib/std/Io/Reader.zig @@ -849,14 +849,27 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { } while (true) { const content_len = r.end - r.seek; + if (r.buffer.len - content_len == 0) break; try fillMore(r); - if (r.buffer.len - content_len == 0) return error.StreamTooLong; const seek = r.seek; const contents = r.buffer[0..r.end]; if (std.mem.findScalarPos(u8, contents, seek + content_len, delimiter)) |end| { return contents[seek .. end + 1]; } } + // It might or might not be end of stream. There is no more buffer space + // left to disambiguate. If `StreamTooLong` was added to `RebaseError` then + // this logic could be replaced by removing the exit condition from the + // above while loop. That error code would represent when `buffer` capacity + // is too small for an operation, replacing the current use of asserts. + var failing_writer = Writer.failing; + while (r.vtable.stream(r, &failing_writer, .limited(1))) |n| { + assert(n == 0); + } else |err| switch (err) { + error.WriteFailed => return error.StreamTooLong, + error.ReadFailed => |e| return e, + error.EndOfStream => |e| return e, + } } /// Returns a slice of the next bytes of buffered data from the stream until diff --git a/test/src/Runner.zig b/test/src/Runner.zig index d993f75e657c428b141e58f03a3ebccca12bf5a8..a73aaf05907e1232bcee34df1659113f0bbd426e 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -574,6 +574,12 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { update.src_dir.close(runner.io); update.prog_node.end(); } + + // hack around `std.Io.Reader` interface bugs by ensuring the + // buffer is larger than the longest supported manifest line + try update.manifest_sr.line_aw.ensureTotalCapacityPrecise(512); + update.manifest_sr.updateBuffer(); + var skip_delimiter = false; while (update.manifest_sr.interface.takeSentinel('\n')) |line| { var line_it = std.mem.tokenizeScalar(u8, line, ' '); -- 2.54.0 From 738fb7835f366dab6bc15ddbaecc0b334f2bb1f9 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 26 Sep 2026 19:08:11 -0400 Subject: [PATCH 22/23] Runner: more error set cleanup --- test/src/Runner.zig | 169 +++++++++++++++++++++++++++++++------------- 1 file changed, 118 insertions(+), 51 deletions(-) diff --git a/test/src/Runner.zig b/test/src/Runner.zig index a73aaf05907e1232bcee34df1659113f0bbd426e..f917fccc32541d0395512e332d9d5317536921c1 100644 --- a/test/src/Runner.zig +++ b/test/src/Runner.zig @@ -1,6 +1,7 @@ const assert = std.debug.assert; const std = @import("std"); const Runner = @This(); +const zig_version_string = @import("builtin").zig_version_string; gpa: std.mem.Allocator, arena: *std.heap.ArenaAllocator, @@ -51,24 +52,33 @@ fn deinit(runner: *Runner) void { runner.* = undefined; } -const Fallible = error{AlreadyReported} || std.mem.Allocator.Error || std.Io.Cancelable; -fn fail(runner: *Runner, comptime fmt: []const u8, args: anytype) Fallible { +const FailError = error{AlreadyReported} || std.mem.Allocator.Error || std.Io.Cancelable; +fn fail(runner: *Runner, comptime fmt: []const u8, args: anytype) FailError { return runner.failString(try runner.eb_wip.printString(fmt, args)); } -fn failString(runner: *Runner, msg: std.zig.ErrorBundle.String) Fallible { +fn failString(runner: *Runner, msg: std.zig.ErrorBundle.String) FailError { try runner.eb_wip.addRootErrorMessage(.{ .msg = msg, }); return error.AlreadyReported; } -pub const ProtocolError = Fallible || std.Io.Reader.Error || std.Io.Writer.Error; +pub const ProtocolError = error{ + ServerReadFailed, + ServerWriteFailed, + ServerEndOfStream, +} || FailError; pub fn runServer(runner: *Runner) ProtocolError { - try runner.server.serveStringMessage(.zig_version, @import("builtin").zig_version_string); + runner.server.serveStringMessage(.zig_version, zig_version_string) catch |err| switch (err) { + error.WriteFailed => return error.ServerWriteFailed, + }; while (true) { _ = runner.arena.reset(.retain_capacity); const arena = runner.arena.allocator(); - const hdr = try runner.server.receiveMessage(); + const hdr = runner.server.receiveMessage() catch |err| switch (err) { + error.ReadFailed => return error.ServerReadFailed, + error.EndOfStream => return error.ServerEndOfStream, + }; switch (hdr.tag) { else => |tag| return runner.fail("unsupported message: {t}", .{tag}), .exit => std.process.exit(0), @@ -273,16 +283,20 @@ pub fn runServer(runner: *Runner) ProtocolError { .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(.{ + runner.server.serveTestMetadata(.{ .names = runner.args.targets.items, .expected_panic_msgs = expected_panic_msgs, .string_bytes = runner.args.target_bytes.items, - }); + }) catch |err| switch (err) { + error.WriteFailed => return error.ServerWriteFailed, + }; }, .run_test => { const test_index = runner.server.receiveBody_u32() catch unreachable; - try runner.server.serveBodylessMessage(.test_started); - try runner.server.serveTestResults(.{ + runner.server.serveBodylessMessage(.test_started) catch |err| switch (err) { + error.WriteFailed => return error.ServerWriteFailed, + }; + runner.server.serveTestResults(.{ .index = test_index, .flags = .{ .status = if (runner.testOne(test_index)) @@ -290,14 +304,18 @@ pub fn runServer(runner: *Runner) ProtocolError { else |err| status: switch (err) { error.Canceled, error.OutOfMemory, - error.ReadFailed, - error.WriteFailed, + error.ServerWriteFailed, => |e| return e, error.AlreadyReported => { var eb = try runner.eb_wip.toOwnedBundle(""); defer eb.deinit(runner.gpa); assert(eb.errorMessageCount() > 0); // already reported what? - try runner.server.serveErrorBundle(.error_bundle, eb); + runner.server.serveErrorBundle( + .error_bundle, + eb, + ) catch |server_err| switch (server_err) { + error.WriteFailed => return error.ServerWriteFailed, + }; const eb_wip: std.zig.ErrorBundle.Wip = try .init(runner.gpa); runner.eb_wip.deinit(); runner.eb_wip = eb_wip; @@ -309,7 +327,9 @@ pub fn runServer(runner: *Runner) ProtocolError { .log_err_count = 0, .leak_count = 0, }, - }); + }) catch |err| switch (err) { + error.WriteFailed => return error.ServerWriteFailed, + }; assert(runner.eb_wip.root_list.items.len == 0); // failed to report }, } @@ -320,6 +340,15 @@ fn discoverInput( runner: *Runner, dir: std.zig.Server.Message.InputDir, sub_path: []const u8, +) error{ServerWriteFailed}!void { + return runner.discoverInputInner(dir, sub_path) catch |err| switch (err) { + error.WriteFailed => return error.ServerWriteFailed, + }; +} +fn discoverInputInner( + runner: *Runner, + dir: std.zig.Server.Message.InputDir, + sub_path: []const u8, ) std.Io.Writer.Error!void { try runner.server.serveMessageHeader(.{ .tag = .discovered_inputs, @@ -435,14 +464,14 @@ pub const SourceReader = struct { src_loc: []const u8, comptime fmt: []const u8, args: anytype, - ) Fallible { + ) FailError { return s.failString(src_loc, try s.eb_wip.printString(fmt, args)); } fn failString( s: *SourceReader, src_loc: []const u8, msg: std.zig.ErrorBundle.String, - ) Fallible { + ) FailError { const line: u32, const column: u32, const source_line = src_loc: { const pos = src_loc.ptr - s.interface.buffer.ptr; const end = std.mem.findScalarPos(u8, s.interface.buffer, pos, '\n') orelse @@ -531,8 +560,8 @@ const Update = struct { const TestError = error{ SkipTest, -} || Fallible || std.Io.Reader.ShortError || std.Io.Writer.Error; -fn testOne(runner: *Runner, test_index: u32) TestError!void { +} || FailError; +fn testOne(runner: *Runner, test_index: u32) (error{ServerWriteFailed} || TestError)!void { const target_query = std.mem.sliceTo(runner.args.target_bytes.items[runner.args.targets.items[test_index]..], 0); const manifest_file = runner.args.manifest_file orelse @@ -660,16 +689,24 @@ fn testOne(runner: *Runner, test_index: u32) TestError!void { error.OutOfMemory, error.AlreadyReported, error.SkipTest, - error.WriteFailed, => |e| return e, - error.ReadFailed => switch (contents_impl) { + error.ContentsReadFailed => switch (contents_impl) { .ending => unreachable, - .dr => return runner.fail("unable to read manifest: {t}", .{manifest_fr.err.?}), - .fr => |fr| return runner.fail("unable to read contents file: {t}", .{fr.err.?}), + .dr => switch (manifest_fr.err orelse return error.OutOfMemory) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to read manifest: {t}", .{e}), + }, + .fr => |fr| switch (fr.err.?) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to read contents file: {t}", .{e}), + }, }, }; } else |err| switch (err) { - error.ReadFailed => |e| return e, + error.ReadFailed => switch (manifest_fr.err orelse return error.OutOfMemory) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to read manifest: {t}", .{e}), + }, error.StreamTooLong => unreachable, error.EndOfStream => if (skip_delimiter) return runner.fail("manifest missing \"#}}\" delimiter", .{}), @@ -687,7 +724,7 @@ fn handleCommand( cmd: Command, arg: ?[]const u8, contents_r: *std.Io.Reader, -) TestError!void { +) (error{ContentsReadFailed} || TestError)!void { 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}{s}", .{ cmd, if (arg) |_| " " else "", arg orelse "", @@ -702,9 +739,8 @@ fn handleCommand( cmd_src_loc, "\"#skip\" must appear before other commands", .{}, - ) else if (std.mem.eql(u8, update.target_query, arg.?)) - return error.SkipTest, - .exe, .lib, .obj => try runner.spawnCompiler(&update.compiler, update.src_dir, switch (cmd) { + ) else if (std.mem.eql(u8, update.target_query, arg.?)) return error.SkipTest, + .exe, .lib, .obj => runner.spawnCompiler(&update.compiler, update.src_dir, switch (cmd) { else => unreachable, .exe => .Exe, .lib => .Lib, @@ -746,7 +782,10 @@ fn handleCommand( .mode = mode, .backend = backend, }; - }), + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory, error.AlreadyReported => |e| return e, + error.ReadFailed => return error.ContentsReadFailed, + }, .write => { const file = update.src_dir.createFile(runner.io, arg.?, .{}) catch |err| switch (err) { error.Canceled => |e| return e, @@ -756,7 +795,7 @@ fn handleCommand( var fw_buffer: [512]u8 = undefined; var fw = file.writer(runner.io, &fw_buffer); _ = contents_r.streamRemaining(&fw.interface) catch |err| switch (err) { - error.ReadFailed => |e| return e, + error.ReadFailed => return error.ContentsReadFailed, error.WriteFailed => switch (fw.err.?) { error.Canceled => |e| return e, else => |e| return update.manifest_sr.fail( @@ -797,7 +836,9 @@ fn handleCommand( )); update.prog_node.end(); update.prog_node = runner.prog_node.start( - switch (try contents_r.readSliceShort(&name_buffer)) { + switch (contents_r.readSliceShort(&name_buffer) catch |err| switch (err) { + error.ReadFailed => return error.ContentsReadFailed, + }) { 0 => switch (update.num) { 0 => "initial update", else => |num| std.mem.print(&name_buffer, "update {d}", .{num}) catch @@ -807,10 +848,17 @@ fn handleCommand( }, 0, ); - _ = try contents_r.discardRemaining(); + _ = contents_r.discardRemaining() catch |err| switch (err) { + error.ReadFailed => return error.ContentsReadFailed, + }; if (comp.state != .idle) return update.manifest_sr.fail(cmd_src_loc, "missing \"#check\"", .{}); - try comp.client.serveBodylessMessage(.update); + comp.client.serveBodylessMessage(.update) catch |err| switch (err) { + error.WriteFailed => switch (comp.fw.err.?) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to send message to compiler: {t}", .{e}), + }, + }; comp.state = .update; update.mtime = update.mtime.addDuration(.fromSeconds(2)); update.num += 1; @@ -820,7 +868,8 @@ fn handleCommand( return update.manifest_sr.fail(arg.?, "unknown check", .{}); const expected = contents_r.allocRemaining(runner.gpa, .unlimited) catch |err| switch (err) { - error.OutOfMemory, error.ReadFailed => |e| return e, + error.OutOfMemory => |e| return e, + error.ReadFailed => return error.ContentsReadFailed, error.StreamTooLong => unreachable, // .unlimited }; defer runner.gpa.free(expected); @@ -860,11 +909,13 @@ fn handleCommand( .errors => { var error_aw: std.Io.Writer.Allocating = .init(runner.gpa); defer error_aw.deinit(); - try eb.renderToWriter(.{ + eb.renderToWriter(.{ .include_reference_trace = false, .include_source_line = false, .include_log_text = true, - }, &error_aw.writer); + }, &error_aw.writer) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + }; const actual = error_aw.written(); std.mem.replaceScalar(u8, actual, '\\', '/'); std.testing.expectEqualStrings(expected, actual) catch |err| switch (err) { @@ -900,7 +951,9 @@ fn handleCommand( return runner.fail("compiler failed to send terminating error_bundle", .{}); }, } - assert(try contents_r.discardRemaining() == 0); + assert(contents_r.discardRemaining() catch |err| switch (err) { + error.ReadFailed => return error.ContentsReadFailed, + } == 0); switch (cmd) { .todo, .skip => {}, else => update.allow_skip = false, @@ -914,7 +967,7 @@ fn spawnCompiler( output_mode: std.lang.OutputMode, args_r: *std.Io.Reader, target: Compiler.Target, -) (Fallible || std.Io.Reader.ShortError)!void { +) (std.Io.Reader.ShortError || FailError)!void { const gpa = runner.gpa; const arena = runner.arena.allocator(); if (compiler.*) |*comp| { @@ -1072,7 +1125,7 @@ const Compiler = struct { }; }; - fn receiveMessage(comp: *Compiler) Fallible!?struct { + fn receiveMessage(comp: *Compiler) FailError!?struct { tag: std.zig.Server.Message.Tag, body: []const u8, } { @@ -1107,7 +1160,7 @@ const Compiler = struct { expected: []const u8, src_dir: std.Io.Dir, digest: *const std.Build.Cache.BinDigest, - ) Fallible!void { + ) FailError!void { const runner = comp.runner; const gpa = runner.gpa; const arena = runner.arena.allocator(); @@ -1288,7 +1341,7 @@ const Compiler = struct { } } - fn compileC(parent_comp: *Compiler, src_dir: std.Io.Dir, c_path: []const u8) Fallible![]const u8 { + fn compileC(parent_comp: *Compiler, src_dir: std.Io.Dir, c_path: []const u8) FailError![]const u8 { const runner = parent_comp.runner; const arena = runner.arena.allocator(); var compiler: ?Compiler = null; @@ -1308,7 +1361,10 @@ const Compiler = struct { }; const comp = &compiler.?; comp.client.serveBodylessMessage(.update) catch |err| switch (err) { - error.WriteFailed => return runner.fail("unable to send message: {t}", .{comp.fw.err.?}), + error.WriteFailed => switch (comp.fw.err.?) { + error.Canceled => |e| return e, + else => |e| return runner.fail("unable to send message: {t}", .{e}), + }, }; comp.state = .update; var out_path: ?[]const u8 = null; @@ -1348,7 +1404,7 @@ const Compiler = struct { return out_path.?; } - fn exit(comp: *Compiler) Fallible!void { + fn exit(comp: *Compiler) FailError!void { const runner = comp.runner; comp.client.serveBodylessMessage(.exit) catch |err| switch (err) { error.WriteFailed => switch (comp.fw.err.?) { @@ -1465,9 +1521,15 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl defer runner.deinit(); switch (runner.runServer()) { error.Canceled, error.OutOfMemory => |e| return e, - error.ReadFailed => fatal("unable to receive message: {t}", .{stdin.err.?}), - error.EndOfStream => fatal("no more messages", .{}), - error.WriteFailed => fatal("unable to send message: {t}", .{stdout.err.?}), + error.ServerReadFailed => switch (stdin.err.?) { + error.Canceled => |e| return e, + else => |e| fatal("unable to receive message: {t}", .{e}), + }, + error.ServerWriteFailed => switch (stdout.err.?) { + error.Canceled => |e| return e, + else => |e| fatal("unable to send message: {t}", .{e}), + }, + error.ServerEndOfStream => fatal("no more messages", .{}), error.AlreadyReported => { var eb = try runner.eb_wip.toOwnedBundle(""); defer eb.deinit(init.gpa); @@ -1590,9 +1652,15 @@ pub fn main(init: std.process.Init) (std.mem.Allocator.Error || std.Io.Cancelabl }; const success = runClient(init.gpa, arena, init.io, &client, &args) catch |err| switch (err) { error.OutOfMemory => |e| return e, - error.ReadFailed => fatal("unable to receive message: {t}", .{child_reader.err.?}), + error.ReadFailed => switch (child_reader.err.?) { + error.Canceled => |e| return e, + else => |e| fatal("unable to receive message: {t}", .{e}), + }, + error.WriteFailed => switch (child_writer.err.?) { + error.Canceled => |e| return e, + else => |e| fatal("unable to send message: {t}", .{e}), + }, error.EndOfStream => fatal("no more messages", .{}), - error.WriteFailed => fatal("unable to send message: {t}", .{child_writer.err.?}), }; _ = child.wait(init.io) catch |err| switch (err) { error.Canceled => |e| return e, @@ -1734,12 +1802,11 @@ fn runClient( switch (hdr.tag) { else => try client.in.discardAll(hdr.bytes_len), .zig_version => { - const expected = @import("builtin").zig_version_string; - const actual = try client.in.take(hdr.bytes_len); - if (!std.mem.eql(u8, expected, actual)) { + const actual_version = try client.in.take(hdr.bytes_len); + if (!std.mem.eql(u8, zig_version_string, actual_version)) { try stderr.print( "error: zig version mismatch compiler test runner vs compiler: {q} vs {q}\n", - .{ expected, actual }, + .{ zig_version_string, actual_version }, ); try stderr.flush(); std.process.exit(1); -- 2.54.0 From 36a308ce42b8dc8c33c733b587152ee533b4bbd4 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Sat, 26 Sep 2026 20:11:45 -0700 Subject: [PATCH 23/23] Threaded: Reduce the number of Windows rename ACCESS_DENIED retries to 3 --- lib/std/Io/Threaded.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c7e68e38615c1fd7061735021b625f0177fa9711..ab888c5e6ee58558a7aa65d1cac8bb263b8a4d2a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1569,6 +1569,10 @@ const poll_buffer_len = 64; pub const default_PATH = "/usr/local/bin:/bin:/usr/bin"; /// There are multiple kernel bugs being worked around with retries. const max_windows_kernel_bug_retries = 13; +/// Similar in nature to `max_windows_kernel_bug_retries`, but much more likely to +/// be a real error that won't go away after a retry, so the number of retries +/// is lower. +const max_windows_rename_retries = 3; comptime { if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX); @@ -7738,7 +7742,7 @@ fn dirRenameWindowsInner( }, .ACCESS_DENIED => { syscall.finish(); - if (max_windows_kernel_bug_retries - attempt == 0) break :need_fallback false; + if (max_windows_rename_retries - attempt == 0) break :need_fallback false; try parking_sleep.sleep(.{ .duration = .{ .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1), .clock = .awake, @@ -7775,7 +7779,7 @@ fn dirRenameWindowsInner( .CANCELLED => try syscall.checkCancel(), .ACCESS_DENIED => { syscall.finish(); - if (max_windows_kernel_bug_retries - attempt == 0) break; + if (max_windows_rename_retries - attempt == 0) break; try parking_sleep.sleep(.{ .duration = .{ .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1), .clock = .awake, -- 2.54.0