zig/lib/std / Build/Step/Run.zig

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 ArrayList = std.ArrayList;
const EnvMap = process.EnvMap;
const assert = std.debug.assert;

const Run = @This();

base_id

pub const base_id: Step.Id = .run;

step: Step,

argv: ArrayList(Arg),

cwd: ?Build.LazyPath,

env_map: ?*EnvMap,

stdio: StdIo = .infer_from_args,

stdin: StdIn = .none,

extra_file_dependencies: []const []const u8 = &.{},

rename_step_with_output_arg: bool = true,

skip_foreign_checks: bool = false,

failing_to_execute_foreign_is_an_error: bool = true,

max_stdio_size: usize = 10 * 1024 * 1024,

captured_stdout: ?*Output = null,
captured_stderr: ?*Output = null,

dep_output_file: ?*Output = null,

has_side_effects: bool = false,

StdIn

See also addArg and addArgs to modifying this directly Use setCwd to set the initial current working directory Override this field to modify the environment, or use setEnvironmentVariable 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. This field must be .none if stdio is inherit. It should be only set using setStdIn. Additional file paths relative to build.zig that, when modified, indicate that the Run step should be re-executed. If the Run step is determined to have side-effects, this field is ignored and the Run step is always executed when it appears in the build graph. 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. 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. 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. If stderr or stdout exceeds this amount, the child process is killed and the step fails.

pub const StdIn = union(enum) {
    none,
    bytes: []const u8,
    lazy_path: std.Build.LazyPath,
};

StdIo

pub const StdIo = union(enum) {
    infer_from_args,
    inherit,
    check: std.ArrayList(Check),
    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,
    };
};

Arg

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. 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. 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. This Run step is running a zig unit test binary and will communicate extra metadata over the IPC protocol.

pub const Arg = union(enum) {
    artifact: *Step.Compile,
    lazy_path: PrefixedLazyPath,
    directory_source: PrefixedLazyPath,
    bytes: []u8,
    output: *Output,
};

PrefixedLazyPath

pub const PrefixedLazyPath = struct {
    prefix: []const u8,
    lazy_path: std.Build.LazyPath,
};

Output

pub const Output = struct {
    generated_file: std.Build.GeneratedFile,
    prefix: []const u8,
    basename: []const u8,
};

create()

pub fn create(owner: *std.Build, name: []const u8) *Run {
    const self = owner.allocator.create(Run) catch @panic("OOM");
    self.* = .{
        .step = Step.init(.{
            .id = base_id,
            .name = name,
            .owner = owner,
            .makeFn = make,
        }),
        .argv = ArrayList(Arg).init(owner.allocator),
        .cwd = null,
        .env_map = null,
    };
    return self;
}

setName()

pub fn setName(self: *Run, name: []const u8) void {
    self.step.name = name;
    self.rename_step_with_output_arg = false;
}

enableTestRunnerMode()

pub fn enableTestRunnerMode(self: *Run) void {
    self.stdio = .zig_test;
    self.addArgs(&.{"--listen=-"});
}

addArtifactArg()

pub fn addArtifactArg(self: *Run, artifact: *Step.Compile) void {
    const bin_file = artifact.getEmittedBin();
    bin_file.addStepDependencies(&self.step);
    self.argv.append(Arg{ .artifact = artifact }) catch @panic("OOM");
}

addOutputFileArg()

This provides file path as a command line argument to the command being run, and returns a LazyPath which can be used as inputs to other APIs throughout the build system.

pub fn addOutputFileArg(self: *Run, basename: []const u8) std.Build.LazyPath {
    return self.addPrefixedOutputFileArg("", basename);
}

addPrefixedOutputFileArg()

