struct Run [src]

Alias for std.Build.Step.Run

Fields

step: Step
argv: std.ArrayListUnmanaged(Arg)See also addArg and addArgs to modifying this directly
cwd: ?Build.LazyPathUse setCwd to set the initial current working directory
env_map: ?*EnvMapOverride this field to modify the environment, or use setEnvironmentVariable
disable_zig_progress: boolWhen true prevents ZIG_PROGRESS environment variable from being passed to the child process, which otherwise would be used for the child to send progress updates to the parent.
stdio: StdIoConfigures whether the Run step is considered to have side-effects, and also whether the Run step will inherit stdio streams, forwarding them to the parent process, in which case will require a global lock to prevent other steps from interfering with stdio while the subprocess associated with this Run step is running. If the Run step is determined to not have side-effects, then execution will be skipped if all output files are up-to-date and input files are unchanged.
stdin: StdInThis field must be .none if stdio is inherit. It should be only set using setStdIn.
file_inputs: std.ArrayListUnmanaged(std.Build.LazyPath)Additional input files that, when modified, indicate that the Run step should be re-executed. If the Run step is determined to have side-effects, the Run step is always executed when it appears in the build graph, regardless of whether these files have been modified.
rename_step_with_output_arg: boolAfter adding an output argument, this step will by default rename itself for a better display name in the build summary. This can be disabled by setting this to false.
skip_foreign_checks: boolIf this is true, a Run step which is configured to check the output of the executed binary will not fail the build if the binary cannot be executed due to being for a foreign binary to the host system which is running the build graph. Command-line arguments such as -fqemu and -fwasmtime may affect whether a binary is detected as foreign, as well as system configuration such as Rosetta (macOS) and binfmt_misc (Linux). If this Run step is considered to have side-effects, then this flag does nothing.
failing_to_execute_foreign_is_an_error: boolIf this is true, failing to execute a foreign binary will be considered an error. However if this is false, the step will be skipped on failure instead. This allows for a Run step to attempt to execute a foreign binary using an external executor (such as qemu) but not fail if the executor is unavailable.
max_stdio_size: usizeIf stderr or stdout exceeds this amount, the child process is killed and the step fails.
captured_stdout: ?*Output
captured_stderr: ?*Output
dep_output_file: ?*Output
has_side_effects: bool
fuzz_tests: std.ArrayListUnmanaged(u32)If this is a Zig unit test binary, this tracks the indexes of the unit tests that are also fuzz tests.
cached_test_metadata: ?CachedTestMetadata = null
rebuilt_executable: ?PathPopulated during the fuzz phase if this run step corresponds to a unit test executable that contains fuzz tests.
producer: ?*Step.CompileIf this Run step was produced by a Compile step, it is tracked here.

Members

Source

