master
1const std = @import("std");
2const builtin = @import("builtin");
3const Build = std.Build;
4const Step = Build.Step;
5const fs = std.fs;
6const mem = std.mem;
7const process = std.process;
8const EnvMap = process.EnvMap;
9const assert = std.debug.assert;
10const Path = Build.Cache.Path;
11
12const Run = @This();
13
14pub const base_id: Step.Id = .run;
15
16step: Step,
17
18/// See also addArg and addArgs to modifying this directly
19argv: std.ArrayList(Arg),
20
21/// Use `setCwd` to set the initial current working directory
22cwd: ?Build.LazyPath,
23
24/// Override this field to modify the environment, or use setEnvironmentVariable
25env_map: ?*EnvMap,
26
27/// Controls the `NO_COLOR` and `CLICOLOR_FORCE` environment variables.
28color: enum {
29 /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset.
30 enable,
31 /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset.
32 disable,
33 /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`.
34 inherit,
35 /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`.
36 auto,
37 /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables.
38 /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`.
39 manual,
40} = .auto,
41
42/// When `true` prevents `ZIG_PROGRESS` environment variable from being passed
43/// to the child process, which otherwise would be used for the child to send
44/// progress updates to the parent.
45disable_zig_progress: bool,
46
47/// Configures whether the Run step is considered to have side-effects, and also
48/// whether the Run step will inherit stdio streams, forwarding them to the
49/// parent process, in which case will require a global lock to prevent other
50/// steps from interfering with stdio while the subprocess associated with this
51/// Run step is running.
52/// If the Run step is determined to not have side-effects, then execution will
53/// be skipped if all output files are up-to-date and input files are
54/// unchanged.
55stdio: StdIo,
56
57/// This field must be `.none` if stdio is `inherit`.
58/// It should be only set using `setStdIn`.
59stdin: StdIn,
60
61/// Additional input files that, when modified, indicate that the Run step
62/// should be re-executed.
63/// If the Run step is determined to have side-effects, the Run step is always
64/// executed when it appears in the build graph, regardless of whether these
65/// files have been modified.
66file_inputs: std.ArrayList(std.Build.LazyPath),
67
68/// After adding an output argument, this step will by default rename itself
69/// for a better display name in the build summary.
70/// This can be disabled by setting this to false.
71rename_step_with_output_arg: bool,
72
73/// If this is true, a Run step which is configured to check the output of the
74/// executed binary will not fail the build if the binary cannot be executed
75/// due to being for a foreign binary to the host system which is running the
76/// build graph.
77/// Command-line arguments such as -fqemu and -fwasmtime may affect whether a
78/// binary is detected as foreign, as well as system configuration such as
79/// Rosetta (macOS) and binfmt_misc (Linux).
80/// If this Run step is considered to have side-effects, then this flag does
81/// nothing.
82skip_foreign_checks: bool,
83
84/// If this is true, failing to execute a foreign binary will be considered an
85/// error. However if this is false, the step will be skipped on failure instead.
86///
87/// This allows for a Run step to attempt to execute a foreign binary using an
88/// external executor (such as qemu) but not fail if the executor is unavailable.
89failing_to_execute_foreign_is_an_error: bool,
90
91/// Deprecated in favor of `stdio_limit`.
92max_stdio_size: usize,
93
94/// If stderr or stdout exceeds this amount, the child process is killed and
95/// the step fails.
96stdio_limit: std.Io.Limit,
97
98captured_stdout: ?*CapturedStdIo,
99captured_stderr: ?*CapturedStdIo,
100
101dep_output_file: ?*Output,
102
103has_side_effects: bool,
104
105/// If this is a Zig unit test binary, this tracks the indexes of the unit
106/// tests that are also fuzz tests.
107fuzz_tests: std.ArrayList(u32),
108cached_test_metadata: ?CachedTestMetadata = null,
109
110/// Populated during the fuzz phase if this run step corresponds to a unit test
111/// executable that contains fuzz tests.
112rebuilt_executable: ?Path,
113
114/// If this Run step was produced by a Compile step, it is tracked here.
115producer: ?*Step.Compile,
116
117pub const StdIn = union(enum) {
118 none,
119 bytes: []const u8,
120 lazy_path: std.Build.LazyPath,
121};
122
123pub const StdIo = union(enum) {
124 /// Whether the Run step has side-effects will be determined by whether or not one
125 /// of the args is an output file (added with `addOutputFileArg`).
126 /// If the Run step is determined to have side-effects, this is the same as `inherit`.
127 /// The step will fail if the subprocess crashes or returns a non-zero exit code.
128 infer_from_args,
129 /// Causes the Run step to be considered to have side-effects, and therefore
130 /// always execute when it appears in the build graph.
131 /// It also means that this step will obtain a global lock to prevent other
132 /// steps from running in the meantime.
133 /// The step will fail if the subprocess crashes or returns a non-zero exit code.
134 inherit,
135 /// Causes the Run step to be considered to *not* have side-effects. The
136 /// process will be re-executed if any of the input dependencies are
137 /// modified. The exit code and standard I/O streams will be checked for
138 /// certain conditions, and the step will succeed or fail based on these
139 /// conditions.
140 /// Note that an explicit check for exit code 0 needs to be added to this
141 /// list if such a check is desirable.
142 check: std.ArrayList(Check),
143 /// This Run step is running a zig unit test binary and will communicate
144 /// extra metadata over the IPC protocol.
145 zig_test,
146
147 pub const Check = union(enum) {
148 expect_stderr_exact: []const u8,
149 expect_stderr_match: []const u8,
150 expect_stdout_exact: []const u8,
151 expect_stdout_match: []const u8,
152 expect_term: std.process.Child.Term,
153 };
154};
155
156pub const Arg = union(enum) {
157 artifact: PrefixedArtifact,
158 lazy_path: PrefixedLazyPath,
159 decorated_directory: DecoratedLazyPath,
160 file_content: PrefixedLazyPath,
161 bytes: []u8,
162 output_file: *Output,
163 output_directory: *Output,
164};
165
166pub const PrefixedArtifact = struct {
167 prefix: []const u8,
168 artifact: *Step.Compile,
169};
170
171pub const PrefixedLazyPath = struct {
172 prefix: []const u8,
173 lazy_path: std.Build.LazyPath,
174};
175
176pub const DecoratedLazyPath = struct {
177 prefix: []const u8,
178 lazy_path: std.Build.LazyPath,
179 suffix: []const u8,
180};
181
182pub const Output = struct {
183 generated_file: std.Build.GeneratedFile,
184 prefix: []const u8,
185 basename: []const u8,
186};
187
188pub const CapturedStdIo = struct {
189 output: Output,
190 trim_whitespace: TrimWhitespace,
191
192 pub const Options = struct {
193 /// `null` means `stdout`/`stderr`.
194 basename: ?[]const u8 = null,
195 /// Does not affect `expectStdOutEqual`/`expectStdErrEqual`.
196 trim_whitespace: TrimWhitespace = .none,
197 };
198
199 pub const TrimWhitespace = enum {
200 none,
201 all,
202 leading,
203 trailing,
204 };
205};
206
207pub fn create(owner: *std.Build, name: []const u8) *Run {
208 const run = owner.allocator.create(Run) catch @panic("OOM");
209 run.* = .{
210 .step = .init(.{
211 .id = base_id,
212 .name = name,
213 .owner = owner,
214 .makeFn = make,
215 }),
216 .argv = .{},
217 .cwd = null,
218 .env_map = null,
219 .disable_zig_progress = false,
220 .stdio = .infer_from_args,
221 .stdin = .none,
222 .file_inputs = .{},
223 .rename_step_with_output_arg = true,
224 .skip_foreign_checks = false,
225 .failing_to_execute_foreign_is_an_error = true,
226 .max_stdio_size = 10 * 1024 * 1024,
227 .stdio_limit = .unlimited,
228 .captured_stdout = null,
229 .captured_stderr = null,
230 .dep_output_file = null,
231 .has_side_effects = false,
232 .fuzz_tests = .{},
233 .rebuilt_executable = null,
234 .producer = null,
235 };
236 return run;
237}
238
239pub fn setName(run: *Run, name: []const u8) void {
240 run.step.name = name;
241 run.rename_step_with_output_arg = false;
242}
243
244pub fn enableTestRunnerMode(run: *Run) void {
245 const b = run.step.owner;
246 run.stdio = .zig_test;
247 run.addPrefixedDirectoryArg("--cache-dir=", .{ .cwd_relative = b.cache_root.path orelse "." });
248 run.addArgs(&.{
249 b.fmt("--seed=0x{x}", .{b.graph.random_seed}),
250 "--listen=-",
251 });
252}
253
254pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void {
255 run.addPrefixedArtifactArg("", artifact);
256}
257
258pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Compile) void {
259 const b = run.step.owner;
260
261 const prefixed_artifact: PrefixedArtifact = .{
262 .prefix = b.dupe(prefix),
263 .artifact = artifact,
264 };
265 run.argv.append(b.allocator, .{ .artifact = prefixed_artifact }) catch @panic("OOM");
266
267 const bin_file = artifact.getEmittedBin();
268 bin_file.addStepDependencies(&run.step);
269}
270
271/// Provides a file path as a command line argument to the command being run.
272///
273/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs
274/// throughout the build system.
275///
276/// Related:
277/// * `addPrefixedOutputFileArg` - same thing but prepends a string to the argument
278/// * `addFileArg` - for input files given to the child process
279pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath {
280 return run.addPrefixedOutputFileArg("", basename);
281}
282
283/// Provides a file path as a command line argument to the command being run.
284/// Asserts `basename` is not empty.
285///
286/// For example, a prefix of "-o" and basename of "output.txt" will result in
287/// the child process seeing something like this: "-ozig-cache/.../output.txt"
288///
289/// The child process will see a single argument, regardless of whether the
290/// prefix or basename have spaces.
291///
292/// The returned `std.Build.LazyPath` can be used as inputs to other APIs
293/// throughout the build system.
294///
295/// Related:
296/// * `addOutputFileArg` - same thing but without the prefix
297/// * `addFileArg` - for input files given to the child process
298pub fn addPrefixedOutputFileArg(
299 run: *Run,
300 prefix: []const u8,
301 basename: []const u8,
302) std.Build.LazyPath {
303 const b = run.step.owner;
304 if (basename.len == 0) @panic("basename must not be empty");
305
306 const output = b.allocator.create(Output) catch @panic("OOM");
307 output.* = .{
308 .prefix = b.dupe(prefix),
309 .basename = b.dupe(basename),
310 .generated_file = .{ .step = &run.step },
311 };
312 run.argv.append(b.allocator, .{ .output_file = output }) catch @panic("OOM");
313
314 if (run.rename_step_with_output_arg) {
315 run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
316 }
317
318 return .{ .generated = .{ .file = &output.generated_file } };
319}
320
321/// Appends an input file to the command line arguments.
322///
323/// The child process will see a file path. Modifications to this file will be
324/// detected as a cache miss in subsequent builds, causing the child process to
325/// be re-executed.
326///
327/// Related:
328/// * `addPrefixedFileArg` - same thing but prepends a string to the argument
329/// * `addOutputFileArg` - for files generated by the child process
330pub fn addFileArg(run: *Run, lp: std.Build.LazyPath) void {
331 run.addPrefixedFileArg("", lp);
332}
333
334/// Appends an input file to the command line arguments prepended with a string.
335///
336/// For example, a prefix of "-F" will result in the child process seeing something
337/// like this: "-Fexample.txt"
338///
339/// The child process will see a single argument, even if the prefix has
340/// spaces. Modifications to this file will be detected as a cache miss in
341/// subsequent builds, causing the child process to be re-executed.
342///
343/// Related:
344/// * `addFileArg` - same thing but without the prefix
345/// * `addOutputFileArg` - for files generated by the child process
346pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
347 const b = run.step.owner;
348
349 const prefixed_file_source: PrefixedLazyPath = .{
350 .prefix = b.dupe(prefix),
351 .lazy_path = lp.dupe(b),
352 };
353 run.argv.append(b.allocator, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM");
354 lp.addStepDependencies(&run.step);
355}
356
357/// Appends the content of an input file to the command line arguments.
358///
359/// The child process will see a single argument, even if the file contains whitespace.
360/// This means that the entire file content up to EOF is rendered as one contiguous
361/// string, including escape sequences. Notably, any (trailing) newlines will show up
362/// like this: "hello,\nfile world!\n"
363///
364/// Modifications to the source file will be detected as a cache miss in subsequent
365/// builds, causing the child process to be re-executed.
366///
367/// This function may not be used to supply the first argument of a `Run` step.
368///
369/// Related:
370/// * `addPrefixedFileContentArg` - same thing but prepends a string to the argument
371pub fn addFileContentArg(run: *Run, lp: std.Build.LazyPath) void {
372 run.addPrefixedFileContentArg("", lp);
373}
374
375/// Appends the content of an input file to the command line arguments prepended with a string.
376///
377/// For example, a prefix of "-F" will result in the child process seeing something
378/// like this: "-Fmy file content"
379///
380/// The child process will see a single argument, even if the prefix and/or the file
381/// contain whitespace.
382/// This means that the entire file content up to EOF is rendered as one contiguous
383/// string, including escape sequences. Notably, any (trailing) newlines will show up
384/// like this: "hello,\nfile world!\n"
385///
386/// Modifications to the source file will be detected as a cache miss in subsequent
387/// builds, causing the child process to be re-executed.
388///
389/// This function may not be used to supply the first argument of a `Run` step.
390///
391/// Related:
392/// * `addFileContentArg` - same thing but without the prefix
393pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
394 const b = run.step.owner;
395
396 // Some parts of this step's configure phase API rely on the first argument being somewhat
397 // transparent/readable, but the content of the file specified by `lp` remains completely
398 // opaque until its path can be resolved during the make phase.
399 if (run.argv.items.len == 0) {
400 @panic("'addFileContentArg'/'addPrefixedFileContentArg' cannot be first argument");
401 }
402
403 const prefixed_file_source: PrefixedLazyPath = .{
404 .prefix = b.dupe(prefix),
405 .lazy_path = lp.dupe(b),
406 };
407 run.argv.append(b.allocator, .{ .file_content = prefixed_file_source }) catch @panic("OOM");
408 lp.addStepDependencies(&run.step);
409}
410
411/// Provides a directory path as a command line argument to the command being run.
412///
413/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs
414/// throughout the build system.
415///
416/// Related:
417/// * `addPrefixedOutputDirectoryArg` - same thing but prepends a string to the argument
418/// * `addDirectoryArg` - for input directories given to the child process
419pub fn addOutputDirectoryArg(run: *Run, basename: []const u8) std.Build.LazyPath {
420 return run.addPrefixedOutputDirectoryArg("", basename);
421}
422
423/// Provides a directory path as a command line argument to the command being run.
424/// Asserts `basename` is not empty.
425///
426/// For example, a prefix of "-o" and basename of "output_dir" will result in
427/// the child process seeing something like this: "-ozig-cache/.../output_dir"
428///
429/// The child process will see a single argument, regardless of whether the
430/// prefix or basename have spaces.
431///
432/// The returned `std.Build.LazyPath` can be used as inputs to other APIs
433/// throughout the build system.
434///
435/// Related:
436/// * `addOutputDirectoryArg` - same thing but without the prefix
437/// * `addDirectoryArg` - for input directories given to the child process
438pub fn addPrefixedOutputDirectoryArg(
439 run: *Run,
440 prefix: []const u8,
441 basename: []const u8,
442) std.Build.LazyPath {
443 if (basename.len == 0) @panic("basename must not be empty");
444 const b = run.step.owner;
445
446 const output = b.allocator.create(Output) catch @panic("OOM");
447 output.* = .{
448 .prefix = b.dupe(prefix),
449 .basename = b.dupe(basename),
450 .generated_file = .{ .step = &run.step },
451 };
452 run.argv.append(b.allocator, .{ .output_directory = output }) catch @panic("OOM");
453
454 if (run.rename_step_with_output_arg) {
455 run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
456 }
457
458 return .{ .generated = .{ .file = &output.generated_file } };
459}
460
461pub fn addDirectoryArg(run: *Run, lazy_directory: std.Build.LazyPath) void {
462 run.addDecoratedDirectoryArg("", lazy_directory, "");
463}
464
465pub fn addPrefixedDirectoryArg(run: *Run, prefix: []const u8, lazy_directory: std.Build.LazyPath) void {
466 const b = run.step.owner;
467 run.argv.append(b.allocator, .{ .decorated_directory = .{
468 .prefix = b.dupe(prefix),
469 .lazy_path = lazy_directory.dupe(b),
470 .suffix = "",
471 } }) catch @panic("OOM");
472 lazy_directory.addStepDependencies(&run.step);
473}
474
475pub fn addDecoratedDirectoryArg(
476 run: *Run,
477 prefix: []const u8,
478 lazy_directory: std.Build.LazyPath,
479 suffix: []const u8,
480) void {
481 const b = run.step.owner;
482 run.argv.append(b.allocator, .{ .decorated_directory = .{
483 .prefix = b.dupe(prefix),
484 .lazy_path = lazy_directory.dupe(b),
485 .suffix = b.dupe(suffix),
486 } }) catch @panic("OOM");
487 lazy_directory.addStepDependencies(&run.step);
488}
489
490/// Add a path argument to a dep file (.d) for the child process to write its
491/// discovered additional dependencies.
492/// Only one dep file argument is allowed by instance.
493pub fn addDepFileOutputArg(run: *Run, basename: []const u8) std.Build.LazyPath {
494 return run.addPrefixedDepFileOutputArg("", basename);
495}
496
497/// Add a prefixed path argument to a dep file (.d) for the child process to
498/// write its discovered additional dependencies.
499/// Only one dep file argument is allowed by instance.
500pub fn addPrefixedDepFileOutputArg(run: *Run, prefix: []const u8, basename: []const u8) std.Build.LazyPath {
501 const b = run.step.owner;
502 assert(run.dep_output_file == null);
503
504 const dep_file = b.allocator.create(Output) catch @panic("OOM");
505 dep_file.* = .{
506 .prefix = b.dupe(prefix),
507 .basename = b.dupe(basename),
508 .generated_file = .{ .step = &run.step },
509 };
510
511 run.dep_output_file = dep_file;
512
513 run.argv.append(b.allocator, .{ .output_file = dep_file }) catch @panic("OOM");
514
515 return .{ .generated = .{ .file = &dep_file.generated_file } };
516}
517
518pub fn addArg(run: *Run, arg: []const u8) void {
519 const b = run.step.owner;
520 run.argv.append(b.allocator, .{ .bytes = b.dupe(arg) }) catch @panic("OOM");
521}
522
523pub fn addArgs(run: *Run, args: []const []const u8) void {
524 for (args) |arg| run.addArg(arg);
525}
526
527pub fn setStdIn(run: *Run, stdin: StdIn) void {
528 switch (stdin) {
529 .lazy_path => |lazy_path| lazy_path.addStepDependencies(&run.step),
530 .bytes, .none => {},
531 }
532 run.stdin = stdin;
533}
534
535pub fn setCwd(run: *Run, cwd: Build.LazyPath) void {
536 cwd.addStepDependencies(&run.step);
537 run.cwd = cwd.dupe(run.step.owner);
538}
539
540pub fn clearEnvironment(run: *Run) void {
541 const b = run.step.owner;
542 const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM");
543 new_env_map.* = .init(b.allocator);
544 run.env_map = new_env_map;
545}
546
547pub fn addPathDir(run: *Run, search_path: []const u8) void {
548 const b = run.step.owner;
549 const env_map = getEnvMapInternal(run);
550
551 const use_wine = b.enable_wine and b.graph.host.result.os.tag != .windows and use_wine: switch (run.argv.items[0]) {
552 .artifact => |p| p.artifact.rootModuleTarget().os.tag == .windows,
553 .lazy_path => |p| {
554 switch (p.lazy_path) {
555 .generated => |g| if (g.file.step.cast(Step.Compile)) |cs| break :use_wine cs.rootModuleTarget().os.tag == .windows,
556 else => {},
557 }
558 break :use_wine std.mem.endsWith(u8, p.lazy_path.basename(b, &run.step), ".exe");
559 },
560 .decorated_directory => false,
561 .file_content => unreachable, // not allowed as first arg
562 .bytes => |bytes| std.mem.endsWith(u8, bytes, ".exe"),
563 .output_file, .output_directory => false,
564 };
565 const key = if (use_wine) "WINEPATH" else "PATH";
566 const prev_path = env_map.get(key);
567
568 if (prev_path) |pp| {
569 const new_path = b.fmt("{s}{c}{s}", .{
570 pp,
571 if (use_wine) fs.path.delimiter_windows else fs.path.delimiter,
572 search_path,
573 });
574 env_map.put(key, new_path) catch @panic("OOM");
575 } else {
576 env_map.put(key, b.dupePath(search_path)) catch @panic("OOM");
577 }
578}
579
580pub fn getEnvMap(run: *Run) *EnvMap {
581 return getEnvMapInternal(run);
582}
583
584fn getEnvMapInternal(run: *Run) *EnvMap {
585 const arena = run.step.owner.allocator;
586 return run.env_map orelse {
587 const env_map = arena.create(EnvMap) catch @panic("OOM");
588 env_map.* = process.getEnvMap(arena) catch @panic("unhandled error");
589 run.env_map = env_map;
590 return env_map;
591 };
592}
593
594pub fn setEnvironmentVariable(run: *Run, key: []const u8, value: []const u8) void {
595 const b = run.step.owner;
596 const env_map = run.getEnvMap();
597 env_map.put(b.dupe(key), b.dupe(value)) catch @panic("unhandled error");
598}
599
600pub fn removeEnvironmentVariable(run: *Run, key: []const u8) void {
601 run.getEnvMap().remove(key);
602}
603
604/// Adds a check for exact stderr match. Does not add any other checks.
605pub fn expectStdErrEqual(run: *Run, bytes: []const u8) void {
606 const new_check: StdIo.Check = .{ .expect_stderr_exact = run.step.owner.dupe(bytes) };
607 run.addCheck(new_check);
608}
609
610/// Adds a check for exact stdout match as well as a check for exit code 0, if
611/// there is not already an expected termination check.
612pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void {
613 const new_check: StdIo.Check = .{ .expect_stdout_exact = run.step.owner.dupe(bytes) };
614 run.addCheck(new_check);
615 if (!run.hasTermCheck()) {
616 run.expectExitCode(0);
617 }
618}
619
620pub fn expectExitCode(run: *Run, code: u8) void {
621 const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } };
622 run.addCheck(new_check);
623}
624
625pub fn hasTermCheck(run: Run) bool {
626 for (run.stdio.check.items) |check| switch (check) {
627 .expect_term => return true,
628 else => continue,
629 };
630 return false;
631}
632
633pub fn addCheck(run: *Run, new_check: StdIo.Check) void {
634 const b = run.step.owner;
635
636 switch (run.stdio) {
637 .infer_from_args => {
638 run.stdio = .{ .check = .{} };
639 run.stdio.check.append(b.allocator, new_check) catch @panic("OOM");
640 },
641 .check => |*checks| checks.append(b.allocator, new_check) catch @panic("OOM"),
642 else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of Run instead"),
643 }
644}
645
646pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath {
647 assert(run.stdio != .inherit);
648 assert(run.stdio != .zig_test);
649
650 const b = run.step.owner;
651
652 if (run.captured_stderr) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
653
654 const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
655 captured.* = .{
656 .output = .{
657 .prefix = "",
658 .basename = if (options.basename) |basename| b.dupe(basename) else "stderr",
659 .generated_file = .{ .step = &run.step },
660 },
661 .trim_whitespace = options.trim_whitespace,
662 };
663 run.captured_stderr = captured;
664 return .{ .generated = .{ .file = &captured.output.generated_file } };
665}
666
667pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath {
668 assert(run.stdio != .inherit);
669 assert(run.stdio != .zig_test);
670
671 const b = run.step.owner;
672
673 if (run.captured_stdout) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
674
675 const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
676 captured.* = .{
677 .output = .{
678 .prefix = "",
679 .basename = if (options.basename) |basename| b.dupe(basename) else "stdout",
680 .generated_file = .{ .step = &run.step },
681 },
682 .trim_whitespace = options.trim_whitespace,
683 };
684 run.captured_stdout = captured;
685 return .{ .generated = .{ .file = &captured.output.generated_file } };
686}
687
688/// Adds an additional input files that, when modified, indicates that this Run
689/// step should be re-executed.
690/// If the Run step is determined to have side-effects, the Run step is always
691/// executed when it appears in the build graph, regardless of whether this
692/// file has been modified.
693pub fn addFileInput(self: *Run, file_input: std.Build.LazyPath) void {
694 file_input.addStepDependencies(&self.step);
695 self.file_inputs.append(self.step.owner.allocator, file_input.dupe(self.step.owner)) catch @panic("OOM");
696}
697
698/// Returns whether the Run step has side effects *other than* updating the output arguments.
699fn hasSideEffects(run: Run) bool {
700 if (run.has_side_effects) return true;
701 return switch (run.stdio) {
702 .infer_from_args => !run.hasAnyOutputArgs(),
703 .inherit => true,
704 .check => false,
705 .zig_test => false,
706 };
707}
708
709fn hasAnyOutputArgs(run: Run) bool {
710 if (run.captured_stdout != null) return true;
711 if (run.captured_stderr != null) return true;
712 for (run.argv.items) |arg| switch (arg) {
713 .output_file, .output_directory => return true,
714 else => continue,
715 };
716 return false;
717}
718
719fn checksContainStdout(checks: []const StdIo.Check) bool {
720 for (checks) |check| switch (check) {
721 .expect_stderr_exact,
722 .expect_stderr_match,
723 .expect_term,
724 => continue,
725
726 .expect_stdout_exact,
727 .expect_stdout_match,
728 => return true,
729 };
730 return false;
731}
732
733fn checksContainStderr(checks: []const StdIo.Check) bool {
734 for (checks) |check| switch (check) {
735 .expect_stdout_exact,
736 .expect_stdout_match,
737 .expect_term,
738 => continue,
739
740 .expect_stderr_exact,
741 .expect_stderr_match,
742 => return true,
743 };
744 return false;
745}
746
747/// If `path` is cwd-relative, make it relative to the cwd of the child instead.
748///
749/// Whenever a path is included in the argv of a child, it should be put through this function first
750/// to make sure the child doesn't see paths relative to a cwd other than its own.
751fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 {
752 const b = run.step.owner;
753 const path_str = path.toString(b.graph.arena) catch @panic("OOM");
754 if (std.fs.path.isAbsolute(path_str)) {
755 // Absolute paths don't need changing.
756 return path_str;
757 }
758 const child_cwd_rel: []const u8 = rel: {
759 const child_lazy_cwd = run.cwd orelse break :rel path_str;
760 const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(b.graph.arena) catch @panic("OOM");
761 // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
762 break :rel std.fs.path.relative(b.graph.arena, child_cwd, path_str) catch @panic("OOM");
763 };
764 // Not every path can be made relative, e.g. if the path and the child cwd are on different
765 // disk designators on Windows. In that case, `relative` will return an absolute path which we can
766 // just return.
767 if (std.fs.path.isAbsolute(child_cwd_rel)) {
768 return child_cwd_rel;
769 }
770 // We're not done yet. In some cases this path must be prefixed with './':
771 // * On POSIX, the executable name cannot be a single component like 'foo'
772 // * Some executables might treat a leading '-' like a flag, which we must avoid
773 // There's no harm in it, so just *always* apply this prefix.
774 return std.fs.path.join(b.graph.arena, &.{ ".", child_cwd_rel }) catch @panic("OOM");
775}
776
777const IndexedOutput = struct {
778 index: usize,
779 tag: @typeInfo(Arg).@"union".tag_type.?,
780 output: *Output,
781};
782fn make(step: *Step, options: Step.MakeOptions) !void {
783 const b = step.owner;
784 const io = b.graph.io;
785 const arena = b.allocator;
786 const run: *Run = @fieldParentPtr("step", step);
787 const has_side_effects = run.hasSideEffects();
788
789 var argv_list = std.array_list.Managed([]const u8).init(arena);
790 var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena);
791
792 var man = b.graph.cache.obtain();
793 defer man.deinit();
794
795 if (run.env_map) |env_map| {
796 const KV = struct { []const u8, []const u8 };
797 var kv_pairs = try std.array_list.Managed(KV).initCapacity(arena, env_map.count());
798 var iter = env_map.iterator();
799 while (iter.next()) |entry| {
800 kv_pairs.appendAssumeCapacity(.{ entry.key_ptr.*, entry.value_ptr.* });
801 }
802
803 std.mem.sortUnstable(KV, kv_pairs.items, {}, struct {
804 fn lessThan(_: void, kv1: KV, kv2: KV) bool {
805 const k1 = kv1[0];
806 const k2 = kv2[0];
807
808 if (k1.len != k2.len) return k1.len < k2.len;
809
810 for (k1, k2) |c1, c2| {
811 if (c1 == c2) continue;
812 return c1 < c2;
813 }
814 unreachable; // two keys cannot be equal
815 }
816 }.lessThan);
817
818 for (kv_pairs.items) |kv| {
819 man.hash.addBytes(kv[0]);
820 man.hash.addBytes(kv[1]);
821 }
822 }
823
824 man.hash.add(run.color);
825 man.hash.add(run.disable_zig_progress);
826
827 for (run.argv.items) |arg| {
828 switch (arg) {
829 .bytes => |bytes| {
830 try argv_list.append(bytes);
831 man.hash.addBytes(bytes);
832 },
833 .lazy_path => |file| {
834 const file_path = file.lazy_path.getPath3(b, step);
835 try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
836 man.hash.addBytes(file.prefix);
837 _ = try man.addFilePath(file_path, null);
838 },
839 .decorated_directory => |dd| {
840 const file_path = dd.lazy_path.getPath3(b, step);
841 const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix });
842 try argv_list.append(resolved_arg);
843 man.hash.addBytes(resolved_arg);
844 },
845 .file_content => |file_plp| {
846 const file_path = file_plp.lazy_path.getPath3(b, step);
847
848 var result: std.Io.Writer.Allocating = .init(arena);
849 errdefer result.deinit();
850 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
851
852 const file = file_path.root_dir.handle.openFile(file_path.subPathOrDot(), .{}) catch |err| {
853 return step.fail(
854 "unable to open input file '{f}': {t}",
855 .{ file_path, err },
856 );
857 };
858 defer file.close();
859
860 var buf: [1024]u8 = undefined;
861 var file_reader = file.reader(io, &buf);
862 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
863 error.ReadFailed => return step.fail(
864 "failed to read from '{f}': {t}",
865 .{ file_path, file_reader.err.? },
866 ),
867 error.WriteFailed => return error.OutOfMemory,
868 };
869
870 try argv_list.append(result.written());
871 man.hash.addBytes(file_plp.prefix);
872 _ = try man.addFilePath(file_path, null);
873 },
874 .artifact => |pa| {
875 const artifact = pa.artifact;
876
877 if (artifact.rootModuleTarget().os.tag == .windows) {
878 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
879 run.addPathForDynLibs(artifact);
880 }
881 const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?;
882
883 try argv_list.append(b.fmt("{s}{s}", .{
884 pa.prefix,
885 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
886 }));
887
888 _ = try man.addFile(file_path, null);
889 },
890 .output_file, .output_directory => |output| {
891 man.hash.addBytes(output.prefix);
892 man.hash.addBytes(output.basename);
893 // Add a placeholder into the argument list because we need the
894 // manifest hash to be updated with all arguments before the
895 // object directory is computed.
896 try output_placeholders.append(.{
897 .index = argv_list.items.len,
898 .tag = arg,
899 .output = output,
900 });
901 _ = try argv_list.addOne();
902 },
903 }
904 }
905
906 switch (run.stdin) {
907 .bytes => |bytes| {
908 man.hash.addBytes(bytes);
909 },
910 .lazy_path => |lazy_path| {
911 const file_path = lazy_path.getPath2(b, step);
912 _ = try man.addFile(file_path, null);
913 },
914 .none => {},
915 }
916
917 if (run.captured_stdout) |captured| {
918 man.hash.addBytes(captured.output.basename);
919 man.hash.add(captured.trim_whitespace);
920 }
921
922 if (run.captured_stderr) |captured| {
923 man.hash.addBytes(captured.output.basename);
924 man.hash.add(captured.trim_whitespace);
925 }
926
927 hashStdIo(&man.hash, run.stdio);
928
929 for (run.file_inputs.items) |lazy_path| {
930 _ = try man.addFile(lazy_path.getPath2(b, step), null);
931 }
932
933 if (run.cwd) |cwd| {
934 const cwd_path = cwd.getPath3(b, step);
935 _ = man.hash.addBytes(try cwd_path.toString(arena));
936 }
937
938 if (!has_side_effects and try step.cacheHitAndWatch(&man)) {
939 // cache hit, skip running command
940 const digest = man.final();
941
942 try populateGeneratedPaths(
943 arena,
944 output_placeholders.items,
945 run.captured_stdout,
946 run.captured_stderr,
947 b.cache_root,
948 &digest,
949 );
950
951 step.result_cached = true;
952 return;
953 }
954
955 const dep_output_file = run.dep_output_file orelse {
956 // We already know the final output paths, use them directly.
957 const digest = if (has_side_effects)
958 man.hash.final()
959 else
960 man.final();
961
962 try populateGeneratedPaths(
963 arena,
964 output_placeholders.items,
965 run.captured_stdout,
966 run.captured_stderr,
967 b.cache_root,
968 &digest,
969 );
970
971 const output_dir_path = "o" ++ fs.path.sep_str ++ &digest;
972 for (output_placeholders.items) |placeholder| {
973 const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename });
974 const output_sub_dir_path = switch (placeholder.tag) {
975 .output_file => fs.path.dirname(output_sub_path).?,
976 .output_directory => output_sub_path,
977 else => unreachable,
978 };
979 b.cache_root.handle.makePath(output_sub_dir_path) catch |err| {
980 return step.fail("unable to make path '{f}{s}': {s}", .{
981 b.cache_root, output_sub_dir_path, @errorName(err),
982 });
983 };
984 const arg_output_path = run.convertPathArg(.{
985 .root_dir = .cwd(),
986 .sub_path = placeholder.output.generated_file.getPath(),
987 });
988 argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
989 arg_output_path
990 else
991 b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path });
992 }
993
994 try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null);
995 if (!has_side_effects) try step.writeManifestAndWatch(&man);
996 return;
997 };
998
999 // We do not know the final output paths yet, use temp paths to run the command.
1000 const rand_int = std.crypto.random.int(u64);
1001 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
1002
1003 for (output_placeholders.items) |placeholder| {
1004 const output_components = .{ tmp_dir_path, placeholder.output.basename };
1005 const output_sub_path = b.pathJoin(&output_components);
1006 const output_sub_dir_path = switch (placeholder.tag) {
1007 .output_file => fs.path.dirname(output_sub_path).?,
1008 .output_directory => output_sub_path,
1009 else => unreachable,
1010 };
1011 b.cache_root.handle.makePath(output_sub_dir_path) catch |err| {
1012 return step.fail("unable to make path '{f}{s}': {s}", .{
1013 b.cache_root, output_sub_dir_path, @errorName(err),
1014 });
1015 };
1016 const raw_output_path: Build.Cache.Path = .{
1017 .root_dir = b.cache_root,
1018 .sub_path = b.pathJoin(&output_components),
1019 };
1020 placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM");
1021 argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{
1022 placeholder.output.prefix,
1023 run.convertPathArg(raw_output_path),
1024 });
1025 }
1026
1027 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null);
1028
1029 const dep_file_dir = std.fs.cwd();
1030 const dep_file_basename = dep_output_file.generated_file.getPath2(b, step);
1031 if (has_side_effects)
1032 try man.addDepFile(dep_file_dir, dep_file_basename)
1033 else
1034 try man.addDepFilePost(dep_file_dir, dep_file_basename);
1035
1036 const digest = if (has_side_effects)
1037 man.hash.final()
1038 else
1039 man.final();
1040
1041 const any_output = output_placeholders.items.len > 0 or
1042 run.captured_stdout != null or run.captured_stderr != null;
1043
1044 // Rename into place
1045 if (any_output) {
1046 const o_sub_path = "o" ++ fs.path.sep_str ++ &digest;
1047
1048 b.cache_root.handle.rename(tmp_dir_path, o_sub_path) catch |err| {
1049 if (err == error.PathAlreadyExists) {
1050 b.cache_root.handle.deleteTree(o_sub_path) catch |del_err| {
1051 return step.fail("unable to remove dir '{f}'{s}: {s}", .{
1052 b.cache_root,
1053 tmp_dir_path,
1054 @errorName(del_err),
1055 });
1056 };
1057 b.cache_root.handle.rename(tmp_dir_path, o_sub_path) catch |retry_err| {
1058 return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {s}", .{
1059 b.cache_root, tmp_dir_path,
1060 b.cache_root, o_sub_path,
1061 @errorName(retry_err),
1062 });
1063 };
1064 } else {
1065 return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {s}", .{
1066 b.cache_root, tmp_dir_path,
1067 b.cache_root, o_sub_path,
1068 @errorName(err),
1069 });
1070 }
1071 };
1072 }
1073
1074 if (!has_side_effects) try step.writeManifestAndWatch(&man);
1075
1076 try populateGeneratedPaths(
1077 arena,
1078 output_placeholders.items,
1079 run.captured_stdout,
1080 run.captured_stderr,
1081 b.cache_root,
1082 &digest,
1083 );
1084}
1085
1086pub fn rerunInFuzzMode(
1087 run: *Run,
1088 fuzz: *std.Build.Fuzz,
1089 unit_test_index: u32,
1090 prog_node: std.Progress.Node,
1091) !void {
1092 const step = &run.step;
1093 const b = step.owner;
1094 const io = b.graph.io;
1095 const arena = b.allocator;
1096 var argv_list: std.ArrayList([]const u8) = .empty;
1097 for (run.argv.items) |arg| {
1098 switch (arg) {
1099 .bytes => |bytes| {
1100 try argv_list.append(arena, bytes);
1101 },
1102 .lazy_path => |file| {
1103 const file_path = file.lazy_path.getPath3(b, step);
1104 try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
1105 },
1106 .decorated_directory => |dd| {
1107 const file_path = dd.lazy_path.getPath3(b, step);
1108 try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));
1109 },
1110 .file_content => |file_plp| {
1111 const file_path = file_plp.lazy_path.getPath3(b, step);
1112
1113 var result: std.Io.Writer.Allocating = .init(arena);
1114 errdefer result.deinit();
1115 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
1116
1117 const file = try file_path.root_dir.handle.openFile(file_path.subPathOrDot(), .{});
1118 defer file.close();
1119
1120 var buf: [1024]u8 = undefined;
1121 var file_reader = file.reader(io, &buf);
1122 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
1123 error.ReadFailed => return file_reader.err.?,
1124 error.WriteFailed => return error.OutOfMemory,
1125 };
1126
1127 try argv_list.append(arena, result.written());
1128 },
1129 .artifact => |pa| {
1130 const artifact = pa.artifact;
1131 const file_path: []const u8 = p: {
1132 if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?});
1133 break :p artifact.installed_path orelse artifact.generated_bin.?.path.?;
1134 };
1135 try argv_list.append(arena, b.fmt("{s}{s}", .{
1136 pa.prefix,
1137 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
1138 }));
1139 },
1140 .output_file, .output_directory => unreachable,
1141 }
1142 }
1143
1144 if (run.step.result_failed_command) |cmd| {
1145 fuzz.gpa.free(cmd);
1146 run.step.result_failed_command = null;
1147 }
1148
1149 const has_side_effects = false;
1150 const rand_int = std.crypto.random.int(u64);
1151 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
1152 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{
1153 .progress_node = prog_node,
1154 .watch = undefined, // not used by `runCommand`
1155 .web_server = null, // only needed for time reports
1156 .ttyconf = fuzz.ttyconf,
1157 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
1158 .gpa = fuzz.gpa,
1159 }, .{
1160 .unit_test_index = unit_test_index,
1161 .fuzz = fuzz,
1162 });
1163}
1164
1165fn populateGeneratedPaths(
1166 arena: std.mem.Allocator,
1167 output_placeholders: []const IndexedOutput,
1168 captured_stdout: ?*CapturedStdIo,
1169 captured_stderr: ?*CapturedStdIo,
1170 cache_root: Build.Cache.Directory,
1171 digest: *const Build.Cache.HexDigest,
1172) !void {
1173 for (output_placeholders) |placeholder| {
1174 placeholder.output.generated_file.path = try cache_root.join(arena, &.{
1175 "o", digest, placeholder.output.basename,
1176 });
1177 }
1178
1179 if (captured_stdout) |captured| {
1180 captured.output.generated_file.path = try cache_root.join(arena, &.{
1181 "o", digest, captured.output.basename,
1182 });
1183 }
1184
1185 if (captured_stderr) |captured| {
1186 captured.output.generated_file.path = try cache_root.join(arena, &.{
1187 "o", digest, captured.output.basename,
1188 });
1189 }
1190}
1191
1192fn formatTerm(term: ?std.process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
1193 if (term) |t| switch (t) {
1194 .Exited => |code| try w.print("exited with code {d}", .{code}),
1195 .Signal => |sig| try w.print("terminated with signal {d}", .{sig}),
1196 .Stopped => |sig| try w.print("stopped with signal {d}", .{sig}),
1197 .Unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
1198 } else {
1199 try w.writeAll("exited with any code");
1200 }
1201}
1202fn fmtTerm(term: ?std.process.Child.Term) std.fmt.Alt(?std.process.Child.Term, formatTerm) {
1203 return .{ .data = term };
1204}
1205
1206fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term) bool {
1207 return if (expected) |e| switch (e) {
1208 .Exited => |expected_code| switch (actual) {
1209 .Exited => |actual_code| expected_code == actual_code,
1210 else => false,
1211 },
1212 .Signal => |expected_sig| switch (actual) {
1213 .Signal => |actual_sig| expected_sig == actual_sig,
1214 else => false,
1215 },
1216 .Stopped => |expected_sig| switch (actual) {
1217 .Stopped => |actual_sig| expected_sig == actual_sig,
1218 else => false,
1219 },
1220 .Unknown => |expected_code| switch (actual) {
1221 .Unknown => |actual_code| expected_code == actual_code,
1222 else => false,
1223 },
1224 } else switch (actual) {
1225 .Exited => true,
1226 else => false,
1227 };
1228}
1229
1230const FuzzContext = struct {
1231 fuzz: *std.Build.Fuzz,
1232 unit_test_index: u32,
1233};
1234
1235fn runCommand(
1236 run: *Run,
1237 argv: []const []const u8,
1238 has_side_effects: bool,
1239 output_dir_path: []const u8,
1240 options: Step.MakeOptions,
1241 fuzz_context: ?FuzzContext,
1242) !void {
1243 const step = &run.step;
1244 const b = step.owner;
1245 const arena = b.allocator;
1246 const gpa = options.gpa;
1247
1248 const cwd: ?[]const u8 = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, step) else null;
1249
1250 try step.handleChildProcUnsupported();
1251 try Step.handleVerbose2(step.owner, cwd, run.env_map, argv);
1252
1253 const allow_skip = switch (run.stdio) {
1254 .check, .zig_test => run.skip_foreign_checks,
1255 else => false,
1256 };
1257
1258 var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);
1259 defer interp_argv.deinit();
1260
1261 var env_map: EnvMap = env: {
1262 const orig = run.env_map orelse &b.graph.env_map;
1263 break :env try orig.clone(gpa);
1264 };
1265 defer env_map.deinit();
1266
1267 color: switch (run.color) {
1268 .manual => {},
1269 .enable => {
1270 try env_map.put("CLICOLOR_FORCE", "1");
1271 env_map.remove("NO_COLOR");
1272 },
1273 .disable => {
1274 try env_map.put("NO_COLOR", "1");
1275 env_map.remove("CLICOLOR_FORCE");
1276 },
1277 .inherit => switch (options.ttyconf) {
1278 .no_color, .windows_api => continue :color .disable,
1279 .escape_codes => continue :color .enable,
1280 },
1281 .auto => {
1282 const capture_stderr = run.captured_stderr != null or switch (run.stdio) {
1283 .check => |checks| checksContainStderr(checks.items),
1284 .infer_from_args, .inherit, .zig_test => false,
1285 };
1286 if (capture_stderr) {
1287 continue :color .disable;
1288 } else {
1289 continue :color .inherit;
1290 }
1291 },
1292 }
1293
1294 const opt_generic_result = spawnChildAndCollect(run, argv, &env_map, has_side_effects, options, fuzz_context) catch |err| term: {
1295 // InvalidExe: cpu arch mismatch
1296 // FileNotFound: can happen with a wrong dynamic linker path
1297 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
1298 // TODO: learn the target from the binary directly rather than from
1299 // relying on it being a Compile step. This will make this logic
1300 // work even for the edge case that the binary was produced by a
1301 // third party.
1302 const exe = switch (run.argv.items[0]) {
1303 .artifact => |exe| exe.artifact,
1304 else => break :interpret,
1305 };
1306 switch (exe.kind) {
1307 .exe, .@"test" => {},
1308 else => break :interpret,
1309 }
1310
1311 const root_target = exe.rootModuleTarget();
1312 const need_cross_libc = exe.is_linking_libc and
1313 (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic));
1314 const other_target = exe.root_module.resolved_target.?.result;
1315 switch (std.zig.system.getExternalExecutor(&b.graph.host.result, &other_target, .{
1316 .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null,
1317 .link_libc = exe.is_linking_libc,
1318 })) {
1319 .native, .rosetta => {
1320 if (allow_skip) return error.MakeSkipped;
1321 break :interpret;
1322 },
1323 .wine => |bin_name| {
1324 if (b.enable_wine) {
1325 try interp_argv.append(bin_name);
1326 try interp_argv.appendSlice(argv);
1327
1328 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
1329 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
1330 if (env_map.get("WINEDEBUG") == null) {
1331 try env_map.put("WINEDEBUG", "-all");
1332 }
1333 } else {
1334 return failForeign(run, "-fwine", argv[0], exe);
1335 }
1336 },
1337 .qemu => |bin_name| {
1338 if (b.enable_qemu) {
1339 try interp_argv.append(bin_name);
1340
1341 if (need_cross_libc) {
1342 if (b.libc_runtimes_dir) |dir| {
1343 try interp_argv.append("-L");
1344 try interp_argv.append(b.pathJoin(&.{
1345 dir,
1346 try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
1347 b.allocator,
1348 root_target.cpu.arch,
1349 root_target.os.tag,
1350 root_target.abi,
1351 ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
1352 b.allocator,
1353 root_target.cpu.arch,
1354 root_target.abi,
1355 ) else unreachable,
1356 }));
1357 } else return failForeign(run, "--libc-runtimes", argv[0], exe);
1358 }
1359
1360 try interp_argv.appendSlice(argv);
1361 } else return failForeign(run, "-fqemu", argv[0], exe);
1362 },
1363 .darling => |bin_name| {
1364 if (b.enable_darling) {
1365 try interp_argv.append(bin_name);
1366 try interp_argv.appendSlice(argv);
1367 } else {
1368 return failForeign(run, "-fdarling", argv[0], exe);
1369 }
1370 },
1371 .wasmtime => |bin_name| {
1372 if (b.enable_wasmtime) {
1373 // https://github.com/bytecodealliance/wasmtime/issues/7384
1374 //
1375 // In Wasmtime versions prior to 14, options passed after the module name
1376 // could be interpreted by Wasmtime if it recognized them. As with many CLI
1377 // tools, the `--` token is used to stop that behavior and indicate that the
1378 // remaining arguments are for the WASM program being executed. Historically,
1379 // we passed `--` after the module name here.
1380 //
1381 // After version 14, the `--` can no longer be passed after the module name,
1382 // but is also not necessary as Wasmtime will no longer try to interpret
1383 // options after the module name. So, we could just simply omit `--` for
1384 // newer Wasmtime versions. But to maintain compatibility for older versions
1385 // that still try to interpret options after the module name, we have moved
1386 // the `--` before the module name. This appears to work for both old and
1387 // new Wasmtime versions.
1388 try interp_argv.append(bin_name);
1389 try interp_argv.append("--dir=.");
1390 try interp_argv.append("--");
1391 try interp_argv.append(argv[0]);
1392 try interp_argv.appendSlice(argv[1..]);
1393 } else {
1394 return failForeign(run, "-fwasmtime", argv[0], exe);
1395 }
1396 },
1397 .bad_dl => |foreign_dl| {
1398 if (allow_skip) return error.MakeSkipped;
1399
1400 const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)";
1401
1402 return step.fail(
1403 \\the host system is unable to execute binaries from the target
1404 \\ because the host dynamic linker is '{s}',
1405 \\ while the target dynamic linker is '{s}'.
1406 \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
1407 , .{ host_dl, foreign_dl });
1408 },
1409 .bad_os_or_cpu => {
1410 if (allow_skip) return error.MakeSkipped;
1411
1412 const host_name = try b.graph.host.result.zigTriple(b.allocator);
1413 const foreign_name = try root_target.zigTriple(b.allocator);
1414
1415 return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{
1416 host_name, foreign_name,
1417 });
1418 },
1419 }
1420
1421 if (root_target.os.tag == .windows) {
1422 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
1423 run.addPathForDynLibs(exe);
1424 }
1425
1426 gpa.free(step.result_failed_command.?);
1427 step.result_failed_command = null;
1428 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
1429
1430 break :term spawnChildAndCollect(run, interp_argv.items, &env_map, has_side_effects, options, fuzz_context) catch |e| {
1431 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
1432 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1433 return step.fail("unable to spawn interpreter {s}: {s}", .{
1434 interp_argv.items[0], @errorName(e),
1435 });
1436 };
1437 }
1438 if (err == error.MakeFailed) return error.MakeFailed; // error already reported
1439
1440 return step.fail("failed to spawn and capture stdio from {s}: {s}", .{ argv[0], @errorName(err) });
1441 };
1442
1443 const generic_result = opt_generic_result orelse {
1444 assert(run.stdio == .zig_test);
1445 // Specific errors have already been reported, and test results are populated. All we need
1446 // to do is report step failure if any test failed.
1447 if (!step.test_results.isSuccess()) return error.MakeFailed;
1448 return;
1449 };
1450
1451 assert(fuzz_context == null);
1452 assert(run.stdio != .zig_test);
1453
1454 // Capture stdout and stderr to GeneratedFile objects.
1455 const Stream = struct {
1456 captured: ?*CapturedStdIo,
1457 bytes: ?[]const u8,
1458 };
1459 for ([_]Stream{
1460 .{
1461 .captured = run.captured_stdout,
1462 .bytes = generic_result.stdout,
1463 },
1464 .{
1465 .captured = run.captured_stderr,
1466 .bytes = generic_result.stderr,
1467 },
1468 }) |stream| {
1469 if (stream.captured) |captured| {
1470 const output_components = .{ output_dir_path, captured.output.basename };
1471 const output_path = try b.cache_root.join(arena, &output_components);
1472 captured.output.generated_file.path = output_path;
1473
1474 const sub_path = b.pathJoin(&output_components);
1475 const sub_path_dirname = fs.path.dirname(sub_path).?;
1476 b.cache_root.handle.makePath(sub_path_dirname) catch |err| {
1477 return step.fail("unable to make path '{f}{s}': {s}", .{
1478 b.cache_root, sub_path_dirname, @errorName(err),
1479 });
1480 };
1481 const data = switch (captured.trim_whitespace) {
1482 .none => stream.bytes.?,
1483 .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
1484 .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
1485 .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
1486 };
1487 b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = data }) catch |err| {
1488 return step.fail("unable to write file '{f}{s}': {s}", .{
1489 b.cache_root, sub_path, @errorName(err),
1490 });
1491 };
1492 }
1493 }
1494
1495 switch (run.stdio) {
1496 .zig_test => unreachable,
1497 .check => |checks| for (checks.items) |check| switch (check) {
1498 .expect_stderr_exact => |expected_bytes| {
1499 if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
1500 return step.fail(
1501 \\========= expected this stderr: =========
1502 \\{s}
1503 \\========= but found: ====================
1504 \\{s}
1505 , .{
1506 expected_bytes,
1507 generic_result.stderr.?,
1508 });
1509 }
1510 },
1511 .expect_stderr_match => |match| {
1512 if (mem.indexOf(u8, generic_result.stderr.?, match) == null) {
1513 return step.fail(
1514 \\========= expected to find in stderr: =========
1515 \\{s}
1516 \\========= but stderr does not contain it: =====
1517 \\{s}
1518 , .{
1519 match,
1520 generic_result.stderr.?,
1521 });
1522 }
1523 },
1524 .expect_stdout_exact => |expected_bytes| {
1525 if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
1526 return step.fail(
1527 \\========= expected this stdout: =========
1528 \\{s}
1529 \\========= but found: ====================
1530 \\{s}
1531 , .{
1532 expected_bytes,
1533 generic_result.stdout.?,
1534 });
1535 }
1536 },
1537 .expect_stdout_match => |match| {
1538 if (mem.indexOf(u8, generic_result.stdout.?, match) == null) {
1539 return step.fail(
1540 \\========= expected to find in stdout: =========
1541 \\{s}
1542 \\========= but stdout does not contain it: =====
1543 \\{s}
1544 , .{
1545 match,
1546 generic_result.stdout.?,
1547 });
1548 }
1549 },
1550 .expect_term => |expected_term| {
1551 if (!termMatches(expected_term, generic_result.term)) {
1552 return step.fail("process {f} (expected {f})", .{
1553 fmtTerm(generic_result.term),
1554 fmtTerm(expected_term),
1555 });
1556 }
1557 },
1558 },
1559 else => {
1560 // On failure, report captured stderr like normal standard error output.
1561 const bad_exit = switch (generic_result.term) {
1562 .Exited => |code| code != 0,
1563 .Signal, .Stopped, .Unknown => true,
1564 };
1565 if (bad_exit) {
1566 if (generic_result.stderr) |bytes| {
1567 run.step.result_stderr = bytes;
1568 }
1569 }
1570
1571 try step.handleChildProcessTerm(generic_result.term);
1572 },
1573 }
1574}
1575
1576const EvalZigTestResult = struct {
1577 test_results: Step.TestResults,
1578 test_metadata: ?TestMetadata,
1579};
1580const EvalGenericResult = struct {
1581 term: std.process.Child.Term,
1582 stdout: ?[]const u8,
1583 stderr: ?[]const u8,
1584};
1585
1586fn spawnChildAndCollect(
1587 run: *Run,
1588 argv: []const []const u8,
1589 env_map: *EnvMap,
1590 has_side_effects: bool,
1591 options: Step.MakeOptions,
1592 fuzz_context: ?FuzzContext,
1593) !?EvalGenericResult {
1594 const b = run.step.owner;
1595 const arena = b.allocator;
1596
1597 if (fuzz_context != null) {
1598 assert(!has_side_effects);
1599 assert(run.stdio == .zig_test);
1600 }
1601
1602 var child = std.process.Child.init(argv, arena);
1603 if (run.cwd) |lazy_cwd| {
1604 child.cwd = lazy_cwd.getPath2(b, &run.step);
1605 }
1606 child.env_map = env_map;
1607 child.request_resource_usage_statistics = true;
1608
1609 child.stdin_behavior = switch (run.stdio) {
1610 .infer_from_args => if (has_side_effects) .Inherit else .Ignore,
1611 .inherit => .Inherit,
1612 .check => .Ignore,
1613 .zig_test => .Pipe,
1614 };
1615 child.stdout_behavior = switch (run.stdio) {
1616 .infer_from_args => if (has_side_effects) .Inherit else .Ignore,
1617 .inherit => .Inherit,
1618 .check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore,
1619 .zig_test => .Pipe,
1620 };
1621 child.stderr_behavior = switch (run.stdio) {
1622 .infer_from_args => if (has_side_effects) .Inherit else .Pipe,
1623 .inherit => .Inherit,
1624 .check => .Pipe,
1625 .zig_test => .Pipe,
1626 };
1627 if (run.captured_stdout != null) child.stdout_behavior = .Pipe;
1628 if (run.captured_stderr != null) child.stderr_behavior = .Pipe;
1629 if (run.stdin != .none) {
1630 assert(run.stdio != .inherit);
1631 child.stdin_behavior = .Pipe;
1632 }
1633
1634 // If an error occurs, it's caused by this command:
1635 assert(run.step.result_failed_command == null);
1636 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, argv);
1637
1638 if (run.stdio == .zig_test) {
1639 var timer = try std.time.Timer.start();
1640 const res = try evalZigTest(run, &child, options, fuzz_context);
1641 run.step.result_duration_ns = timer.read();
1642 run.step.test_results = res.test_results;
1643 if (res.test_metadata) |tm| {
1644 run.cached_test_metadata = tm.toCachedTestMetadata();
1645 if (options.web_server) |ws| {
1646 if (b.graph.time_report) {
1647 ws.updateTimeReportRunTest(
1648 run,
1649 &run.cached_test_metadata.?,
1650 tm.ns_per_test,
1651 );
1652 }
1653 }
1654 }
1655 return null;
1656 } else {
1657 const inherit = child.stdout_behavior == .Inherit or child.stderr_behavior == .Inherit;
1658 if (!run.disable_zig_progress and !inherit) {
1659 child.progress_node = options.progress_node;
1660 }
1661 if (inherit) std.debug.lockStdErr();
1662 defer if (inherit) std.debug.unlockStdErr();
1663 var timer = try std.time.Timer.start();
1664 const res = try evalGeneric(run, &child);
1665 run.step.result_duration_ns = timer.read();
1666 return .{ .term = res.term, .stdout = res.stdout, .stderr = res.stderr };
1667 }
1668}
1669
1670const StdioPollEnum = enum { stdout, stderr };
1671
1672fn evalZigTest(
1673 run: *Run,
1674 child: *std.process.Child,
1675 options: Step.MakeOptions,
1676 fuzz_context: ?FuzzContext,
1677) !EvalZigTestResult {
1678 const gpa = run.step.owner.allocator;
1679 const arena = run.step.owner.allocator;
1680
1681 // We will update this every time a child runs.
1682 run.step.result_peak_rss = 0;
1683
1684 var result: EvalZigTestResult = .{
1685 .test_results = .{
1686 .test_count = 0,
1687 .skip_count = 0,
1688 .fail_count = 0,
1689 .crash_count = 0,
1690 .timeout_count = 0,
1691 .leak_count = 0,
1692 .log_err_count = 0,
1693 },
1694 .test_metadata = null,
1695 };
1696
1697 while (true) {
1698 try child.spawn();
1699 var poller = std.Io.poll(gpa, StdioPollEnum, .{
1700 .stdout = child.stdout.?,
1701 .stderr = child.stderr.?,
1702 });
1703 var child_killed = false;
1704 defer if (!child_killed) {
1705 _ = child.kill() catch {};
1706 poller.deinit();
1707 run.step.result_peak_rss = @max(
1708 run.step.result_peak_rss,
1709 child.resource_usage_statistics.getMaxRss() orelse 0,
1710 );
1711 };
1712
1713 try child.waitForSpawn();
1714
1715 switch (try pollZigTest(
1716 run,
1717 child,
1718 options,
1719 fuzz_context,
1720 &poller,
1721 &result.test_metadata,
1722 &result.test_results,
1723 )) {
1724 .write_failed => |err| {
1725 // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
1726 // all available stderr to make our error output as useful as possible.
1727 while (try poller.poll()) {}
1728 run.step.result_stderr = try arena.dupe(u8, poller.reader(.stderr).buffered());
1729
1730 // Clean up everything and wait for the child to exit.
1731 child.stdin.?.close();
1732 child.stdin = null;
1733 poller.deinit();
1734 child_killed = true;
1735 const term = try child.wait();
1736 run.step.result_peak_rss = @max(
1737 run.step.result_peak_rss,
1738 child.resource_usage_statistics.getMaxRss() orelse 0,
1739 );
1740
1741 try run.step.addError("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) });
1742 return result;
1743 },
1744 .no_poll => |no_poll| {
1745 // This might be a success (we requested exit and the child dutifully closed stdout) or
1746 // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
1747 const stderr_owned = try arena.dupe(u8, poller.reader(.stderr).buffered());
1748 poller.reader(.stderr).tossBuffered();
1749
1750 // Clean up everything and wait for the child to exit.
1751 child.stdin.?.close();
1752 child.stdin = null;
1753 poller.deinit();
1754 child_killed = true;
1755 const term = try child.wait();
1756 run.step.result_peak_rss = @max(
1757 run.step.result_peak_rss,
1758 child.resource_usage_statistics.getMaxRss() orelse 0,
1759 );
1760
1761 if (no_poll.active_test_index) |test_index| {
1762 // A test was running, so this is definitely a crash. Report it against that
1763 // test, and continue to the next test.
1764 result.test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
1765 result.test_results.crash_count += 1;
1766 try run.step.addError("'{s}' {f}{s}{s}", .{
1767 result.test_metadata.?.testName(test_index),
1768 fmtTerm(term),
1769 if (stderr_owned.len != 0) " with stderr:\n" else "",
1770 std.mem.trim(u8, stderr_owned, "\n"),
1771 });
1772 continue;
1773 }
1774
1775 // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
1776 run.step.result_stderr = stderr_owned;
1777 const tests_done = result.test_metadata != null and result.test_metadata.?.next_index == std.math.maxInt(u32);
1778 if (!tests_done or !termMatches(.{ .Exited = 0 }, term)) {
1779 try run.step.addError("test process unexpectedly {f}", .{fmtTerm(term)});
1780 }
1781 return result;
1782 },
1783 .timeout => |timeout| {
1784 const stderr = poller.reader(.stderr).buffered();
1785 poller.reader(.stderr).tossBuffered();
1786 if (timeout.active_test_index) |test_index| {
1787 // A test was running. Report the timeout against that test, and continue on to
1788 // the next test.
1789 result.test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
1790 result.test_results.timeout_count += 1;
1791 try run.step.addError("'{s}' timed out after {D}{s}{s}", .{
1792 result.test_metadata.?.testName(test_index),
1793 timeout.ns_elapsed,
1794 if (stderr.len != 0) " with stderr:\n" else "",
1795 std.mem.trim(u8, stderr, "\n"),
1796 });
1797 continue;
1798 }
1799 // Just log an error and let the child be killed.
1800 run.step.result_stderr = try arena.dupe(u8, stderr);
1801 return run.step.fail("test runner failed to respond for {D}", .{timeout.ns_elapsed});
1802 },
1803 }
1804 comptime unreachable;
1805 }
1806}
1807
1808/// Polls stdout of a Zig test process until a termination condition is reached:
1809/// * A write fails, indicating the child unexpectedly closed stdin
1810/// * A test (or a response from the test runner) times out
1811/// * `poll` fails, indicating the child closed stdout and stderr
1812fn pollZigTest(
1813 run: *Run,
1814 child: *std.process.Child,
1815 options: Step.MakeOptions,
1816 fuzz_context: ?FuzzContext,
1817 poller: *std.Io.Poller(StdioPollEnum),
1818 opt_metadata: *?TestMetadata,
1819 results: *Step.TestResults,
1820) !union(enum) {
1821 write_failed: anyerror,
1822 no_poll: struct {
1823 active_test_index: ?u32,
1824 ns_elapsed: u64,
1825 },
1826 timeout: struct {
1827 active_test_index: ?u32,
1828 ns_elapsed: u64,
1829 },
1830} {
1831 const gpa = run.step.owner.allocator;
1832 const arena = run.step.owner.allocator;
1833 const io = run.step.owner.graph.io;
1834
1835 var sub_prog_node: ?std.Progress.Node = null;
1836 defer if (sub_prog_node) |n| n.end();
1837
1838 if (fuzz_context) |ctx| {
1839 assert(opt_metadata.* == null); // fuzz processes are never restarted
1840 switch (ctx.fuzz.mode) {
1841 .forever => {
1842 sendRunFuzzTestMessage(
1843 child.stdin.?,
1844 ctx.unit_test_index,
1845 .forever,
1846 0, // instance ID; will be used by multiprocess forever fuzzing in the future
1847 ) catch |err| return .{ .write_failed = err };
1848 },
1849 .limit => |limit| {
1850 sendRunFuzzTestMessage(
1851 child.stdin.?,
1852 ctx.unit_test_index,
1853 .iterations,
1854 limit.amount,
1855 ) catch |err| return .{ .write_failed = err };
1856 },
1857 }
1858 } else if (opt_metadata.*) |*md| {
1859 // Previous unit test process died or was killed; we're continuing where it left off
1860 requestNextTest(child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
1861 } else {
1862 // Running unit tests normally
1863 run.fuzz_tests.clearRetainingCapacity();
1864 sendMessage(child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
1865 }
1866
1867 var active_test_index: ?u32 = null;
1868
1869 // `null` means this host does not support `std.time.Timer`. This timer is `reset()` whenever we
1870 // change `active_test_index`, i.e. whenever a test starts or finishes.
1871 var timer: ?std.time.Timer = std.time.Timer.start() catch null;
1872
1873 var coverage_id: ?u64 = null;
1874
1875 // This timeout is used when we're waiting on the test runner itself rather than a user-specified
1876 // test. For instance, if the test runner leaves this much time between us requesting a test to
1877 // start and it acknowledging the test starting, we terminate the child and raise an error. This
1878 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
1879 const response_timeout_ns: ?u64 = ns: {
1880 if (fuzz_context != null) break :ns null; // don't timeout fuzz tests
1881 break :ns @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1882 };
1883
1884 const stdout = poller.reader(.stdout);
1885 const stderr = poller.reader(.stderr);
1886
1887 while (true) {
1888 const Header = std.zig.Server.Message.Header;
1889
1890 // This block is exited when `stdout` contains enough bytes for a `Header`.
1891 header_ready: {
1892 if (stdout.buffered().len >= @sizeOf(Header)) {
1893 // We already have one, no need to poll!
1894 break :header_ready;
1895 }
1896
1897 // Always `null` if `timer` is `null`.
1898 const opt_timeout_ns: ?u64 = ns: {
1899 if (timer == null) break :ns null;
1900 if (active_test_index == null) break :ns response_timeout_ns;
1901 break :ns options.unit_test_timeout_ns;
1902 };
1903
1904 if (opt_timeout_ns) |timeout_ns| {
1905 const remaining_ns = timeout_ns -| timer.?.read();
1906 if (!try poller.pollTimeout(remaining_ns)) return .{ .no_poll = .{
1907 .active_test_index = active_test_index,
1908 .ns_elapsed = if (timer) |*t| t.read() else 0,
1909 } };
1910 } else {
1911 if (!try poller.poll()) return .{ .no_poll = .{
1912 .active_test_index = active_test_index,
1913 .ns_elapsed = if (timer) |*t| t.read() else 0,
1914 } };
1915 }
1916
1917 if (stdout.buffered().len >= @sizeOf(Header)) {
1918 // There wasn't a header before, but there is one after the `poll`.
1919 break :header_ready;
1920 }
1921
1922 if (opt_timeout_ns) |timeout_ns| {
1923 const cur_ns = timer.?.read();
1924 if (cur_ns >= timeout_ns) return .{ .timeout = .{
1925 .active_test_index = active_test_index,
1926 .ns_elapsed = cur_ns,
1927 } };
1928 }
1929 continue;
1930 }
1931 // There is definitely a header available now -- read it.
1932 const header = stdout.takeStruct(Header, .little) catch unreachable;
1933
1934 while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) return .{ .no_poll = .{
1935 .active_test_index = active_test_index,
1936 .ns_elapsed = if (timer) |*t| t.read() else 0,
1937 } };
1938 const body = stdout.take(header.bytes_len) catch unreachable;
1939 var body_r: std.Io.Reader = .fixed(body);
1940 switch (header.tag) {
1941 .zig_version => {
1942 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail(
1943 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
1944 .{ builtin.zig_version_string, body },
1945 );
1946 },
1947 .test_metadata => {
1948 assert(fuzz_context == null);
1949
1950 // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
1951 // only request it once (and importantly, we don't re-request it if we kill and
1952 // restart the test runner).
1953 assert(opt_metadata.* == null);
1954
1955 const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
1956 results.test_count = tm_hdr.tests_len;
1957
1958 const names = try arena.alloc(u32, results.test_count);
1959 for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
1960
1961 const expected_panic_msgs = try arena.alloc(u32, results.test_count);
1962 for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
1963
1964 const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
1965
1966 options.progress_node.setEstimatedTotalItems(names.len);
1967 opt_metadata.* = .{
1968 .string_bytes = try arena.dupe(u8, string_bytes),
1969 .ns_per_test = try arena.alloc(u64, results.test_count),
1970 .names = names,
1971 .expected_panic_msgs = expected_panic_msgs,
1972 .next_index = 0,
1973 .prog_node = options.progress_node,
1974 };
1975 @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
1976
1977 active_test_index = null;
1978 if (timer) |*t| t.reset();
1979
1980 requestNextTest(child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
1981 },
1982 .test_started => {
1983 active_test_index = opt_metadata.*.?.next_index - 1;
1984 if (timer) |*t| t.reset();
1985 },
1986 .test_results => {
1987 assert(fuzz_context == null);
1988 const md = &opt_metadata.*.?;
1989
1990 const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
1991 assert(tr_hdr.index == active_test_index);
1992
1993 switch (tr_hdr.flags.status) {
1994 .pass => {},
1995 .skip => results.skip_count +|= 1,
1996 .fail => results.fail_count +|= 1,
1997 }
1998 const leak_count = tr_hdr.flags.leak_count;
1999 const log_err_count = tr_hdr.flags.log_err_count;
2000 results.leak_count +|= leak_count;
2001 results.log_err_count +|= log_err_count;
2002
2003 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index);
2004
2005 if (tr_hdr.flags.status == .fail) {
2006 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2007 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
2008 stderr.tossBuffered();
2009 if (stderr_bytes.len == 0) {
2010 try run.step.addError("'{s}' failed without output", .{name});
2011 } else {
2012 try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
2013 }
2014 } else if (leak_count > 0) {
2015 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2016 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
2017 stderr.tossBuffered();
2018 try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
2019 } else if (log_err_count > 0) {
2020 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2021 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
2022 stderr.tossBuffered();
2023 try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
2024 }
2025
2026 active_test_index = null;
2027 if (timer) |*t| md.ns_per_test[tr_hdr.index] = t.lap();
2028
2029 requestNextTest(child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
2030 },
2031 .coverage_id => {
2032 coverage_id = body_r.takeInt(u64, .little) catch unreachable;
2033 const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
2034 const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
2035 const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
2036
2037 {
2038 const fuzz = fuzz_context.?.fuzz;
2039 fuzz.queue_mutex.lockUncancelable(io);
2040 defer fuzz.queue_mutex.unlock(io);
2041 try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{
2042 .id = coverage_id.?,
2043 .cumulative = .{
2044 .runs = cumulative_runs,
2045 .unique = cumulative_unique,
2046 .coverage = cumulative_coverage,
2047 },
2048 .run = run,
2049 } });
2050 fuzz.queue_cond.signal(io);
2051 }
2052 },
2053 .fuzz_start_addr => {
2054 const fuzz = fuzz_context.?.fuzz;
2055 const addr = body_r.takeInt(u64, .little) catch unreachable;
2056 {
2057 fuzz.queue_mutex.lockUncancelable(io);
2058 defer fuzz.queue_mutex.unlock(io);
2059 try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{
2060 .addr = addr,
2061 .coverage_id = coverage_id.?,
2062 } });
2063 fuzz.queue_cond.signal(io);
2064 }
2065 },
2066 else => {}, // ignore other messages
2067 }
2068 }
2069}
2070
2071const TestMetadata = struct {
2072 names: []const u32,
2073 ns_per_test: []u64,
2074 expected_panic_msgs: []const u32,
2075 string_bytes: []const u8,
2076 next_index: u32,
2077 prog_node: std.Progress.Node,
2078
2079 fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
2080 return .{
2081 .names = tm.names,
2082 .string_bytes = tm.string_bytes,
2083 };
2084 }
2085
2086 fn testName(tm: TestMetadata, index: u32) []const u8 {
2087 return tm.toCachedTestMetadata().testName(index);
2088 }
2089};
2090
2091pub const CachedTestMetadata = struct {
2092 names: []const u32,
2093 string_bytes: []const u8,
2094
2095 pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
2096 return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
2097 }
2098};
2099
2100fn requestNextTest(in: fs.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
2101 while (metadata.next_index < metadata.names.len) {
2102 const i = metadata.next_index;
2103 metadata.next_index += 1;
2104
2105 if (metadata.expected_panic_msgs[i] != 0) continue;
2106
2107 const name = metadata.testName(i);
2108 if (sub_prog_node.*) |n| n.end();
2109 sub_prog_node.* = metadata.prog_node.start(name, 0);
2110
2111 try sendRunTestMessage(in, .run_test, i);
2112 return;
2113 } else {
2114 metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
2115 try sendMessage(in, .exit);
2116 }
2117}
2118
2119fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
2120 const header: std.zig.Client.Message.Header = .{
2121 .tag = tag,
2122 .bytes_len = 0,
2123 };
2124 var w = file.writer(&.{});
2125 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2126 error.WriteFailed => return w.err.?,
2127 };
2128}
2129
2130fn sendRunTestMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
2131 const header: std.zig.Client.Message.Header = .{
2132 .tag = tag,
2133 .bytes_len = 4,
2134 };
2135 var w = file.writer(&.{});
2136 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2137 error.WriteFailed => return w.err.?,
2138 };
2139 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
2140 error.WriteFailed => return w.err.?,
2141 };
2142}
2143
2144fn sendRunFuzzTestMessage(
2145 file: std.fs.File,
2146 index: u32,
2147 kind: std.Build.abi.fuzz.LimitKind,
2148 amount_or_instance: u64,
2149) !void {
2150 const header: std.zig.Client.Message.Header = .{
2151 .tag = .start_fuzzing,
2152 .bytes_len = 4 + 1 + 8,
2153 };
2154 var w = file.writer(&.{});
2155 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2156 error.WriteFailed => return w.err.?,
2157 };
2158 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
2159 error.WriteFailed => return w.err.?,
2160 };
2161 w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
2162 error.WriteFailed => return w.err.?,
2163 };
2164 w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
2165 error.WriteFailed => return w.err.?,
2166 };
2167}
2168
2169fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult {
2170 const b = run.step.owner;
2171 const io = b.graph.io;
2172 const arena = b.allocator;
2173
2174 try child.spawn();
2175 errdefer _ = child.kill() catch {};
2176
2177 try child.waitForSpawn();
2178
2179 switch (run.stdin) {
2180 .bytes => |bytes| {
2181 child.stdin.?.writeAll(bytes) catch |err| {
2182 return run.step.fail("unable to write stdin: {s}", .{@errorName(err)});
2183 };
2184 child.stdin.?.close();
2185 child.stdin = null;
2186 },
2187 .lazy_path => |lazy_path| {
2188 const path = lazy_path.getPath3(b, &run.step);
2189 const file = path.root_dir.handle.openFile(path.subPathOrDot(), .{}) catch |err| {
2190 return run.step.fail("unable to open stdin file: {s}", .{@errorName(err)});
2191 };
2192 defer file.close();
2193 // TODO https://github.com/ziglang/zig/issues/23955
2194 var read_buffer: [1024]u8 = undefined;
2195 var file_reader = file.reader(io, &read_buffer);
2196 var write_buffer: [1024]u8 = undefined;
2197 var stdin_writer = child.stdin.?.writer(&write_buffer);
2198 _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
2199 error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{
2200 path, file_reader.err.?,
2201 }),
2202 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
2203 stdin_writer.err.?,
2204 }),
2205 };
2206 stdin_writer.interface.flush() catch |err| switch (err) {
2207 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
2208 stdin_writer.err.?,
2209 }),
2210 };
2211 child.stdin.?.close();
2212 child.stdin = null;
2213 },
2214 .none => {},
2215 }
2216
2217 var stdout_bytes: ?[]const u8 = null;
2218 var stderr_bytes: ?[]const u8 = null;
2219
2220 run.stdio_limit = run.stdio_limit.min(.limited(run.max_stdio_size));
2221 if (child.stdout) |stdout| {
2222 if (child.stderr) |stderr| {
2223 var poller = std.Io.poll(arena, enum { stdout, stderr }, .{
2224 .stdout = stdout,
2225 .stderr = stderr,
2226 });
2227 defer poller.deinit();
2228
2229 while (try poller.poll()) {
2230 if (run.stdio_limit.toInt()) |limit| {
2231 if (poller.reader(.stderr).buffered().len > limit)
2232 return error.StdoutStreamTooLong;
2233 if (poller.reader(.stderr).buffered().len > limit)
2234 return error.StderrStreamTooLong;
2235 }
2236 }
2237
2238 stdout_bytes = try poller.toOwnedSlice(.stdout);
2239 stderr_bytes = try poller.toOwnedSlice(.stderr);
2240 } else {
2241 var stdout_reader = stdout.readerStreaming(io, &.{});
2242 stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
2243 error.OutOfMemory => return error.OutOfMemory,
2244 error.ReadFailed => return stdout_reader.err.?,
2245 error.StreamTooLong => return error.StdoutStreamTooLong,
2246 };
2247 }
2248 } else if (child.stderr) |stderr| {
2249 var stderr_reader = stderr.readerStreaming(io, &.{});
2250 stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
2251 error.OutOfMemory => return error.OutOfMemory,
2252 error.ReadFailed => return stderr_reader.err.?,
2253 error.StreamTooLong => return error.StderrStreamTooLong,
2254 };
2255 }
2256
2257 if (stderr_bytes) |bytes| if (bytes.len > 0) {
2258 // Treat stderr as an error message.
2259 const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) {
2260 .check => |checks| !checksContainStderr(checks.items),
2261 else => true,
2262 };
2263 if (stderr_is_diagnostic) {
2264 run.step.result_stderr = bytes;
2265 }
2266 };
2267
2268 run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
2269
2270 return .{
2271 .term = try child.wait(),
2272 .stdout = stdout_bytes,
2273 .stderr = stderr_bytes,
2274 };
2275}
2276
2277fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void {
2278 const b = run.step.owner;
2279 const compiles = artifact.getCompileDependencies(true);
2280 for (compiles) |compile| {
2281 if (compile.root_module.resolved_target.?.result.os.tag == .windows and
2282 compile.isDynamicLibrary())
2283 {
2284 addPathDir(run, fs.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?);
2285 }
2286 }
2287}
2288
2289fn failForeign(
2290 run: *Run,
2291 suggested_flag: []const u8,
2292 argv0: []const u8,
2293 exe: *Step.Compile,
2294) error{ MakeFailed, MakeSkipped, OutOfMemory } {
2295 switch (run.stdio) {
2296 .check, .zig_test => {
2297 if (run.skip_foreign_checks)
2298 return error.MakeSkipped;
2299
2300 const b = run.step.owner;
2301 const host_name = try b.graph.host.result.zigTriple(b.allocator);
2302 const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator);
2303
2304 return run.step.fail(
2305 \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
2306 \\ consider using {s} or enabling skip_foreign_checks in the Run step
2307 , .{ argv0, foreign_name, host_name, suggested_flag });
2308 },
2309 else => {
2310 return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0});
2311 },
2312 }
2313}
2314
2315fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void {
2316 switch (stdio) {
2317 .infer_from_args, .inherit, .zig_test => {},
2318 .check => |checks| for (checks.items) |check| {
2319 hh.add(@as(std.meta.Tag(StdIo.Check), check));
2320 switch (check) {
2321 .expect_stderr_exact,
2322 .expect_stderr_match,
2323 .expect_stdout_exact,
2324 .expect_stdout_match,
2325 => |s| hh.addBytes(s),
2326
2327 .expect_term => |term| {
2328 hh.add(@as(std.meta.Tag(std.process.Child.Term), term));
2329 switch (term) {
2330 .Exited => |x| hh.add(x),
2331 .Signal, .Stopped, .Unknown => |x| hh.add(x),
2332 }
2333 },
2334 }
2335 },
2336 }
2337}