pub fn addPrefixedOutputFileArg(
    self: *Run,
    prefix: []const u8,
    basename: []const u8,
) std.Build.LazyPath {
    const b = self.step.owner;

    const output = b.allocator.create(Output) catch @panic("OOM");
    output.* = .{
        .prefix = prefix,
        .basename = basename,
        .generated_file = .{ .step = &self.step },
    };
    self.argv.append(.{ .output = output }) catch @panic("OOM");

    if (self.rename_step_with_output_arg) {
        self.setName(b.fmt("{s} ({s})", .{ self.step.name, basename }));
    }

    return .{ .generated = &output.generated_file };
}

addFileSourceArg

deprecated: use addFileArg

pub const addFileSourceArg = addFileArg;

addFileArg()

pub fn addFileArg(self: *Run, lp: std.Build.LazyPath) void {
    self.addPrefixedFileArg("", lp);
}

// deprecated: use `addPrefixedFileArg`

addPrefixedFileSourceArg

pub const addPrefixedFileSourceArg = addPrefixedFileArg;

addPrefixedFileArg()

pub fn addPrefixedFileArg(self: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
    const b = self.step.owner;

    const prefixed_file_source: PrefixedLazyPath = .{
        .prefix = b.dupe(prefix),
        .lazy_path = lp.dupe(b),
    };
    self.argv.append(.{ .lazy_path = prefixed_file_source }) catch @panic("OOM");
    lp.addStepDependencies(&self.step);
}

addDirectorySourceArg

deprecated: use addDirectoryArg

pub const addDirectorySourceArg = addDirectoryArg;

addDirectoryArg()

pub fn addDirectoryArg(self: *Run, directory_source: std.Build.LazyPath) void {
    self.addPrefixedDirectoryArg("", directory_source);
}

// deprecated: use `addPrefixedDirectoryArg`

addPrefixedDirectorySourceArg

pub const addPrefixedDirectorySourceArg = addPrefixedDirectoryArg;

addPrefixedDirectoryArg()

pub fn addPrefixedDirectoryArg(self: *Run, prefix: []const u8, directory_source: std.Build.LazyPath) void {
    const b = self.step.owner;

    const prefixed_directory_source: PrefixedLazyPath = .{
        .prefix = b.dupe(prefix),
        .lazy_path = directory_source.dupe(b),
    };
    self.argv.append(.{ .directory_source = prefixed_directory_source }) catch @panic("OOM");
    directory_source.addStepDependencies(&self.step);
}

addDepFileOutputArg()

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(self: *Run, basename: []const u8) std.Build.LazyPath {
    return self.addPrefixedDepFileOutputArg("", basename);
}

addPrefixedDepFileOutputArg()

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(self: *Run, prefix: []const u8, basename: []const u8) void {
    assert(self.dep_output_file == null);

    const b = self.step.owner;

    const dep_file = b.allocator.create(Output) catch @panic("OOM");
    dep_file.* = .{
        .prefix = b.dupe(prefix),
        .basename = b.dupe(basename),
        .generated_file = .{ .step = &self.step },
    };

    self.dep_output_file = dep_file;

    self.argv.append(.{ .output = dep_file }) catch @panic("OOM");
}

addArg()

pub fn addArg(self: *Run, arg: []const u8) void {
    self.argv.append(.{ .bytes = self.step.owner.dupe(arg) }) catch @panic("OOM");
}

addArgs()

pub fn addArgs(self: *Run, args: []const []const u8) void {
    for (args) |arg| {
        self.addArg(arg);
    }
}

setStdIn()

pub fn setStdIn(self: *Run, stdin: StdIn) void {
    switch (stdin) {
        .lazy_path => |lazy_path| lazy_path.addStepDependencies(&self.step),
        .bytes, .none => {},
    }
    self.stdin = stdin;
}

setCwd()

pub fn setCwd(self: *Run, cwd: Build.LazyPath) void {
    cwd.addStepDependencies(&self.step);
    self.cwd = cwd;
}

clearEnvironment()

pub fn clearEnvironment(self: *Run) void {
    const b = self.step.owner;
    const new_env_map = b.allocator.create(EnvMap) catch @panic("OOM");
    new_env_map.* = EnvMap.init(b.allocator);
    self.env_map = new_env_map;
}

