    //! Unlike `RunStep` this step will provide emulation, when enabled, to run foreign binaries.
    //! When a binary is foreign, but emulation for the target is disabled, the specified binary
    //! will not be run and therefore also not validated against its output.
    //! This step can be useful when wishing to run a built binary on multiple platforms,
    //! without having to verify if it's possible to be ran against.

    const std = @import("../std.zig");
    const build = std.build;
    const Step = std.build.Step;
    const Builder = std.build.Builder;
    const LibExeObjStep = std.build.LibExeObjStep;
    const RunStep = std.build.RunStep;

    const fs = std.fs;
    const process = std.process;
    const EnvMap = process.EnvMap;

    const EmulatableRunStep = @This();

    pub const base_id = .emulatable_run;

    const max_stdout_size = 1 * 1024 * 1024; // 1 MiB

    step: Step,
    builder: *Builder,

    /// The artifact (executable) to be run by this step
    exe: *LibExeObjStep,

    /// Set this to `null` to ignore the exit code for the purpose of determining a successful execution
    expected_exit_code: ?u8 = 0,

    /// Override this field to modify the environment
    env_map: ?*EnvMap,

    /// Set this to modify the current working directory
    cwd: ?[]const u8,

    stdout_action: RunStep.StdIoAction = .inherit,
    stderr_action: RunStep.StdIoAction = .inherit,

    /// When set to true, hides the warning of skipping a foreign binary which cannot be run on the host
    /// or through emulation.
    hide_foreign_binaries_warning: bool,

    /// Creates a step that will execute the given artifact. This step will allow running the
    /// binary through emulation when any of the emulation options such as `enable_rosetta` are set to true.
    /// When set to false, and the binary is foreign, running the executable is skipped.
    /// Asserts given artifact is an executable.
    pub fn create(builder: *Builder, name: []const u8, artifact: *LibExeObjStep) *EmulatableRunStep {
        std.debug.assert(artifact.kind == .exe or artifact.kind == .test_exe);
        const self = builder.allocator.create(EmulatableRunStep) catch unreachable;

        const option_name = "hide-foreign-warnings";
        const hide_warnings = if (builder.available_options_map.get(option_name) == null) warn: {
            break :warn builder.option(bool, option_name, "Hide the warning when a foreign binary which is incompatible is skipped") orelse false;
        } else false;

        self.* = .{
            .builder = builder,
            .step = Step.init(.emulatable_run, name, builder.allocator, make),
            .exe = artifact,
            .env_map = null,
            .cwd = null,
            .hide_foreign_binaries_warning = hide_warnings,
        };
        self.step.dependOn(&artifact.step);

        return self;
    }

    fn make(step: *Step) !void {
        const self = @fieldParentPtr(EmulatableRunStep, "step", step);
        const host_info = self.builder.host;

        var argv_list = std.ArrayList([]const u8).init(self.builder.allocator);
        defer argv_list.deinit();

        const need_cross_glibc = self.exe.target.isGnuLibC() and self.exe.is_linking_libc;
        switch (host_info.getExternalExecutor(self.exe.target_info, .{
            .qemu_fixes_dl = need_cross_glibc and self.builder.glibc_runtimes_dir != null,
            .link_libc = self.exe.is_linking_libc,
        })) {
            .native => {},
            .rosetta => if (!self.builder.enable_rosetta) return warnAboutForeignBinaries(self),
            .wine => |bin_name| if (self.builder.enable_wine) {
                try argv_list.append(bin_name);
            } else return,
            .qemu => |bin_name| if (self.builder.enable_qemu) {
                const glibc_dir_arg = if (need_cross_glibc)
                    self.builder.glibc_runtimes_dir orelse return
                else
                    null;
                try argv_list.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

                    // "i386" which is why we do it manually here.

                    const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}";
                    const cpu_arch = self.exe.target.getCpuArch();
                    const os_tag = self.exe.target.getOsTag();
                    const abi = self.exe.target.getAbi();
                    const cpu_arch_name: []const u8 = if (cpu_arch == .i386)
                        "i686"
                    else
                        @tagName(cpu_arch);
                    const full_dir = try std.fmt.allocPrint(self.builder.allocator, fmt_str, .{
                        dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
                    });

                    try argv_list.append("-L");
                    try argv_list.append(full_dir);
                }
            } else return warnAboutForeignBinaries(self),
            .darling => |bin_name| if (self.builder.enable_darling) {
                try argv_list.append(bin_name);
            } else return warnAboutForeignBinaries(self),
            .wasmtime => |bin_name| if (self.builder.enable_wasmtime) {
                try argv_list.append(bin_name);
                try argv_list.append("--dir=.");
            } else return warnAboutForeignBinaries(self),
            else => return warnAboutForeignBinaries(self),
        }

        if (self.exe.target.isWindows()) {
            // On Windows we don't have rpaths so we have to add .dll search paths to PATH

            RunStep.addPathForDynLibsInternal(&self.step, self.builder, self.exe);
        }

        const executable_path = self.exe.installed_path orelse self.exe.getOutputSource().getPath(self.builder);
        try argv_list.append(executable_path);

        try RunStep.runCommand(
            argv_list.items,
            self.builder,
            self.expected_exit_code,
            self.stdout_action,
            self.stderr_action,
            .Inherit,
            self.env_map,
            self.cwd,
            false,
        );
    }

    pub fn expectStdErrEqual(self: *EmulatableRunStep, bytes: []const u8) void {
        self.stderr_action = .{ .expect_exact = self.builder.dupe(bytes) };
    }

    pub fn expectStdOutEqual(self: *EmulatableRunStep, bytes: []const u8) void {
        self.stdout_action = .{ .expect_exact = self.builder.dupe(bytes) };
    }

    fn warnAboutForeignBinaries(step: *EmulatableRunStep) void {
        if (step.hide_foreign_binaries_warning) return;
        const builder = step.builder;
        const artifact = step.exe;

        const host_name = builder.host.target.zigTriple(builder.allocator) catch unreachable;
        const foreign_name = artifact.target.zigTriple(builder.allocator) catch unreachable;
        const target_info = std.zig.system.NativeTargetInfo.detect(artifact.target) catch unreachable;
        const need_cross_glibc = artifact.target.isGnuLibC() and artifact.is_linking_libc;
        switch (builder.host.getExternalExecutor(target_info, .{
            .qemu_fixes_dl = need_cross_glibc and builder.glibc_runtimes_dir != null,
            .link_libc = artifact.is_linking_libc,
        })) {
            .native => unreachable,
            .bad_dl => |foreign_dl| {
                const host_dl = builder.host.dynamic_linker.get() orelse "(none)";
                std.debug.print("the host system does not appear to be capable of executing binaries from the target because the host dynamic linker is '{s}', while the target dynamic linker is '{s}'. Consider setting the dynamic linker as '{s}'.\n", .{
                    host_dl, foreign_dl, host_dl,
                });
            },
            .bad_os_or_cpu => {
                std.debug.print("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}).\n", .{
                    host_name, foreign_name,
                });
            },
            .darling => if (!builder.enable_darling) {
                std.debug.print(
                    "the host system ({s}) does not appear to be capable of executing binaries " ++
                        "from the target ({s}). Consider enabling darling.\n",
                    .{ host_name, foreign_name },
                );
            },
            .rosetta => if (!builder.enable_rosetta) {
                std.debug.print(
                    "the host system ({s}) does not appear to be capable of executing binaries " ++
                        "from the target ({s}). Consider enabling rosetta.\n",
                    .{ host_name, foreign_name },
                );
            },
            .wine => if (!builder.enable_wine) {
                std.debug.print(
                    "the host system ({s}) does not appear to be capable of executing binaries " ++
                        "from the target ({s}). Consider enabling wine.\n",
                    .{ host_name, foreign_name },
                );
            },
            .qemu => if (!builder.enable_qemu) {
                std.debug.print(
                    "the host system ({s}) does not appear to be capable of executing binaries " ++
                        "from the target ({s}). Consider enabling qemu.\n",
                    .{ host_name, foreign_name },
                );
            },
            .wasmtime => {
                std.debug.print(
                    "the host system ({s}) does not appear to be capable of executing binaries " ++
                        "from the target ({s}). Consider enabling wasmtime.\n",
                    .{ host_name, foreign_name },
                );
            },
        }
    }