1const Run = @This();
2
3const builtin = @import("builtin");
4
5const std = @import("std");
6const Cache = std.Build.Cache;
7const Configuration = std.Build.Configuration;
8const Dir = std.Io.Dir;
9const EnvMap = std.process.Environ.Map;
10const Io = std.Io;
11const Path = std.Build.Cache.Path;
12const assert = std.debug.assert;
13const mem = std.mem;
14const process = std.process;
15const Allocator = std.mem.Allocator;
16
17const Step = @import("../Step.zig");
18const Maker = @import("../../Maker.zig");
19const Fuzz = @import("../../Maker/Fuzz.zig");
20
21/// If this is a Zig unit test binary, this tracks the names of the unit
22/// tests that are also fuzz tests. Indexes cannot be used as they may
23/// change between reruns.
24fuzz_tests: std.ArrayList([]const u8) = .empty,
25cached_test_metadata: ?CachedTestMetadata = null,
26
27/// Populated during the fuzz phase if this run step corresponds to a unit test
28/// executable that contains fuzz tests.
29rebuilt_executable: ?Path = null,
30
31pub fn make(
32 run: *Run,
33 run_index: Configuration.Step.Index,
34 maker: *Maker,
35 progress_node: std.Progress.Node,
36) Step.ExtendedMakeError!void {
37 const graph = maker.graph;
38 const gpa = maker.gpa;
39 const step = maker.stepByIndex(run_index);
40 const io = graph.io;
41 const conf = &maker.scanned_config.configuration;
42 const conf_step = run_index.ptr(conf);
43 const conf_run = conf_step.extended.get(conf.extra).run;
44 const cache_root = graph.local_cache_root;
45
46 var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
47 defer arena_allocator.deinit();
48 const arena = arena_allocator.allocator();
49
50 var argv_list: std.ArrayList([]const u8) = .empty;
51 defer argv_list.deinit(gpa);
52
53 var output_placeholders: std.ArrayList(IndexedOutput) = .empty;
54 defer output_placeholders.deinit(gpa);
55
56 var man = graph.cache.obtain();
57 defer man.deinit();
58
59 if (conf_run.environ_map.value) |environ_map_index| {
60 const environ_map = environ_map_index.get(conf);
61 for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| {
62 man.hash.addBytesZ(key.slice(conf));
63 man.hash.addBytesZ(value.slice(conf));
64 }
65 }
66
67 for (conf_run.preopens.slice) |preopen| {
68 man.hash.addBytesZ(preopen.name.slice(conf));
69 const cwd_path = try maker.resolveLazyPathIndex(arena, preopen.path, run_index);
70 man.hash.addBytes(try cwd_path.toString(arena));
71 }
72
73 man.hash.add(graph.fuzzing);
74 man.hash.add(conf_run.flags.color);
75 man.hash.add(conf_run.flags.disable_zig_progress);
76
77 var any_dep_files = false;
78 var any_output_args = false;
79 var any_cli_positionals = false;
80
81 for (conf_run.args.slice) |arg_index| {
82 const arg = arg_index.get(conf);
83 try argv_list.ensureUnusedCapacity(gpa, 1);
84 switch (arg.flags.tag) {
85 .string => {
86 const prefix = arg.prefix.value.?.slice(conf);
87 argv_list.appendAssumeCapacity(prefix);
88 man.hash.addBytesZ(prefix);
89 },
90 .path_file => {
91 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
92 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
93 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
94 argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
95 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
96 }));
97 man.hash.add(arg.flags.make_absolute);
98 man.hash.addBytesZ(prefix);
99 man.hash.addBytesZ(suffix);
100 _ = try man.addFilePath(file_path, null);
101 },
102 .path_directory => {
103 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
104 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
105 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
106 const resolved_arg = try mem.concat(arena, u8, &.{
107 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
108 });
109 argv_list.appendAssumeCapacity(resolved_arg);
110 man.hash.add(arg.flags.make_absolute);
111 man.hash.addBytes(resolved_arg);
112 },
113 .file_content => {
114 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
115 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
116 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
117
118 var result: std.Io.Writer.Allocating = .init(arena);
119 result.writer.writeAll(prefix) catch return error.OutOfMemory;
120
121 const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err|
122 return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err });
123 defer file.close(io);
124
125 var file_reader = file.reader(io, &.{});
126 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
127 error.ReadFailed => switch (file_reader.err.?) {
128 error.Canceled => |e| return e,
129 else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }),
130 },
131 error.WriteFailed => return error.OutOfMemory,
132 };
133 result.writer.writeAll(suffix) catch return error.OutOfMemory;
134
135 argv_list.appendAssumeCapacity(result.written());
136 man.hash.addBytesZ(prefix);
137 man.hash.addBytesZ(suffix);
138 _ = try man.addFilePath(file_path, null);
139 },
140 .artifact => {
141 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
142 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
143 const producer_index = arg.producer.value.?;
144 const producer_step = producer_index.ptr(conf);
145 const producer = producer_step.extended.get(conf.extra).compile;
146 const producer_make_comp_step = maker.stepByIndex(producer_index);
147 const producer_make_comp = &producer_make_comp_step.extended.compile;
148
149 const file_path = producer_make_comp.installed_path orelse maker.generatedPath(producer.generated_bin.value.?).*;
150
151 argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
152 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
153 }));
154
155 man.hash.add(arg.flags.make_absolute);
156 man.hash.addBytesZ(prefix);
157 man.hash.addBytesZ(suffix);
158 _ = try man.addFilePath(file_path, null);
159 },
160 .output_file, .output_directory => {
161 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
162 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
163 const basename = arg.basename.value.?.slice(conf);
164
165 man.hash.add(arg.flags.make_absolute);
166 man.hash.addBytesZ(prefix);
167 man.hash.addBytesZ(basename);
168 man.hash.addBytesZ(suffix);
169 man.hash.add(arg.flags.dep_file);
170
171 any_dep_files = any_dep_files or arg.flags.dep_file;
172 any_output_args = true;
173
174 // Add a placeholder into the argument list because we need the
175 // manifest hash to be updated with all arguments before the
176 // object directory is computed.
177 try output_placeholders.append(gpa, .{
178 .index = @intCast(argv_list.items.len),
179 .arg_index = arg_index,
180 });
181 argv_list.items.len += 1;
182 },
183 .passthru => {
184 any_cli_positionals = true;
185 if (maker.run_args) |run_args| {
186 try argv_list.appendSlice(gpa, run_args);
187 man.hash.addListOfBytes(run_args);
188 }
189 },
190 .enable_darling => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value),
191 .enable_qemu => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value),
192 .enable_rosetta => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value),
193 .enable_wasmtime => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value),
194 .enable_wine => thirdPartyToggle(&man.hash, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value),
195 }
196 }
197
198 man.hash.add(conf_run.flags.test_runner_mode);
199 if (conf_run.flags.test_runner_mode) {
200 const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false);
201
202 try argv_list.ensureUnusedCapacity(gpa, 3);
203 argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string}));
204 argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed}));
205 argv_list.appendAssumeCapacity("--listen=-");
206 }
207
208 switch (conf_run.stdin.u) {
209 .bytes => |bytes| {
210 man.hash.addBytes(bytes.slice(conf));
211 },
212 .lazy_path => |lazy_path| {
213 const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
214 _ = try man.addFilePath(file_path, null);
215 },
216 .none => {},
217 }
218
219 if (conf_run.captured_stdout.value) |captured| {
220 man.hash.addBytes(captured.basename.slice(conf));
221 man.hash.add(conf_run.flags.stdout_trim_whitespace);
222 }
223
224 if (conf_run.captured_stderr.value) |captured| {
225 man.hash.addBytes(captured.basename.slice(conf));
226 man.hash.add(conf_run.flags.stderr_trim_whitespace);
227 }
228
229 switch (conf_run.flags.stdio) {
230 .infer_from_args, .inherit, .zig_test => {},
231 .check => {
232 man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else "");
233 man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else "");
234 for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf));
235 for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf));
236 man.hash.add(conf_run.flags2.expect_term_status);
237 man.hash.addOptional(conf_run.expect_term_value.value);
238 },
239 }
240
241 for (conf_run.file_inputs.slice) |lazy_path| {
242 const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
243 _ = try man.addFilePath(file_path, null);
244 }
245
246 if (conf_run.cwd.value) |lazy_path| {
247 const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
248 _ = man.hash.addBytes(try cwd_path.toString(arena));
249 }
250
251 // Whether the Run step has side effects *other than* updating the output arguments.
252 // When fuzzing we need to always run the test runner to populate fuzz_tests.
253 const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or
254 switch (conf_run.flags.stdio) {
255 .infer_from_args => !any_output_args and
256 conf_run.captured_stdout.value == null and
257 conf_run.captured_stderr.value == null,
258 .inherit => true,
259 .check, .zig_test => false,
260 };
261
262 if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) {
263 // Cache hit; skip running command.
264 const digest = man.final();
265 try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
266 try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest);
267 step.result_cached = true;
268 return;
269 }
270
271 if (!any_dep_files) {
272 // We already know the final output paths; use them directly.
273 const digest = if (has_side_effects) man.hash.final() else man.final();
274 const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest;
275 try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
276 try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, argv_list.items);
277 try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, output_dir_path, null);
278 if (!has_side_effects) try step.writeManifestAndWatch(maker, &man);
279 return;
280 }
281
282 // We do not know the final output paths yet; use temporary directory to run the command.
283 var rand_int: u64 = undefined;
284 io.random(@ptrCast(&rand_int));
285 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
286
287 try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, argv_list.items);
288 try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, tmp_dir_path, null);
289
290 for (output_placeholders.items) |placeholder| {
291 const arg = placeholder.arg_index.get(conf);
292 switch (arg.flags.tag) {
293 .output_file => if (arg.flags.dep_file) {
294 const generated_path = maker.generatedPath(arg.generated.value.?).*;
295 const result = if (has_side_effects)
296 man.addDepFile(generated_path.root_dir.handle, generated_path.sub_path)
297 else
298 man.addDepFilePost(generated_path.root_dir.handle, generated_path.sub_path);
299 result catch |err| switch (err) {
300 error.OutOfMemory, error.Canceled => |e| return e,
301 else => |e| return step.fail(maker, "failed adding to cache the file {f}: {t}", .{
302 generated_path, e,
303 }),
304 };
305 },
306 .output_directory => continue,
307 else => unreachable,
308 }
309 }
310
311 const digest = if (has_side_effects) man.hash.final() else man.final();
312
313 const any_output = output_placeholders.items.len > 0 or
314 conf_run.captured_stdout.value != null or conf_run.captured_stderr.value != null;
315
316 if (any_output) {
317 // Rename into place.
318 const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = tmp_dir_path };
319 const dst_path: Path = .{ .root_dir = cache_root, .sub_path = "o" ++ Dir.path.sep_str ++ &digest };
320 Dir.rename(
321 tmp_path.root_dir.handle,
322 tmp_path.sub_path,
323 dst_path.root_dir.handle,
324 dst_path.sub_path,
325 io,
326 ) catch |err| switch (err) {
327 error.DirNotEmpty => {
328 dst_path.root_dir.handle.deleteTree(io, dst_path.sub_path) catch |del_err|
329 return step.fail(maker, "failed to remove tree {f}: {t}", .{ dst_path, del_err });
330
331 Dir.rename(
332 tmp_path.root_dir.handle,
333 tmp_path.sub_path,
334 dst_path.root_dir.handle,
335 dst_path.sub_path,
336 io,
337 ) catch |retry_err| return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{
338 tmp_path, dst_path, retry_err,
339 });
340 },
341 else => return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{
342 tmp_path, dst_path, err,
343 }),
344 };
345 }
346
347 if (!has_side_effects) try step.writeManifestAndWatch(maker, &man);
348
349 try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
350 try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest);
351
352 // The utility functions that spawn the child process must unconditionally allocate
353 // the failed command because at that point it is not known whether the step will
354 // pass or fail based on the process termination. Here we free the memory since
355 // the step has succeeded.
356 step.clearFailedCommand(gpa);
357}
358
359fn thirdPartyToggle(
360 man_hash: ?*Cache.HashHelper,
361 argv_list: *std.ArrayList([]const u8),
362 conf: *const Configuration,
363 setting: bool,
364 enable: ?Configuration.String,
365 disable: ?Configuration.String,
366) void {
367 if (setting) {
368 if (enable) |string| {
369 const slice = string.slice(conf);
370 if (man_hash) |h| h.addBytesZ(slice);
371 argv_list.appendAssumeCapacity(slice);
372 }
373 } else {
374 if (disable) |string| {
375 const slice = string.slice(conf);
376 if (man_hash) |h| h.addBytesZ(slice);
377 argv_list.appendAssumeCapacity(slice);
378 }
379 }
380}
381
382/// Reads stdout of a Zig test process until a termination condition is reached:
383/// * A write fails, indicating the child unexpectedly closed stdin
384/// * A test (or a response from the test runner) times out
385/// * The wait fails, indicating the child closed stdout and stderr
386fn waitZigTest(
387 arena: Allocator,
388 run: *Run,
389 run_index: Configuration.Step.Index,
390 maker: *Maker,
391 child: *process.Child,
392 progress_node: std.Progress.Node,
393 multi_reader: *Io.File.MultiReader,
394 opt_metadata: *?TestMetadata,
395 results: *Step.TestResults,
396) !union(enum) {
397 write_failed: anyerror,
398 no_poll: struct {
399 active_test_index: ?u32,
400 ns_elapsed: u64,
401 },
402 timeout: struct {
403 active_test_index: ?u32,
404 ns_elapsed: u64,
405 },
406} {
407 const graph = maker.graph;
408 const gpa = maker.gpa;
409 const io = graph.io;
410 const step = maker.stepByIndex(run_index);
411
412 var sub_prog_node: ?std.Progress.Node = null;
413 defer if (sub_prog_node) |n| n.end();
414
415 const stdout = multi_reader.reader(0);
416 const stderr = multi_reader.reader(1);
417
418 var stdin_writer = child.stdin.?.writerStreaming(io, &.{});
419
420 var client: std.zig.Client = .{
421 .in = stdout,
422 .out = &stdin_writer.interface,
423 };
424
425 if (opt_metadata.*) |*md| {
426 // Previous unit test process died or was killed; we're continuing where it left off
427 requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
428 } else {
429 // Running unit tests normally
430 run.fuzz_tests.clearRetainingCapacity();
431 client.serveBodylessMessage(.query_test_metadata) catch |err| return .{ .write_failed = err };
432 }
433
434 var active_test_index: ?u32 = null;
435
436 var last_update: Io.Clock.Timestamp = .now(io, .awake);
437
438 // This timeout is used when we're waiting on the test runner itself rather than a user-specified
439 // test. For instance, if the test runner leaves this much time between us requesting a test to
440 // start and it acknowledging the test starting, we terminate the child and raise an error. This
441 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
442 const response_timeout: Io.Clock.Duration = t: {
443 const ns = @max(maker.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
444 break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
445 };
446 const test_timeout: ?Io.Clock.Duration = if (maker.unit_test_timeout_ns) |ns| .{
447 .clock = .awake,
448 .raw = .fromNanoseconds(ns),
449 } else null;
450
451 while (true) {
452 const timeout: Io.Timeout = t: {
453 const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
454 const duration = opt_duration orelse break :t .none;
455 break :t .{ .deadline = last_update.addDuration(duration) };
456 };
457
458 const header = client.receiveMessageWithMultiReader(multi_reader, timeout) catch |err| switch (err) {
459 error.Timeout => return .{ .timeout = .{
460 .active_test_index = active_test_index,
461 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
462 } },
463 error.EndOfStream => return .{ .no_poll = .{
464 .active_test_index = active_test_index,
465 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
466 } },
467 else => |e| return e,
468 };
469 const body = client.in.take(header.bytes_len) catch unreachable;
470 var body_r: std.Io.Reader = .fixed(body);
471
472 switch (header.tag) {
473 .zig_version => {
474 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
475 maker,
476 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
477 .{ builtin.zig_version_string, body },
478 );
479 },
480 .test_metadata => {
481 // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
482 // only request it once (and importantly, we don't re-request it if we kill and
483 // restart the test runner).
484 assert(opt_metadata.* == null);
485
486 const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
487 results.test_count = tm_hdr.tests_len;
488
489 const names = try arena.alloc(u32, results.test_count);
490 for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
491
492 const expected_panic_msgs = try arena.alloc(u32, results.test_count);
493 for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
494
495 const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
496
497 progress_node.setEstimatedTotalItems(names.len);
498 opt_metadata.* = .{
499 .string_bytes = try arena.dupe(u8, string_bytes),
500 .ns_per_test = try arena.alloc(u64, results.test_count),
501 .names = names,
502 .expected_panic_msgs = expected_panic_msgs,
503 .next_index = 0,
504 .prog_node = progress_node,
505 };
506 @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
507
508 active_test_index = null;
509 last_update = .now(io, .awake);
510
511 requestNextTest(&client, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
512 },
513 .test_started => {
514 active_test_index = opt_metadata.*.?.next_index - 1;
515 last_update = .now(io, .awake);
516 },
517 .test_results => {
518 const md = &opt_metadata.*.?;
519
520 const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
521 assert(tr_hdr.index == active_test_index);
522
523 switch (tr_hdr.flags.status) {
524 .pass => {},
525 .skip => results.skip_count +|= 1,
526 .fail => results.fail_count +|= 1,
527 }
528 const leak_count = tr_hdr.flags.leak_count;
529 const log_err_count = tr_hdr.flags.log_err_count;
530 results.leak_count +|= leak_count;
531 results.log_err_count +|= log_err_count;
532
533 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
534
535 if (tr_hdr.flags.status == .fail) {
536 const name = md.testName(tr_hdr.index);
537 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
538 stderr.tossBuffered();
539 if (stderr_bytes.len == 0) {
540 try step.addError(maker, "'{s}' failed without output", .{name});
541 } else {
542 try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes });
543 }
544 } else if (leak_count > 0) {
545 const name = md.testName(tr_hdr.index);
546 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
547 stderr.tossBuffered();
548 try step.addError(maker, "'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
549 } else if (log_err_count > 0) {
550 const name = md.testName(tr_hdr.index);
551 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
552 stderr.tossBuffered();
553 try step.addError(maker, "'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
554 }
555
556 active_test_index = null;
557
558 const now: Io.Clock.Timestamp = .now(io, .awake);
559 md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
560 last_update = now;
561
562 requestNextTest(&client, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
563 },
564 else => {}, // ignore other messages
565 }
566 }
567}
568
569const FuzzTestRunner = struct {
570 run: *Run,
571 run_index: Configuration.Step.Index,
572 ctx: FuzzContext,
573 coverage_id: ?u64,
574
575 instances: []Instance,
576 /// The indexes of this are layed out such that it is effectively an array
577 /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr.
578 batch: Io.Batch,
579 /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter.
580 pending_broadcasts: std.ArrayList(u8),
581 broadcast: std.ArrayList(u8),
582 broadcast_undelivered: u32,
583
584 const Instance = struct {
585 child: process.Child,
586 message: std.array_list.Aligned(u8, .@"4"),
587 broadcast_written: usize,
588 stderr: std.ArrayList(u8),
589 stdin_vec: [1][]u8,
590 stdout_vec: [1][]u8,
591 stderr_vec: [1][]u8,
592 progress_node: std.Progress.Node,
593
594 fn messageHeader(instance: *Instance) InHeader {
595 assert(instance.message.items.len >= @sizeOf(InHeader));
596 const header_ptr: *InHeader = @ptrCast(instance.message.items);
597 var header = header_ptr.*;
598 if (std.builtin.Endian.native != .little) {
599 std.mem.byteSwapAllFields(InHeader, &header);
600 }
601 return header;
602 }
603 };
604
605 const PendingBroadcastFooter = struct {
606 from_id: u32,
607 body_len: u32,
608 };
609
610 const InHeader = std.zig.Server.Message.Header;
611 const OutHeader = std.zig.Client.Message.Header;
612
613 const stdin_i = 0;
614 const stdout_i = 1;
615 const stderr_i = 2;
616
617 fn init(
618 run: *Run,
619 run_index: Configuration.Step.Index,
620 ctx: FuzzContext,
621 progress_node: std.Progress.Node,
622 spawn_options: process.SpawnOptions,
623 ) !FuzzTestRunner {
624 const maker = ctx.fuzz.maker;
625 const graph = maker.graph;
626 const gpa = maker.gpa;
627 const io = graph.io;
628
629 const n_instances = switch (ctx.fuzz.mode) {
630 .forever => graph.max_jobs orelse @min(
631 std.Thread.getCpuCount() catch 1,
632 (std.math.maxInt(u32) - 2) / 3,
633 ),
634 .limit => 1,
635 };
636 const instances = try gpa.alloc(Instance, n_instances);
637 errdefer gpa.free(instances);
638 const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3);
639 errdefer gpa.free(batch_storage);
640
641 @memset(instances, .{
642 .child = undefined,
643 .message = .empty,
644 .broadcast_written = undefined,
645 .stderr = .empty,
646 .stdin_vec = undefined,
647 .stdout_vec = undefined,
648 .stderr_vec = undefined,
649 .progress_node = undefined,
650 });
651 for (0.., instances) |id, *instance| {
652 errdefer for (instances[0..id]) |*spawned| {
653 spawned.child.kill(io);
654 spawned.progress_node.end();
655 };
656 instance.child = try process.spawn(io, spawn_options);
657 instance.progress_node = progress_node.start("starting fuzzer", 0);
658 }
659
660 return .{
661 .run = run,
662 .run_index = run_index,
663 .ctx = ctx,
664 .coverage_id = null,
665
666 .instances = instances,
667 .batch = .init(batch_storage),
668 .pending_broadcasts = .empty,
669 .broadcast = .empty,
670 .broadcast_undelivered = 0,
671 };
672 }
673
674 fn deinit(f: *FuzzTestRunner) void {
675 const maker = f.ctx.fuzz.maker;
676 const run_index = f.run_index;
677
678 const graph = maker.graph;
679 const gpa = maker.gpa;
680 const io = graph.io;
681 const step = maker.stepByIndex(run_index);
682
683 f.batch.cancel(io);
684 gpa.free(f.batch.storage);
685 var total_rss: usize = 0;
686 for (f.instances) |*instance| {
687 instance.child.kill(io);
688 instance.message.deinit(gpa);
689 instance.stderr.deinit(gpa);
690 instance.progress_node.end();
691 total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0;
692 }
693 step.result_peak_rss = @max(step.result_peak_rss, total_rss);
694 gpa.free(f.instances);
695 }
696
697 fn startInstances(f: *FuzzTestRunner) !void {
698 const maker = f.ctx.fuzz.maker;
699 const run_index = f.run_index;
700 const run = f.run;
701
702 const graph = maker.graph;
703 const io = graph.io;
704 const step = maker.stepByIndex(run_index);
705
706 for (0.., f.instances) |id, *instance| {
707 const id32: u32 = @intCast(id);
708 var writer = instance.child.stdin.?.writerStreaming(io, &.{});
709 const client: std.zig.Client = .{
710 .in = undefined,
711 .out = &writer.interface,
712 };
713 (switch (f.ctx.fuzz.mode) {
714 .forever => client.serveRunFuzzTestMessage(
715 run.fuzz_tests.items,
716 .forever,
717 id32,
718 ),
719 .limit => |limit| client.serveRunFuzzTestMessage(
720 run.fuzz_tests.items,
721 .iterations,
722 limit.amount,
723 ),
724 }) catch |write_err| {
725 // The runner unexpectedly closed stdin, which means it crashed during initialization.
726 // Clean up everything and wait for the child to exit.
727 instance.child.stdin.?.close(io);
728 instance.child.stdin = null;
729 const term = try instance.child.wait(io);
730 return step.fail(
731 maker,
732 "unable to write stdin ({t}); test process unexpectedly {f}",
733 .{ write_err, fmtTerm(term) },
734 );
735 };
736
737 try f.addStdoutRead(id32, @sizeOf(InHeader));
738 try f.addStderrRead(id32);
739 }
740 }
741
742 fn listen(f: *FuzzTestRunner) !void {
743 const maker = f.ctx.fuzz.maker;
744 const graph = maker.graph;
745 const io = graph.io;
746
747 while (true) {
748 try f.batch.awaitConcurrent(io, .none);
749 while (f.batch.next()) |completion| {
750 const id = completion.index / 3;
751 const result = completion.result;
752 switch (completion.index % 3) {
753 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) {
754 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
755 // that all stderr is collected.
756 error.BrokenPipe => continue,
757 else => |write_e| return write_e,
758 }),
759 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) {
760 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
761 // that all stderr is collected.
762 error.EndOfStream => continue,
763 else => |read_e| return read_e,
764 }),
765 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) {
766 error.EndOfStream => return f.instanceEos(id),
767 else => |read_e| return read_e,
768 }),
769 else => unreachable,
770 }
771 }
772 }
773 }
774
775 fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
776 const maker = f.ctx.fuzz.maker;
777 const instance = &f.instances[id];
778 const run_index = f.run_index;
779 const run = f.run;
780
781 const graph = maker.graph;
782 const gpa = maker.gpa;
783 const io = graph.io;
784 const step = maker.stepByIndex(run_index);
785
786 instance.message.items.len += n;
787 const total_read = instance.message.items.len;
788 if (total_read < @sizeOf(InHeader)) {
789 try f.addStdoutRead(id, @sizeOf(InHeader));
790 return;
791 }
792
793 const header = instance.messageHeader();
794 const body = instance.message.items[@sizeOf(InHeader)..];
795 if (body.len != header.bytes_len) {
796 try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len);
797 return;
798 }
799
800 switch (header.tag) {
801 .zig_version => {
802 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
803 maker,
804 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
805 .{ builtin.zig_version_string, body },
806 );
807 },
808 .coverage_id => {
809 var body_r: Io.Reader = .fixed(body);
810 f.coverage_id = body_r.takeInt(u64, .little) catch unreachable;
811 const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
812 const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
813 const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
814
815 const fuzz = f.ctx.fuzz;
816 fuzz.queue_mutex.lockUncancelable(io);
817 defer fuzz.queue_mutex.unlock(io);
818 try fuzz.msg_queue.append(gpa, .{ .coverage = .{
819 .id = f.coverage_id.?,
820 .cumulative = .{
821 .runs = cumulative_runs,
822 .unique = cumulative_unique,
823 .coverage = cumulative_coverage,
824 },
825 .run = run_index,
826 } });
827 fuzz.queue_cond.signal(io);
828 },
829 .fuzz_start_addr => {
830 var body_r: Io.Reader = .fixed(body);
831 const fuzz = f.ctx.fuzz;
832 const addr = body_r.takeInt(u64, .little) catch unreachable;
833
834 fuzz.queue_mutex.lockUncancelable(io);
835 defer fuzz.queue_mutex.unlock(io);
836 try fuzz.msg_queue.append(gpa, .{ .entry_point = .{
837 .addr = addr,
838 .coverage_id = f.coverage_id.?,
839 } });
840 fuzz.queue_cond.signal(io);
841 },
842 .fuzz_test_change => {
843 const test_i = std.mem.readInt(u32, body[0..4], .little);
844 instance.progress_node.setName(run.fuzz_tests.items[test_i]);
845 },
846 .broadcast_fuzz_input => {
847 if (f.instances.len == 1) {
848 // No other processes to broadcast to.
849 } else if (f.broadcast_undelivered == 0) {
850 try f.instanceBroadcast(id, body);
851 } else {
852 const footer: PendingBroadcastFooter = .{
853 .from_id = id,
854 .body_len = @intCast(body.len),
855 };
856 // There is another broadcast in progress so add this one to the queue.
857 const size = @sizeOf(PendingBroadcastFooter) + body.len;
858 try f.pending_broadcasts.ensureUnusedCapacity(gpa, size);
859 f.pending_broadcasts.appendSliceAssumeCapacity(body);
860 f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer));
861 }
862 },
863 else => {}, // ignore other messages
864 }
865
866 instance.message.clearRetainingCapacity();
867 try f.addStdoutRead(id, @sizeOf(InHeader));
868 }
869
870 fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
871 const instance = &f.instances[id];
872 instance.stderr.items.len += n;
873 try f.addStderrRead(id);
874 }
875
876 fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void {
877 const instance = &f.instances[id];
878
879 instance.broadcast_written += n;
880 if (instance.broadcast_written == f.broadcast.items.len) {
881 f.broadcast_undelivered -= 1;
882 if (f.broadcast_undelivered == 0) {
883 try f.broadcastComplete();
884 }
885 } else {
886 f.addStdinWrite(id);
887 }
888 }
889
890 fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void {
891 const maker = f.ctx.fuzz.maker;
892 const gpa = maker.gpa;
893 const instance = &f.instances[id];
894
895 try instance.message.ensureTotalCapacity(gpa, end);
896 const start = instance.message.items.len;
897 instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]};
898 f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{
899 .file = instance.child.stdout.?,
900 .data = &instance.stdout_vec,
901 } });
902 }
903
904 fn addStderrRead(f: *FuzzTestRunner, id: u32) !void {
905 const maker = f.ctx.fuzz.maker;
906 const gpa = maker.gpa;
907 const instance = &f.instances[id];
908
909 try instance.stderr.ensureUnusedCapacity(gpa, 1);
910 instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()};
911 f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{
912 .file = instance.child.stderr.?,
913 .data = &instance.stderr_vec,
914 } });
915 }
916
917 fn addStdinWrite(f: *FuzzTestRunner, id: u32) void {
918 const instance = &f.instances[id];
919
920 assert(f.broadcast.items.len != instance.broadcast_written);
921 instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]};
922 f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{
923 .file = instance.child.stdin.?,
924 .data = &instance.stdin_vec,
925 } });
926 }
927
928 fn instanceEos(f: *FuzzTestRunner, id: u32) !void {
929 const maker = f.ctx.fuzz.maker;
930 const gpa = maker.gpa;
931 const instance = &f.instances[id];
932 const run_index = f.run_index;
933
934 const graph = maker.graph;
935 const io = graph.io;
936 const step = maker.stepByIndex(run_index);
937
938 instance.child.stdin.?.close(io);
939 instance.child.stdin = null;
940 const term = try instance.child.wait(io);
941 if (!termMatches(.{ .exited = 0 }, term)) {
942 step.takeResultStderr(gpa, try f.mergedStderr(gpa));
943 try f.saveCrash(id, term);
944 return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)});
945 }
946 }
947
948 fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void {
949 const fuzz = f.ctx.fuzz;
950 const run_index = f.run_index;
951 const run = f.run;
952
953 const maker = fuzz.maker;
954 const step = maker.stepByIndex(run_index);
955 const graph = maker.graph;
956 const io = graph.io;
957 const cache_root = graph.local_cache_root;
958
959 if (f.coverage_id == null) return;
960
961 // Search for the input file corresponding to the instance
962 const InputHeader = std.Build.abi.fuzz.MmapInputHeader;
963 var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined;
964 var in_r: Io.File.Reader = undefined;
965 var in_f: Io.File = undefined;
966 var in_name_buf: [12]u8 = undefined;
967 var in_name: []const u8 = undefined;
968 var i: u32 = 0;
969 const header: InputHeader = while (true) : ({
970 if (i == std.math.maxInt(u32)) return;
971 i += 1;
972 }) {
973 const name_prefix = "f" ++ Dir.path.sep_str ++ "in";
974 in_name = std.mem.print(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable;
975 in_f = cache_root.handle.openFile(io, in_name, .{
976 .lock = .exclusive,
977 .lock_nonblocking = true,
978 }) catch |e| switch (e) {
979 error.FileNotFound => return,
980 error.WouldBlock => continue, // Can not be from
981 // the crashed instance since it is still locked.
982 else => return step.fail(maker, "failed to open file '{f}{s}': {t}", .{
983 cache_root, in_name, e,
984 }),
985 };
986
987 in_r = in_f.readerStreaming(io, &in_r_buf);
988 const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| {
989 in_f.close(io);
990 switch (e) {
991 error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{
992 cache_root, in_name, in_r.err.?,
993 }),
994 error.EndOfStream => continue,
995 }
996 };
997
998 if (header.pc_digest == f.coverage_id.? and
999 header.instance_id == id and
1000 header.test_i < run.fuzz_tests.items.len)
1001 {
1002 break header;
1003 }
1004
1005 in_f.close(io);
1006 };
1007 defer in_f.close(io);
1008
1009 // Save it to a seperate file
1010 const crash_name = "f" ++ Dir.path.sep_str ++ "crash";
1011 const out = cache_root.handle.createFile(io, crash_name, .{
1012 .lock = .exclusive, // Multiple run steps could have found a crash at the same time
1013 }) catch |e| return step.fail(maker, "failed to create file '{f}{s}': {t}", .{
1014 cache_root, crash_name, e,
1015 });
1016 defer out.close(io);
1017
1018 var out_w_buf: [512]u8 = undefined;
1019 var out_w = out.writerStreaming(io, &out_w_buf);
1020 _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) {
1021 error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{
1022 cache_root, in_name, in_r.err.?,
1023 }),
1024 error.WriteFailed => return step.fail(maker, "failed to write file '{f}{s}': {t}", .{
1025 cache_root, crash_name, out_w.err.?,
1026 }),
1027 };
1028
1029 return step.fail(maker, "test '{s}' {f}; input saved to '{f}{s}'", .{
1030 run.fuzz_tests.items[header.test_i],
1031 fmtTerm(term),
1032 cache_root,
1033 crash_name,
1034 });
1035 }
1036
1037 fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void {
1038 assert(f.instances.len > 1);
1039 assert(f.broadcast_undelivered == 0); // no other broadcast is progress
1040 assert(f.broadcast.items.len == 0);
1041 assert(from_id < f.instances.len);
1042
1043 const maker = f.ctx.fuzz.maker;
1044 const gpa = maker.gpa;
1045
1046 var out_header: OutHeader = .{
1047 .tag = .new_fuzz_input,
1048 .bytes_len = @intCast(bytes.len),
1049 };
1050 if (std.builtin.Endian.native != .little) {
1051 std.mem.byteSwapAllFields(OutHeader, &out_header);
1052 }
1053 try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len);
1054 f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header));
1055 f.broadcast.appendSliceAssumeCapacity(bytes);
1056
1057 f.broadcast_undelivered = @intCast(f.instances.len - 1);
1058 for (0.., f.instances) |to_id, *instance| {
1059 if (to_id == from_id) continue;
1060 instance.broadcast_written = 0;
1061 f.addStdinWrite(@intCast(to_id));
1062 }
1063 }
1064
1065 fn broadcastComplete(f: *FuzzTestRunner) !void {
1066 assert(f.instances.len > 1);
1067 assert(f.broadcast_undelivered == 0);
1068 f.broadcast.clearRetainingCapacity();
1069
1070 const pending = &f.pending_broadcasts;
1071 if (pending.items.len != 0) {
1072 // Another broadcast is pending; copy it over to `broadcast`
1073
1074 const footer_len = @sizeOf(PendingBroadcastFooter);
1075 const footer_bytes = pending.items[pending.items.len - footer_len ..];
1076 const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes);
1077 pending.items.len -= footer_len;
1078
1079 const body = pending.items[pending.items.len - footer.body_len ..];
1080 try f.instanceBroadcast(footer.from_id, body);
1081 pending.items.len -= body.len;
1082 }
1083 }
1084
1085 fn mergedStderr(f: *FuzzTestRunner, gpa: Allocator) Allocator.Error![]const u8 {
1086 // Collect any available stderr
1087 while (f.batch.next()) |completion| {
1088 if (completion.index % 3 != 2) continue;
1089 const len = completion.result.file_read_streaming catch continue;
1090 f.instances[completion.index / 3].stderr.items.len += len;
1091 }
1092
1093 var stderr_len: usize = 0;
1094 for (f.instances) |*instance| stderr_len += instance.stderr.items.len;
1095 const stderr = try gpa.alloc(u8, stderr_len);
1096
1097 stderr_len = 0;
1098 for (f.instances) |*instance| {
1099 @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items);
1100 stderr_len += instance.stderr.items.len;
1101 }
1102 return stderr;
1103 }
1104};
1105
1106fn evalFuzzTest(
1107 run: *Run,
1108 run_index: Configuration.Step.Index,
1109 progress_node: std.Progress.Node,
1110 spawn_options: process.SpawnOptions,
1111 fuzz_context: FuzzContext,
1112) !void {
1113 var f: FuzzTestRunner = try .init(run, run_index, fuzz_context, progress_node, spawn_options);
1114 defer f.deinit();
1115 try f.startInstances();
1116 try f.listen();
1117}
1118
1119const StdioPollEnum = enum { stdout, stderr };
1120
1121fn evalZigTest(
1122 run: *Run,
1123 run_index: Configuration.Step.Index,
1124 maker: *Maker,
1125 progress_node: std.Progress.Node,
1126 spawn_options: process.SpawnOptions,
1127 fuzz_context: ?FuzzContext,
1128) !void {
1129 if (fuzz_context != null) {
1130 try evalFuzzTest(run, run_index, progress_node, spawn_options, fuzz_context.?);
1131 return;
1132 }
1133
1134 const graph = maker.graph;
1135 const gpa = maker.gpa;
1136 const io = graph.io;
1137 const step = maker.stepByIndex(run_index);
1138
1139 // We will update this every time a child runs.
1140 step.result_peak_rss = 0;
1141
1142 var test_results: Step.TestResults = .{
1143 .test_count = 0,
1144 .skip_count = 0,
1145 .fail_count = 0,
1146 .crash_count = 0,
1147 .timeout_count = 0,
1148 .leak_count = 0,
1149 .log_err_count = 0,
1150 };
1151 var test_metadata: ?TestMetadata = null;
1152
1153 while (true) {
1154 var child = try process.spawn(io, spawn_options);
1155 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1156 var multi_reader: Io.File.MultiReader = undefined;
1157 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
1158 var child_killed = false;
1159 defer if (!child_killed) {
1160 child.kill(io);
1161 multi_reader.deinit();
1162 step.result_peak_rss = @max(
1163 step.result_peak_rss,
1164 child.resource_usage_statistics.getMaxRss() orelse 0,
1165 );
1166 };
1167
1168 switch (try waitZigTest(
1169 graph.arena,
1170 run,
1171 run_index,
1172 maker,
1173 &child,
1174 progress_node,
1175 &multi_reader,
1176 &test_metadata,
1177 &test_results,
1178 )) {
1179 .write_failed => |err| {
1180 // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
1181 // all available stderr to make our error output as useful as possible.
1182 const stderr_fr = multi_reader.fileReader(1);
1183 while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
1184 error.ReadFailed => return stderr_fr.err.?,
1185 error.EndOfStream => {},
1186 }
1187 step.takeResultStderr(gpa, try multi_reader.toOwnedSlice(1));
1188
1189 // Clean up everything and wait for the child to exit.
1190 child.stdin.?.close(io);
1191 child.stdin = null;
1192 multi_reader.deinit();
1193 child_killed = true;
1194 const term = try child.wait(io);
1195 step.result_peak_rss = @max(
1196 step.result_peak_rss,
1197 child.resource_usage_statistics.getMaxRss() orelse 0,
1198 );
1199
1200 // The individual unit test results are irrelevant: the test runner itself broke!
1201 // Fail immediately without populating `s.test_results`.
1202 return step.fail(maker, "unable to write stdin ({t}); test process unexpectedly {f}", .{
1203 err, fmtTerm(term),
1204 });
1205 },
1206 .no_poll => |no_poll| {
1207 // This might be a success (we requested exit and the child dutifully closed stdout) or
1208 // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
1209 const stderr_owned = try multi_reader.toOwnedSlice(1);
1210 var keep_stderr_owned = false;
1211 defer if (!keep_stderr_owned) gpa.free(stderr_owned);
1212
1213 // Clean up everything and wait for the child to exit.
1214 child.stdin.?.close(io);
1215 child.stdin = null;
1216 multi_reader.deinit();
1217 child_killed = true;
1218 const term = try child.wait(io);
1219 step.result_peak_rss = @max(
1220 step.result_peak_rss,
1221 child.resource_usage_statistics.getMaxRss() orelse 0,
1222 );
1223
1224 if (no_poll.active_test_index) |test_index| {
1225 // A test was running, so this is definitely a crash. Report it against that
1226 // test, and continue to the next test.
1227 test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
1228 test_results.crash_count += 1;
1229 try step.addError(maker, "'{s}' {f}{s}{s}", .{
1230 test_metadata.?.testName(test_index),
1231 fmtTerm(term),
1232 if (stderr_owned.len != 0) " with stderr:\n" else "",
1233 std.mem.trim(u8, stderr_owned, "\n"),
1234 });
1235 continue;
1236 }
1237
1238 // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
1239 step.takeResultStderr(gpa, stderr_owned);
1240 keep_stderr_owned = true;
1241
1242 const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
1243 if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
1244 // The individual unit test results are irrelevant: the test runner itself broke!
1245 // Fail immediately without populating `s.test_results`.
1246 return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)});
1247 }
1248
1249 // We're done with all of the tests! Commit the test results and return.
1250 step.test_results = test_results;
1251 if (test_metadata) |tm| {
1252 run.cached_test_metadata = tm.toCachedTestMetadata();
1253 if (maker.web_server) |ws| {
1254 if (graph.time_report) {
1255 ws.updateTimeReportRunTest(
1256 run_index,
1257 &run.cached_test_metadata.?,
1258 tm.ns_per_test,
1259 );
1260 }
1261 }
1262 }
1263 return;
1264 },
1265 .timeout => |timeout| {
1266 const stderr_owned = try multi_reader.toOwnedSlice(1);
1267 var keep_stderr_owned = false;
1268 defer if (!keep_stderr_owned) gpa.free(stderr_owned);
1269
1270 if (timeout.active_test_index) |test_index| {
1271 // A test was running. Report the timeout against that test, and continue on to
1272 // the next test.
1273 test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
1274 test_results.timeout_count += 1;
1275 try step.addError(maker, "'{s}' timed out after {f}{s}{s}", .{
1276 test_metadata.?.testName(test_index),
1277 Io.Duration{ .nanoseconds = timeout.ns_elapsed },
1278 if (stderr_owned.len != 0) " with stderr:\n" else "",
1279 std.mem.trim(u8, stderr_owned, "\n"),
1280 });
1281 continue;
1282 }
1283 // Just log an error and let the child be killed.
1284 step.takeResultStderr(gpa, stderr_owned);
1285 keep_stderr_owned = true;
1286
1287 // The individual unit test results in `results` are irrelevant: the test runner
1288 // is broken! Fail immediately without populating `s.test_results`.
1289 return step.fail(maker, "test runner failed to respond for {f}", .{
1290 Io.Duration{ .nanoseconds = timeout.ns_elapsed },
1291 });
1292 },
1293 }
1294 comptime unreachable;
1295 }
1296}
1297
1298const TestMetadata = struct {
1299 names: []const u32,
1300 ns_per_test: []u64,
1301 expected_panic_msgs: []const u32,
1302 string_bytes: []const u8,
1303 next_index: u32,
1304 prog_node: std.Progress.Node,
1305
1306 fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
1307 return .{
1308 .names = tm.names,
1309 .string_bytes = tm.string_bytes,
1310 };
1311 }
1312
1313 fn testName(tm: TestMetadata, index: u32) []const u8 {
1314 return tm.toCachedTestMetadata().testName(index);
1315 }
1316};
1317
1318pub const CachedTestMetadata = struct {
1319 names: []const u32,
1320 string_bytes: []const u8,
1321
1322 pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
1323 return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
1324 }
1325};
1326
1327fn requestNextTest(client: *std.zig.Client, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
1328 while (metadata.next_index < metadata.names.len) {
1329 const i = metadata.next_index;
1330 metadata.next_index += 1;
1331
1332 if (metadata.expected_panic_msgs[i] != 0) continue;
1333
1334 const name = metadata.testName(i);
1335 if (sub_prog_node.*) |n| n.end();
1336 sub_prog_node.* = metadata.prog_node.start(name, 0);
1337
1338 try client.serveRunTest(i);
1339 return;
1340 } else {
1341 metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
1342 try client.serveBodylessMessage(.exit);
1343 }
1344}
1345
1346/// Uses `arena` to allocate the result.
1347fn evalGeneric(
1348 arena: Allocator,
1349 run_index: Configuration.Step.Index,
1350 maker: *Maker,
1351 spawn_options: process.SpawnOptions,
1352) !EvalGenericResult {
1353 const graph = maker.graph;
1354 const io = graph.io;
1355 const conf = &maker.scanned_config.configuration;
1356 const conf_step = run_index.ptr(conf);
1357 const conf_run = conf_step.extended.get(conf.extra).run;
1358 const step = maker.stepByIndex(run_index);
1359
1360 var child = try process.spawn(io, spawn_options);
1361 defer child.kill(io);
1362
1363 switch (conf_run.stdin.u) {
1364 .bytes => |bytes| {
1365 child.stdin.?.writeStreamingAll(io, bytes.slice(conf)) catch |err| {
1366 return step.fail(maker, "failed to write stdin: {t}", .{err});
1367 };
1368 child.stdin.?.close(io);
1369 child.stdin = null;
1370 },
1371 .lazy_path => |lazy_path| {
1372 const path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
1373 const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| {
1374 return step.fail(maker, "failed to open stdin file: {t}", .{err});
1375 };
1376 defer file.close(io);
1377 // TODO https://github.com/ziglang/zig/issues/23955
1378 var read_buffer: [1024]u8 = undefined;
1379 var file_reader = file.reader(io, &read_buffer);
1380 var write_buffer: [1024]u8 = undefined;
1381 var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer);
1382 _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
1383 error.ReadFailed => return step.fail(maker, "failed to read from {f}: {t}", .{
1384 path, file_reader.err.?,
1385 }),
1386 error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{
1387 stdin_writer.err.?,
1388 }),
1389 };
1390 stdin_writer.interface.flush() catch |err| switch (err) {
1391 error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{
1392 stdin_writer.err.?,
1393 }),
1394 };
1395 child.stdin.?.close(io);
1396 child.stdin = null;
1397 },
1398 .none => {},
1399 }
1400
1401 var stdout_bytes: ?[]const u8 = null;
1402 var stderr_bytes: ?[]const u8 = null;
1403
1404 if (child.stdout) |stdout| {
1405 if (child.stderr) |stderr| {
1406 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1407 var multi_reader: Io.File.MultiReader = undefined;
1408 multi_reader.init(arena, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
1409
1410 const stdout_reader = multi_reader.reader(0);
1411 const stderr_reader = multi_reader.reader(1);
1412
1413 while (multi_reader.fill(64, .none)) |_| {
1414 if (conf_run.stdio_limit.value) |limit| {
1415 if (stdout_reader.buffered().len > limit)
1416 return error.StdoutStreamTooLong;
1417 if (stderr_reader.buffered().len > limit)
1418 return error.StderrStreamTooLong;
1419 }
1420 } else |err| switch (err) {
1421 error.Timeout => unreachable,
1422 error.EndOfStream => {},
1423 else => |e| return e,
1424 }
1425
1426 try multi_reader.checkAnyError();
1427
1428 stdout_bytes = multi_reader.reader(0).buffered();
1429 stderr_bytes = multi_reader.reader(1).buffered();
1430 } else {
1431 var stdout_reader = stdout.readerStreaming(io, &.{});
1432 const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited64(x) else .unlimited;
1433 stdout_bytes = stdout_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) {
1434 error.OutOfMemory => |e| return e,
1435 error.ReadFailed => return stdout_reader.err.?,
1436 error.StreamTooLong => return error.StdoutStreamTooLong,
1437 };
1438 }
1439 } else if (child.stderr) |stderr| {
1440 var stderr_reader = stderr.readerStreaming(io, &.{});
1441 const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited64(x) else .unlimited;
1442 stderr_bytes = stderr_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) {
1443 error.OutOfMemory => |e| return e,
1444 error.ReadFailed => return stderr_reader.err.?,
1445 error.StreamTooLong => return error.StderrStreamTooLong,
1446 };
1447 }
1448
1449 if (stderr_bytes) |bytes| if (bytes.len > 0) {
1450 // Treat stderr as an error message.
1451 const stderr_is_diagnostic = conf_run.captured_stderr.value == null and switch (conf_run.flags.stdio) {
1452 .check => !checksContainStderr(&conf_run),
1453 else => true,
1454 };
1455 if (stderr_is_diagnostic) {
1456 try step.setResultStderr(maker.gpa, bytes);
1457 }
1458 };
1459
1460 step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
1461
1462 return .{
1463 .term = try child.wait(io),
1464 .stdout = stdout_bytes,
1465 .stderr = stderr_bytes,
1466 };
1467}
1468
1469const IndexedOutput = struct {
1470 index: u32,
1471 arg_index: Configuration.Step.Run.Arg.Index,
1472};
1473
1474pub fn rerunInFuzzMode(
1475 run: *Run,
1476 run_index: Configuration.Step.Index,
1477 fuzz: *Fuzz,
1478 prog_node: std.Progress.Node,
1479) !void {
1480 const maker = fuzz.maker;
1481 const graph = maker.graph;
1482 const step = maker.stepByIndex(run_index);
1483 const io = graph.io;
1484 const gpa = maker.gpa;
1485 const conf = &maker.scanned_config.configuration;
1486 const conf_step = run_index.ptr(conf);
1487 const conf_run = conf_step.extended.get(conf.extra).run;
1488 const cache_root = graph.local_cache_root;
1489
1490 var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
1491 defer arena_allocator.deinit();
1492 const arena = arena_allocator.allocator();
1493
1494 var argv_list: std.ArrayList([]const u8) = .empty;
1495 defer argv_list.deinit(gpa);
1496
1497 for (conf_run.args.slice) |arg_index| {
1498 const arg = arg_index.get(conf);
1499 try argv_list.ensureUnusedCapacity(gpa, 1);
1500 switch (arg.flags.tag) {
1501 .string => {
1502 const prefix = arg.prefix.value.?.slice(conf);
1503 argv_list.appendAssumeCapacity(prefix);
1504 },
1505 .path_file => {
1506 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
1507 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
1508 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
1509 argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
1510 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
1511 }));
1512 },
1513 .path_directory => {
1514 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
1515 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
1516 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
1517 const resolved_arg = try mem.concat(arena, u8, &.{
1518 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
1519 });
1520 argv_list.appendAssumeCapacity(resolved_arg);
1521 },
1522 .file_content => {
1523 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
1524 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
1525 const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
1526
1527 var result: std.Io.Writer.Allocating = .init(arena);
1528 result.writer.writeAll(prefix) catch return error.OutOfMemory;
1529
1530 const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err|
1531 return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err });
1532 defer file.close(io);
1533
1534 var file_reader = file.reader(io, &.{});
1535 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
1536 error.ReadFailed => switch (file_reader.err.?) {
1537 error.Canceled => |e| return e,
1538 else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }),
1539 },
1540 error.WriteFailed => return error.OutOfMemory,
1541 };
1542 result.writer.writeAll(suffix) catch return error.OutOfMemory;
1543
1544 argv_list.appendAssumeCapacity(result.written());
1545 },
1546 .artifact => {
1547 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
1548 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
1549 const producer_index = arg.producer.value.?;
1550 const producer_step = producer_index.ptr(conf);
1551 const producer = producer_step.extended.get(conf.extra).compile;
1552 const producer_make_comp_step = maker.stepByIndex(producer_index);
1553 const producer_make_comp = &producer_make_comp_step.extended.compile;
1554 const file_path: Path = if (producer_index == conf_run.producer.value.?)
1555 run.rebuilt_executable.?
1556 else
1557 producer_make_comp.installed_path orelse
1558 maker.generatedPath(producer.generated_bin.value.?).*;
1559 argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
1560 prefix, try convertPathArg(arena, run_index, maker, file_path, arg.flags.make_absolute), suffix,
1561 }));
1562 },
1563 .output_file => unreachable,
1564 .output_directory => unreachable,
1565 .passthru => unreachable,
1566 .enable_darling => thirdPartyToggle(null, &argv_list, conf, graph.enable_darling, arg.prefix.value, arg.suffix.value),
1567 .enable_qemu => thirdPartyToggle(null, &argv_list, conf, graph.enable_qemu, arg.prefix.value, arg.suffix.value),
1568 .enable_rosetta => thirdPartyToggle(null, &argv_list, conf, graph.enable_rosetta, arg.prefix.value, arg.suffix.value),
1569 .enable_wasmtime => thirdPartyToggle(null, &argv_list, conf, graph.enable_wasmtime, arg.prefix.value, arg.suffix.value),
1570 .enable_wine => thirdPartyToggle(null, &argv_list, conf, graph.enable_wine, arg.prefix.value, arg.suffix.value),
1571 }
1572 }
1573
1574 if (conf_run.flags.test_runner_mode) {
1575 const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false);
1576
1577 try argv_list.ensureUnusedCapacity(gpa, 3);
1578 argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string}));
1579 argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed}));
1580 argv_list.appendAssumeCapacity("--listen=-");
1581 }
1582
1583 step.clearFailedCommand(gpa);
1584
1585 const has_side_effects = false;
1586 var rand_int: u64 = undefined;
1587 io.random(@ptrCast(&rand_int));
1588 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
1589 try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, has_side_effects, tmp_dir_path, .{
1590 .fuzz = fuzz,
1591 });
1592}
1593
1594fn populateGeneratedPaths(
1595 maker: *Maker,
1596 output_placeholders: []const IndexedOutput,
1597 cache_root: Cache.Directory,
1598 digest: *const Cache.HexDigest,
1599) !void {
1600 const conf = &maker.scanned_config.configuration;
1601 const graph = maker.graph;
1602
1603 for (output_placeholders) |placeholder| {
1604 const arg = placeholder.arg_index.get(conf);
1605 maker.generatedPath(arg.generated.value.?).* = .{
1606 .root_dir = cache_root,
1607 .sub_path = try Dir.path.join(graph.arena, &.{
1608 "o", digest, arg.basename.value.?.slice(conf),
1609 }),
1610 };
1611 }
1612}
1613
1614fn populateGeneratedPathsCreateDirs(
1615 arena: Allocator,
1616 run_index: Configuration.Step.Index,
1617 maker: *Maker,
1618 output_dir_path: []const u8,
1619 output_placeholders: []const IndexedOutput,
1620 argv: [][]const u8,
1621) !void {
1622 const step = maker.stepByIndex(run_index);
1623 const conf = &maker.scanned_config.configuration;
1624 const graph = maker.graph;
1625 const io = graph.io;
1626 const cache_root = graph.local_cache_root;
1627
1628 for (output_placeholders) |placeholder| {
1629 const arg = placeholder.arg_index.get(conf);
1630 const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
1631 const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
1632 const basename = arg.basename.value.?.slice(conf);
1633
1634 const generated_path: Path = .{
1635 .root_dir = cache_root,
1636 .sub_path = try Dir.path.join(graph.arena, &.{ output_dir_path, basename }),
1637 };
1638 const create_path: Path = .{
1639 .root_dir = cache_root,
1640 .sub_path = switch (arg.flags.tag) {
1641 .output_file => Dir.path.dirname(generated_path.sub_path).?,
1642 .output_directory => generated_path.sub_path,
1643 else => unreachable,
1644 },
1645 };
1646 create_path.root_dir.handle.createDirPath(io, create_path.sub_path) catch |err|
1647 return step.fail(maker, "unable to make path {f}: {t}", .{ create_path, err });
1648
1649 maker.generatedPath(arg.generated.value.?).* = generated_path;
1650
1651 const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path, arg.flags.make_absolute);
1652 argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix });
1653 }
1654}
1655
1656fn populateGeneratedStdIo(
1657 maker: *Maker,
1658 conf_run: *const Configuration.Step.Run,
1659 cache_root: Cache.Directory,
1660 digest: *const Cache.HexDigest,
1661) !void {
1662 const conf = &maker.scanned_config.configuration;
1663 const graph = maker.graph;
1664
1665 if (conf_run.captured_stdout.value) |captured| {
1666 maker.generatedPath(captured.generated_file).* = .{
1667 .root_dir = cache_root,
1668 .sub_path = try Dir.path.join(graph.arena, &.{
1669 "o", digest, captured.basename.slice(conf),
1670 }),
1671 };
1672 }
1673
1674 if (conf_run.captured_stderr.value) |captured| {
1675 maker.generatedPath(captured.generated_file).* = .{
1676 .root_dir = cache_root,
1677 .sub_path = try Dir.path.join(graph.arena, &.{
1678 "o", digest, captured.basename.slice(conf),
1679 }),
1680 };
1681 }
1682}
1683
1684fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
1685 if (term) |t| switch (t) {
1686 .exited => |code| try w.print("exited with code {d}", .{code}),
1687 .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
1688 .stopped => |sig| try w.print("stopped with signal {t}", .{sig}),
1689 .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
1690 } else {
1691 try w.writeAll("exited with any code");
1692 }
1693}
1694fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
1695 return .{ .data = term };
1696}
1697
1698const FuzzContext = struct {
1699 fuzz: *Fuzz,
1700};
1701
1702fn runCommand(
1703 arena: Allocator,
1704 run: *Run,
1705 run_index: Configuration.Step.Index,
1706 maker: *Maker,
1707 progress_node: std.Progress.Node,
1708 argv: []const []const u8,
1709 has_side_effects: bool,
1710 output_dir_path: []const u8,
1711 fuzz_context: ?FuzzContext,
1712) Step.ExtendedMakeError!void {
1713 const graph = maker.graph;
1714 const gpa = maker.gpa;
1715 const step = maker.stepByIndex(run_index);
1716 const io = graph.io;
1717 const cache_root = graph.local_cache_root;
1718 const conf = &maker.scanned_config.configuration;
1719 const conf_step = run_index.ptr(conf);
1720 const conf_run = conf_step.extended.get(conf.extra).run;
1721
1722 const cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd|
1723 .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) }
1724 else
1725 .inherit;
1726
1727 const allow_skip = switch (conf_run.flags.stdio) {
1728 .check, .zig_test => conf_run.flags.skip_foreign_checks,
1729 else => false,
1730 } or !conf_run.flags.failing_to_execute_foreign_is_an_error;
1731
1732 var interp_argv: std.ArrayList([]const u8) = .empty;
1733
1734 var environ_map: std.process.Environ.Map = .init(gpa);
1735 defer environ_map.deinit();
1736
1737 // In either case we add to this mutatable data structure so that we can
1738 // tweak the environment below.
1739 if (conf_run.environ_map.value) |env_map_index| {
1740 const conf_env_map = env_map_index.get(conf);
1741 for (conf_env_map.keys.slice(conf), conf_env_map.values.slice(conf)) |k, v| {
1742 try environ_map.put(k.slice(conf), v.slice(conf));
1743 }
1744 } else {
1745 try environ_map.putAll(&graph.environ_map);
1746 }
1747
1748 // Now that we have the environ map, we might need to mutate it to insert
1749 // .dll search paths because Windows doesn't have rpaths.
1750 const arg0 = conf_run.args.slice[0].get(conf);
1751 if (arg0.producer.value) |producer_index| {
1752 const producer_step = producer_index.ptr(conf);
1753 const producer = producer_step.extended.get(conf.extra).compile;
1754 const root_module = producer.root_module.get(conf);
1755 const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf);
1756 if (root_module_target.flags.os_tag == .windows) {
1757 try addPathForDynLibs(maker, arena, producer_index, &environ_map, argv[0]);
1758 }
1759 }
1760
1761 const cwd_string = switch (cwd) {
1762 .path => |p| p,
1763 .dir => unreachable,
1764 .inherit => null,
1765 };
1766 try graph.handleVerbose(cwd_string, &environ_map, argv);
1767
1768 const opt_generic_result = spawnChildAndCollect(
1769 arena,
1770 run_index,
1771 run,
1772 maker,
1773 progress_node,
1774 argv,
1775 &environ_map,
1776 has_side_effects,
1777 fuzz_context,
1778 ) catch |err| term: {
1779 switch (err) {
1780 error.InvalidExe, // cpu arch mismatch
1781 error.FileNotFound, // can happen with a wrong dynamic linker path
1782 => interpret: {
1783 const producer_index = arg0.producer.value orelse break :interpret;
1784 const producer_step = producer_index.ptr(conf);
1785 const producer = producer_step.extended.get(conf.extra).compile;
1786 switch (producer.flags3.kind) {
1787 .exe, .@"test" => {},
1788 else => break :interpret,
1789 }
1790 const root_module = producer.root_module.get(conf);
1791 const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf);
1792 const root_target = root_module_target.unwrapTarget(conf);
1793 const link_libc = maker.stepByIndex(producer_index).extended.compile.is_linking_libc;
1794
1795 const host: std.Target = std.zig.system.resolveTargetQuery(io, .{}) catch |he| switch (he) {
1796 error.Canceled => |e| return e,
1797 else => builtin.target,
1798 };
1799
1800 const need_cross_libc = link_libc and root_target.os.tag == .linux and
1801 switch (producer.flags2.linkage) {
1802 .static => false,
1803 .dynamic => true,
1804 .default => root_target.isGnuLibC(),
1805 };
1806 switch (std.zig.system.getExternalExecutor(io, &root_target, .{
1807 .host_cpu_arch = host.cpu.arch,
1808 .host_os_tag = host.os.tag,
1809 .qemu_fixes_dl = need_cross_libc and graph.libc_runtimes_dir != null,
1810 .link_libc = link_libc,
1811 })) {
1812 .native, .rosetta => {
1813 if (allow_skip) return error.MakeSkipped;
1814 break :interpret;
1815 },
1816 .wine => |bin_name| {
1817 if (graph.enable_wine) {
1818 try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len);
1819 interp_argv.appendAssumeCapacity(bin_name);
1820 interp_argv.appendSliceAssumeCapacity(argv);
1821
1822 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
1823 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
1824 if (environ_map.get("WINEDEBUG") == null) {
1825 try environ_map.put("WINEDEBUG", "-all");
1826 }
1827 } else {
1828 return failForeign(arena, &conf_run, maker, run_index, "-fwine", argv[0], &root_target, &host);
1829 }
1830 },
1831 .qemu => |bin_name| {
1832 if (graph.enable_qemu) {
1833 try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len);
1834 interp_argv.appendAssumeCapacity(bin_name);
1835
1836 if (need_cross_libc) {
1837 if (graph.libc_runtimes_dir) |dir| {
1838 interp_argv.appendAssumeCapacity("-L");
1839 interp_argv.appendAssumeCapacity(try Dir.path.join(arena, &.{
1840 dir,
1841 try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
1842 arena,
1843 root_target.cpu.arch,
1844 root_target.os.tag,
1845 root_target.abi,
1846 ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
1847 arena,
1848 root_target.cpu.arch,
1849 root_target.abi,
1850 ) else unreachable,
1851 }));
1852 } else return failForeign(arena, &conf_run, maker, run_index, "--libc-runtimes", argv[0], &root_target, &host);
1853 }
1854
1855 interp_argv.appendSliceAssumeCapacity(argv);
1856 } else return failForeign(arena, &conf_run, maker, run_index, "-fqemu", argv[0], &root_target, &host);
1857 },
1858 .darling => |bin_name| {
1859 if (graph.enable_darling) {
1860 try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len);
1861 interp_argv.appendAssumeCapacity(bin_name);
1862 interp_argv.appendSliceAssumeCapacity(argv);
1863 } else {
1864 return failForeign(arena, &conf_run, maker, run_index, "-fdarling", argv[0], &root_target, &host);
1865 }
1866 },
1867 .wasmtime => |bin_name| {
1868 if (graph.enable_wasmtime) {
1869 try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len + conf_run.preopens.slice.len);
1870 interp_argv.appendAssumeCapacity(bin_name);
1871 interp_argv.appendAssumeCapacity("--dir=.");
1872 for (conf_run.preopens.slice) |preopen| {
1873 const path = try maker.resolveLazyPath(arena, preopen.path.get(conf), run_index);
1874 path.root_dir.handle.createDirPath(io, path.subPathOrDot()) catch |e|
1875 return step.fail(maker, "failed creating directory {f}: {t}", .{ path, e });
1876 interp_argv.appendAssumeCapacity(try arena.print("--dir={f}::{s}", .{ path, preopen.name.slice(conf) }));
1877 }
1878 // Wasmtime doeesn't inherit environment variables from the parent process
1879 // by default. '-S inherit-env' was added in Wasmtime version 20.
1880 interp_argv.appendAssumeCapacity("-Sinherit-env");
1881 interp_argv.appendSliceAssumeCapacity(argv);
1882
1883 // Enable more detailed backtraces by default, but allow the user to override this (e.g.
1884 // with `WASMTIME_BACKTRACE_DETAILS=0`) if desired.
1885 if (environ_map.get("WASMTIME_BACKTRACE_DETAILS") == null) {
1886 try environ_map.put("WASMTIME_BACKTRACE_DETAILS", "1");
1887 }
1888 } else {
1889 return failForeign(arena, &conf_run, maker, run_index, "-fwasmtime", argv[0], &root_target, &host);
1890 }
1891 },
1892 .bad_dl => |foreign_dl| {
1893 if (allow_skip) return error.MakeSkipped;
1894
1895 const host_dl = host.dynamic_linker.get() orelse "(none)";
1896
1897 return step.fail(maker,
1898 \\the host system is unable to execute binaries from the target
1899 \\ because the host dynamic linker is '{s}',
1900 \\ while the target dynamic linker is '{s}'.
1901 \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
1902 , .{ host_dl, foreign_dl });
1903 },
1904 .bad_os_or_cpu => {
1905 if (allow_skip) return error.MakeSkipped;
1906
1907 const host_name = try host.zigTriple(arena);
1908 const foreign_name = try root_target.zigTriple(arena);
1909
1910 return step.fail(maker, "the host system ({s}) is unable to execute binaries from the target ({s})", .{
1911 host_name, foreign_name,
1912 });
1913 },
1914 }
1915
1916 step.clearFailedCommand(gpa);
1917 try graph.handleVerbose(cwd_string, &environ_map, interp_argv.items);
1918
1919 break :term spawnChildAndCollect(
1920 arena,
1921 run_index,
1922 run,
1923 maker,
1924 progress_node,
1925 interp_argv.items,
1926 &environ_map,
1927 has_side_effects,
1928 fuzz_context,
1929 ) catch |e| {
1930 if (!conf_run.flags.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
1931 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1932 return step.fail(maker, "unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
1933 };
1934 },
1935 error.MakeFailed, error.OutOfMemory, error.Canceled => |e| return e,
1936 else => {},
1937 }
1938 return step.fail(maker, "failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
1939 };
1940
1941 const generic_result = opt_generic_result orelse {
1942 assert(conf_run.flags.stdio == .zig_test);
1943 // Specific errors have already been reported, and test results are populated. All we need
1944 // to do is report step failure if any test failed.
1945 if (!step.test_results.isSuccess()) return error.MakeFailed;
1946 return;
1947 };
1948
1949 assert(fuzz_context == null);
1950 assert(conf_run.flags.stdio != .zig_test);
1951
1952 // Capture stdout and stderr to GeneratedFile objects.
1953 const Stream = struct {
1954 captured: ?Configuration.Step.Run.CapturedStream,
1955 bytes: ?[]const u8,
1956 trim_whitespace: Configuration.Step.Run.TrimWhitespace,
1957 };
1958 for (&[_]Stream{
1959 .{
1960 .captured = conf_run.captured_stdout.value,
1961 .bytes = generic_result.stdout,
1962 .trim_whitespace = conf_run.flags.stdout_trim_whitespace,
1963 },
1964 .{
1965 .captured = conf_run.captured_stderr.value,
1966 .bytes = generic_result.stderr,
1967 .trim_whitespace = conf_run.flags.stderr_trim_whitespace,
1968 },
1969 }) |*stream| {
1970 if (stream.captured) |captured| {
1971 const output_path: Path = .{
1972 .root_dir = cache_root,
1973 .sub_path = try Dir.path.join(graph.arena, &.{
1974 output_dir_path, captured.basename.slice(conf),
1975 }),
1976 };
1977 maker.generatedPath(captured.generated_file).* = output_path;
1978
1979 const sub_path_parent = output_path.dirname().?;
1980 sub_path_parent.root_dir.handle.createDirPath(io, sub_path_parent.sub_path) catch |err|
1981 return step.fail(maker, "unable to make path {f}: {t}", .{ sub_path_parent, err });
1982
1983 const data = switch (stream.trim_whitespace) {
1984 .none => stream.bytes.?,
1985 .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
1986 .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
1987 .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
1988 };
1989 output_path.root_dir.handle.writeFile(io, .{
1990 .sub_path = output_path.sub_path,
1991 .data = data,
1992 }) catch |err| return step.fail(maker, "unable to write file {f}: {t}", .{ output_path, err });
1993 }
1994 }
1995
1996 switch (conf_run.flags.stdio) {
1997 .zig_test => unreachable,
1998 .check => {
1999 if (conf_run.expect_stderr_exact.value) |bytes| {
2000 const expected_bytes = bytes.slice(conf);
2001 if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
2002 return step.fail(maker,
2003 \\========= expected this stderr: =========
2004 \\{s}
2005 \\========= but found: ====================
2006 \\{s}
2007 , .{
2008 expected_bytes,
2009 generic_result.stderr.?,
2010 });
2011 }
2012 }
2013 if (conf_run.expect_stdout_exact.value) |bytes| {
2014 const expected_bytes = bytes.slice(conf);
2015 if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
2016 return step.fail(maker,
2017 \\========= expected this stdout: =========
2018 \\{s}
2019 \\========= but found: ====================
2020 \\{s}
2021 , .{
2022 expected_bytes,
2023 generic_result.stdout.?,
2024 });
2025 }
2026 }
2027 for (conf_run.expect_stderr_match.slice) |bytes| {
2028 const match = bytes.slice(conf);
2029 if (mem.find(u8, generic_result.stderr.?, match) == null) {
2030 return step.fail(maker,
2031 \\========= expected to find in stderr: =========
2032 \\{s}
2033 \\========= but stderr does not contain it: =====
2034 \\{s}
2035 , .{
2036 match,
2037 generic_result.stderr.?,
2038 });
2039 }
2040 }
2041 for (conf_run.expect_stdout_match.slice) |bytes| {
2042 const match = bytes.slice(conf);
2043 if (mem.find(u8, generic_result.stdout.?, match) == null) {
2044 return step.fail(maker,
2045 \\========= expected to find in stdout: =========
2046 \\{s}
2047 \\========= but stdout does not contain it: =====
2048 \\{s}
2049 , .{
2050 match,
2051 generic_result.stdout.?,
2052 });
2053 }
2054 }
2055 if (conf_run.expect_term_value.value) |expected_term_value| {
2056 const expected_term: process.Child.Term = switch (conf_run.flags2.expect_term_status) {
2057 .exited => .{ .exited = @intCast(expected_term_value) },
2058 .signal => .{ .signal = @fromBackingInt(@intCast(expected_term_value)) },
2059 .stopped => .{ .stopped = @fromBackingInt(@intCast(expected_term_value)) },
2060 .unknown => .{ .unknown = expected_term_value },
2061 };
2062 if (!termMatches(expected_term, generic_result.term)) {
2063 return step.fail(maker, "process {f} (expected {f})", .{
2064 fmtTerm(generic_result.term),
2065 fmtTerm(expected_term),
2066 });
2067 }
2068 }
2069 const snapshots: []const ?struct {
2070 path: Cache.Path,
2071 result: enum { stderr, stdout },
2072 } = &.{
2073 if (conf_run.expect_stderr_snapshot.value) |path| .{
2074 .path = try maker.resolveLazyPathIndex(arena, path, run_index),
2075 .result = .stderr,
2076 } else null,
2077 if (conf_run.expect_stdout_snapshot.value) |path| .{
2078 .path = try maker.resolveLazyPathIndex(arena, path, run_index),
2079 .result = .stdout,
2080 } else null,
2081 };
2082 for (snapshots) |opt_snapshot| {
2083 const snapshot = opt_snapshot orelse continue;
2084
2085 const file = snapshot.path.root_dir.handle.openFile(io, snapshot.path.sub_path, .{}) catch |err|
2086 return step.fail(maker, "unable to open snapshot file {f}: {t}", .{ snapshot.path, err });
2087 defer file.close(io);
2088
2089 var file_reader = file.reader(io, &.{});
2090 const snapshot_contents = file_reader.interface.allocRemaining(gpa, .unlimited) catch |err|
2091 return step.fail(maker, "unable to read snapshot file {f}: {t}", .{ snapshot.path, err });
2092 defer gpa.free(snapshot_contents);
2093
2094 const result = switch (snapshot.result) {
2095 .stderr => generic_result.stderr.?,
2096 .stdout => generic_result.stdout.?,
2097 };
2098 if (std.mem.findDiff(u8, snapshot_contents, result)) |diff_index| {
2099 var diff_line_number: usize = 1;
2100
2101 for (snapshot_contents[0..diff_index]) |value| {
2102 if (value == '\n') diff_line_number += 1;
2103 }
2104
2105 return step.fail(maker,
2106 \\
2107 \\========= snapshot file: =========
2108 \\{f}
2109 \\========= contained: =============
2110 \\{s}
2111 \\========= {t} output was: ========
2112 \\{s}
2113 \\==================================
2114 \\first difference on line {d}:
2115 \\expected:
2116 \\{f}
2117 \\found:
2118 \\{f}
2119 , .{
2120 snapshot.path,
2121 snapshot_contents,
2122 snapshot.result,
2123 result,
2124 diff_line_number,
2125 fmtSnapshotIndicatorLine(snapshot_contents, diff_index),
2126 fmtSnapshotIndicatorLine(result, diff_index),
2127 });
2128 }
2129 }
2130 },
2131 else => {
2132 // On failure, report captured stderr like normal standard error output.
2133 if (!generic_result.term.success()) {
2134 if (generic_result.stderr) |bytes| {
2135 try step.setResultStderr(gpa, bytes);
2136 }
2137 }
2138 try step.handleChildProcessTerm(maker, generic_result.term);
2139 },
2140 }
2141}
2142
2143const FmtIndicatorLine = struct {
2144 buf: []const u8,
2145 index: usize,
2146};
2147
2148fn fmtSnapshotIndicatorLine(buf: []const u8, index: usize) std.fmt.Alt(
2149 FmtIndicatorLine,
2150 snapshotIndicatorLine,
2151) {
2152 return .{ .data = .{ .buf = buf, .index = index } };
2153}
2154
2155fn snapshotIndicatorLine(line: FmtIndicatorLine, w: *std.Io.Writer) std.Io.Writer.Error!void {
2156 const line_begin_index = if (std.mem.findScalarLast(u8, line.buf[0..line.index], '\n')) |line_begin|
2157 line_begin + 1
2158 else
2159 0;
2160 const line_end_index = if (std.mem.findScalar(u8, line.buf[line.index..], '\n')) |line_end|
2161 (line.index + line_end)
2162 else
2163 line.buf.len;
2164
2165 try w.writeAll(line.buf[line_begin_index..line_end_index]);
2166 try w.writeByte('\n');
2167 try w.splatByteAll(' ', line_end_index - line_begin_index);
2168 try w.writeByte('\n');
2169 if (line.index >= line.buf.len)
2170 try w.writeAll("^ (end of file)")
2171 else
2172 try w.print("^ ('\\x{x:0>2}')\n", .{line.buf[line.index]});
2173}
2174
2175const EvalGenericResult = struct {
2176 term: process.Child.Term,
2177 stdout: ?[]const u8,
2178 stderr: ?[]const u8,
2179};
2180
2181fn spawnChildAndCollect(
2182 arena: Allocator,
2183 run_index: Configuration.Step.Index,
2184 run: *Run,
2185 maker: *Maker,
2186 progress_node: std.Progress.Node,
2187 argv: []const []const u8,
2188 environ_map: *EnvMap,
2189 has_side_effects: bool,
2190 fuzz_context: ?FuzzContext,
2191) !?EvalGenericResult {
2192 const step = maker.stepByIndex(run_index);
2193 const graph = maker.graph;
2194 const io = graph.io;
2195 const gpa = maker.gpa;
2196 const conf = &maker.scanned_config.configuration;
2197 const conf_step = run_index.ptr(conf);
2198 const conf_run = conf_step.extended.get(conf.extra).run;
2199
2200 if (fuzz_context != null) {
2201 assert(!has_side_effects);
2202 assert(conf_run.flags.stdio == .zig_test);
2203 }
2204
2205 const child_cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd|
2206 .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) }
2207 else
2208 .inherit;
2209
2210 // If an error occurs, it's caused by this command:
2211 const cwd_string = switch (child_cwd) {
2212 .path => |p| p,
2213 .dir => unreachable,
2214 .inherit => null,
2215 };
2216 // We have to set the failed command here regardless of whether this
2217 // function returns an error because only after this function returns
2218 // does the logic determine whether the child process termination was
2219 // success or failure.
2220 step.setFailedCommand(gpa, argv, .{
2221 .cwd = cwd_string,
2222 .child_env = environ_map,
2223 .parent_env = &graph.environ_map,
2224 });
2225
2226 try step.handleChildProcUnsupported(maker);
2227
2228 var spawn_options: process.SpawnOptions = .{
2229 .argv = argv,
2230 .cwd = child_cwd,
2231 .environ_map = environ_map,
2232 .request_resource_usage_statistics = true,
2233 .stdin = if (conf_run.stdin.u != .none) s: {
2234 assert(conf_run.flags.stdio != .inherit);
2235 break :s .pipe;
2236 } else switch (conf_run.flags.stdio) {
2237 .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore,
2238 .inherit => .inherit,
2239 .check => .ignore,
2240 .zig_test => .pipe,
2241 },
2242 .stdout = if (conf_run.captured_stdout.value != null) .pipe else switch (conf_run.flags.stdio) {
2243 .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .ignore,
2244 .inherit => .inherit,
2245 .check => if (checksContainStdout(&conf_run)) .pipe else .ignore,
2246 .zig_test => .pipe,
2247 },
2248 .stderr = if (conf_run.captured_stderr.value != null) .pipe else switch (conf_run.flags.stdio) {
2249 .infer_from_args => if (maker.protocol_server == null and has_side_effects) .inherit else .pipe,
2250 .inherit => if (maker.protocol_server == null) .inherit else .pipe,
2251 .check => .pipe,
2252 .zig_test => .pipe,
2253 },
2254 };
2255
2256 if (maker.protocol_server != null) {
2257 if (spawn_options.stdin == .inherit) {
2258 return step.fail(maker, "Cannot inherit stdin when running through over the build system protocol", .{});
2259 }
2260 if (spawn_options.stdout == .inherit) {
2261 return step.fail(maker, "Cannot inherit stdout when running through over the build system protocol", .{});
2262 }
2263 assert(spawn_options.stderr != .inherit);
2264 }
2265
2266 if (conf_run.flags.stdio == .zig_test) {
2267 try setColorEnvironmentVariables(&conf_run, environ_map, graph.stderr_mode.?);
2268 const started: Io.Clock.Timestamp = .now(io, .awake);
2269 const result = evalZigTest(run, run_index, maker, progress_node, spawn_options, fuzz_context) catch |err| switch (err) {
2270 error.Canceled => |e| return e,
2271 else => |e| e,
2272 };
2273 step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
2274 try result;
2275 return null;
2276 } else {
2277 const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
2278 if (!conf_run.flags.disable_zig_progress and !inherit) {
2279 spawn_options.progress_node = progress_node;
2280 }
2281 const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
2282 const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
2283 break :m stderr.terminal_mode;
2284 } else .no_color;
2285 defer if (inherit) io.unlockStderr();
2286 try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode);
2287
2288 const started: Io.Clock.Timestamp = .now(io, .awake);
2289 const result = evalGeneric(arena, run_index, maker, spawn_options) catch |err| switch (err) {
2290 error.Canceled => |e| return e,
2291 else => |e| e,
2292 };
2293 step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
2294 return try result;
2295 }
2296}
2297
2298fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
2299 return if (expected) |e| switch (e) {
2300 .exited => |expected_code| switch (actual) {
2301 .exited => |actual_code| expected_code == actual_code,
2302 else => false,
2303 },
2304 .signal => |expected_sig| switch (actual) {
2305 .signal => |actual_sig| expected_sig == actual_sig,
2306 else => false,
2307 },
2308 .stopped => |expected_sig| switch (actual) {
2309 .stopped => |actual_sig| expected_sig == actual_sig,
2310 else => false,
2311 },
2312 .unknown => |expected_code| switch (actual) {
2313 .unknown => |actual_code| expected_code == actual_code,
2314 else => false,
2315 },
2316 } else switch (actual) {
2317 .exited => true,
2318 else => false,
2319 };
2320}
2321
2322fn setColorEnvironmentVariables(
2323 conf_run: *const Configuration.Step.Run,
2324 environ_map: *EnvMap,
2325 terminal_mode: Io.Terminal.Mode,
2326) !void {
2327 color: switch (conf_run.flags.color) {
2328 .manual => {},
2329 .enable => {
2330 try environ_map.put("CLICOLOR_FORCE", "1");
2331 _ = environ_map.swapRemove("NO_COLOR");
2332 },
2333 .disable => {
2334 try environ_map.put("NO_COLOR", "1");
2335 _ = environ_map.swapRemove("CLICOLOR_FORCE");
2336 },
2337 .inherit => switch (terminal_mode) {
2338 .no_color, .windows_api => continue :color .disable,
2339 .escape_codes => continue :color .enable,
2340 },
2341 .auto => {
2342 const capture_stderr = conf_run.captured_stderr.value != null or switch (conf_run.flags.stdio) {
2343 .check => checksContainStderr(conf_run),
2344 .infer_from_args, .inherit, .zig_test => false,
2345 };
2346 if (capture_stderr) {
2347 continue :color .disable;
2348 } else {
2349 continue :color .inherit;
2350 }
2351 },
2352 }
2353}
2354
2355fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool {
2356 return conf_run.expect_stdout_exact.value != null or
2357 conf_run.expect_stdout_match.slice.len != 0 or
2358 conf_run.expect_stdout_snapshot.value != null;
2359}
2360
2361fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool {
2362 return conf_run.expect_stderr_exact.value != null or
2363 conf_run.expect_stderr_match.slice.len != 0 or
2364 conf_run.expect_stderr_snapshot.value != null;
2365}
2366
2367/// If `path` is absolute, return it unchanged. If `make_absolute` is true, make it absolute.
2368/// Otherwise, make it relative to the cwd of the child.
2369///
2370/// Whenever a path is included in the argv of a child, it should be put through this function
2371/// first.
2372fn convertPathArg(
2373 arena: Allocator,
2374 run_index: Configuration.Step.Index,
2375 maker: *Maker,
2376 path: Path,
2377 make_absolute: bool,
2378) ![]const u8 {
2379 const conf = &maker.scanned_config.configuration;
2380 const conf_step = run_index.ptr(conf);
2381 const conf_run = conf_step.extended.get(conf.extra).run;
2382 const graph = maker.graph;
2383
2384 const path_str = try path.toString(arena);
2385 if (Dir.path.isAbsolute(path_str)) {
2386 // Absolute paths don't need changing.
2387 return path_str;
2388 }
2389
2390 if (make_absolute) {
2391 return Dir.path.join(arena, &.{ graph.cache.cwd, path_str });
2392 }
2393
2394 const child_cwd_rel: []const u8 = rel: {
2395 const child_lazy_cwd = conf_run.cwd.value orelse break :rel path_str;
2396 const child_cwd = try maker.resolveLazyPathIndexAbs(arena, child_lazy_cwd, run_index);
2397 // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
2398 break :rel try Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str);
2399 };
2400 // Not every path can be made relative, e.g. if the path and the child cwd are on different
2401 // disk designators on Windows. In that case, `relative` will return an absolute path which we can
2402 // just return.
2403 if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel;
2404
2405 // We're not done yet. In some cases this path must be prefixed with './':
2406 // * On POSIX, the executable name cannot be a single component like 'foo'
2407 // * Some executables might treat a leading '-' like a flag, which we must avoid
2408 // There's no harm in it, so just *always* apply this prefix.
2409 return Dir.path.join(arena, &.{ ".", child_cwd_rel });
2410}
2411
2412fn addPathForDynLibs(
2413 maker: *Maker,
2414 arena: Allocator,
2415 artifact: Configuration.Step.Index,
2416 environ_map: *process.Environ.Map,
2417 argv0: []const u8,
2418) !void {
2419 const conf = &maker.scanned_config.configuration;
2420 const graph = maker.graph;
2421 const use_wine = graph.enable_wine and builtin.os.tag != .windows and std.ascii.endsWithIgnoreCase(argv0, ".exe");
2422 const path_key = if (use_wine) "WINEPATH" else "PATH";
2423 const path_delimiter: u8 = if (builtin.os.tag == .windows or use_wine)
2424 Dir.path.delimiter_windows
2425 else
2426 Dir.path.delimiter;
2427
2428 var module_graph: Step.Compile.ModuleGraph = .empty;
2429 const compile_deps = try Step.Compile.getCompileDependencies(arena, &module_graph, conf, artifact, true);
2430
2431 for (compile_deps) |dep_index| {
2432 const conf_comp_step = dep_index.ptr(conf);
2433 const conf_comp = conf_comp_step.extended.get(conf.extra).compile;
2434 const root_module = conf_comp.root_module.get(conf);
2435 const target = root_module.resolved_target.get(conf).?.result.get(conf);
2436 if (target.flags.os_tag == .windows and conf_comp.isDynamicLibrary()) {
2437 const dll_path = try maker.generatedPath(conf_comp.generated_bin.value.?).toString(arena);
2438 const search_path = Dir.path.dirname(dll_path).?;
2439 if (environ_map.get(path_key)) |prev_path| {
2440 const new_path = try arena.print("{s}{c}{s}", .{ prev_path, path_delimiter, search_path });
2441 try environ_map.put(path_key, new_path);
2442 } else {
2443 try environ_map.put(path_key, search_path);
2444 }
2445 }
2446 }
2447}
2448
2449fn failForeign(
2450 arena: Allocator,
2451 conf_run: *const Configuration.Step.Run,
2452 maker: *Maker,
2453 step_index: Configuration.Step.Index,
2454 suggested_flag: []const u8,
2455 argv0: []const u8,
2456 artifact_target: *const std.Target,
2457 host_target: *const std.Target,
2458) Step.ExtendedMakeError {
2459 const step = maker.stepByIndex(step_index);
2460 switch (conf_run.flags.stdio) {
2461 .check, .zig_test => {
2462 if (conf_run.flags.skip_foreign_checks) return error.MakeSkipped;
2463
2464 const host_name = try host_target.zigTriple(arena);
2465 const foreign_name = try artifact_target.zigTriple(arena);
2466
2467 return step.fail(maker,
2468 \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
2469 \\ consider using {s} or enabling skip_foreign_checks in the Run step
2470 , .{ argv0, foreign_name, host_name, suggested_flag });
2471 },
2472 else => {
2473 return step.fail(maker, "unable to spawn foreign binary '{s}'", .{argv0});
2474 },
2475 }
2476}