addPathDir()

pub fn addPathDir(self: *Run, search_path: []const u8) void {
    const b = self.step.owner;
    const env_map = getEnvMapInternal(self);

    const key = "PATH";
    var 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");
    }
}

getEnvMap()

pub fn getEnvMap(self: *Run) *EnvMap {
    return getEnvMapInternal(self);
}

fn getEnvMapInternal(self: *Run) *EnvMap {
    const arena = self.step.owner.allocator;
    return self.env_map orelse {
        const env_map = arena.create(EnvMap) catch @panic("OOM");
        env_map.* = process.getEnvMap(arena) catch @panic("unhandled error");
        self.env_map = env_map;
        return env_map;
    };
}

setEnvironmentVariable()

pub fn setEnvironmentVariable(self: *Run, key: []const u8, value: []const u8) void {
    const b = self.step.owner;
    const env_map = self.getEnvMap();
    env_map.put(b.dupe(key), b.dupe(value)) catch @panic("unhandled error");
}

removeEnvironmentVariable()

pub fn removeEnvironmentVariable(self: *Run, key: []const u8) void {
    self.getEnvMap().remove(key);
}

expectStdErrEqual()

Adds a check for exact stderr match. Does not add any other checks.

pub fn expectStdErrEqual(self: *Run, bytes: []const u8) void {
    const new_check: StdIo.Check = .{ .expect_stderr_exact = self.step.owner.dupe(bytes) };
    self.addCheck(new_check);
}

expectStdOutEqual()

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(self: *Run, bytes: []const u8) void {
    const new_check: StdIo.Check = .{ .expect_stdout_exact = self.step.owner.dupe(bytes) };
    self.addCheck(new_check);
    if (!self.hasTermCheck()) {
        self.expectExitCode(0);
    }
}

expectExitCode()

pub fn expectExitCode(self: *Run, code: u8) void {
    const new_check: StdIo.Check = .{ .expect_term = .{ .Exited = code } };
    self.addCheck(new_check);
}

hasTermCheck()

pub fn hasTermCheck(self: Run) bool {
    for (self.stdio.check.items) |check| switch (check) {
        .expect_term => return true,
        else => continue,
    };
    return false;
}

addCheck()

pub fn addCheck(self: *Run, new_check: StdIo.Check) void {
    switch (self.stdio) {
        .infer_from_args => {
            self.stdio = .{ .check = std.ArrayList(StdIo.Check).init(self.step.owner.allocator) };
            self.stdio.check.append(new_check) catch @panic("OOM");
        },
        .check => |*checks| checks.append(new_check) catch @panic("OOM"),
        else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of Run instead"),
    }
}

captureStdErr()

pub fn captureStdErr(self: *Run) std.Build.LazyPath {
    assert(self.stdio != .inherit);

    if (self.captured_stderr) |output| return .{ .generated = &output.generated_file };

    const output = self.step.owner.allocator.create(Output) catch @panic("OOM");
    output.* = .{
        .prefix = "",
        .basename = "stderr",
        .generated_file = .{ .step = &self.step },
    };
    self.captured_stderr = output;
    return .{ .generated = &output.generated_file };
}

captureStdOut()

pub fn captureStdOut(self: *Run) std.Build.LazyPath {
    assert(self.stdio != .inherit);

    if (self.captured_stdout) |output| return .{ .generated = &output.generated_file };

    const output = self.step.owner.allocator.create(Output) catch @panic("OOM");
    output.* = .{
        .prefix = "",
        .basename = "stdout",
        .generated_file = .{ .step = &self.step },
    };
    self.captured_stdout = output;
    return .{ .generated = &output.generated_file };
}

fn hasSideEffects(self: Run) bool {
    if (self.has_side_effects) return true;
    return switch (self.stdio) {
        .infer_from_args => !self.hasAnyOutputArgs(),
        .inherit => true,
        .check => false,
        .zig_test => false,
    };
}