const std = @import("std"); const builtin = @import("builtin"); const Build = std.Build; const Step = Build.Step; const fs = std.fs; const mem = std.mem; const process = std.process; const EnvMap = process.EnvMap; const assert = std.debug.assert; const Path = Build.Cache.Path; const Run = @This(); pub const base_id: Step.Id = .run; step: Step, /// See also addArg and addArgs to modifying this directly argv: std.ArrayListUnmanaged(Arg), /// Use `setCwd` to set the initial current working directory cwd: ?Build.LazyPath, /// Override this field to modify the environment, or use setEnvironmentVariable env_map: ?*EnvMap, /// When `true` prevents `ZIG_PROGRESS` environment variable from being passed /// to the child process, which otherwise would be used for the child to send /// progress updates to the parent. disable_zig_progress: bool, /// Configures whether the Run step is considered to have side-effects, and also /// whether the Run step will inherit stdio streams, forwarding them to the /// parent process, in which case will require a global lock to prevent other /// steps from interfering with stdio while the subprocess associated with this /// Run step is running. /// If the Run step is determined to not have side-effects, then execution will /// be skipped if all output files are up-to-date and input files are /// unchanged. stdio: StdIo, /// This field must be `.none` if stdio is `inherit`. /// It should be only set using `setStdIn`. stdin: StdIn, /// Additional input files that, when modified, indicate that the Run step /// should be re-executed. /// If the Run step is determined to have side-effects, the Run step is always /// executed when it appears in the build graph, regardless of whether these /// files have been modified. file_inputs: std.ArrayListUnmanaged(std.Build.LazyPath), /// After adding an output argument, this step will by default rename itself /// for a better display name in the build summary. /// This can be disabled by setting this to false. rename_step_with_output_arg: bool, /// If this is true, a Run step which is configured to check the output of the /// executed binary will not fail the build if the binary cannot be executed /// due to being for a foreign binary to the host system which is running the /// build graph. /// Command-line arguments such as -fqemu and -fwasmtime may affect whether a /// binary is detected as foreign, as well as system configuration such as /// Rosetta (macOS) and binfmt_misc (Linux). /// If this Run step is considered to have side-effects, then this flag does /// nothing. skip_foreign_checks: bool, /// If this is true, failing to execute a foreign binary will be considered an /// error. However if this is false, the step will be skipped on failure instead. /// /// This allows for a Run step to attempt to execute a foreign binary using an /// external executor (such as qemu) but not fail if the executor is unavailable. failing_to_execute_foreign_is_an_error: bool, /// If stderr or stdout exceeds this amount, the child process is killed and /// the step fails. max_stdio_size: usize, captured_stdout: ?*Output, captured_stderr: ?*Output, dep_output_file: ?*Output, has_side_effects: bool, /// If this is a Zig unit test binary, this tracks the indexes of the unit /// tests that are also fuzz tests. fuzz_tests: std.ArrayListUnmanaged(u32), cached_test_metadata: ?CachedTestMetadata = null, /// Populated during the fuzz phase if this run step corresponds to a unit test /// executable that contains fuzz tests. rebuilt_executable: ?Path, /// If this Run step was produced by a Compile step, it is tracked here. producer: ?*Step.Compile, pub const StdIn = union(enum) { none, bytes: []const u8, lazy_path: std.Build.LazyPath, }; pub const StdIo = union(enum) { /// Whether the Run step has side-effects will be determined by whether or not one /// of the args is an output file (added with `addOutputFileArg`). /// If the Run step is determined to have side-effects, this is the same as `inherit`. /// The step will fail if the subprocess crashes or returns a non-zero exit code. infer_from_args, /// Causes the Run step to be considered to have side-effects, and therefore /// always execute when it appears in the build graph. /// It also means that this step will obtain a global lock to prevent other /// steps from running in the meantime. /// The step will fail if the subprocess crashes or returns a non-zero exit code. inherit, /// Causes the Run step to be considered to *not* have side-effects. The /// process will be re-executed if any of the input dependencies are /// modified. The exit code and standard I/O streams will be checked for /// certain conditions, and the step will succeed or fail based on these /// conditions. /// Note that an explicit check for exit code 0 needs to be added to this /// list if such a check is desirable. check: std.ArrayListUnmanaged(Check), /// This Run step is running a zig unit test binary and will communicate /// extra metadata over the IPC protocol. zig_test, pub const Check = union(enum) { expect_stderr_exact: []const u8, expect_stderr_match: []const u8, expect_stdout_exact: []const u8, expect_stdout_match: []const u8, expect_term: std.process.Child.Term, }; }; pub const Arg = union(enum) { artifact: PrefixedArtifact, lazy_path: PrefixedLazyPath, directory_source: PrefixedLazyPath, bytes: []u8, output_file: *Output, output_directory: *Output, }; pub const PrefixedArtifact = struct { prefix: []const u8, artifact: *Step.Compile, }; pub const PrefixedLazyPath = struct { prefix: []const u8, lazy_path: std.Build.LazyPath, }; pub const Output = struct { generated_file: std.Build.GeneratedFile, prefix: []const u8, basename: []const u8, }; pub fn create(owner: *std.Build, name: []const u8) *Run { const run = owner.allocator.create(Run) catch @panic("OOM"); run.* = .{ .step = Step.init(.{ .id = base_id, .name = name, .owner = owner, .makeFn = make, }), .argv = .{}, .cwd = null, .env_map = null, .disable_zig_progress = false, .stdio = .infer_from_args, .stdin = .none, .file_inputs = .{}, .rename_step_with_output_arg = true, .skip_foreign_checks = false, .failing_to_execute_foreign_is_an_error = true, .max_stdio_size = 10 * 1024 * 1024, .captured_stdout = null, .captured_stderr = null, .dep_output_file = null, .has_side_effects = false, .fuzz_tests = .{}, .rebuilt_executable = null, .producer = null, }; return run; } pub fn setName(run: *Run, name: []const u8) void { run.step.name = name; run.rename_step_with_output_arg = false; } pub fn enableTestRunnerMode(run: *Run) void { const b = run.step.owner; const arena = b.allocator; run.stdio = .zig_test; run.addArgs(&.{ std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"), std.fmt.allocPrint(arena, "--cache-dir={s}", .{b.cache_root.path orelse ""}) catch @panic("OOM"), "--listen=-", }); } pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void { run.addPrefixedArtifactArg("", artifact); } pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Compile) void { const b = run.step.owner; const prefixed_artifact: PrefixedArtifact = .{ .prefix = b.dupe(prefix), .artifact = artifact, }; run.argv.append(b.allocator, .{ .artifact = prefixed_artifact }) catch @panic("OOM"); const bin_file = artifact.getEmittedBin(); bin_file.addStepDependencies(&run.step); } /// Provides a file path as a command line argument to the command being run. /// /// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs /// throughout the build system. /// /// Related: /// * `addPrefixedOutputFileArg` - same thing but prepends a string to the argument /// * `addFileArg` - for input files given to the child process pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath { return run.addPrefixedOutputFileArg("", basename); } /// Provides a file path as a command line argument to the command being run. /// Asserts `basename` is not empty. /// /// For example, a prefix of "-o" and basename of "output.txt" will result in /// the child process seeing something like this: "-ozig-cache/.../output.txt" /// /// The child process will see a single argument, regardless of whether the /// prefix or basename have spaces. /// /// The returned `std.Build.LazyPath` can be used as inputs to other APIs /// throughout the build system. /// /// Related: /// * `addOutputFileArg` - same thing but without the prefix /// * `addFileArg` - for input files given to the child process pub fn addPrefixedOutputFileArg( run: *Run, prefix: []const u8, basename: []const u8, ) std.Build.LazyPath { const b = run.step.owner; if (basename.len == 0) @panic("basename must not be empty"); const output = b.allocator.create(Output) catch @panic("OOM"); output.* = .{ .prefix = b.dupe(prefix), .basename = b.dupe(basename), .generated_file = .{ .step = &run.step }, }; run.argv.append(b.allocator, .{ .output_file = output }) catch @panic("OOM"); if (run.rename_step_with_output_arg) { run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename })); } return .{ .generated = .{ .file = &output.generated_file } }; } /// Appends an input file to the command line arguments. /// /// The child process will see a file path. Modifications to this file will be /// detected as a cache miss in subsequent builds, causing the child process to /// be re-executed. /// /// Related: /// * `addPrefixedFileArg` - same thing but prepends a string to the argument /// * `addOutputFileArg` - for files generated by the child process pub fn addFileArg(run: *Run, lp: std.Build.LazyPath) void { run.addPrefixedFileArg("", lp); } /// Appends an input file to the command line arguments prepended with a string. /// /// For example, a prefix of "-F" will result in the child process seeing something /// like this: "-Fexample.txt" /// /// The child process will see a single argument, even if the prefix has /// spaces. Modifications to this file will be detected as a cache miss in /// subsequent builds, causing the child process to be re-executed. /// /// Related: /// * `addFileArg` - same thing but without the prefix /// * `addOutputFileArg` - for files generated by the child process pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void { const b = run.step.owner; const prefixed_file_source: PrefixedLazyPath = .{ .prefix = b.dupe(prefix), .lazy_path = lp.dupe(b), }; run.argv.append(b.allocator, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM"); lp.addStepDependencies(&run.step); } /// Provides a directory path as a command line argument to the command being run. /// /// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs /// throughout the build system. /// /// Related: /// * `addPrefixedOutputDirectoryArg` - same thing but prepends a string to the argument /// * `addDirectoryArg` - for input directories given to the child process pub fn addOutputDirectoryArg(run: *Run, basename: []const u8) std.Build.LazyPath { return run.addPrefixedOutputDirectoryArg("", basename); } /// Provides a directory path as a command line argument to the command being run. /// Asserts `basename` is not empty. /// /// For example, a prefix of "-o" and basename of "output_dir" will result in /// the child process seeing something like this: "-ozig-cache/.../output_dir" /// /// The child process will see a single argument, regardless of whether the /// prefix or basename have spaces. /// /// The returned `std.Build.LazyPath` can be used as inputs to other APIs /// throughout the build system. /// /// Related: /// * `addOutputDirectoryArg` - same thing but without the prefix /// * `addDirectoryArg` - for input directories given to the child process pub fn addPrefixedOutputDirectoryArg( run: *Run, prefix: []const u8, basename: []const u8, ) std.Build.LazyPath { if (basename.len == 0) @panic("basename must not be empty"); const b = run.step.owner; const output = b.allocator.create(Output) catch @panic("OOM"); output.* = .{ .prefix = b.dupe(prefix), .basename = b.dupe(basename), .generated_file = .{ .step = &run.step }, }; run.argv.append(b.allocator, .{ .output_directory = output }) catch @panic("OOM"); if (run.rename_step_with_output_arg) { run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename })); } return .{ .generated = .{ .file = &output.generated_file } }; } pub fn addDirectoryArg(run: *Run, directory_source: std.Build.LazyPath) void { run.addPrefixedDirectoryArg("", directory_source); } pub fn addPrefixedDirectoryArg(run: *Run, prefix: []const u8, directory_source: std.Build.LazyPath) void { const b = run.step.owner; const prefixed_directory_source: PrefixedLazyPath = .{ .prefix = b.dupe(prefix), .lazy_path = directory_source.dupe(b), }; run.argv.append(b.allocator, .{ .directory_source = prefixed_directory_source }) catch @panic("OOM"); directory_source.addStepDependencies(&run.step); } /// Add a path argument to a dep file (.d) for the child process to write its /// discovered additional dependencies. /// Only one dep file argument is allowed by instance. pub fn addDepFileOutputArg(run: *Run, basename: []const u8) std.Build.LazyPath { return run.addPrefixedDepFileOutputArg("", basename); } /// Add a prefixed path argument to a dep file (.d) for the child process to /// write its discovered additional dependencies. /// Only one dep file argument is allowed by instance. pub fn addPrefixedDepFileOutputArg(run: *Run, prefix: []const u8, basename: []const u8) std.Build.LazyPath { const b = run.step.owner; assert(run.dep_output_file == null); const dep_file = b.allocator.create(Output) catch @panic("OOM"); dep_file.* = .{ .prefix = b.dupe(prefix), .basename = b.dupe(basename), .generated_file = .{ .step = &run.step }, }; run.dep_output_file = dep_file; run.argv.append(b.allocator, .{ .output_file = dep_file }) catch @panic("OOM"); return .{ .generated = .{ .file = &dep_file.generated_file } }; } pub fn addArg(run: *Run, arg: []const u8) void { const b = run.step.owner; run.argv.append(b.allocator, .{ .bytes = b.dupe(arg) }) catch @panic("OOM"); } pub fn addArgs(run: *Run, args: []const []const u8) void { for (args) |arg| run.addArg(arg); } pub fn setStdIn(run: *Run, stdin: StdIn) void { switch (stdin) { .lazy_path => |lazy_path| lazy_path.addStepDependencies(&run.step), .bytes, .none => {}, } run.stdin = stdin; } pub fn setCwd(run: *Run, cwd: Build.LazyPath) void { cwd.addStepDependencies(&run.step); run.cwd = cwd.dupe(run.step.owner); } pub fn clearEnvironment(run: *Run) void { const b = run.step.owner; const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM"); new_env_map.* = EnvMap.init(b.allocator); run.env_map = new_env_map; } pub fn addPathDir(run: *Run, search_path: []const u8) void { const b = run.step.owner; const env_map = getEnvMapInternal(run); const key = "PATH"; const prev_path = env_map.get(key); if (prev_path) |pp| { const new_path = b.fmt("{s}" ++ [1]u8{fs.path.delimiter} ++ "{s}", .{ pp, search_path }); env_map.put(key, new_path) catch @panic("OOM"); } else { env_map.put(key, b.dupePath(search_path)) catch @panic("OOM"); } } pub fn getEnvMap(run: *Run) *EnvMap { return getEnvMapInternal(run); } fn getEnvMapInternal(run: *Run) *EnvMap { const arena = run.step.owner.allocator; return run.env_map orelse { const env_map = arena.create(EnvMap) catch @panic("OOM"); env_map.* = process.getEnvMap(arena) catch @panic("unhandled error"); run.env_map = env_map; return env_map; }; } pub fn setEnvironmentVariable(run: *Run, key: []const u8, value: []const u8) void { const b = run.step.owner; const env_map = run.getEnvMap(); env_map.put(b.dupe(key), b.dupe(value)) catch @panic("unhandled error"); } pub fn removeEnvironmentVariable(run: *Run, key: []const u8) void { run.getEnvMap().remove(key); } /// Adds a check for exact stderr match. Does not add any other checks. pub fn expectStdErrEqual(run: *Run, bytes: []const u8) void { const new_check: StdIo.Check = .{ .expect_stderr_exact = run.step.owner.dupe(bytes) }; run.addCheck(new_check); } /// Adds a check for exact stdout match as well as a check for exit code 0, if /// there is not already an expected termination check. pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void { const new_check: StdIo.Check = .{ .expect_stdout_exact = run.step.owner.dupe(bytes) }; run.addCheck(new_check); if (!run.hasTermCheck()) { run.expectExitCode(0); } } pub fn expectExitCode(run: *Run, code: u8) void { const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } }; run.addCheck(new_check); } pub fn hasTermCheck(run: Run) bool { for (run.stdio.check.items) |check| switch (check) { .expect_term => return true, else => continue, }; return false; } pub fn addCheck(run: *Run, new_check: StdIo.Check) void { const b = run.step.owner; switch (run.stdio) { .infer_from_args => { run.stdio = .{ .check = .{} }; run.stdio.check.append(b.allocator, new_check) catch @panic("OOM"); }, .check => |*checks| checks.append(b.allocator, new_check) catch @panic("OOM"), else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of Run instead"), } } pub fn captureStdErr(run: *Run) std.Build.LazyPath { assert(run.stdio != .inherit); if (run.captured_stderr) |output| return .{ .generated = .{ .file = &output.generated_file } }; const output = run.step.owner.allocator.create(Output) catch @panic("OOM"); output.* = .{ .prefix = "", .basename = "stderr", .generated_file = .{ .step = &run.step }, }; run.captured_stderr = output; return .{ .generated = .{ .file = &output.generated_file } }; } pub fn captureStdOut(run: *Run) std.Build.LazyPath { assert(run.stdio != .inherit); if (run.captured_stdout) |output| return .{ .generated = .{ .file = &output.generated_file } }; const output = run.step.owner.allocator.create(Output) catch @panic("OOM"); output.* = .{ .prefix = "", .basename = "stdout", .generated_file = .{ .step = &run.step }, }; run.captured_stdout = output; return .{ .generated = .{ .file = &output.generated_file } }; } /// Adds an additional input files that, when modified, indicates that this Run /// step should be re-executed. /// If the Run step is determined to have side-effects, the Run step is always /// executed when it appears in the build graph, regardless of whether this /// file has been modified. pub fn addFileInput(self: *Run, file_input: std.Build.LazyPath) void { file_input.addStepDependencies(&self.step); self.file_inputs.append(self.step.owner.allocator, file_input.dupe(self.step.owner)) catch @panic("OOM"); } /// Returns whether the Run step has side effects *other than* updating the output arguments. fn hasSideEffects(run: Run) bool { if (run.has_side_effects) return true; return switch (run.stdio) { .infer_from_args => !run.hasAnyOutputArgs(), .inherit => true, .check => false, .zig_test => false, }; } fn hasAnyOutputArgs(run: Run) bool { if (run.captured_stdout != null) return true; if (run.captured_stderr != null) return true; for (run.argv.items) |arg| switch (arg) { .output_file, .output_directory => return true, else => continue, }; return false; } fn checksContainStdout(checks: []const StdIo.Check) bool { for (checks) |check| switch (check) { .expect_stderr_exact, .expect_stderr_match, .expect_term, => continue, .expect_stdout_exact, .expect_stdout_match, => return true, }; return false; } fn checksContainStderr(checks: []const StdIo.Check) bool { for (checks) |check| switch (check) { .expect_stdout_exact, .expect_stdout_match, .expect_term, => continue, .expect_stderr_exact, .expect_stderr_match, => return true, }; return false; } const IndexedOutput = struct { index: usize, tag: @typeInfo(Arg).@"union".tag_type.?, output: *Output, }; fn make(step: *Step, options: Step.MakeOptions) !void { const prog_node = options.progress_node; const b = step.owner; const arena = b.allocator; const run: *Run = @fieldParentPtr("step", step); const has_side_effects = run.hasSideEffects(); var argv_list = std.ArrayList([]const u8).init(arena); var output_placeholders = std.ArrayList(IndexedOutput).init(arena); var man = b.graph.cache.obtain(); defer man.deinit(); for (run.argv.items) |arg| { switch (arg) { .bytes => |bytes| { try argv_list.append(bytes); man.hash.addBytes(bytes); }, .lazy_path => |file| { const file_path = file.lazy_path.getPath2(b, step); try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, file_path })); man.hash.addBytes(file.prefix); _ = try man.addFile(file_path, null); }, .directory_source => |file| { const file_path = file.lazy_path.getPath2(b, step); try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, file_path })); man.hash.addBytes(file.prefix); man.hash.addBytes(file_path); }, .artifact => |pa| { const artifact = pa.artifact; if (artifact.rootModuleTarget().os.tag == .windows) { // On Windows we don't have rpaths so we have to add .dll search paths to PATH run.addPathForDynLibs(artifact); } const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; try argv_list.append(b.fmt("{s}{s}", .{ pa.prefix, file_path })); _ = try man.addFile(file_path, null); }, .output_file, .output_directory => |output| { man.hash.addBytes(output.prefix); man.hash.addBytes(output.basename); // Add a placeholder into the argument list because we need the // manifest hash to be updated with all arguments before the // object directory is computed. try output_placeholders.append(.{ .index = argv_list.items.len, .tag = arg, .output = output, }); _ = try argv_list.addOne(); }, } } switch (run.stdin) { .bytes => |bytes| { man.hash.addBytes(bytes); }, .lazy_path => |lazy_path| { const file_path = lazy_path.getPath2(b, step); _ = try man.addFile(file_path, null); }, .none => {}, } if (run.captured_stdout) |output| { man.hash.addBytes(output.basename); } if (run.captured_stderr) |output| { man.hash.addBytes(output.basename); } hashStdIo(&man.hash, run.stdio); for (run.file_inputs.items) |lazy_path| { _ = try man.addFile(lazy_path.getPath2(b, step), null); } if (!has_side_effects and try step.cacheHitAndWatch(&man)) { // cache hit, skip running command const digest = man.final(); try populateGeneratedPaths( arena, output_placeholders.items, run.captured_stdout, run.captured_stderr, b.cache_root, &digest, ); step.result_cached = true; return; } const dep_output_file = run.dep_output_file orelse { // We already know the final output paths, use them directly. const digest = if (has_side_effects) man.hash.final() else man.final(); try populateGeneratedPaths( arena, output_placeholders.items, run.captured_stdout, run.captured_stderr, b.cache_root, &digest, ); const output_dir_path = "o" ++ fs.path.sep_str ++ &digest; for (output_placeholders.items) |placeholder| { const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename }); const output_sub_dir_path = switch (placeholder.tag) { .output_file => fs.path.dirname(output_sub_path).?, .output_directory => output_sub_path, else => unreachable, }; b.cache_root.handle.makePath(output_sub_dir_path) catch |err| { return step.fail("unable to make path '{}{s}': {s}", .{ b.cache_root, output_sub_dir_path, @errorName(err), }); }; const output_path = placeholder.output.generated_file.path.?; argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0) output_path else b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path }); } try runCommand(run, argv_list.items, has_side_effects, output_dir_path, prog_node, null); if (!has_side_effects) try step.writeManifestAndWatch(&man); return; }; // We do not know the final output paths yet, use temp paths to run the command. const rand_int = std.crypto.random.int(u64); const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int); for (output_placeholders.items) |placeholder| { const output_components = .{ tmp_dir_path, placeholder.output.basename }; const output_sub_path = b.pathJoin(&output_components); const output_sub_dir_path = switch (placeholder.tag) { .output_file => fs.path.dirname(output_sub_path).?, .output_directory => output_sub_path, else => unreachable, }; b.cache_root.handle.makePath(output_sub_dir_path) catch |err| { return step.fail("unable to make path '{}{s}': {s}", .{ b.cache_root, output_sub_dir_path, @errorName(err), }); }; const output_path = try b.cache_root.join(arena, &output_components); placeholder.output.generated_file.path = output_path; argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0) output_path else b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path }); } try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, null); const dep_file_dir = std.fs.cwd(); const dep_file_basename = dep_output_file.generated_file.getPath(); if (has_side_effects) try man.addDepFile(dep_file_dir, dep_file_basename) else try man.addDepFilePost(dep_file_dir, dep_file_basename); const digest = if (has_side_effects) man.hash.final() else man.final(); const any_output = output_placeholders.items.len > 0 or run.captured_stdout != null or run.captured_stderr != null; // Rename into place if (any_output) { const o_sub_path = "o" ++ fs.path.sep_str ++ &digest; b.cache_root.handle.rename(tmp_dir_path, o_sub_path) catch |err| { if (err == error.PathAlreadyExists) { b.cache_root.handle.deleteTree(o_sub_path) catch |del_err| { return step.fail("unable to remove dir '{}'{s}: {s}", .{ b.cache_root, tmp_dir_path, @errorName(del_err), }); }; b.cache_root.handle.rename(tmp_dir_path, o_sub_path) catch |retry_err| { return step.fail("unable to rename dir '{}{s}' to '{}{s}': {s}", .{ b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, @errorName(retry_err), }); }; } else { return step.fail("unable to rename dir '{}{s}' to '{}{s}': {s}", .{ b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, @errorName(err), }); } }; } if (!has_side_effects) try step.writeManifestAndWatch(&man); try populateGeneratedPaths( arena, output_placeholders.items, run.captured_stdout, run.captured_stderr, b.cache_root, &digest, ); } pub fn rerunInFuzzMode( run: *Run, web_server: *std.Build.Fuzz.WebServer, unit_test_index: u32, prog_node: std.Progress.Node, ) !void { const step = &run.step; const b = step.owner; const arena = b.allocator; var argv_list: std.ArrayListUnmanaged([]const u8) = .empty; for (run.argv.items) |arg| { switch (arg) { .bytes => |bytes| { try argv_list.append(arena, bytes); }, .lazy_path => |file| { const file_path = file.lazy_path.getPath2(b, step); try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, file_path })); }, .directory_source => |file| { const file_path = file.lazy_path.getPath2(b, step); try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, file_path })); }, .artifact => |pa| { const artifact = pa.artifact; const file_path = if (artifact == run.producer.?) b.fmt("{}", .{run.rebuilt_executable.?}) else (artifact.installed_path orelse artifact.generated_bin.?.path.?); try argv_list.append(arena, b.fmt("{s}{s}", .{ pa.prefix, file_path })); }, .output_file, .output_directory => unreachable, } } const has_side_effects = false; const rand_int = std.crypto.random.int(u64); const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int); try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, .{ .unit_test_index = unit_test_index, .web_server = web_server, }); } fn populateGeneratedPaths( arena: std.mem.Allocator, output_placeholders: []const IndexedOutput, captured_stdout: ?*Output, captured_stderr: ?*Output, cache_root: Build.Cache.Directory, digest: *const Build.Cache.HexDigest, ) !void { for (output_placeholders) |placeholder| { placeholder.output.generated_file.path = try cache_root.join(arena, &.{ "o", digest, placeholder.output.basename, }); } if (captured_stdout) |output| { output.generated_file.path = try cache_root.join(arena, &.{ "o", digest, output.basename, }); } if (captured_stderr) |output| { output.generated_file.path = try cache_root.join(arena, &.{ "o", digest, output.basename, }); } } fn formatTerm( term: ?std.process.Child.Term, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, ) !void { _ = fmt; _ = options; if (term) |t| switch (t) { .Exited => |code| try writer.print("exited with code {}", .{code}), .Signal => |sig| try writer.print("terminated with signal {}", .{sig}), .Stopped => |sig| try writer.print("stopped with signal {}", .{sig}), .Unknown => |code| try writer.print("terminated for unknown reason with code {}", .{code}), } else { try writer.writeAll("exited with any code"); } } fn fmtTerm(term: ?std.process.Child.Term) std.fmt.Formatter(formatTerm) { return .{ .data = term }; } fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term) bool { return if (expected) |e| switch (e) { .Exited => |expected_code| switch (actual) { .Exited => |actual_code| expected_code == actual_code, else => false, }, .Signal => |expected_sig| switch (actual) { .Signal => |actual_sig| expected_sig == actual_sig, else => false, }, .Stopped => |expected_sig| switch (actual) { .Stopped => |actual_sig| expected_sig == actual_sig, else => false, }, .Unknown => |expected_code| switch (actual) { .Unknown => |actual_code| expected_code == actual_code, else => false, }, } else switch (actual) { .Exited => true, else => false, }; } const FuzzContext = struct { web_server: *std.Build.Fuzz.WebServer, unit_test_index: u32, }; fn runCommand( run: *Run, argv: []const []const u8, has_side_effects: bool, output_dir_path: []const u8, prog_node: std.Progress.Node, fuzz_context: ?FuzzContext, ) !void { const step = &run.step; const b = step.owner; const arena = b.allocator; const cwd: ?[]const u8 = if (run.cwd) |lazy_cwd| lazy_cwd.getPath2(b, step) else null; try step.handleChildProcUnsupported(cwd, argv); try Step.handleVerbose2(step.owner, cwd, run.env_map, argv); const allow_skip = switch (run.stdio) { .check, .zig_test => run.skip_foreign_checks, else => false, }; var interp_argv = std.ArrayList([]const u8).init(b.allocator); defer interp_argv.deinit(); const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_context) catch |err| term: { // InvalidExe: cpu arch mismatch // FileNotFound: can happen with a wrong dynamic linker path if (err == error.InvalidExe or err == error.FileNotFound) interpret: { // TODO: learn the target from the binary directly rather than from // relying on it being a Compile step. This will make this logic // work even for the edge case that the binary was produced by a // third party. const exe = switch (run.argv.items[0]) { .artifact => |exe| exe.artifact, else => break :interpret, }; switch (exe.kind) { .exe, .@"test" => {}, else => break :interpret, } const root_target = exe.rootModuleTarget(); const need_cross_glibc = root_target.isGnuLibC() and exe.is_linking_libc; const other_target = exe.root_module.resolved_target.?.result; switch (std.zig.system.getExternalExecutor(b.graph.host.result, &other_target, .{ .qemu_fixes_dl = need_cross_glibc and b.glibc_runtimes_dir != null, .link_libc = exe.is_linking_libc, })) { .native, .rosetta => { if (allow_skip) return error.MakeSkipped; break :interpret; }, .wine => |bin_name| { if (b.enable_wine) { try interp_argv.append(bin_name); try interp_argv.appendSlice(argv); } else { return failForeign(run, "-fwine", argv[0], exe); } }, .qemu => |bin_name| { if (b.enable_qemu) { const glibc_dir_arg = if (need_cross_glibc) b.glibc_runtimes_dir orelse return failForeign(run, "--glibc-runtimes", argv[0], exe) else null; try interp_argv.append(bin_name); if (glibc_dir_arg) |dir| { try interp_argv.append("-L"); try interp_argv.append(b.pathJoin(&.{ dir, try std.zig.target.glibcRuntimeTriple( b.allocator, root_target.cpu.arch, root_target.os.tag, root_target.abi, ), })); } try interp_argv.appendSlice(argv); } else { return failForeign(run, "-fqemu", argv[0], exe); } }, .darling => |bin_name| { if (b.enable_darling) { try interp_argv.append(bin_name); try interp_argv.appendSlice(argv); } else { return failForeign(run, "-fdarling", argv[0], exe); } }, .wasmtime => |bin_name| { if (b.enable_wasmtime) { // https://github.com/bytecodealliance/wasmtime/issues/7384 // // In Wasmtime versions prior to 14, options passed after the module name // could be interpreted by Wasmtime if it recognized them. As with many CLI // tools, the `--` token is used to stop that behavior and indicate that the // remaining arguments are for the WASM program being executed. Historically, // we passed `--` after the module name here. // // After version 14, the `--` can no longer be passed after the module name, // but is also not necessary as Wasmtime will no longer try to interpret // options after the module name. So, we could just simply omit `--` for // newer Wasmtime versions. But to maintain compatibility for older versions // that still try to interpret options after the module name, we have moved // the `--` before the module name. This appears to work for both old and // new Wasmtime versions. try interp_argv.append(bin_name); try interp_argv.append("--dir=."); try interp_argv.append("--"); try interp_argv.append(argv[0]); try interp_argv.appendSlice(argv[1..]); } else { return failForeign(run, "-fwasmtime", argv[0], exe); } }, .bad_dl => |foreign_dl| { if (allow_skip) return error.MakeSkipped; const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)"; return step.fail( \\the host system is unable to execute binaries from the target \\ because the host dynamic linker is '{s}', \\ while the target dynamic linker is '{s}'. \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step , .{ host_dl, foreign_dl }); }, .bad_os_or_cpu => { if (allow_skip) return error.MakeSkipped; const host_name = try b.graph.host.result.zigTriple(b.allocator); const foreign_name = try root_target.zigTriple(b.allocator); return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{ host_name, foreign_name, }); }, } if (root_target.os.tag == .windows) { // On Windows we don't have rpaths so we have to add .dll search paths to PATH run.addPathForDynLibs(exe); } try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items); break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_context) catch |e| { if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; return step.fail("unable to spawn interpreter {s}: {s}", .{ interp_argv.items[0], @errorName(e), }); }; } return step.fail("failed to spawn and capture stdio from {s}: {s}", .{ argv[0], @errorName(err) }); }; step.result_duration_ns = result.elapsed_ns; step.result_peak_rss = result.peak_rss; step.test_results = result.stdio.test_results; if (result.stdio.test_metadata) |tm| run.cached_test_metadata = tm.toCachedTestMetadata(); const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items; if (fuzz_context != null) { try step.handleChildProcessTerm(result.term, cwd, final_argv); return; } // Capture stdout and stderr to GeneratedFile objects. const Stream = struct { captured: ?*Output, bytes: ?[]const u8, }; for ([_]Stream{ .{ .captured = run.captured_stdout, .bytes = result.stdio.stdout, }, .{ .captured = run.captured_stderr, .bytes = result.stdio.stderr, }, }) |stream| { if (stream.captured) |output| { const output_components = .{ output_dir_path, output.basename }; const output_path = try b.cache_root.join(arena, &output_components); output.generated_file.path = output_path; const sub_path = b.pathJoin(&output_components); const sub_path_dirname = fs.path.dirname(sub_path).?; b.cache_root.handle.makePath(sub_path_dirname) catch |err| { return step.fail("unable to make path '{}{s}': {s}", .{ b.cache_root, sub_path_dirname, @errorName(err), }); }; b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = stream.bytes.? }) catch |err| { return step.fail("unable to write file '{}{s}': {s}", .{ b.cache_root, sub_path, @errorName(err), }); }; } } switch (run.stdio) { .check => |checks| for (checks.items) |check| switch (check) { .expect_stderr_exact => |expected_bytes| { if (!mem.eql(u8, expected_bytes, result.stdio.stderr.?)) { return step.fail( \\ \\========= expected this stderr: ========= \\{s} \\========= but found: ==================== \\{s} \\========= from the following command: === \\{s} , .{ expected_bytes, result.stdio.stderr.?, try Step.allocPrintCmd(arena, cwd, final_argv), }); } }, .expect_stderr_match => |match| { if (mem.indexOf(u8, result.stdio.stderr.?, match) == null) { return step.fail( \\ \\========= expected to find in stderr: ========= \\{s} \\========= but stderr does not contain it: ===== \\{s} \\========= from the following command: ========= \\{s} , .{ match, result.stdio.stderr.?, try Step.allocPrintCmd(arena, cwd, final_argv), }); } }, .expect_stdout_exact => |expected_bytes| { if (!mem.eql(u8, expected_bytes, result.stdio.stdout.?)) { return step.fail( \\ \\========= expected this stdout: ========= \\{s} \\========= but found: ==================== \\{s} \\========= from the following command: === \\{s} , .{ expected_bytes, result.stdio.stdout.?, try Step.allocPrintCmd(arena, cwd, final_argv), }); } }, .expect_stdout_match => |match| { if (mem.indexOf(u8, result.stdio.stdout.?, match) == null) { return step.fail( \\ \\========= expected to find in stdout: ========= \\{s} \\========= but stdout does not contain it: ===== \\{s} \\========= from the following command: ========= \\{s} , .{ match, result.stdio.stdout.?, try Step.allocPrintCmd(arena, cwd, final_argv), }); } }, .expect_term => |expected_term| { if (!termMatches(expected_term, result.term)) { return step.fail("the following command {} (expected {}):\n{s}", .{ fmtTerm(result.term), fmtTerm(expected_term), try Step.allocPrintCmd(arena, cwd, final_argv), }); } }, }, .zig_test => { const prefix: []const u8 = p: { if (result.stdio.test_metadata) |tm| { if (tm.next_index > 0 and tm.next_index <= tm.names.len) { const name = tm.testName(tm.next_index - 1); break :p b.fmt("while executing test '{s}', ", .{name}); } } break :p ""; }; const expected_term: std.process.Child.Term = .{ .Exited = 0 }; if (!termMatches(expected_term, result.term)) { return step.fail("{s}the following command {} (expected {}):\n{s}", .{ prefix, fmtTerm(result.term), fmtTerm(expected_term), try Step.allocPrintCmd(arena, cwd, final_argv), }); } if (!result.stdio.test_results.isSuccess()) { return step.fail( "{s}the following test command failed:\n{s}", .{ prefix, try Step.allocPrintCmd(arena, cwd, final_argv) }, ); } }, else => { try step.handleChildProcessTerm(result.term, cwd, final_argv); }, } } const ChildProcResult = struct { term: std.process.Child.Term, elapsed_ns: u64, peak_rss: usize, stdio: StdIoResult, }; fn spawnChildAndCollect( run: *Run, argv: []const []const u8, has_side_effects: bool, prog_node: std.Progress.Node, fuzz_context: ?FuzzContext, ) !ChildProcResult { const b = run.step.owner; const arena = b.allocator; if (fuzz_context != null) { assert(!has_side_effects); assert(run.stdio == .zig_test); } var child = std.process.Child.init(argv, arena); if (run.cwd) |lazy_cwd| { child.cwd = lazy_cwd.getPath2(b, &run.step); } else { child.cwd = b.build_root.path; child.cwd_dir = b.build_root.handle; } child.env_map = run.env_map orelse &b.graph.env_map; child.request_resource_usage_statistics = true; child.stdin_behavior = switch (run.stdio) { .infer_from_args => if (has_side_effects) .Inherit else .Ignore, .inherit => .Inherit, .check => .Ignore, .zig_test => .Pipe, }; child.stdout_behavior = switch (run.stdio) { .infer_from_args => if (has_side_effects) .Inherit else .Ignore, .inherit => .Inherit, .check => |checks| if (checksContainStdout(checks.items)) .Pipe else .Ignore, .zig_test => .Pipe, }; child.stderr_behavior = switch (run.stdio) { .infer_from_args => if (has_side_effects) .Inherit else .Pipe, .inherit => .Inherit, .check => .Pipe, .zig_test => .Pipe, }; if (run.captured_stdout != null) child.stdout_behavior = .Pipe; if (run.captured_stderr != null) child.stderr_behavior = .Pipe; if (run.stdin != .none) { assert(run.stdio != .inherit); child.stdin_behavior = .Pipe; } const inherit = child.stdout_behavior == .Inherit or child.stderr_behavior == .Inherit; if (run.stdio != .zig_test and !run.disable_zig_progress and !inherit) { child.progress_node = prog_node; } const term, const result, const elapsed_ns = t: { if (inherit) std.debug.lockStdErr(); defer if (inherit) std.debug.unlockStdErr(); try child.spawn(); errdefer { _ = child.kill() catch {}; } // We need to report `error.InvalidExe` *now* if applicable. try child.waitForSpawn(); var timer = try std.time.Timer.start(); const result = if (run.stdio == .zig_test) try evalZigTest(run, &child, prog_node, fuzz_context) else try evalGeneric(run, &child); break :t .{ try child.wait(), result, timer.read() }; }; return .{ .stdio = result, .term = term, .elapsed_ns = elapsed_ns, .peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0, }; } const StdIoResult = struct { stdout: ?[]const u8, stderr: ?[]const u8, test_results: Step.TestResults, test_metadata: ?TestMetadata, }; fn evalZigTest( run: *Run, child: *std.process.Child, prog_node: std.Progress.Node, fuzz_context: ?FuzzContext, ) !StdIoResult { const gpa = run.step.owner.allocator; const arena = run.step.owner.allocator; var poller = std.io.poll(gpa, enum { stdout, stderr }, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); defer poller.deinit(); // If this is `true`, we avoid ever entering the polling loop below, because the stdin pipe has // somehow already closed; instead, we go straight to capturing stderr in case it has anything // useful. const first_write_failed = if (fuzz_context) |fuzz| failed: { sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index) catch |err| { try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); break :failed true; }; break :failed false; } else failed: { run.fuzz_tests.clearRetainingCapacity(); sendMessage(child.stdin.?, .query_test_metadata) catch |err| { try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); break :failed true; }; break :failed false; }; const Header = std.zig.Server.Message.Header; const stdout = poller.fifo(.stdout); const stderr = poller.fifo(.stderr); var fail_count: u32 = 0; var skip_count: u32 = 0; var leak_count: u32 = 0; var test_count: u32 = 0; var log_err_count: u32 = 0; var metadata: ?TestMetadata = null; var coverage_id: ?u64 = null; var sub_prog_node: ?std.Progress.Node = null; defer if (sub_prog_node) |n| n.end(); const any_write_failed = first_write_failed or poll: while (true) { while (stdout.readableLength() < @sizeOf(Header)) { if (!(try poller.poll())) break :poll false; } const header = stdout.reader().readStruct(Header) catch unreachable; while (stdout.readableLength() < header.bytes_len) { if (!(try poller.poll())) break :poll false; } const body = stdout.readableSliceOfLen(header.bytes_len); switch (header.tag) { .zig_version => { if (!std.mem.eql(u8, builtin.zig_version_string, body)) { return run.step.fail( "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", .{ builtin.zig_version_string, body }, ); } }, .test_metadata => { assert(fuzz_context == null); const TmHdr = std.zig.Server.Message.TestMetadata; const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body)); test_count = tm_hdr.tests_len; const names_bytes = body[@sizeOf(TmHdr)..][0 .. test_count * @sizeOf(u32)]; const expected_panic_msgs_bytes = body[@sizeOf(TmHdr) + names_bytes.len ..][0 .. test_count * @sizeOf(u32)]; const string_bytes = body[@sizeOf(TmHdr) + names_bytes.len + expected_panic_msgs_bytes.len ..][0..tm_hdr.string_bytes_len]; const names = std.mem.bytesAsSlice(u32, names_bytes); const expected_panic_msgs = std.mem.bytesAsSlice(u32, expected_panic_msgs_bytes); const names_aligned = try arena.alloc(u32, names.len); for (names_aligned, names) |*dest, src| dest.* = src; const expected_panic_msgs_aligned = try arena.alloc(u32, expected_panic_msgs.len); for (expected_panic_msgs_aligned, expected_panic_msgs) |*dest, src| dest.* = src; prog_node.setEstimatedTotalItems(names.len); metadata = .{ .string_bytes = try arena.dupe(u8, string_bytes), .names = names_aligned, .expected_panic_msgs = expected_panic_msgs_aligned, .next_index = 0, .prog_node = prog_node, }; requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node) catch |err| { try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); break :poll true; }; }, .test_results => { assert(fuzz_context == null); const md = metadata.?; const TrHdr = std.zig.Server.Message.TestResults; const tr_hdr = @as(*align(1) const TrHdr, @ptrCast(body)); fail_count +|= @intFromBool(tr_hdr.flags.fail); skip_count +|= @intFromBool(tr_hdr.flags.skip); leak_count +|= @intFromBool(tr_hdr.flags.leak); log_err_count +|= tr_hdr.flags.log_err_count; if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index); if (tr_hdr.flags.fail or tr_hdr.flags.leak or tr_hdr.flags.log_err_count > 0) { const name = std.mem.sliceTo(md.string_bytes[md.names[tr_hdr.index]..], 0); const orig_msg = stderr.readableSlice(0); defer stderr.discard(orig_msg.len); const msg = std.mem.trim(u8, orig_msg, "\n"); const label = if (tr_hdr.flags.fail) "failed" else if (tr_hdr.flags.leak) "leaked" else if (tr_hdr.flags.log_err_count > 0) "logged errors" else unreachable; if (msg.len > 0) { try run.step.addError("'{s}' {s}: {s}", .{ name, label, msg }); } else { try run.step.addError("'{s}' {s}", .{ name, label }); } } requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node) catch |err| { try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); break :poll true; }; }, .coverage_id => { const web_server = fuzz_context.?.web_server; const msg_ptr: *align(1) const u64 = @ptrCast(body); coverage_id = msg_ptr.*; { web_server.mutex.lock(); defer web_server.mutex.unlock(); try web_server.msg_queue.append(web_server.gpa, .{ .coverage = .{ .id = coverage_id.?, .run = run, } }); web_server.condition.signal(); } }, .fuzz_start_addr => { const web_server = fuzz_context.?.web_server; const msg_ptr: *align(1) const u64 = @ptrCast(body); const addr = msg_ptr.*; { web_server.mutex.lock(); defer web_server.mutex.unlock(); try web_server.msg_queue.append(web_server.gpa, .{ .entry_point = .{ .addr = addr, .coverage_id = coverage_id.?, } }); web_server.condition.signal(); } }, else => {}, // ignore other messages } stdout.discard(body.len); }; if (any_write_failed) { // The compiler unexpectedly closed stdin; something is very wrong and has probably crashed. // We want to make sure we've captured all of stderr so that it's logged below. while (try poller.poll()) {} } if (stderr.readableLength() > 0) { const msg = std.mem.trim(u8, try stderr.toOwnedSlice(), "\n"); if (msg.len > 0) run.step.result_stderr = msg; } // Send EOF to stdin. child.stdin.?.close(); child.stdin = null; return .{ .stdout = null, .stderr = null, .test_results = .{ .test_count = test_count, .fail_count = fail_count, .skip_count = skip_count, .leak_count = leak_count, .log_err_count = log_err_count, }, .test_metadata = metadata, }; } const TestMetadata = struct { names: []const u32, expected_panic_msgs: []const u32, string_bytes: []const u8, next_index: u32, prog_node: std.Progress.Node, fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { return .{ .names = tm.names, .string_bytes = tm.string_bytes, }; } fn testName(tm: TestMetadata, index: u32) []const u8 { return tm.toCachedTestMetadata().testName(index); } }; pub const CachedTestMetadata = struct { names: []const u32, string_bytes: []const u8, pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); } }; fn requestNextTest(in: fs.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { while (metadata.next_index < metadata.names.len) { const i = metadata.next_index; metadata.next_index += 1; if (metadata.expected_panic_msgs[i] != 0) continue; const name = metadata.testName(i); if (sub_prog_node.*) |n| n.end(); sub_prog_node.* = metadata.prog_node.start(name, 0); try sendRunTestMessage(in, .run_test, i); return; } else { try sendMessage(in, .exit); } } fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void { const header: std.zig.Client.Message.Header = .{ .tag = tag, .bytes_len = 0, }; try file.writeAll(std.mem.asBytes(&header)); } fn sendRunTestMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag, index: u32) !void { const header: std.zig.Client.Message.Header = .{ .tag = tag, .bytes_len = 4, }; const full_msg = std.mem.asBytes(&header) ++ std.mem.asBytes(&index); try file.writeAll(full_msg); } fn evalGeneric(run: *Run, child: *std.process.Child) !StdIoResult { const b = run.step.owner; const arena = b.allocator; switch (run.stdin) { .bytes => |bytes| { child.stdin.?.writeAll(bytes) catch |err| { return run.step.fail("unable to write stdin: {s}", .{@errorName(err)}); }; child.stdin.?.close(); child.stdin = null; }, .lazy_path => |lazy_path| { const path = lazy_path.getPath2(b, &run.step); const file = b.build_root.handle.openFile(path, .{}) catch |err| { return run.step.fail("unable to open stdin file: {s}", .{@errorName(err)}); }; defer file.close(); child.stdin.?.writeFileAll(file, .{}) catch |err| { return run.step.fail("unable to write file to stdin: {s}", .{@errorName(err)}); }; child.stdin.?.close(); child.stdin = null; }, .none => {}, } var stdout_bytes: ?[]const u8 = null; var stderr_bytes: ?[]const u8 = null; if (child.stdout) |stdout| { if (child.stderr) |stderr| { var poller = std.io.poll(arena, enum { stdout, stderr }, .{ .stdout = stdout, .stderr = stderr, }); defer poller.deinit(); while (try poller.poll()) { if (poller.fifo(.stdout).count > run.max_stdio_size) return error.StdoutStreamTooLong; if (poller.fifo(.stderr).count > run.max_stdio_size) return error.StderrStreamTooLong; } stdout_bytes = try poller.fifo(.stdout).toOwnedSlice(); stderr_bytes = try poller.fifo(.stderr).toOwnedSlice(); } else { stdout_bytes = try stdout.reader().readAllAlloc(arena, run.max_stdio_size); } } else if (child.stderr) |stderr| { stderr_bytes = try stderr.reader().readAllAlloc(arena, run.max_stdio_size); } if (stderr_bytes) |bytes| if (bytes.len > 0) { // Treat stderr as an error message. const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) { .check => |checks| !checksContainStderr(checks.items), else => true, }; if (stderr_is_diagnostic) { run.step.result_stderr = bytes; } }; return .{ .stdout = stdout_bytes, .stderr = stderr_bytes, .test_results = .{}, .test_metadata = null, }; } fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void { const b = run.step.owner; const compiles = artifact.getCompileDependencies(true); for (compiles) |compile| { if (compile.root_module.resolved_target.?.result.os.tag == .windows and compile.isDynamicLibrary()) { addPathDir(run, fs.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?); } } } fn failForeign( run: *Run, suggested_flag: []const u8, argv0: []const u8, exe: *Step.Compile, ) error{ MakeFailed, MakeSkipped, OutOfMemory } { switch (run.stdio) { .check, .zig_test => { if (run.skip_foreign_checks) return error.MakeSkipped; const b = run.step.owner; const host_name = try b.graph.host.result.zigTriple(b.allocator); const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator); return run.step.fail( \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) \\ consider using {s} or enabling skip_foreign_checks in the Run step , .{ argv0, foreign_name, host_name, suggested_flag }); }, else => { return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0}); }, } } fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void { switch (stdio) { .infer_from_args, .inherit, .zig_test => {}, .check => |checks| for (checks.items) |check| { hh.add(@as(std.meta.Tag(StdIo.Check), check)); switch (check) { .expect_stderr_exact, .expect_stderr_match, .expect_stdout_exact, .expect_stdout_match, => |s| hh.addBytes(s), .expect_term => |term| { hh.add(@as(std.meta.Tag(std.process.Child.Term), term)); switch (term) { .Exited => |x| hh.add(x), .Signal, .Stopped, .Unknown => |x| hh.add(x), } }, } }, } }