fn hasAnyOutputArgs(self: Run) bool {
    if (self.captured_stdout != null) return true;
    if (self.captured_stderr != null) return true;
    for (self.argv.items) |arg| switch (arg) {
        .output => 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;
}

fn make(step: *Step, prog_node: *std.Progress.Node) !void {
    const b = step.owner;
    const arena = b.allocator;
    const self = @fieldParentPtr(Run, "step", step);
    const has_side_effects = self.hasSideEffects();

    var argv_list = ArrayList([]const u8).init(arena);
    var output_placeholders = ArrayList(struct {
        index: usize,
        output: *Output,
    }).init(arena);

    var man = b.cache.obtain();
    defer man.deinit();

    for (self.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.getPath(b);
                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.getPath(b);
                try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, file_path }));
                man.hash.addBytes(file.prefix);
                man.hash.addBytes(file_path);
            },
            .artifact => |artifact| {
                if (artifact.target.isWindows()) {
                    // On Windows we don't have rpaths so we have to add .dll search paths to PATH
                    self.addPathForDynLibs(artifact);
                }
                const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; // the path is guaranteed to be set

                try argv_list.append(file_path);

                _ = try man.addFile(file_path, null);
            },
            .output => |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 argv_list.append("");
                try output_placeholders.append(.{
                    .index = argv_list.items.len - 1,
                    .output = output,
                });
            },
        }
    }

    switch (self.stdin) {
        .bytes => |bytes| {
            man.hash.addBytes(bytes);
        },
        .lazy_path => |lazy_path| {
            const file_path = lazy_path.getPath(b);
            _ = try man.addFile(file_path, null);
        },
        .none => {},
    }

    if (self.captured_stdout) |output| {
        man.hash.addBytes(output.basename);
    }

    if (self.captured_stderr) |output| {
        man.hash.addBytes(output.basename);
    }

    hashStdIo(&man.hash, self.stdio);

    if (has_side_effects) {
        try runCommand(self, argv_list.items, has_side_effects, null, prog_node);
        return;
    }

    for (self.extra_file_dependencies) |file_path| {
        _ = try man.addFile(b.pathFromRoot(file_path), null);
    }

    if (try step.cacheHit(&man)) {
        // cache hit, skip running command
        const digest = man.final();
        for (output_placeholders.items) |placeholder| {
            placeholder.output.generated_file.path = try b.cache_root.join(arena, &.{
                "o", &digest, placeholder.output.basename,
            });
        }

        if (self.captured_stdout) |output| {
            output.generated_file.path = try b.cache_root.join(arena, &.{
                "o", &digest, output.basename,
            });
        }

        if (self.captured_stderr) |output| {
            output.generated_file.path = try b.cache_root.join(arena, &.{
                "o", &digest, output.basename,
            });
        }

        step.result_cached = true;
        return;
    }

    const digest = man.final();

    for (output_placeholders.items) |placeholder| {
        const output_components = .{ "o", &digest, placeholder.output.basename };
        const output_sub_path = try fs.path.join(arena, &output_components);
        const output_sub_dir_path = fs.path.dirname(output_sub_path).?;
        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;
        const cli_arg = if (placeholder.output.prefix.len == 0)
            output_path
        else
            b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path });
        argv_list.items[placeholder.index] = cli_arg;
    }

    try runCommand(self, argv_list.items, has_side_effects, &digest, prog_node);

    if (self.dep_output_file) |dep_output_file|
        try man.addDepFilePost(std.fs.cwd(), dep_output_file.generated_file.getPath());

    try step.writeManifest(&man);
}

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,
    };
}

fn runCommand(
    self: *Run,
    argv: []const []const u8,
    has_side_effects: bool,
    digest: ?*const [std.Build.Cache.hex_digest_len]u8,
    prog_node: *std.Progress.Node,
) !void {
    const step = &self.step;
    const b = step.owner;
    const arena = b.allocator;

    const cwd: ?[]const u8 = if (self.cwd) |lazy_cwd| lazy_cwd.getPath(b) else null;

    try step.handleChildProcUnsupported(cwd, argv);
    try Step.handleVerbose2(step.owner, cwd, self.env_map, argv);

    const allow_skip = switch (self.stdio) {
        .check, .zig_test => self.skip_foreign_checks,
        else => false,
    };

    var interp_argv = std.ArrayList([]const u8).init(b.allocator);
    defer interp_argv.deinit();

    const result = spawnChildAndCollect(self, argv, has_side_effects, prog_node) 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 (self.argv.items[0]) {
                .artifact => |exe| exe,
                else => break :interpret,
            };
            switch (exe.kind) {
                .exe, .@"test" => {},
                else => break :interpret,
            }

            const need_cross_glibc = exe.target.isGnuLibC() and exe.is_linking_libc;
            switch (b.host.getExternalExecutor(&exe.target_info, .{
                .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(self, "-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(self, "--glibc-runtimes", argv[0], exe)
                        else
                            null;

                        try interp_argv.append(bin_name);

                        if (glibc_dir_arg) |dir| {
                            // TODO look into making this a call to `linuxTriple`. This
                            // needs the directory to be called "i686" rather than
                            // "x86" which is why we do it manually here.
                            const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}";
                            const cpu_arch = exe.target.getCpuArch();
                            const os_tag = exe.target.getOsTag();
                            const abi = exe.target.getAbi();
                            const cpu_arch_name: []const u8 = if (cpu_arch == .x86)
                                "i686"
                            else
                                @tagName(cpu_arch);
                            const full_dir = try std.fmt.allocPrint(b.allocator, fmt_str, .{
                                dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
                            });

                            try interp_argv.append("-L");
                            try interp_argv.append(full_dir);
                        }

                        try interp_argv.appendSlice(argv);
                    } else {
                        return failForeign(self, "-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(self, "-fdarling", argv[0], exe);
                    }
                },
                .wasmtime => |bin_name| {
                    if (b.enable_wasmtime) {
                        try interp_argv.append(bin_name);
                        try interp_argv.append("--dir=.");
                        try interp_argv.append(argv[0]);
                        try interp_argv.append("--");
                        try interp_argv.appendSlice(argv[1..]);
                    } else {
                        return failForeign(self, "-fwasmtime", argv[0], exe);
                    }
                },
                .bad_dl => |foreign_dl| {
                    if (allow_skip) return error.MakeSkipped;

                    const host_dl = b.host.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.host.target.zigTriple(b.allocator);
                    const foreign_name = try exe.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 (exe.target.isWindows()) {
                // On Windows we don't have rpaths so we have to add .dll search paths to PATH
                self.addPathForDynLibs(exe);
            }

            try Step.handleVerbose2(step.owner, cwd, self.env_map, interp_argv.items);

            break :term spawnChildAndCollect(self, interp_argv.items, has_side_effects, prog_node) catch |e| {
                if (!self.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("unable to spawn {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;

    // Capture stdout and stderr to GeneratedFile objects.
    const Stream = struct {
        captured: ?*Output,
        bytes: ?[]const u8,
    };
    for ([_]Stream{
        .{
            .captured = self.captured_stdout,
            .bytes = result.stdio.stdout,
        },
        .{
            .captured = self.captured_stderr,
            .bytes = result.stdio.stderr,
        },
    }) |stream| {
        if (stream.captured) |output| {
            const output_components = .{ "o", digest.?, output.basename };
            const output_path = try b.cache_root.join(arena, &output_components);
            output.generated_file.path = output_path;

            const sub_path = try fs.path.join(arena, &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, stream.bytes.?) catch |err| {
                return step.fail("unable to write file '{}{s}': {s}", .{
                    b.cache_root, sub_path, @errorName(err),
                });
            };
        }
    }

    const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;

    switch (self.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(
    self: *Run,
    argv: []const []const u8,
    has_side_effects: bool,
    prog_node: *std.Progress.Node,
) !ChildProcResult {
    const b = self.step.owner;
    const arena = b.allocator;

    var child = std.process.Child.init(argv, arena);
    if (self.cwd) |lazy_cwd| {
        child.cwd = lazy_cwd.getPath(b);
    } else {
        child.cwd = b.build_root.path;
        child.cwd_dir = b.build_root.handle;
    }
    child.env_map = self.env_map orelse b.env_map;
    child.request_resource_usage_statistics = true;

    child.stdin_behavior = switch (self.stdio) {
        .infer_from_args => if (has_side_effects) .Inherit else .Ignore,
        .inherit => .Inherit,
        .check => .Ignore,
        .zig_test => .Pipe,
    };
    child.stdout_behavior = switch (self.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 (self.stdio) {
        .infer_from_args => if (has_side_effects) .Inherit else .Pipe,
        .inherit => .Inherit,
        .check => .Pipe,
        .zig_test => .Pipe,
    };
    if (self.captured_stdout != null) child.stdout_behavior = .Pipe;
    if (self.captured_stderr != null) child.stderr_behavior = .Pipe;
    if (self.stdin != .none) {
        assert(self.stdio != .inherit);
        child.stdin_behavior = .Pipe;
    }

    try child.spawn();
    var timer = try std.time.Timer.start();

    const result = if (self.stdio == .zig_test)
        evalZigTest(self, &child, prog_node)
    else
        evalGeneric(self, &child);

    const term = try child.wait();
    const elapsed_ns = timer.read();

    return .{
        .stdio = try 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(
    self: *Run,
    child: *std.process.Child,
    prog_node: *std.Progress.Node,
) !StdIoResult {
    const gpa = self.step.owner.allocator;
    const arena = self.step.owner.allocator;

    var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
        .stdout = child.stdout.?,
        .stderr = child.stderr.?,
    });
    defer poller.deinit();

    try sendMessage(child.stdin.?, .query_test_metadata);

    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 sub_prog_node: ?std.Progress.Node = null;
    defer if (sub_prog_node) |*n| n.end();

    poll: while (true) {
        while (stdout.readableLength() < @sizeOf(Header)) {
            if (!(try poller.poll())) break :poll;
        }
        const header = stdout.reader().readStruct(Header) catch unreachable;
        while (stdout.readableLength() < header.bytes_len) {
            if (!(try poller.poll())) break :poll;
        }
        const body = stdout.readableSliceOfLen(header.bytes_len);

        switch (header.tag) {
            .zig_version => {
                if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
                    return self.step.fail(
                        "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
                        .{ builtin.zig_version_string, body },
                    );
                }
            },
            .test_metadata => {
                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 async_frame_lens_bytes = body[@sizeOf(TmHdr) + names_bytes.len ..][0 .. test_count * @sizeOf(u32)];
                const expected_panic_msgs_bytes = body[@sizeOf(TmHdr) + names_bytes.len + async_frame_lens_bytes.len ..][0 .. test_count * @sizeOf(u32)];
                const string_bytes = body[@sizeOf(TmHdr) + names_bytes.len + async_frame_lens_bytes.len + expected_panic_msgs_bytes.len ..][0..tm_hdr.string_bytes_len];

                const names = std.mem.bytesAsSlice(u32, names_bytes);
                const async_frame_lens = std.mem.bytesAsSlice(u32, async_frame_lens_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 async_frame_lens_aligned = try arena.alloc(u32, async_frame_lens.len);
                for (async_frame_lens_aligned, async_frame_lens) |*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,
                    .async_frame_lens = async_frame_lens_aligned,
                    .expected_panic_msgs = expected_panic_msgs_aligned,
                    .next_index = 0,
                    .prog_node = prog_node,
                };

                try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
            },
            .test_results => {
                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.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 msg = std.mem.trim(u8, stderr.readableSlice(0), "\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 self.step.addError("'{s}' {s}: {s}", .{ name, label, msg });
                    } else {
                        try self.step.addError("'{s}' {s}", .{ name, label });
                    }
                    stderr.discard(msg.len);
                }

                try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
            },
            else => {}, // ignore other messages
        }

        stdout.discard(body.len);
    }

    if (stderr.readableLength() > 0) {
        const msg = std.mem.trim(u8, try stderr.toOwnedSlice(), "\n");
        if (msg.len > 0) try self.step.result_error_msgs.append(arena, 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,
    async_frame_lens: []const u32,
    expected_panic_msgs: []const u32,
    string_bytes: []const u8,
    next_index: u32,
    prog_node: *std.Progress.Node,

    fn testName(tm: TestMetadata, 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.async_frame_lens[i] != 0) continue;
        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, 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, index: u32) !void {
    const header: std.zig.Client.Message.Header = .{
        .tag = .run_test,
        .bytes_len = 4,
    };
    const full_msg = std.mem.asBytes(&header) ++ std.mem.asBytes(&index);
    try file.writeAll(full_msg);
}

fn evalGeneric(self: *Run, child: *std.process.Child) !StdIoResult {
    const arena = self.step.owner.allocator;

    switch (self.stdin) {
        .bytes => |bytes| {
            child.stdin.?.writeAll(bytes) catch |err| {
                return self.step.fail("unable to write stdin: {s}", .{@errorName(err)});
            };
            child.stdin.?.close();
            child.stdin = null;
        },
        .lazy_path => |lazy_path| {
            const path = lazy_path.getPath(self.step.owner);
            const file = self.step.owner.build_root.handle.openFile(path, .{}) catch |err| {
                return self.step.fail("unable to open stdin file: {s}", .{@errorName(err)});
            };
            defer file.close();
            child.stdin.?.writeFileAll(file, .{}) catch |err| {
                return self.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 > self.max_stdio_size)
                    return error.StdoutStreamTooLong;
                if (poller.fifo(.stderr).count > self.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, self.max_stdio_size);
        }
    } else if (child.stderr) |stderr| {
        stderr_bytes = try stderr.reader().readAllAlloc(arena, self.max_stdio_size);
    }

    if (stderr_bytes) |bytes| if (bytes.len > 0) {
        // Treat stderr as an error message.
        const stderr_is_diagnostic = self.captured_stderr == null and switch (self.stdio) {
            .check => |checks| !checksContainStderr(checks.items),
            else => true,
        };
        if (stderr_is_diagnostic) {
            try self.step.result_error_msgs.append(arena, bytes);
        }
    };

    return .{
        .stdout = stdout_bytes,
        .stderr = stderr_bytes,
        .test_results = .{},
        .test_metadata = null,
    };
}

fn addPathForDynLibs(self: *Run, artifact: *Step.Compile) void {
    const b = self.step.owner;
    for (artifact.link_objects.items) |link_object| {
        switch (link_object) {
            .other_step => |other| {
                if (other.target.isWindows() and other.isDynamicLibrary()) {
                    addPathDir(self, fs.path.dirname(other.getEmittedBin().getPath(b)).?);
                    addPathForDynLibs(self, other);
                }
            },
            else => {},
        }
    }
}

fn failForeign(
    self: *Run,
    suggested_flag: []const u8,
    argv0: []const u8,
    exe: *Step.Compile,
) error{ MakeFailed, MakeSkipped, OutOfMemory } {
    switch (self.stdio) {
        .check, .zig_test => {
            if (self.skip_foreign_checks)
                return error.MakeSkipped;

            const b = self.step.owner;
            const host_name = try b.host.target.zigTriple(b.allocator);
            const foreign_name = try exe.target.zigTriple(b.allocator);

            return self.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 self.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),
                    }
                },
            }
        },
    }
}