Commit f3edff439e

Andrew Kelley <andrew@ziglang.org>
2021-12-03 05:51:14
improve detection of how to execute binaries on the host
`getExternalExecutor` is moved from `std.zig.CrossTarget` to `std.zig.system.NativeTargetInfo.getExternalExecutor`. The function also now communicates a bit more information about *why* the host is unable to execute a binary. The CLI is updated to report this information in a useful manner. `getExternalExecutor` is also improved to detect such patterns as: * x86_64 is able to execute x86 binaries * aarch64 is able to execute arm binaries * etc. Added qemu-hexagon support to `getExternalExecutor`. `std.Target.canExecBinaries` of is removed; callers should use the more powerful `getExternalExecutor` instead. Now that `zig test` tries to run the resulting binary no matter what, this commit has a follow-up change to the build system and docgen to utilize the `getExternalExecutor` function and pass `--test-no-exec` in some cases to avoid getting the error. Additionally: * refactor: extract NativePaths and NativeTargetInfo into their own files named after the structs. * small improvement to langref to reduce the complexity of the `callconv` expression in a couple examples.
1 parent 0cd8710
doc/docgen.zig
@@ -1202,6 +1202,7 @@ fn genHtml(
     var env_map = try process.getEnvMap(allocator);
     try env_map.put("ZIG_DEBUG_COLOR", "1");
 
+    const host = try std.zig.system.NativeTargetInfo.detect(allocator, .{});
     const builtin_code = try getBuiltinCode(allocator, &env_map, zig_exe);
 
     for (toc.nodes) |node| {
@@ -1424,13 +1425,17 @@ fn genHtml(
                         var test_args = std.ArrayList([]const u8).init(allocator);
                         defer test_args.deinit();
 
-                        try test_args.appendSlice(&[_][]const u8{ zig_exe, "test", tmp_source_file_name });
+                        try test_args.appendSlice(&[_][]const u8{
+                            zig_exe, "test", tmp_source_file_name,
+                        });
                         try shell_out.print("$ zig test {s}.zig ", .{code.name});
 
                         switch (code.mode) {
                             .Debug => {},
                             else => {
-                                try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) });
+                                try test_args.appendSlice(&[_][]const u8{
+                                    "-O", @tagName(code.mode),
+                                });
                                 try shell_out.print("-O {s} ", .{@tagName(code.mode)});
                             },
                         }
@@ -1441,8 +1446,26 @@ fn genHtml(
                         if (code.target_str) |triple| {
                             try test_args.appendSlice(&[_][]const u8{ "-target", triple });
                             try shell_out.print("-target {s} ", .{triple});
+
+                            const cross_target = try std.zig.CrossTarget.parse(.{
+                                .arch_os_abi = triple,
+                            });
+                            const target_info = try std.zig.system.NativeTargetInfo.detect(
+                                allocator,
+                                cross_target,
+                            );
+                            switch (host.getExternalExecutor(target_info, .{
+                                .link_libc = code.link_libc,
+                            })) {
+                                .native => {},
+                                else => {
+                                    try test_args.appendSlice(&[_][]const u8{"--test-no-exec"});
+                                    try shell_out.writeAll("--test-no-exec");
+                                },
+                            }
                         }
-                        const result = exec(allocator, &env_map, test_args.items) catch return parseError(tokenizer, code.source_token, "test failed", .{});
+                        const result = exec(allocator, &env_map, test_args.items) catch
+                            return parseError(tokenizer, code.source_token, "test failed", .{});
                         const escaped_stderr = try escapeHtml(allocator, result.stderr);
                         const escaped_stdout = try escapeHtml(allocator, result.stdout);
                         try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout });
@@ -1504,9 +1527,7 @@ fn genHtml(
                         defer test_args.deinit();
 
                         try test_args.appendSlice(&[_][]const u8{
-                            zig_exe,
-                            "test",
-                            tmp_source_file_name,
+                            zig_exe, "test", tmp_source_file_name,
                         });
                         var mode_arg: []const u8 = "";
                         switch (code.mode) {
doc/langref.html.in
@@ -4763,7 +4763,13 @@ test "noreturn" {
       <p>Another use case for {#syntax#}noreturn{#endsyntax#} is the {#syntax#}exit{#endsyntax#} function:</p>
       {#code_begin|test|noreturn_from_exit#}
       {#target_windows#}
-pub extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(if (@import("builtin").target.cpu.arch == .i386) .Stdcall else .C) noreturn;
+const std = @import("std");
+const builtin = @import("builtin");
+const native_arch = builtin.cpu.arch;
+const expect = std.testing.expect;
+
+const WINAPI: std.builtin.CallingConvention = if (native_arch == .i386) .Stdcall else .C;
+extern "kernel32" fn ExitProcess(exit_code: c_uint) callconv(WINAPI) noreturn;
 
 test "foo" {
     const value = bar() catch ExitProcess(1);
@@ -4774,12 +4780,15 @@ fn bar() anyerror!u32 {
     return 1234;
 }
 
-const expect = @import("std").testing.expect;
       {#code_end#}
       {#header_close#}
+
       {#header_open|Functions#}
       {#code_begin|test|functions#}
-const expect = @import("std").testing.expect;
+const std = @import("std");
+const builtin = @import("builtin");
+const native_arch = builtin.cpu.arch;
+const expect = std.testing.expect;
 
 // Functions are declared like this
 fn add(a: i8, b: i8) i8 {
@@ -4798,7 +4807,8 @@ export fn sub(a: i8, b: i8) i8 { return a - b; }
 // at link time, when linking statically, or at runtime, when linking
 // dynamically.
 // The callconv specifier changes the calling convention of the function.
-extern "kernel32" fn ExitProcess(exit_code: u32) callconv(if (@import("builtin").target.cpu.arch == .i386) .Stdcall else .C) noreturn;
+const WINAPI: std.builtin.CallingConvention = if (native_arch == .i386) .Stdcall else .C;
+extern "kernel32" fn ExitProcess(exit_code: u32) callconv(WINAPI) noreturn;
 extern "c" fn atan2(a: f64, b: f64) f64;
 
 // The @setCold builtin tells the optimizer that a function is rarely called.
lib/std/zig/system/NativePaths.zig
@@ -0,0 +1,205 @@
+const std = @import("../../std.zig");
+const builtin = @import("builtin");
+const ArrayList = std.ArrayList;
+const Allocator = std.mem.Allocator;
+const process = std.process;
+const mem = std.mem;
+
+const NativePaths = @This();
+const NativeTargetInfo = std.zig.system.NativeTargetInfo;
+
+include_dirs: ArrayList([:0]u8),
+lib_dirs: ArrayList([:0]u8),
+framework_dirs: ArrayList([:0]u8),
+rpaths: ArrayList([:0]u8),
+warnings: ArrayList([:0]u8),
+
+pub fn detect(allocator: Allocator, native_info: NativeTargetInfo) !NativePaths {
+    const native_target = native_info.target;
+
+    var self: NativePaths = .{
+        .include_dirs = ArrayList([:0]u8).init(allocator),
+        .lib_dirs = ArrayList([:0]u8).init(allocator),
+        .framework_dirs = ArrayList([:0]u8).init(allocator),
+        .rpaths = ArrayList([:0]u8).init(allocator),
+        .warnings = ArrayList([:0]u8).init(allocator),
+    };
+    errdefer self.deinit();
+
+    var is_nix = false;
+    if (process.getEnvVarOwned(allocator, "NIX_CFLAGS_COMPILE")) |nix_cflags_compile| {
+        defer allocator.free(nix_cflags_compile);
+
+        is_nix = true;
+        var it = mem.tokenize(u8, nix_cflags_compile, " ");
+        while (true) {
+            const word = it.next() orelse break;
+            if (mem.eql(u8, word, "-isystem")) {
+                const include_path = it.next() orelse {
+                    try self.addWarning("Expected argument after -isystem in NIX_CFLAGS_COMPILE");
+                    break;
+                };
+                try self.addIncludeDir(include_path);
+            } else {
+                if (mem.startsWith(u8, word, "-frandom-seed=")) {
+                    continue;
+                }
+                try self.addWarningFmt("Unrecognized C flag from NIX_CFLAGS_COMPILE: {s}", .{word});
+            }
+        }
+    } else |err| switch (err) {
+        error.InvalidUtf8 => {},
+        error.EnvironmentVariableNotFound => {},
+        error.OutOfMemory => |e| return e,
+    }
+    if (process.getEnvVarOwned(allocator, "NIX_LDFLAGS")) |nix_ldflags| {
+        defer allocator.free(nix_ldflags);
+
+        is_nix = true;
+        var it = mem.tokenize(u8, nix_ldflags, " ");
+        while (true) {
+            const word = it.next() orelse break;
+            if (mem.eql(u8, word, "-rpath")) {
+                const rpath = it.next() orelse {
+                    try self.addWarning("Expected argument after -rpath in NIX_LDFLAGS");
+                    break;
+                };
+                try self.addRPath(rpath);
+            } else if (word.len > 2 and word[0] == '-' and word[1] == 'L') {
+                const lib_path = word[2..];
+                try self.addLibDir(lib_path);
+            } else {
+                try self.addWarningFmt("Unrecognized C flag from NIX_LDFLAGS: {s}", .{word});
+                break;
+            }
+        }
+    } else |err| switch (err) {
+        error.InvalidUtf8 => {},
+        error.EnvironmentVariableNotFound => {},
+        error.OutOfMemory => |e| return e,
+    }
+    if (is_nix) {
+        return self;
+    }
+
+    if (comptime builtin.target.isDarwin()) {
+        try self.addIncludeDir("/usr/include");
+        try self.addIncludeDir("/usr/local/include");
+
+        try self.addLibDir("/usr/lib");
+        try self.addLibDir("/usr/local/lib");
+
+        try self.addFrameworkDir("/Library/Frameworks");
+        try self.addFrameworkDir("/System/Library/Frameworks");
+
+        return self;
+    }
+
+    if (comptime native_target.os.tag == .solaris) {
+        try self.addLibDir("/usr/lib/64");
+        try self.addLibDir("/usr/local/lib/64");
+        try self.addLibDir("/lib/64");
+
+        try self.addIncludeDir("/usr/include");
+        try self.addIncludeDir("/usr/local/include");
+
+        return self;
+    }
+
+    if (native_target.os.tag != .windows) {
+        const triple = try native_target.linuxTriple(allocator);
+        const qual = native_target.cpu.arch.ptrBitWidth();
+
+        // TODO: $ ld --verbose | grep SEARCH_DIR
+        // the output contains some paths that end with lib64, maybe include them too?
+        // TODO: what is the best possible order of things?
+        // TODO: some of these are suspect and should only be added on some systems. audit needed.
+
+        try self.addIncludeDir("/usr/local/include");
+        try self.addLibDirFmt("/usr/local/lib{d}", .{qual});
+        try self.addLibDir("/usr/local/lib");
+
+        try self.addIncludeDirFmt("/usr/include/{s}", .{triple});
+        try self.addLibDirFmt("/usr/lib/{s}", .{triple});
+
+        try self.addIncludeDir("/usr/include");
+        try self.addLibDirFmt("/lib{d}", .{qual});
+        try self.addLibDir("/lib");
+        try self.addLibDirFmt("/usr/lib{d}", .{qual});
+        try self.addLibDir("/usr/lib");
+
+        // example: on a 64-bit debian-based linux distro, with zlib installed from apt:
+        // zlib.h is in /usr/include (added above)
+        // libz.so.1 is in /lib/x86_64-linux-gnu (added here)
+        try self.addLibDirFmt("/lib/{s}", .{triple});
+    }
+
+    return self;
+}
+
+pub fn deinit(self: *NativePaths) void {
+    deinitArray(&self.include_dirs);
+    deinitArray(&self.lib_dirs);
+    deinitArray(&self.framework_dirs);
+    deinitArray(&self.rpaths);
+    deinitArray(&self.warnings);
+    self.* = undefined;
+}
+
+fn deinitArray(array: *ArrayList([:0]u8)) void {
+    for (array.items) |item| {
+        array.allocator.free(item);
+    }
+    array.deinit();
+}
+
+pub fn addIncludeDir(self: *NativePaths, s: []const u8) !void {
+    return self.appendArray(&self.include_dirs, s);
+}
+
+pub fn addIncludeDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
+    const item = try std.fmt.allocPrintZ(self.include_dirs.allocator, fmt, args);
+    errdefer self.include_dirs.allocator.free(item);
+    try self.include_dirs.append(item);
+}
+
+pub fn addLibDir(self: *NativePaths, s: []const u8) !void {
+    return self.appendArray(&self.lib_dirs, s);
+}
+
+pub fn addLibDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
+    const item = try std.fmt.allocPrintZ(self.lib_dirs.allocator, fmt, args);
+    errdefer self.lib_dirs.allocator.free(item);
+    try self.lib_dirs.append(item);
+}
+
+pub fn addWarning(self: *NativePaths, s: []const u8) !void {
+    return self.appendArray(&self.warnings, s);
+}
+
+pub fn addFrameworkDir(self: *NativePaths, s: []const u8) !void {
+    return self.appendArray(&self.framework_dirs, s);
+}
+
+pub fn addFrameworkDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
+    const item = try std.fmt.allocPrintZ(self.framework_dirs.allocator, fmt, args);
+    errdefer self.framework_dirs.allocator.free(item);
+    try self.framework_dirs.append(item);
+}
+
+pub fn addWarningFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
+    const item = try std.fmt.allocPrintZ(self.warnings.allocator, fmt, args);
+    errdefer self.warnings.allocator.free(item);
+    try self.warnings.append(item);
+}
+
+pub fn addRPath(self: *NativePaths, s: []const u8) !void {
+    return self.appendArray(&self.rpaths, s);
+}
+
+fn appendArray(self: *NativePaths, array: *ArrayList([:0]u8), s: []const u8) !void {
+    _ = self;
+    const item = try array.allocator.dupeZ(u8, s);
+    errdefer array.allocator.free(item);
+    try array.append(item);
+}
lib/std/zig/system/NativeTargetInfo.zig
@@ -0,0 +1,937 @@
+const std = @import("../../std.zig");
+const builtin = @import("builtin");
+const mem = std.mem;
+const assert = std.debug.assert;
+const fs = std.fs;
+const elf = std.elf;
+const native_endian = builtin.cpu.arch.endian();
+
+const NativeTargetInfo = @This();
+const Target = std.Target;
+const Allocator = std.mem.Allocator;
+const CrossTarget = std.zig.CrossTarget;
+const windows = std.zig.system.windows;
+const darwin = std.zig.system.darwin;
+const linux = std.zig.system.linux;
+
+target: Target,
+dynamic_linker: DynamicLinker = DynamicLinker{},
+
+pub const DynamicLinker = Target.DynamicLinker;
+
+pub const DetectError = error{
+    OutOfMemory,
+    FileSystem,
+    SystemResources,
+    SymLinkLoop,
+    ProcessFdQuotaExceeded,
+    SystemFdQuotaExceeded,
+    DeviceBusy,
+    OSVersionDetectionFail,
+};
+
+/// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected
+/// natively, which should be standard or default, and which are provided explicitly, this function
+/// resolves the native components by detecting the native system, and then resolves standard/default parts
+/// relative to that.
+/// Any resources this function allocates are released before returning, and so there is no
+/// deinitialization method.
+/// TODO Remove the Allocator requirement from this function.
+pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!NativeTargetInfo {
+    var os = cross_target.getOsTag().defaultVersionRange(cross_target.getCpuArch());
+    if (cross_target.os_tag == null) {
+        switch (builtin.target.os.tag) {
+            .linux => {
+                const uts = std.os.uname();
+                const release = mem.sliceTo(&uts.release, 0);
+                // The release field sometimes has a weird format,
+                // `Version.parse` will attempt to find some meaningful interpretation.
+                if (std.builtin.Version.parse(release)) |ver| {
+                    os.version_range.linux.range.min = ver;
+                    os.version_range.linux.range.max = ver;
+                } else |err| switch (err) {
+                    error.Overflow => {},
+                    error.InvalidCharacter => {},
+                    error.InvalidVersion => {},
+                }
+            },
+            .solaris => {
+                const uts = std.os.uname();
+                const release = mem.sliceTo(&uts.release, 0);
+                if (std.builtin.Version.parse(release)) |ver| {
+                    os.version_range.semver.min = ver;
+                    os.version_range.semver.max = ver;
+                } else |err| switch (err) {
+                    error.Overflow => {},
+                    error.InvalidCharacter => {},
+                    error.InvalidVersion => {},
+                }
+            },
+            .windows => {
+                const detected_version = windows.detectRuntimeVersion();
+                os.version_range.windows.min = detected_version;
+                os.version_range.windows.max = detected_version;
+            },
+            .macos => try darwin.macos.detect(&os),
+            .freebsd, .netbsd, .dragonfly => {
+                const key = switch (builtin.target.os.tag) {
+                    .freebsd => "kern.osreldate",
+                    .netbsd, .dragonfly => "kern.osrevision",
+                    else => unreachable,
+                };
+                var value: u32 = undefined;
+                var len: usize = @sizeOf(@TypeOf(value));
+
+                std.os.sysctlbynameZ(key, &value, &len, null, 0) catch |err| switch (err) {
+                    error.NameTooLong => unreachable, // constant, known good value
+                    error.PermissionDenied => unreachable, // only when setting values,
+                    error.SystemResources => unreachable, // memory already on the stack
+                    error.UnknownName => unreachable, // constant, known good value
+                    error.Unexpected => return error.OSVersionDetectionFail,
+                };
+
+                switch (builtin.target.os.tag) {
+                    .freebsd => {
+                        // https://www.freebsd.org/doc/en_US.ISO8859-1/books/porters-handbook/versions.html
+                        // Major * 100,000 has been convention since FreeBSD 2.2 (1997)
+                        // Minor * 1(0),000 summed has been convention since FreeBSD 2.2 (1997)
+                        // e.g. 492101 = 4.11-STABLE = 4.(9+2)
+                        const major = value / 100_000;
+                        const minor1 = value % 100_000 / 10_000; // usually 0 since 5.1
+                        const minor2 = value % 10_000 / 1_000; // 0 before 5.1, minor version since
+                        const patch = value % 1_000;
+                        os.version_range.semver.min = .{ .major = major, .minor = minor1 + minor2, .patch = patch };
+                        os.version_range.semver.max = os.version_range.semver.min;
+                    },
+                    .netbsd => {
+                        // #define __NetBSD_Version__ MMmmrrpp00
+                        //
+                        // M = major version
+                        // m = minor version; a minor number of 99 indicates current.
+                        // r = 0 (*)
+                        // p = patchlevel
+                        const major = value / 100_000_000;
+                        const minor = value % 100_000_000 / 1_000_000;
+                        const patch = value % 10_000 / 100;
+                        os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch };
+                        os.version_range.semver.max = os.version_range.semver.min;
+                    },
+                    .dragonfly => {
+                        // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cb2cde83771754aeef9bb3251ee48959138dec87/Makefile.inc1#L15-L17
+                        // flat base10 format: Mmmmpp
+                        //   M = major
+                        //   m = minor; odd-numbers indicate current dev branch
+                        //   p = patch
+                        const major = value / 100_000;
+                        const minor = value % 100_000 / 100;
+                        const patch = value % 100;
+                        os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch };
+                        os.version_range.semver.max = os.version_range.semver.min;
+                    },
+                    else => unreachable,
+                }
+            },
+            .openbsd => {
+                const mib: [2]c_int = [_]c_int{
+                    std.os.CTL.KERN,
+                    std.os.KERN.OSRELEASE,
+                };
+                var buf: [64]u8 = undefined;
+                var len: usize = buf.len;
+
+                std.os.sysctl(&mib, &buf, &len, null, 0) catch |err| switch (err) {
+                    error.NameTooLong => unreachable, // constant, known good value
+                    error.PermissionDenied => unreachable, // only when setting values,
+                    error.SystemResources => unreachable, // memory already on the stack
+                    error.UnknownName => unreachable, // constant, known good value
+                    error.Unexpected => return error.OSVersionDetectionFail,
+                };
+
+                if (std.builtin.Version.parse(buf[0 .. len - 1])) |ver| {
+                    os.version_range.semver.min = ver;
+                    os.version_range.semver.max = ver;
+                } else |_| {
+                    return error.OSVersionDetectionFail;
+                }
+            },
+            else => {
+                // Unimplemented, fall back to default version range.
+            },
+        }
+    }
+
+    if (cross_target.os_version_min) |min| switch (min) {
+        .none => {},
+        .semver => |semver| switch (cross_target.getOsTag()) {
+            .linux => os.version_range.linux.range.min = semver,
+            else => os.version_range.semver.min = semver,
+        },
+        .windows => |win_ver| os.version_range.windows.min = win_ver,
+    };
+
+    if (cross_target.os_version_max) |max| switch (max) {
+        .none => {},
+        .semver => |semver| switch (cross_target.getOsTag()) {
+            .linux => os.version_range.linux.range.max = semver,
+            else => os.version_range.semver.max = semver,
+        },
+        .windows => |win_ver| os.version_range.windows.max = win_ver,
+    };
+
+    if (cross_target.glibc_version) |glibc| {
+        assert(cross_target.isGnuLibC());
+        os.version_range.linux.glibc = glibc;
+    }
+
+    // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the
+    // native CPU architecture as being different than the current target), we use this:
+    const cpu_arch = cross_target.getCpuArch();
+
+    var cpu = switch (cross_target.cpu_model) {
+        .native => detectNativeCpuAndFeatures(cpu_arch, os, cross_target),
+        .baseline => Target.Cpu.baseline(cpu_arch),
+        .determined_by_cpu_arch => if (cross_target.cpu_arch == null)
+            detectNativeCpuAndFeatures(cpu_arch, os, cross_target)
+        else
+            Target.Cpu.baseline(cpu_arch),
+        .explicit => |model| model.toCpu(cpu_arch),
+    } orelse backup_cpu_detection: {
+        break :backup_cpu_detection Target.Cpu.baseline(cpu_arch);
+    };
+    var result = try detectAbiAndDynamicLinker(allocator, cpu, os, cross_target);
+    // For x86, we need to populate some CPU feature flags depending on architecture
+    // and mode:
+    //  * 16bit_mode => if the abi is code16
+    //  * 32bit_mode => if the arch is i386
+    // However, the "mode" flags can be used as overrides, so if the user explicitly
+    // sets one of them, that takes precedence.
+    switch (cpu_arch) {
+        .i386 => {
+            if (!std.Target.x86.featureSetHasAny(cross_target.cpu_features_add, .{
+                .@"16bit_mode", .@"32bit_mode",
+            })) {
+                switch (result.target.abi) {
+                    .code16 => result.target.cpu.features.addFeature(
+                        @enumToInt(std.Target.x86.Feature.@"16bit_mode"),
+                    ),
+                    else => result.target.cpu.features.addFeature(
+                        @enumToInt(std.Target.x86.Feature.@"32bit_mode"),
+                    ),
+                }
+            }
+        },
+        .arm, .armeb => {
+            // XXX What do we do if the target has the noarm feature?
+            //     What do we do if the user specifies +thumb_mode?
+        },
+        .thumb, .thumbeb => {
+            result.target.cpu.features.addFeature(
+                @enumToInt(std.Target.arm.Feature.thumb_mode),
+            );
+        },
+        else => {},
+    }
+    cross_target.updateCpuFeatures(&result.target.cpu.features);
+    return result;
+}
+
+/// First we attempt to use the executable's own binary. If it is dynamically
+/// linked, then it should answer both the C ABI question and the dynamic linker question.
+/// If it is statically linked, then we try /usr/bin/env. If that does not provide the answer, then
+/// we fall back to the defaults.
+/// TODO Remove the Allocator requirement from this function.
+fn detectAbiAndDynamicLinker(
+    allocator: Allocator,
+    cpu: Target.Cpu,
+    os: Target.Os,
+    cross_target: CrossTarget,
+) DetectError!NativeTargetInfo {
+    const native_target_has_ld = comptime builtin.target.hasDynamicLinker();
+    const is_linux = builtin.target.os.tag == .linux;
+    const have_all_info = cross_target.dynamic_linker.get() != null and
+        cross_target.abi != null and (!is_linux or cross_target.abi.?.isGnu());
+    const os_is_non_native = cross_target.os_tag != null;
+    if (!native_target_has_ld or have_all_info or os_is_non_native) {
+        return defaultAbiAndDynamicLinker(cpu, os, cross_target);
+    }
+    if (cross_target.abi) |abi| {
+        if (abi.isMusl()) {
+            // musl implies static linking.
+            return defaultAbiAndDynamicLinker(cpu, os, cross_target);
+        }
+    }
+    // The current target's ABI cannot be relied on for this. For example, we may build the zig
+    // compiler for target riscv64-linux-musl and provide a tarball for users to download.
+    // A user could then run that zig compiler on riscv64-linux-gnu. This use case is well-defined
+    // and supported by Zig. But that means that we must detect the system ABI here rather than
+    // relying on `builtin.target`.
+    const all_abis = comptime blk: {
+        assert(@enumToInt(Target.Abi.none) == 0);
+        const fields = std.meta.fields(Target.Abi)[1..];
+        var array: [fields.len]Target.Abi = undefined;
+        inline for (fields) |field, i| {
+            array[i] = @field(Target.Abi, field.name);
+        }
+        break :blk array;
+    };
+    var ld_info_list_buffer: [all_abis.len]LdInfo = undefined;
+    var ld_info_list_len: usize = 0;
+
+    for (all_abis) |abi| {
+        // This may be a nonsensical parameter. We detect this with error.UnknownDynamicLinkerPath and
+        // skip adding it to `ld_info_list`.
+        const target: Target = .{
+            .cpu = cpu,
+            .os = os,
+            .abi = abi,
+        };
+        const ld = target.standardDynamicLinkerPath();
+        if (ld.get() == null) continue;
+
+        ld_info_list_buffer[ld_info_list_len] = .{
+            .ld = ld,
+            .abi = abi,
+        };
+        ld_info_list_len += 1;
+    }
+    const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];
+
+    // Best case scenario: the executable is dynamically linked, and we can iterate
+    // over our own shared objects and find a dynamic linker.
+    self_exe: {
+        const lib_paths = try std.process.getSelfExeSharedLibPaths(allocator);
+        defer {
+            for (lib_paths) |lib_path| {
+                allocator.free(lib_path);
+            }
+            allocator.free(lib_paths);
+        }
+
+        var found_ld_info: LdInfo = undefined;
+        var found_ld_path: [:0]const u8 = undefined;
+
+        // Look for dynamic linker.
+        // This is O(N^M) but typical case here is N=2 and M=10.
+        find_ld: for (lib_paths) |lib_path| {
+            for (ld_info_list) |ld_info| {
+                const standard_ld_basename = fs.path.basename(ld_info.ld.get().?);
+                if (std.mem.endsWith(u8, lib_path, standard_ld_basename)) {
+                    found_ld_info = ld_info;
+                    found_ld_path = lib_path;
+                    break :find_ld;
+                }
+            }
+        } else break :self_exe;
+
+        // Look for glibc version.
+        var os_adjusted = os;
+        if (builtin.target.os.tag == .linux and found_ld_info.abi.isGnu() and
+            cross_target.glibc_version == null)
+        {
+            for (lib_paths) |lib_path| {
+                if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) {
+                    os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) {
+                        error.UnrecognizedGnuLibCFileName => continue,
+                        error.InvalidGnuLibCVersion => continue,
+                        error.GnuLibCVersionUnavailable => continue,
+                        else => |e| return e,
+                    };
+                    break;
+                }
+            }
+        }
+
+        var result: NativeTargetInfo = .{
+            .target = .{
+                .cpu = cpu,
+                .os = os_adjusted,
+                .abi = cross_target.abi orelse found_ld_info.abi,
+            },
+            .dynamic_linker = if (cross_target.dynamic_linker.get() == null)
+                DynamicLinker.init(found_ld_path)
+            else
+                cross_target.dynamic_linker,
+        };
+        return result;
+    }
+
+    const env_file = std.fs.openFileAbsoluteZ("/usr/bin/env", .{}) catch |err| switch (err) {
+        error.NoSpaceLeft => unreachable,
+        error.NameTooLong => unreachable,
+        error.PathAlreadyExists => unreachable,
+        error.SharingViolation => unreachable,
+        error.InvalidUtf8 => unreachable,
+        error.BadPathName => unreachable,
+        error.PipeBusy => unreachable,
+        error.FileLocksNotSupported => unreachable,
+        error.WouldBlock => unreachable,
+
+        error.IsDir,
+        error.NotDir,
+        error.AccessDenied,
+        error.NoDevice,
+        error.FileNotFound,
+        error.FileTooBig,
+        error.Unexpected,
+        => return defaultAbiAndDynamicLinker(cpu, os, cross_target),
+
+        else => |e| return e,
+    };
+    defer env_file.close();
+
+    // If Zig is statically linked, such as via distributed binary static builds, the above
+    // trick won't work. The next thing we fall back to is the same thing, but for /usr/bin/env.
+    // Since that path is hard-coded into the shebang line of many portable scripts, it's a
+    // reasonably reliable path to check for.
+    return abiAndDynamicLinkerFromFile(env_file, cpu, os, ld_info_list, cross_target) catch |err| switch (err) {
+        error.FileSystem,
+        error.SystemResources,
+        error.SymLinkLoop,
+        error.ProcessFdQuotaExceeded,
+        error.SystemFdQuotaExceeded,
+        => |e| return e,
+
+        error.UnableToReadElfFile,
+        error.InvalidElfClass,
+        error.InvalidElfVersion,
+        error.InvalidElfEndian,
+        error.InvalidElfFile,
+        error.InvalidElfMagic,
+        error.Unexpected,
+        error.UnexpectedEndOfFile,
+        error.NameTooLong,
+        // Finally, we fall back on the standard path.
+        => defaultAbiAndDynamicLinker(cpu, os, cross_target),
+    };
+}
+
+const glibc_so_basename = "libc.so.6";
+
+fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version {
+    var link_buf: [std.os.PATH_MAX]u8 = undefined;
+    const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) {
+        error.AccessDenied => return error.GnuLibCVersionUnavailable,
+        error.FileSystem => return error.FileSystem,
+        error.SymLinkLoop => return error.SymLinkLoop,
+        error.NameTooLong => unreachable,
+        error.NotLink => return error.GnuLibCVersionUnavailable,
+        error.FileNotFound => return error.GnuLibCVersionUnavailable,
+        error.SystemResources => return error.SystemResources,
+        error.NotDir => return error.GnuLibCVersionUnavailable,
+        error.Unexpected => return error.GnuLibCVersionUnavailable,
+        error.InvalidUtf8 => unreachable, // Windows only
+        error.BadPathName => unreachable, // Windows only
+        error.UnsupportedReparsePointType => unreachable, // Windows only
+    };
+    return glibcVerFromLinkName(link_name);
+}
+
+fn glibcVerFromLinkName(link_name: []const u8) !std.builtin.Version {
+    // example: "libc-2.3.4.so"
+    // example: "libc-2.27.so"
+    const prefix = "libc-";
+    const suffix = ".so";
+    if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) {
+        return error.UnrecognizedGnuLibCFileName;
+    }
+    // chop off "libc-" and ".so"
+    const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len];
+    return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) {
+        error.Overflow => return error.InvalidGnuLibCVersion,
+        error.InvalidCharacter => return error.InvalidGnuLibCVersion,
+        error.InvalidVersion => return error.InvalidGnuLibCVersion,
+    };
+}
+
+pub const AbiAndDynamicLinkerFromFileError = error{
+    FileSystem,
+    SystemResources,
+    SymLinkLoop,
+    ProcessFdQuotaExceeded,
+    SystemFdQuotaExceeded,
+    UnableToReadElfFile,
+    InvalidElfClass,
+    InvalidElfVersion,
+    InvalidElfEndian,
+    InvalidElfFile,
+    InvalidElfMagic,
+    Unexpected,
+    UnexpectedEndOfFile,
+    NameTooLong,
+};
+
+pub fn abiAndDynamicLinkerFromFile(
+    file: fs.File,
+    cpu: Target.Cpu,
+    os: Target.Os,
+    ld_info_list: []const LdInfo,
+    cross_target: CrossTarget,
+) AbiAndDynamicLinkerFromFileError!NativeTargetInfo {
+    var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined;
+    _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len);
+    const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf);
+    const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf);
+    if (!mem.eql(u8, hdr32.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic;
+    const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) {
+        elf.ELFDATA2LSB => .Little,
+        elf.ELFDATA2MSB => .Big,
+        else => return error.InvalidElfEndian,
+    };
+    const need_bswap = elf_endian != native_endian;
+    if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
+
+    const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) {
+        elf.ELFCLASS32 => false,
+        elf.ELFCLASS64 => true,
+        else => return error.InvalidElfClass,
+    };
+    var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff);
+    const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize);
+    const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum);
+
+    var result: NativeTargetInfo = .{
+        .target = .{
+            .cpu = cpu,
+            .os = os,
+            .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os),
+        },
+        .dynamic_linker = cross_target.dynamic_linker,
+    };
+    var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC
+    const look_for_ld = cross_target.dynamic_linker.get() == null;
+
+    var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined;
+    if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile;
+
+    var ph_i: u16 = 0;
+    while (ph_i < phnum) {
+        // Reserve some bytes so that we can deref the 64-bit struct fields
+        // even when the ELF file is 32-bits.
+        const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr);
+        const ph_read_byte_len = try preadMin(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize);
+        var ph_buf_i: usize = 0;
+        while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({
+            ph_i += 1;
+            phoff += phentsize;
+            ph_buf_i += phentsize;
+        }) {
+            const ph32 = @ptrCast(*elf.Elf32_Phdr, @alignCast(@alignOf(elf.Elf32_Phdr), &ph_buf[ph_buf_i]));
+            const ph64 = @ptrCast(*elf.Elf64_Phdr, @alignCast(@alignOf(elf.Elf64_Phdr), &ph_buf[ph_buf_i]));
+            const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type);
+            switch (p_type) {
+                elf.PT_INTERP => if (look_for_ld) {
+                    const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset);
+                    const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz);
+                    if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong;
+                    const filesz = @intCast(usize, p_filesz);
+                    _ = try preadMin(file, result.dynamic_linker.buffer[0..filesz], p_offset, filesz);
+                    // PT_INTERP includes a null byte in filesz.
+                    const len = filesz - 1;
+                    // dynamic_linker.max_byte is "max", not "len".
+                    // We know it will fit in u8 because we check against dynamic_linker.buffer.len above.
+                    result.dynamic_linker.max_byte = @intCast(u8, len - 1);
+
+                    // Use it to determine ABI.
+                    const full_ld_path = result.dynamic_linker.buffer[0..len];
+                    for (ld_info_list) |ld_info| {
+                        const standard_ld_basename = fs.path.basename(ld_info.ld.get().?);
+                        if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) {
+                            result.target.abi = ld_info.abi;
+                            break;
+                        }
+                    }
+                },
+                // We only need this for detecting glibc version.
+                elf.PT_DYNAMIC => if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and
+                    cross_target.glibc_version == null)
+                {
+                    var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset);
+                    const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz);
+                    const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn);
+                    const dyn_num = p_filesz / dyn_size;
+                    var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined;
+                    var dyn_i: usize = 0;
+                    dyn: while (dyn_i < dyn_num) {
+                        // Reserve some bytes so that we can deref the 64-bit struct fields
+                        // even when the ELF file is 32-bits.
+                        const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn);
+                        const dyn_read_byte_len = try preadMin(
+                            file,
+                            dyn_buf[0 .. dyn_buf.len - dyn_reserve],
+                            dyn_off,
+                            dyn_size,
+                        );
+                        var dyn_buf_i: usize = 0;
+                        while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({
+                            dyn_i += 1;
+                            dyn_off += dyn_size;
+                            dyn_buf_i += dyn_size;
+                        }) {
+                            const dyn32 = @ptrCast(
+                                *elf.Elf32_Dyn,
+                                @alignCast(@alignOf(elf.Elf32_Dyn), &dyn_buf[dyn_buf_i]),
+                            );
+                            const dyn64 = @ptrCast(
+                                *elf.Elf64_Dyn,
+                                @alignCast(@alignOf(elf.Elf64_Dyn), &dyn_buf[dyn_buf_i]),
+                            );
+                            const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag);
+                            const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val);
+                            if (tag == elf.DT_RUNPATH) {
+                                rpath_offset = val;
+                                break :dyn;
+                            }
+                        }
+                    }
+                },
+                else => continue,
+            }
+        }
+    }
+
+    if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and cross_target.glibc_version == null) {
+        if (rpath_offset) |rpoff| {
+            const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx);
+
+            var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff);
+            const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize);
+            const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx);
+
+            var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined;
+            if (sh_buf.len < shentsize) return error.InvalidElfFile;
+
+            _ = try preadMin(file, &sh_buf, str_section_off, shentsize);
+            const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf));
+            const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf));
+            const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset);
+            const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size);
+            var strtab_buf: [4096:0]u8 = undefined;
+            const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len);
+            const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len);
+            const shstrtab = strtab_buf[0..shstrtab_read_len];
+
+            const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum);
+            var sh_i: u16 = 0;
+            const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) {
+                // Reserve some bytes so that we can deref the 64-bit struct fields
+                // even when the ELF file is 32-bits.
+                const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr);
+                const sh_read_byte_len = try preadMin(
+                    file,
+                    sh_buf[0 .. sh_buf.len - sh_reserve],
+                    shoff,
+                    shentsize,
+                );
+                var sh_buf_i: usize = 0;
+                while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({
+                    sh_i += 1;
+                    shoff += shentsize;
+                    sh_buf_i += shentsize;
+                }) {
+                    const sh32 = @ptrCast(
+                        *elf.Elf32_Shdr,
+                        @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]),
+                    );
+                    const sh64 = @ptrCast(
+                        *elf.Elf64_Shdr,
+                        @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]),
+                    );
+                    const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
+                    // TODO this pointer cast should not be necessary
+                    const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0);
+                    if (mem.eql(u8, sh_name, ".dynstr")) {
+                        break :find_dyn_str .{
+                            .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset),
+                            .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size),
+                        };
+                    }
+                }
+            } else null;
+
+            if (dynstr) |ds| {
+                const strtab_len = std.math.min(ds.size, strtab_buf.len);
+                const strtab_read_len = try preadMin(file, &strtab_buf, ds.offset, strtab_len);
+                const strtab = strtab_buf[0..strtab_read_len];
+                // TODO this pointer cast should not be necessary
+                const rpoff_usize = std.math.cast(usize, rpoff) catch |err| switch (err) {
+                    error.Overflow => return error.InvalidElfFile,
+                };
+                const rpath_list = mem.sliceTo(std.meta.assumeSentinel(strtab[rpoff_usize..].ptr, 0), 0);
+                var it = mem.tokenize(u8, rpath_list, ":");
+                while (it.next()) |rpath| {
+                    var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) {
+                        error.NameTooLong => unreachable,
+                        error.InvalidUtf8 => unreachable,
+                        error.BadPathName => unreachable,
+                        error.DeviceBusy => unreachable,
+
+                        error.FileNotFound,
+                        error.NotDir,
+                        error.AccessDenied,
+                        error.NoDevice,
+                        => continue,
+
+                        error.ProcessFdQuotaExceeded,
+                        error.SystemFdQuotaExceeded,
+                        error.SystemResources,
+                        error.SymLinkLoop,
+                        error.Unexpected,
+                        => |e| return e,
+                    };
+                    defer dir.close();
+
+                    var link_buf: [std.os.PATH_MAX]u8 = undefined;
+                    const link_name = std.os.readlinkatZ(
+                        dir.fd,
+                        glibc_so_basename,
+                        &link_buf,
+                    ) catch |err| switch (err) {
+                        error.NameTooLong => unreachable,
+                        error.InvalidUtf8 => unreachable, // Windows only
+                        error.BadPathName => unreachable, // Windows only
+                        error.UnsupportedReparsePointType => unreachable, // Windows only
+
+                        error.AccessDenied,
+                        error.FileNotFound,
+                        error.NotLink,
+                        error.NotDir,
+                        => continue,
+
+                        error.SystemResources,
+                        error.FileSystem,
+                        error.SymLinkLoop,
+                        error.Unexpected,
+                        => |e| return e,
+                    };
+                    result.target.os.version_range.linux.glibc = glibcVerFromLinkName(
+                        link_name,
+                    ) catch |err| switch (err) {
+                        error.UnrecognizedGnuLibCFileName,
+                        error.InvalidGnuLibCVersion,
+                        => continue,
+                    };
+                    break;
+                }
+            }
+        }
+    }
+
+    return result;
+}
+
+fn preadMin(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize {
+    var i: usize = 0;
+    while (i < min_read_len) {
+        const len = file.pread(buf[i..], offset + i) catch |err| switch (err) {
+            error.OperationAborted => unreachable, // Windows-only
+            error.WouldBlock => unreachable, // Did not request blocking mode
+            error.NotOpenForReading => unreachable,
+            error.SystemResources => return error.SystemResources,
+            error.IsDir => return error.UnableToReadElfFile,
+            error.BrokenPipe => return error.UnableToReadElfFile,
+            error.Unseekable => return error.UnableToReadElfFile,
+            error.ConnectionResetByPeer => return error.UnableToReadElfFile,
+            error.ConnectionTimedOut => return error.UnableToReadElfFile,
+            error.Unexpected => return error.Unexpected,
+            error.InputOutput => return error.FileSystem,
+            error.AccessDenied => return error.Unexpected,
+        };
+        if (len == 0) return error.UnexpectedEndOfFile;
+        i += len;
+    }
+    return i;
+}
+
+fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, cross_target: CrossTarget) !NativeTargetInfo {
+    const target: Target = .{
+        .cpu = cpu,
+        .os = os,
+        .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os),
+    };
+    return NativeTargetInfo{
+        .target = target,
+        .dynamic_linker = if (cross_target.dynamic_linker.get() == null)
+            target.standardDynamicLinkerPath()
+        else
+            cross_target.dynamic_linker,
+    };
+}
+
+pub const LdInfo = struct {
+    ld: DynamicLinker,
+    abi: Target.Abi,
+};
+
+pub fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) {
+    if (is_64) {
+        if (need_bswap) {
+            return @byteSwap(@TypeOf(int_64), int_64);
+        } else {
+            return int_64;
+        }
+    } else {
+        if (need_bswap) {
+            return @byteSwap(@TypeOf(int_32), int_32);
+        } else {
+            return int_32;
+        }
+    }
+}
+
+fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, cross_target: CrossTarget) ?Target.Cpu {
+    // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`,
+    // although it is a runtime value, is guaranteed to be one of the architectures in the set
+    // of the respective switch prong.
+    switch (builtin.cpu.arch) {
+        .x86_64, .i386 => {
+            return @import("x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, cross_target);
+        },
+        else => {},
+    }
+
+    switch (builtin.os.tag) {
+        .linux => return linux.detectNativeCpuAndFeatures(),
+        .macos => return darwin.macos.detectNativeCpuAndFeatures(),
+        else => {},
+    }
+
+    // This architecture does not have CPU model & feature detection yet.
+    // See https://github.com/ziglang/zig/issues/4591
+    return null;
+}
+
+pub const Executor = union(enum) {
+    native,
+    rosetta,
+    qemu: []const u8,
+    wine: []const u8,
+    wasmtime: []const u8,
+    darling: []const u8,
+    bad_dl: []const u8,
+    bad_os_or_cpu,
+};
+
+pub const GetExternalExecutorOptions = struct {
+    allow_darling: bool = true,
+    allow_qemu: bool = true,
+    allow_rosetta: bool = true,
+    allow_wasmtime: bool = true,
+    allow_wine: bool = true,
+    qemu_fixes_dl: bool = false,
+    link_libc: bool = false,
+};
+
+/// Return whether or not the given host target is capable of executing natively executables
+/// of the other target.
+pub fn getExternalExecutor(
+    host: NativeTargetInfo,
+    candidate: NativeTargetInfo,
+    options: GetExternalExecutorOptions,
+) Executor {
+    const os_match = host.target.os.tag == candidate.target.os.tag;
+    const cpu_ok = cpu_ok: {
+        if (host.target.cpu.arch == candidate.target.cpu.arch)
+            break :cpu_ok true;
+
+        if (host.target.cpu.arch == .x86_64 and candidate.target.cpu.arch == .i386)
+            break :cpu_ok true;
+
+        if (host.target.cpu.arch == .aarch64 and candidate.target.cpu.arch == .arm)
+            break :cpu_ok true;
+
+        if (host.target.cpu.arch == .aarch64_be and candidate.target.cpu.arch == .armeb)
+            break :cpu_ok true;
+
+        // TODO additionally detect incompatible CPU features.
+        // Note that in some cases the OS kernel will emulate missing CPU features
+        // when an illegal instruction is encountered.
+
+        break :cpu_ok false;
+    };
+
+    var bad_result: Executor = .bad_os_or_cpu;
+
+    if (os_match and cpu_ok) native: {
+        if (options.link_libc) {
+            if (candidate.dynamic_linker.get()) |candidate_dl| {
+                fs.cwd().access(candidate_dl, .{}) catch {
+                    bad_result = .{ .bad_dl = candidate_dl };
+                    break :native;
+                };
+            }
+        }
+        return .native;
+    }
+
+    // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2
+    // to emulate the foreign architecture.
+    if (options.allow_rosetta and os_match and
+        host.target.os.tag == .macos and host.target.cpu.arch == .aarch64)
+    {
+        switch (candidate.target.cpu.arch) {
+            .x86_64 => return .rosetta,
+            else => return bad_result,
+        }
+    }
+
+    // If the OS matches, we can use QEMU to emulate a foreign architecture.
+    if (options.allow_qemu and os_match and (!cpu_ok or options.qemu_fixes_dl)) {
+        return switch (candidate.target.cpu.arch) {
+            .aarch64 => Executor{ .qemu = "qemu-aarch64" },
+            .aarch64_be => Executor{ .qemu = "qemu-aarch64_be" },
+            .arm => Executor{ .qemu = "qemu-arm" },
+            .armeb => Executor{ .qemu = "qemu-armeb" },
+            .hexagon => Executor{ .qemu = "qemu-hexagon" },
+            .i386 => Executor{ .qemu = "qemu-i386" },
+            .m68k => Executor{ .qemu = "qemu-m68k" },
+            .mips => Executor{ .qemu = "qemu-mips" },
+            .mipsel => Executor{ .qemu = "qemu-mipsel" },
+            .mips64 => Executor{ .qemu = "qemu-mips64" },
+            .mips64el => Executor{ .qemu = "qemu-mips64el" },
+            .powerpc => Executor{ .qemu = "qemu-ppc" },
+            .powerpc64 => Executor{ .qemu = "qemu-ppc64" },
+            .powerpc64le => Executor{ .qemu = "qemu-ppc64le" },
+            .riscv32 => Executor{ .qemu = "qemu-riscv32" },
+            .riscv64 => Executor{ .qemu = "qemu-riscv64" },
+            .s390x => Executor{ .qemu = "qemu-s390x" },
+            .sparc => Executor{ .qemu = "qemu-sparc" },
+            .x86_64 => Executor{ .qemu = "qemu-x86_64" },
+            else => return bad_result,
+        };
+    }
+
+    switch (candidate.target.os.tag) {
+        .windows => {
+            if (options.allow_wine) {
+                switch (candidate.target.cpu.arch.ptrBitWidth()) {
+                    32 => return Executor{ .wine = "wine" },
+                    64 => return Executor{ .wine = "wine64" },
+                    else => return bad_result,
+                }
+            }
+            return bad_result;
+        },
+        .wasi => {
+            if (options.allow_wasmtime) {
+                switch (candidate.target.cpu.arch.ptrBitWidth()) {
+                    32 => return Executor{ .wasmtime = "wasmtime" },
+                    else => return bad_result,
+                }
+            }
+            return bad_result;
+        },
+        .macos => {
+            if (options.allow_darling) {
+                // This check can be loosened once darling adds a QEMU-based emulation
+                // layer for non-host architectures:
+                // https://github.com/darlinghq/darling/issues/863
+                if (candidate.target.cpu.arch != builtin.cpu.arch) {
+                    return bad_result;
+                }
+                return Executor{ .darling = "darling" };
+            }
+            return bad_result;
+        },
+        else => return bad_result,
+    }
+}
lib/std/zig/CrossTarget.zig
@@ -610,95 +610,6 @@ pub fn vcpkgTriplet(self: CrossTarget, allocator: mem.Allocator, linkage: VcpkgL
     return std.fmt.allocPrint(allocator, "{s}-{s}{s}", .{ arch, os, static_suffix });
 }
 
-pub const Executor = union(enum) {
-    native,
-    rosetta,
-    qemu: []const u8,
-    wine: []const u8,
-    wasmtime: []const u8,
-    darling: []const u8,
-    unavailable,
-};
-
-/// Note that even a `CrossTarget` which returns `false` for `isNative` could still be natively executed.
-/// For example `-target arm-native` running on an aarch64 host.
-pub fn getExternalExecutor(self: CrossTarget) Executor {
-    const cpu_arch = self.getCpuArch();
-    const os_tag = self.getOsTag();
-    const os_match = os_tag == builtin.os.tag;
-
-    // If the OS and CPU arch match, the binary can be considered native.
-    // TODO additionally match the CPU features. This `getExternalExecutor` function should
-    // be moved to std.Target and match any chosen target against the native target.
-    if (os_match and cpu_arch == builtin.cpu.arch) {
-        // However, we also need to verify that the dynamic linker path is valid.
-        if (self.os_tag == null) {
-            return .native;
-        }
-        // TODO here we call toTarget, a deprecated function, because of the above TODO about moving
-        // this code to std.Target.
-        const opt_dl = self.dynamic_linker.get() orelse self.toTarget().standardDynamicLinkerPath().get();
-        if (opt_dl) |dl| blk: {
-            std.fs.cwd().access(dl, .{}) catch break :blk;
-            return .native;
-        }
-    }
-    // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2
-    // to emulate the foreign architecture.
-    if (os_match and os_tag == .macos and builtin.cpu.arch == .aarch64) {
-        return switch (cpu_arch) {
-            .x86_64 => .rosetta,
-            else => .unavailable,
-        };
-    }
-
-    // If the OS matches, we can use QEMU to emulate a foreign architecture.
-    if (os_match) {
-        return switch (cpu_arch) {
-            .aarch64 => Executor{ .qemu = "qemu-aarch64" },
-            .aarch64_be => Executor{ .qemu = "qemu-aarch64_be" },
-            .arm => Executor{ .qemu = "qemu-arm" },
-            .armeb => Executor{ .qemu = "qemu-armeb" },
-            .i386 => Executor{ .qemu = "qemu-i386" },
-            .mips => Executor{ .qemu = "qemu-mips" },
-            .mipsel => Executor{ .qemu = "qemu-mipsel" },
-            .mips64 => Executor{ .qemu = "qemu-mips64" },
-            .mips64el => Executor{ .qemu = "qemu-mips64el" },
-            .powerpc => Executor{ .qemu = "qemu-ppc" },
-            .powerpc64 => Executor{ .qemu = "qemu-ppc64" },
-            .powerpc64le => Executor{ .qemu = "qemu-ppc64le" },
-            .riscv32 => Executor{ .qemu = "qemu-riscv32" },
-            .riscv64 => Executor{ .qemu = "qemu-riscv64" },
-            .s390x => Executor{ .qemu = "qemu-s390x" },
-            .sparc => Executor{ .qemu = "qemu-sparc" },
-            .x86_64 => Executor{ .qemu = "qemu-x86_64" },
-            else => return .unavailable,
-        };
-    }
-
-    switch (os_tag) {
-        .windows => switch (cpu_arch.ptrBitWidth()) {
-            32 => return Executor{ .wine = "wine" },
-            64 => return Executor{ .wine = "wine64" },
-            else => return .unavailable,
-        },
-        .wasi => switch (cpu_arch.ptrBitWidth()) {
-            32 => return Executor{ .wasmtime = "wasmtime" },
-            else => return .unavailable,
-        },
-        .macos => {
-            // TODO loosen this check once upstream adds QEMU-based emulation
-            // layer for non-host architectures:
-            // https://github.com/darlinghq/darling/issues/863
-            if (cpu_arch != builtin.cpu.arch) {
-                return .unavailable;
-            }
-            return Executor{ .darling = "darling" };
-        },
-        else => return .unavailable,
-    }
-}
-
 pub fn isGnuLibC(self: CrossTarget) bool {
     return Target.isGnuLibC_os_tag_abi(self.getOsTag(), self.getAbi());
 }
lib/std/zig/system.zig
@@ -1,1007 +1,15 @@
-const std = @import("../std.zig");
-const builtin = @import("builtin");
-const elf = std.elf;
-const mem = std.mem;
-const fs = std.fs;
-const Allocator = std.mem.Allocator;
-const ArrayList = std.ArrayList;
-const assert = std.debug.assert;
-const process = std.process;
-const Target = std.Target;
-const CrossTarget = std.zig.CrossTarget;
-const native_endian = builtin.cpu.arch.endian();
-const linux = @import("system/linux.zig");
+pub const NativePaths = @import("system/NativePaths.zig");
+pub const NativeTargetInfo = @import("system/NativeTargetInfo.zig");
+
 pub const windows = @import("system/windows.zig");
 pub const darwin = @import("system/darwin.zig");
-
-pub const NativePaths = struct {
-    include_dirs: ArrayList([:0]u8),
-    lib_dirs: ArrayList([:0]u8),
-    framework_dirs: ArrayList([:0]u8),
-    rpaths: ArrayList([:0]u8),
-    warnings: ArrayList([:0]u8),
-
-    pub fn detect(allocator: Allocator, native_info: NativeTargetInfo) !NativePaths {
-        const native_target = native_info.target;
-
-        var self: NativePaths = .{
-            .include_dirs = ArrayList([:0]u8).init(allocator),
-            .lib_dirs = ArrayList([:0]u8).init(allocator),
-            .framework_dirs = ArrayList([:0]u8).init(allocator),
-            .rpaths = ArrayList([:0]u8).init(allocator),
-            .warnings = ArrayList([:0]u8).init(allocator),
-        };
-        errdefer self.deinit();
-
-        var is_nix = false;
-        if (process.getEnvVarOwned(allocator, "NIX_CFLAGS_COMPILE")) |nix_cflags_compile| {
-            defer allocator.free(nix_cflags_compile);
-
-            is_nix = true;
-            var it = mem.tokenize(u8, nix_cflags_compile, " ");
-            while (true) {
-                const word = it.next() orelse break;
-                if (mem.eql(u8, word, "-isystem")) {
-                    const include_path = it.next() orelse {
-                        try self.addWarning("Expected argument after -isystem in NIX_CFLAGS_COMPILE");
-                        break;
-                    };
-                    try self.addIncludeDir(include_path);
-                } else {
-                    if (mem.startsWith(u8, word, "-frandom-seed=")) {
-                        continue;
-                    }
-                    try self.addWarningFmt("Unrecognized C flag from NIX_CFLAGS_COMPILE: {s}", .{word});
-                }
-            }
-        } else |err| switch (err) {
-            error.InvalidUtf8 => {},
-            error.EnvironmentVariableNotFound => {},
-            error.OutOfMemory => |e| return e,
-        }
-        if (process.getEnvVarOwned(allocator, "NIX_LDFLAGS")) |nix_ldflags| {
-            defer allocator.free(nix_ldflags);
-
-            is_nix = true;
-            var it = mem.tokenize(u8, nix_ldflags, " ");
-            while (true) {
-                const word = it.next() orelse break;
-                if (mem.eql(u8, word, "-rpath")) {
-                    const rpath = it.next() orelse {
-                        try self.addWarning("Expected argument after -rpath in NIX_LDFLAGS");
-                        break;
-                    };
-                    try self.addRPath(rpath);
-                } else if (word.len > 2 and word[0] == '-' and word[1] == 'L') {
-                    const lib_path = word[2..];
-                    try self.addLibDir(lib_path);
-                } else {
-                    try self.addWarningFmt("Unrecognized C flag from NIX_LDFLAGS: {s}", .{word});
-                    break;
-                }
-            }
-        } else |err| switch (err) {
-            error.InvalidUtf8 => {},
-            error.EnvironmentVariableNotFound => {},
-            error.OutOfMemory => |e| return e,
-        }
-        if (is_nix) {
-            return self;
-        }
-
-        if (comptime builtin.target.isDarwin()) {
-            try self.addIncludeDir("/usr/include");
-            try self.addIncludeDir("/usr/local/include");
-
-            try self.addLibDir("/usr/lib");
-            try self.addLibDir("/usr/local/lib");
-
-            try self.addFrameworkDir("/Library/Frameworks");
-            try self.addFrameworkDir("/System/Library/Frameworks");
-
-            return self;
-        }
-
-        if (comptime native_target.os.tag == .solaris) {
-            try self.addLibDir("/usr/lib/64");
-            try self.addLibDir("/usr/local/lib/64");
-            try self.addLibDir("/lib/64");
-
-            try self.addIncludeDir("/usr/include");
-            try self.addIncludeDir("/usr/local/include");
-
-            return self;
-        }
-
-        if (native_target.os.tag != .windows) {
-            const triple = try native_target.linuxTriple(allocator);
-            const qual = native_target.cpu.arch.ptrBitWidth();
-
-            // TODO: $ ld --verbose | grep SEARCH_DIR
-            // the output contains some paths that end with lib64, maybe include them too?
-            // TODO: what is the best possible order of things?
-            // TODO: some of these are suspect and should only be added on some systems. audit needed.
-
-            try self.addIncludeDir("/usr/local/include");
-            try self.addLibDirFmt("/usr/local/lib{d}", .{qual});
-            try self.addLibDir("/usr/local/lib");
-
-            try self.addIncludeDirFmt("/usr/include/{s}", .{triple});
-            try self.addLibDirFmt("/usr/lib/{s}", .{triple});
-
-            try self.addIncludeDir("/usr/include");
-            try self.addLibDirFmt("/lib{d}", .{qual});
-            try self.addLibDir("/lib");
-            try self.addLibDirFmt("/usr/lib{d}", .{qual});
-            try self.addLibDir("/usr/lib");
-
-            // example: on a 64-bit debian-based linux distro, with zlib installed from apt:
-            // zlib.h is in /usr/include (added above)
-            // libz.so.1 is in /lib/x86_64-linux-gnu (added here)
-            try self.addLibDirFmt("/lib/{s}", .{triple});
-        }
-
-        return self;
-    }
-
-    pub fn deinit(self: *NativePaths) void {
-        deinitArray(&self.include_dirs);
-        deinitArray(&self.lib_dirs);
-        deinitArray(&self.framework_dirs);
-        deinitArray(&self.rpaths);
-        deinitArray(&self.warnings);
-        self.* = undefined;
-    }
-
-    fn deinitArray(array: *ArrayList([:0]u8)) void {
-        for (array.items) |item| {
-            array.allocator.free(item);
-        }
-        array.deinit();
-    }
-
-    pub fn addIncludeDir(self: *NativePaths, s: []const u8) !void {
-        return self.appendArray(&self.include_dirs, s);
-    }
-
-    pub fn addIncludeDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
-        const item = try std.fmt.allocPrintZ(self.include_dirs.allocator, fmt, args);
-        errdefer self.include_dirs.allocator.free(item);
-        try self.include_dirs.append(item);
-    }
-
-    pub fn addLibDir(self: *NativePaths, s: []const u8) !void {
-        return self.appendArray(&self.lib_dirs, s);
-    }
-
-    pub fn addLibDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
-        const item = try std.fmt.allocPrintZ(self.lib_dirs.allocator, fmt, args);
-        errdefer self.lib_dirs.allocator.free(item);
-        try self.lib_dirs.append(item);
-    }
-
-    pub fn addWarning(self: *NativePaths, s: []const u8) !void {
-        return self.appendArray(&self.warnings, s);
-    }
-
-    pub fn addFrameworkDir(self: *NativePaths, s: []const u8) !void {
-        return self.appendArray(&self.framework_dirs, s);
-    }
-
-    pub fn addFrameworkDirFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
-        const item = try std.fmt.allocPrintZ(self.framework_dirs.allocator, fmt, args);
-        errdefer self.framework_dirs.allocator.free(item);
-        try self.framework_dirs.append(item);
-    }
-
-    pub fn addWarningFmt(self: *NativePaths, comptime fmt: []const u8, args: anytype) !void {
-        const item = try std.fmt.allocPrintZ(self.warnings.allocator, fmt, args);
-        errdefer self.warnings.allocator.free(item);
-        try self.warnings.append(item);
-    }
-
-    pub fn addRPath(self: *NativePaths, s: []const u8) !void {
-        return self.appendArray(&self.rpaths, s);
-    }
-
-    fn appendArray(self: *NativePaths, array: *ArrayList([:0]u8), s: []const u8) !void {
-        _ = self;
-        const item = try array.allocator.dupeZ(u8, s);
-        errdefer array.allocator.free(item);
-        try array.append(item);
-    }
-};
-
-pub const NativeTargetInfo = struct {
-    target: Target,
-
-    dynamic_linker: DynamicLinker = DynamicLinker{},
-
-    pub const DynamicLinker = Target.DynamicLinker;
-
-    pub const DetectError = error{
-        OutOfMemory,
-        FileSystem,
-        SystemResources,
-        SymLinkLoop,
-        ProcessFdQuotaExceeded,
-        SystemFdQuotaExceeded,
-        DeviceBusy,
-        OSVersionDetectionFail,
-    };
-
-    /// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected
-    /// natively, which should be standard or default, and which are provided explicitly, this function
-    /// resolves the native components by detecting the native system, and then resolves standard/default parts
-    /// relative to that.
-    /// Any resources this function allocates are released before returning, and so there is no
-    /// deinitialization method.
-    /// TODO Remove the Allocator requirement from this function.
-    pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!NativeTargetInfo {
-        var os = cross_target.getOsTag().defaultVersionRange(cross_target.getCpuArch());
-        if (cross_target.os_tag == null) {
-            switch (builtin.target.os.tag) {
-                .linux => {
-                    const uts = std.os.uname();
-                    const release = mem.sliceTo(&uts.release, 0);
-                    // The release field sometimes has a weird format,
-                    // `Version.parse` will attempt to find some meaningful interpretation.
-                    if (std.builtin.Version.parse(release)) |ver| {
-                        os.version_range.linux.range.min = ver;
-                        os.version_range.linux.range.max = ver;
-                    } else |err| switch (err) {
-                        error.Overflow => {},
-                        error.InvalidCharacter => {},
-                        error.InvalidVersion => {},
-                    }
-                },
-                .solaris => {
-                    const uts = std.os.uname();
-                    const release = mem.sliceTo(&uts.release, 0);
-                    if (std.builtin.Version.parse(release)) |ver| {
-                        os.version_range.semver.min = ver;
-                        os.version_range.semver.max = ver;
-                    } else |err| switch (err) {
-                        error.Overflow => {},
-                        error.InvalidCharacter => {},
-                        error.InvalidVersion => {},
-                    }
-                },
-                .windows => {
-                    const detected_version = windows.detectRuntimeVersion();
-                    os.version_range.windows.min = detected_version;
-                    os.version_range.windows.max = detected_version;
-                },
-                .macos => try darwin.macos.detect(&os),
-                .freebsd, .netbsd, .dragonfly => {
-                    const key = switch (builtin.target.os.tag) {
-                        .freebsd => "kern.osreldate",
-                        .netbsd, .dragonfly => "kern.osrevision",
-                        else => unreachable,
-                    };
-                    var value: u32 = undefined;
-                    var len: usize = @sizeOf(@TypeOf(value));
-
-                    std.os.sysctlbynameZ(key, &value, &len, null, 0) catch |err| switch (err) {
-                        error.NameTooLong => unreachable, // constant, known good value
-                        error.PermissionDenied => unreachable, // only when setting values,
-                        error.SystemResources => unreachable, // memory already on the stack
-                        error.UnknownName => unreachable, // constant, known good value
-                        error.Unexpected => return error.OSVersionDetectionFail,
-                    };
-
-                    switch (builtin.target.os.tag) {
-                        .freebsd => {
-                            // https://www.freebsd.org/doc/en_US.ISO8859-1/books/porters-handbook/versions.html
-                            // Major * 100,000 has been convention since FreeBSD 2.2 (1997)
-                            // Minor * 1(0),000 summed has been convention since FreeBSD 2.2 (1997)
-                            // e.g. 492101 = 4.11-STABLE = 4.(9+2)
-                            const major = value / 100_000;
-                            const minor1 = value % 100_000 / 10_000; // usually 0 since 5.1
-                            const minor2 = value % 10_000 / 1_000; // 0 before 5.1, minor version since
-                            const patch = value % 1_000;
-                            os.version_range.semver.min = .{ .major = major, .minor = minor1 + minor2, .patch = patch };
-                            os.version_range.semver.max = os.version_range.semver.min;
-                        },
-                        .netbsd => {
-                            // #define __NetBSD_Version__ MMmmrrpp00
-                            //
-                            // M = major version
-                            // m = minor version; a minor number of 99 indicates current.
-                            // r = 0 (*)
-                            // p = patchlevel
-                            const major = value / 100_000_000;
-                            const minor = value % 100_000_000 / 1_000_000;
-                            const patch = value % 10_000 / 100;
-                            os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch };
-                            os.version_range.semver.max = os.version_range.semver.min;
-                        },
-                        .dragonfly => {
-                            // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cb2cde83771754aeef9bb3251ee48959138dec87/Makefile.inc1#L15-L17
-                            // flat base10 format: Mmmmpp
-                            //   M = major
-                            //   m = minor; odd-numbers indicate current dev branch
-                            //   p = patch
-                            const major = value / 100_000;
-                            const minor = value % 100_000 / 100;
-                            const patch = value % 100;
-                            os.version_range.semver.min = .{ .major = major, .minor = minor, .patch = patch };
-                            os.version_range.semver.max = os.version_range.semver.min;
-                        },
-                        else => unreachable,
-                    }
-                },
-                .openbsd => {
-                    const mib: [2]c_int = [_]c_int{
-                        std.os.CTL.KERN,
-                        std.os.KERN.OSRELEASE,
-                    };
-                    var buf: [64]u8 = undefined;
-                    var len: usize = buf.len;
-
-                    std.os.sysctl(&mib, &buf, &len, null, 0) catch |err| switch (err) {
-                        error.NameTooLong => unreachable, // constant, known good value
-                        error.PermissionDenied => unreachable, // only when setting values,
-                        error.SystemResources => unreachable, // memory already on the stack
-                        error.UnknownName => unreachable, // constant, known good value
-                        error.Unexpected => return error.OSVersionDetectionFail,
-                    };
-
-                    if (std.builtin.Version.parse(buf[0 .. len - 1])) |ver| {
-                        os.version_range.semver.min = ver;
-                        os.version_range.semver.max = ver;
-                    } else |_| {
-                        return error.OSVersionDetectionFail;
-                    }
-                },
-                else => {
-                    // Unimplemented, fall back to default version range.
-                },
-            }
-        }
-
-        if (cross_target.os_version_min) |min| switch (min) {
-            .none => {},
-            .semver => |semver| switch (cross_target.getOsTag()) {
-                .linux => os.version_range.linux.range.min = semver,
-                else => os.version_range.semver.min = semver,
-            },
-            .windows => |win_ver| os.version_range.windows.min = win_ver,
-        };
-
-        if (cross_target.os_version_max) |max| switch (max) {
-            .none => {},
-            .semver => |semver| switch (cross_target.getOsTag()) {
-                .linux => os.version_range.linux.range.max = semver,
-                else => os.version_range.semver.max = semver,
-            },
-            .windows => |win_ver| os.version_range.windows.max = win_ver,
-        };
-
-        if (cross_target.glibc_version) |glibc| {
-            assert(cross_target.isGnuLibC());
-            os.version_range.linux.glibc = glibc;
-        }
-
-        // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the
-        // native CPU architecture as being different than the current target), we use this:
-        const cpu_arch = cross_target.getCpuArch();
-
-        var cpu = switch (cross_target.cpu_model) {
-            .native => detectNativeCpuAndFeatures(cpu_arch, os, cross_target),
-            .baseline => Target.Cpu.baseline(cpu_arch),
-            .determined_by_cpu_arch => if (cross_target.cpu_arch == null)
-                detectNativeCpuAndFeatures(cpu_arch, os, cross_target)
-            else
-                Target.Cpu.baseline(cpu_arch),
-            .explicit => |model| model.toCpu(cpu_arch),
-        } orelse backup_cpu_detection: {
-            break :backup_cpu_detection Target.Cpu.baseline(cpu_arch);
-        };
-        var result = try detectAbiAndDynamicLinker(allocator, cpu, os, cross_target);
-        // For x86, we need to populate some CPU feature flags depending on architecture
-        // and mode:
-        //  * 16bit_mode => if the abi is code16
-        //  * 32bit_mode => if the arch is i386
-        // However, the "mode" flags can be used as overrides, so if the user explicitly
-        // sets one of them, that takes precedence.
-        switch (cpu_arch) {
-            .i386 => {
-                if (!std.Target.x86.featureSetHasAny(cross_target.cpu_features_add, .{
-                    .@"16bit_mode", .@"32bit_mode",
-                })) {
-                    switch (result.target.abi) {
-                        .code16 => result.target.cpu.features.addFeature(
-                            @enumToInt(std.Target.x86.Feature.@"16bit_mode"),
-                        ),
-                        else => result.target.cpu.features.addFeature(
-                            @enumToInt(std.Target.x86.Feature.@"32bit_mode"),
-                        ),
-                    }
-                }
-            },
-            .arm, .armeb => {
-                // XXX What do we do if the target has the noarm feature?
-                //     What do we do if the user specifies +thumb_mode?
-            },
-            .thumb, .thumbeb => {
-                result.target.cpu.features.addFeature(
-                    @enumToInt(std.Target.arm.Feature.thumb_mode),
-                );
-            },
-            else => {},
-        }
-        cross_target.updateCpuFeatures(&result.target.cpu.features);
-        return result;
-    }
-
-    /// First we attempt to use the executable's own binary. If it is dynamically
-    /// linked, then it should answer both the C ABI question and the dynamic linker question.
-    /// If it is statically linked, then we try /usr/bin/env. If that does not provide the answer, then
-    /// we fall back to the defaults.
-    /// TODO Remove the Allocator requirement from this function.
-    fn detectAbiAndDynamicLinker(
-        allocator: Allocator,
-        cpu: Target.Cpu,
-        os: Target.Os,
-        cross_target: CrossTarget,
-    ) DetectError!NativeTargetInfo {
-        const native_target_has_ld = comptime builtin.target.hasDynamicLinker();
-        const is_linux = builtin.target.os.tag == .linux;
-        const have_all_info = cross_target.dynamic_linker.get() != null and
-            cross_target.abi != null and (!is_linux or cross_target.abi.?.isGnu());
-        const os_is_non_native = cross_target.os_tag != null;
-        if (!native_target_has_ld or have_all_info or os_is_non_native) {
-            return defaultAbiAndDynamicLinker(cpu, os, cross_target);
-        }
-        if (cross_target.abi) |abi| {
-            if (abi.isMusl()) {
-                // musl implies static linking.
-                return defaultAbiAndDynamicLinker(cpu, os, cross_target);
-            }
-        }
-        // The current target's ABI cannot be relied on for this. For example, we may build the zig
-        // compiler for target riscv64-linux-musl and provide a tarball for users to download.
-        // A user could then run that zig compiler on riscv64-linux-gnu. This use case is well-defined
-        // and supported by Zig. But that means that we must detect the system ABI here rather than
-        // relying on `builtin.target`.
-        const all_abis = comptime blk: {
-            assert(@enumToInt(Target.Abi.none) == 0);
-            const fields = std.meta.fields(Target.Abi)[1..];
-            var array: [fields.len]Target.Abi = undefined;
-            inline for (fields) |field, i| {
-                array[i] = @field(Target.Abi, field.name);
-            }
-            break :blk array;
-        };
-        var ld_info_list_buffer: [all_abis.len]LdInfo = undefined;
-        var ld_info_list_len: usize = 0;
-
-        for (all_abis) |abi| {
-            // This may be a nonsensical parameter. We detect this with error.UnknownDynamicLinkerPath and
-            // skip adding it to `ld_info_list`.
-            const target: Target = .{
-                .cpu = cpu,
-                .os = os,
-                .abi = abi,
-            };
-            const ld = target.standardDynamicLinkerPath();
-            if (ld.get() == null) continue;
-
-            ld_info_list_buffer[ld_info_list_len] = .{
-                .ld = ld,
-                .abi = abi,
-            };
-            ld_info_list_len += 1;
-        }
-        const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];
-
-        // Best case scenario: the executable is dynamically linked, and we can iterate
-        // over our own shared objects and find a dynamic linker.
-        self_exe: {
-            const lib_paths = try std.process.getSelfExeSharedLibPaths(allocator);
-            defer {
-                for (lib_paths) |lib_path| {
-                    allocator.free(lib_path);
-                }
-                allocator.free(lib_paths);
-            }
-
-            var found_ld_info: LdInfo = undefined;
-            var found_ld_path: [:0]const u8 = undefined;
-
-            // Look for dynamic linker.
-            // This is O(N^M) but typical case here is N=2 and M=10.
-            find_ld: for (lib_paths) |lib_path| {
-                for (ld_info_list) |ld_info| {
-                    const standard_ld_basename = fs.path.basename(ld_info.ld.get().?);
-                    if (std.mem.endsWith(u8, lib_path, standard_ld_basename)) {
-                        found_ld_info = ld_info;
-                        found_ld_path = lib_path;
-                        break :find_ld;
-                    }
-                }
-            } else break :self_exe;
-
-            // Look for glibc version.
-            var os_adjusted = os;
-            if (builtin.target.os.tag == .linux and found_ld_info.abi.isGnu() and
-                cross_target.glibc_version == null)
-            {
-                for (lib_paths) |lib_path| {
-                    if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) {
-                        os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) {
-                            error.UnrecognizedGnuLibCFileName => continue,
-                            error.InvalidGnuLibCVersion => continue,
-                            error.GnuLibCVersionUnavailable => continue,
-                            else => |e| return e,
-                        };
-                        break;
-                    }
-                }
-            }
-
-            var result: NativeTargetInfo = .{
-                .target = .{
-                    .cpu = cpu,
-                    .os = os_adjusted,
-                    .abi = cross_target.abi orelse found_ld_info.abi,
-                },
-                .dynamic_linker = if (cross_target.dynamic_linker.get() == null)
-                    DynamicLinker.init(found_ld_path)
-                else
-                    cross_target.dynamic_linker,
-            };
-            return result;
-        }
-
-        const env_file = std.fs.openFileAbsoluteZ("/usr/bin/env", .{}) catch |err| switch (err) {
-            error.NoSpaceLeft => unreachable,
-            error.NameTooLong => unreachable,
-            error.PathAlreadyExists => unreachable,
-            error.SharingViolation => unreachable,
-            error.InvalidUtf8 => unreachable,
-            error.BadPathName => unreachable,
-            error.PipeBusy => unreachable,
-            error.FileLocksNotSupported => unreachable,
-            error.WouldBlock => unreachable,
-
-            error.IsDir,
-            error.NotDir,
-            error.AccessDenied,
-            error.NoDevice,
-            error.FileNotFound,
-            error.FileTooBig,
-            error.Unexpected,
-            => return defaultAbiAndDynamicLinker(cpu, os, cross_target),
-
-            else => |e| return e,
-        };
-        defer env_file.close();
-
-        // If Zig is statically linked, such as via distributed binary static builds, the above
-        // trick won't work. The next thing we fall back to is the same thing, but for /usr/bin/env.
-        // Since that path is hard-coded into the shebang line of many portable scripts, it's a
-        // reasonably reliable path to check for.
-        return abiAndDynamicLinkerFromFile(env_file, cpu, os, ld_info_list, cross_target) catch |err| switch (err) {
-            error.FileSystem,
-            error.SystemResources,
-            error.SymLinkLoop,
-            error.ProcessFdQuotaExceeded,
-            error.SystemFdQuotaExceeded,
-            => |e| return e,
-
-            error.UnableToReadElfFile,
-            error.InvalidElfClass,
-            error.InvalidElfVersion,
-            error.InvalidElfEndian,
-            error.InvalidElfFile,
-            error.InvalidElfMagic,
-            error.Unexpected,
-            error.UnexpectedEndOfFile,
-            error.NameTooLong,
-            // Finally, we fall back on the standard path.
-            => defaultAbiAndDynamicLinker(cpu, os, cross_target),
-        };
-    }
-
-    const glibc_so_basename = "libc.so.6";
-
-    fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version {
-        var link_buf: [std.os.PATH_MAX]u8 = undefined;
-        const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) {
-            error.AccessDenied => return error.GnuLibCVersionUnavailable,
-            error.FileSystem => return error.FileSystem,
-            error.SymLinkLoop => return error.SymLinkLoop,
-            error.NameTooLong => unreachable,
-            error.NotLink => return error.GnuLibCVersionUnavailable,
-            error.FileNotFound => return error.GnuLibCVersionUnavailable,
-            error.SystemResources => return error.SystemResources,
-            error.NotDir => return error.GnuLibCVersionUnavailable,
-            error.Unexpected => return error.GnuLibCVersionUnavailable,
-            error.InvalidUtf8 => unreachable, // Windows only
-            error.BadPathName => unreachable, // Windows only
-            error.UnsupportedReparsePointType => unreachable, // Windows only
-        };
-        return glibcVerFromLinkName(link_name);
-    }
-
-    fn glibcVerFromLinkName(link_name: []const u8) !std.builtin.Version {
-        // example: "libc-2.3.4.so"
-        // example: "libc-2.27.so"
-        const prefix = "libc-";
-        const suffix = ".so";
-        if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) {
-            return error.UnrecognizedGnuLibCFileName;
-        }
-        // chop off "libc-" and ".so"
-        const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len];
-        return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) {
-            error.Overflow => return error.InvalidGnuLibCVersion,
-            error.InvalidCharacter => return error.InvalidGnuLibCVersion,
-            error.InvalidVersion => return error.InvalidGnuLibCVersion,
-        };
-    }
-
-    pub const AbiAndDynamicLinkerFromFileError = error{
-        FileSystem,
-        SystemResources,
-        SymLinkLoop,
-        ProcessFdQuotaExceeded,
-        SystemFdQuotaExceeded,
-        UnableToReadElfFile,
-        InvalidElfClass,
-        InvalidElfVersion,
-        InvalidElfEndian,
-        InvalidElfFile,
-        InvalidElfMagic,
-        Unexpected,
-        UnexpectedEndOfFile,
-        NameTooLong,
-    };
-
-    pub fn abiAndDynamicLinkerFromFile(
-        file: fs.File,
-        cpu: Target.Cpu,
-        os: Target.Os,
-        ld_info_list: []const LdInfo,
-        cross_target: CrossTarget,
-    ) AbiAndDynamicLinkerFromFileError!NativeTargetInfo {
-        var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined;
-        _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len);
-        const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf);
-        const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf);
-        if (!mem.eql(u8, hdr32.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic;
-        const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) {
-            elf.ELFDATA2LSB => .Little,
-            elf.ELFDATA2MSB => .Big,
-            else => return error.InvalidElfEndian,
-        };
-        const need_bswap = elf_endian != native_endian;
-        if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
-
-        const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) {
-            elf.ELFCLASS32 => false,
-            elf.ELFCLASS64 => true,
-            else => return error.InvalidElfClass,
-        };
-        var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff);
-        const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize);
-        const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum);
-
-        var result: NativeTargetInfo = .{
-            .target = .{
-                .cpu = cpu,
-                .os = os,
-                .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os),
-            },
-            .dynamic_linker = cross_target.dynamic_linker,
-        };
-        var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC
-        const look_for_ld = cross_target.dynamic_linker.get() == null;
-
-        var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined;
-        if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile;
-
-        var ph_i: u16 = 0;
-        while (ph_i < phnum) {
-            // Reserve some bytes so that we can deref the 64-bit struct fields
-            // even when the ELF file is 32-bits.
-            const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr);
-            const ph_read_byte_len = try preadMin(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize);
-            var ph_buf_i: usize = 0;
-            while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({
-                ph_i += 1;
-                phoff += phentsize;
-                ph_buf_i += phentsize;
-            }) {
-                const ph32 = @ptrCast(*elf.Elf32_Phdr, @alignCast(@alignOf(elf.Elf32_Phdr), &ph_buf[ph_buf_i]));
-                const ph64 = @ptrCast(*elf.Elf64_Phdr, @alignCast(@alignOf(elf.Elf64_Phdr), &ph_buf[ph_buf_i]));
-                const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type);
-                switch (p_type) {
-                    elf.PT_INTERP => if (look_for_ld) {
-                        const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset);
-                        const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz);
-                        if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong;
-                        const filesz = @intCast(usize, p_filesz);
-                        _ = try preadMin(file, result.dynamic_linker.buffer[0..filesz], p_offset, filesz);
-                        // PT_INTERP includes a null byte in filesz.
-                        const len = filesz - 1;
-                        // dynamic_linker.max_byte is "max", not "len".
-                        // We know it will fit in u8 because we check against dynamic_linker.buffer.len above.
-                        result.dynamic_linker.max_byte = @intCast(u8, len - 1);
-
-                        // Use it to determine ABI.
-                        const full_ld_path = result.dynamic_linker.buffer[0..len];
-                        for (ld_info_list) |ld_info| {
-                            const standard_ld_basename = fs.path.basename(ld_info.ld.get().?);
-                            if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) {
-                                result.target.abi = ld_info.abi;
-                                break;
-                            }
-                        }
-                    },
-                    // We only need this for detecting glibc version.
-                    elf.PT_DYNAMIC => if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and
-                        cross_target.glibc_version == null)
-                    {
-                        var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset);
-                        const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz);
-                        const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn);
-                        const dyn_num = p_filesz / dyn_size;
-                        var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined;
-                        var dyn_i: usize = 0;
-                        dyn: while (dyn_i < dyn_num) {
-                            // Reserve some bytes so that we can deref the 64-bit struct fields
-                            // even when the ELF file is 32-bits.
-                            const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn);
-                            const dyn_read_byte_len = try preadMin(
-                                file,
-                                dyn_buf[0 .. dyn_buf.len - dyn_reserve],
-                                dyn_off,
-                                dyn_size,
-                            );
-                            var dyn_buf_i: usize = 0;
-                            while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({
-                                dyn_i += 1;
-                                dyn_off += dyn_size;
-                                dyn_buf_i += dyn_size;
-                            }) {
-                                const dyn32 = @ptrCast(
-                                    *elf.Elf32_Dyn,
-                                    @alignCast(@alignOf(elf.Elf32_Dyn), &dyn_buf[dyn_buf_i]),
-                                );
-                                const dyn64 = @ptrCast(
-                                    *elf.Elf64_Dyn,
-                                    @alignCast(@alignOf(elf.Elf64_Dyn), &dyn_buf[dyn_buf_i]),
-                                );
-                                const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag);
-                                const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val);
-                                if (tag == elf.DT_RUNPATH) {
-                                    rpath_offset = val;
-                                    break :dyn;
-                                }
-                            }
-                        }
-                    },
-                    else => continue,
-                }
-            }
-        }
-
-        if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and cross_target.glibc_version == null) {
-            if (rpath_offset) |rpoff| {
-                const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx);
-
-                var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff);
-                const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize);
-                const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx);
-
-                var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined;
-                if (sh_buf.len < shentsize) return error.InvalidElfFile;
-
-                _ = try preadMin(file, &sh_buf, str_section_off, shentsize);
-                const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf));
-                const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf));
-                const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset);
-                const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size);
-                var strtab_buf: [4096:0]u8 = undefined;
-                const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len);
-                const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len);
-                const shstrtab = strtab_buf[0..shstrtab_read_len];
-
-                const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum);
-                var sh_i: u16 = 0;
-                const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) {
-                    // Reserve some bytes so that we can deref the 64-bit struct fields
-                    // even when the ELF file is 32-bits.
-                    const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr);
-                    const sh_read_byte_len = try preadMin(
-                        file,
-                        sh_buf[0 .. sh_buf.len - sh_reserve],
-                        shoff,
-                        shentsize,
-                    );
-                    var sh_buf_i: usize = 0;
-                    while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({
-                        sh_i += 1;
-                        shoff += shentsize;
-                        sh_buf_i += shentsize;
-                    }) {
-                        const sh32 = @ptrCast(
-                            *elf.Elf32_Shdr,
-                            @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]),
-                        );
-                        const sh64 = @ptrCast(
-                            *elf.Elf64_Shdr,
-                            @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]),
-                        );
-                        const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
-                        // TODO this pointer cast should not be necessary
-                        const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0);
-                        if (mem.eql(u8, sh_name, ".dynstr")) {
-                            break :find_dyn_str .{
-                                .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset),
-                                .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size),
-                            };
-                        }
-                    }
-                } else null;
-
-                if (dynstr) |ds| {
-                    const strtab_len = std.math.min(ds.size, strtab_buf.len);
-                    const strtab_read_len = try preadMin(file, &strtab_buf, ds.offset, strtab_len);
-                    const strtab = strtab_buf[0..strtab_read_len];
-                    // TODO this pointer cast should not be necessary
-                    const rpoff_usize = std.math.cast(usize, rpoff) catch |err| switch (err) {
-                        error.Overflow => return error.InvalidElfFile,
-                    };
-                    const rpath_list = mem.sliceTo(std.meta.assumeSentinel(strtab[rpoff_usize..].ptr, 0), 0);
-                    var it = mem.tokenize(u8, rpath_list, ":");
-                    while (it.next()) |rpath| {
-                        var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) {
-                            error.NameTooLong => unreachable,
-                            error.InvalidUtf8 => unreachable,
-                            error.BadPathName => unreachable,
-                            error.DeviceBusy => unreachable,
-
-                            error.FileNotFound,
-                            error.NotDir,
-                            error.AccessDenied,
-                            error.NoDevice,
-                            => continue,
-
-                            error.ProcessFdQuotaExceeded,
-                            error.SystemFdQuotaExceeded,
-                            error.SystemResources,
-                            error.SymLinkLoop,
-                            error.Unexpected,
-                            => |e| return e,
-                        };
-                        defer dir.close();
-
-                        var link_buf: [std.os.PATH_MAX]u8 = undefined;
-                        const link_name = std.os.readlinkatZ(
-                            dir.fd,
-                            glibc_so_basename,
-                            &link_buf,
-                        ) catch |err| switch (err) {
-                            error.NameTooLong => unreachable,
-                            error.InvalidUtf8 => unreachable, // Windows only
-                            error.BadPathName => unreachable, // Windows only
-                            error.UnsupportedReparsePointType => unreachable, // Windows only
-
-                            error.AccessDenied,
-                            error.FileNotFound,
-                            error.NotLink,
-                            error.NotDir,
-                            => continue,
-
-                            error.SystemResources,
-                            error.FileSystem,
-                            error.SymLinkLoop,
-                            error.Unexpected,
-                            => |e| return e,
-                        };
-                        result.target.os.version_range.linux.glibc = glibcVerFromLinkName(
-                            link_name,
-                        ) catch |err| switch (err) {
-                            error.UnrecognizedGnuLibCFileName,
-                            error.InvalidGnuLibCVersion,
-                            => continue,
-                        };
-                        break;
-                    }
-                }
-            }
-        }
-
-        return result;
-    }
-
-    fn preadMin(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize {
-        var i: usize = 0;
-        while (i < min_read_len) {
-            const len = file.pread(buf[i..], offset + i) catch |err| switch (err) {
-                error.OperationAborted => unreachable, // Windows-only
-                error.WouldBlock => unreachable, // Did not request blocking mode
-                error.NotOpenForReading => unreachable,
-                error.SystemResources => return error.SystemResources,
-                error.IsDir => return error.UnableToReadElfFile,
-                error.BrokenPipe => return error.UnableToReadElfFile,
-                error.Unseekable => return error.UnableToReadElfFile,
-                error.ConnectionResetByPeer => return error.UnableToReadElfFile,
-                error.ConnectionTimedOut => return error.UnableToReadElfFile,
-                error.Unexpected => return error.Unexpected,
-                error.InputOutput => return error.FileSystem,
-                error.AccessDenied => return error.Unexpected,
-            };
-            if (len == 0) return error.UnexpectedEndOfFile;
-            i += len;
-        }
-        return i;
-    }
-
-    fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, cross_target: CrossTarget) !NativeTargetInfo {
-        const target: Target = .{
-            .cpu = cpu,
-            .os = os,
-            .abi = cross_target.abi orelse Target.Abi.default(cpu.arch, os),
-        };
-        return NativeTargetInfo{
-            .target = target,
-            .dynamic_linker = if (cross_target.dynamic_linker.get() == null)
-                target.standardDynamicLinkerPath()
-            else
-                cross_target.dynamic_linker,
-        };
-    }
-
-    pub const LdInfo = struct {
-        ld: DynamicLinker,
-        abi: Target.Abi,
-    };
-
-    pub fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) {
-        if (is_64) {
-            if (need_bswap) {
-                return @byteSwap(@TypeOf(int_64), int_64);
-            } else {
-                return int_64;
-            }
-        } else {
-            if (need_bswap) {
-                return @byteSwap(@TypeOf(int_32), int_32);
-            } else {
-                return int_32;
-            }
-        }
-    }
-
-    fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, cross_target: CrossTarget) ?Target.Cpu {
-        // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`,
-        // although it is a runtime value, is guaranteed to be one of the architectures in the set
-        // of the respective switch prong.
-        switch (builtin.cpu.arch) {
-            .x86_64, .i386 => {
-                return @import("system/x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, cross_target);
-            },
-            else => {},
-        }
-
-        switch (builtin.os.tag) {
-            .linux => return linux.detectNativeCpuAndFeatures(),
-            .macos => return darwin.macos.detectNativeCpuAndFeatures(),
-            else => {},
-        }
-
-        // This architecture does not have CPU model & feature detection yet.
-        // See https://github.com/ziglang/zig/issues/4591
-        return null;
-    }
-};
+pub const linux = @import("system/linux.zig");
 
 test {
-    _ = @import("system/darwin.zig");
-    _ = @import("system/linux.zig");
+    _ = NativePaths;
+    _ = NativeTargetInfo;
+
+    _ = darwin;
+    _ = linux;
+    _ = windows;
 }
lib/std/build.zig
@@ -16,6 +16,7 @@ const BufMap = std.BufMap;
 const fmt_lib = std.fmt;
 const File = std.fs.File;
 const CrossTarget = std.zig.CrossTarget;
+const NativeTargetInfo = std.zig.system.NativeTargetInfo;
 
 pub const FmtStep = @import("build/FmtStep.zig");
 pub const TranslateCStep = @import("build/TranslateCStep.zig");
@@ -86,6 +87,9 @@ pub const Builder = struct {
     /// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
     glibc_runtimes_dir: ?[]const u8 = null,
 
+    /// Information about the native target. Computed before build() is invoked.
+    host: NativeTargetInfo,
+
     const PkgConfigError = error{
         PkgConfigCrashed,
         PkgConfigFailed,
@@ -159,6 +163,8 @@ pub const Builder = struct {
         const env_map = try allocator.create(BufMap);
         env_map.* = try process.getEnvMap(allocator);
 
+        const host = try NativeTargetInfo.detect(allocator, .{});
+
         const self = try allocator.create(Builder);
         self.* = Builder{
             .zig_exe = zig_exe,
@@ -204,6 +210,7 @@ pub const Builder = struct {
             .install_path = undefined,
             .vcpkg_root = VcpkgRoot{ .unattempted = {} },
             .args = null,
+            .host = host,
         };
         try self.top_level_steps.append(&self.install_tls);
         try self.top_level_steps.append(&self.uninstall_tls);
@@ -1436,6 +1443,7 @@ pub const LibExeObjStep = struct {
     builder: *Builder,
     name: []const u8,
     target: CrossTarget = CrossTarget{},
+    target_info: NativeTargetInfo,
     linker_script: ?FileSource = null,
     version_script: ?[]const u8 = null,
     out_filename: []const u8,
@@ -1655,6 +1663,8 @@ pub const LibExeObjStep = struct {
             .output_lib_path_source = GeneratedFile{ .step = &self.step },
             .output_h_path_source = GeneratedFile{ .step = &self.step },
             .output_pdb_path_source = GeneratedFile{ .step = &self.step },
+
+            .target_info = undefined, // populated in computeOutFileNames
         };
         self.computeOutFileNames();
         if (root_src) |rs| rs.addStepDependencies(&self.step);
@@ -1662,11 +1672,11 @@ pub const LibExeObjStep = struct {
     }
 
     fn computeOutFileNames(self: *LibExeObjStep) void {
-        const target_info = std.zig.system.NativeTargetInfo.detect(
-            self.builder.allocator,
-            self.target,
-        ) catch unreachable;
-        const target = target_info.target;
+        self.target_info = NativeTargetInfo.detect(self.builder.allocator, self.target) catch
+            unreachable;
+
+        const target = self.target_info.target;
+
         self.out_filename = std.zig.binNameAlloc(self.builder.allocator, .{
             .root_name = self.name,
             .target = target,
@@ -2526,74 +2536,80 @@ pub const LibExeObjStep = struct {
                     try zig_args.append("--test-cmd-bin");
                 }
             }
-        } else switch (self.target.getExternalExecutor()) {
-            .native => {},
-            .unavailable => {
-                try zig_args.append("--test-no-exec");
-            },
-            .rosetta => if (builder.enable_rosetta) {
-                try zig_args.append("--test-cmd-bin");
-            } else {
-                try zig_args.append("--test-no-exec");
-            },
-            .qemu => |bin_name| ok: {
-                if (builder.enable_qemu) qemu: {
-                    const need_cross_glibc = self.target.isGnuLibC() and self.is_linking_libc;
-                    const glibc_dir_arg = if (need_cross_glibc)
-                        builder.glibc_runtimes_dir orelse break :qemu
-                    else
-                        null;
-                    try zig_args.append("--test-cmd");
-                    try zig_args.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.target.getCpuArch();
-                        const os_tag = self.target.getOsTag();
-                        const abi = self.target.getAbi();
-                        const cpu_arch_name: []const u8 = if (cpu_arch == .i386)
-                            "i686"
+        } else {
+            const need_cross_glibc = self.target.isGnuLibC() and self.is_linking_libc;
+
+            switch (self.builder.host.getExternalExecutor(self.target_info, .{
+                .qemu_fixes_dl = need_cross_glibc and builder.glibc_runtimes_dir != null,
+                .link_libc = self.is_linking_libc,
+            })) {
+                .native => {},
+                .bad_dl, .bad_os_or_cpu => {
+                    try zig_args.append("--test-no-exec");
+                },
+                .rosetta => if (builder.enable_rosetta) {
+                    try zig_args.append("--test-cmd-bin");
+                } else {
+                    try zig_args.append("--test-no-exec");
+                },
+                .qemu => |bin_name| ok: {
+                    if (builder.enable_qemu) qemu: {
+                        const glibc_dir_arg = if (need_cross_glibc)
+                            builder.glibc_runtimes_dir orelse break :qemu
                         else
-                            @tagName(cpu_arch);
-                        const full_dir = try std.fmt.allocPrint(builder.allocator, fmt_str, .{
-                            dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
-                        });
-
+                            null;
                         try zig_args.append("--test-cmd");
-                        try zig_args.append("-L");
-                        try zig_args.append("--test-cmd");
-                        try zig_args.append(full_dir);
+                        try zig_args.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.target.getCpuArch();
+                            const os_tag = self.target.getOsTag();
+                            const abi = self.target.getAbi();
+                            const cpu_arch_name: []const u8 = if (cpu_arch == .i386)
+                                "i686"
+                            else
+                                @tagName(cpu_arch);
+                            const full_dir = try std.fmt.allocPrint(builder.allocator, fmt_str, .{
+                                dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
+                            });
+
+                            try zig_args.append("--test-cmd");
+                            try zig_args.append("-L");
+                            try zig_args.append("--test-cmd");
+                            try zig_args.append(full_dir);
+                        }
+                        try zig_args.append("--test-cmd-bin");
+                        break :ok;
                     }
+                    try zig_args.append("--test-no-exec");
+                },
+                .wine => |bin_name| if (builder.enable_wine) {
+                    try zig_args.append("--test-cmd");
+                    try zig_args.append(bin_name);
                     try zig_args.append("--test-cmd-bin");
-                    break :ok;
-                }
-                try zig_args.append("--test-no-exec");
-            },
-            .wine => |bin_name| if (builder.enable_wine) {
-                try zig_args.append("--test-cmd");
-                try zig_args.append(bin_name);
-                try zig_args.append("--test-cmd-bin");
-            } else {
-                try zig_args.append("--test-no-exec");
-            },
-            .wasmtime => |bin_name| if (builder.enable_wasmtime) {
-                try zig_args.append("--test-cmd");
-                try zig_args.append(bin_name);
-                try zig_args.append("--test-cmd");
-                try zig_args.append("--dir=.");
-                try zig_args.append("--test-cmd-bin");
-            } else {
-                try zig_args.append("--test-no-exec");
-            },
-            .darling => |bin_name| if (builder.enable_darling) {
-                try zig_args.append("--test-cmd");
-                try zig_args.append(bin_name);
-                try zig_args.append("--test-cmd-bin");
-            } else {
-                try zig_args.append("--test-no-exec");
-            },
+                } else {
+                    try zig_args.append("--test-no-exec");
+                },
+                .wasmtime => |bin_name| if (builder.enable_wasmtime) {
+                    try zig_args.append("--test-cmd");
+                    try zig_args.append(bin_name);
+                    try zig_args.append("--test-cmd");
+                    try zig_args.append("--dir=.");
+                    try zig_args.append("--test-cmd-bin");
+                } else {
+                    try zig_args.append("--test-no-exec");
+                },
+                .darling => |bin_name| if (builder.enable_darling) {
+                    try zig_args.append("--test-cmd");
+                    try zig_args.append(bin_name);
+                    try zig_args.append("--test-cmd-bin");
+                } else {
+                    try zig_args.append("--test-no-exec");
+                },
+            }
         }
 
         for (self.packages.items) |pkg| {
lib/std/target.zig
@@ -1689,27 +1689,6 @@ pub const Target = struct {
         }
     }
 
-    /// Return whether or not the given host target is capable of executing natively executables
-    /// of the other target.
-    pub fn canExecBinariesOf(host_target: Target, binary_target: Target) bool {
-        if (host_target.os.tag != binary_target.os.tag)
-            return false;
-
-        if (host_target.cpu.arch == binary_target.cpu.arch)
-            return true;
-
-        if (host_target.cpu.arch == .x86_64 and binary_target.cpu.arch == .i386)
-            return true;
-
-        if (host_target.cpu.arch == .aarch64 and binary_target.cpu.arch == .arm)
-            return true;
-
-        if (host_target.cpu.arch == .aarch64_be and binary_target.cpu.arch == .armeb)
-            return true;
-
-        return false;
-    }
-
     /// 0c spim    little-endian MIPS 3000 family
     /// 1c 68000   Motorola MC68000
     /// 2c 68020   Motorola MC68020
src/main.zig
@@ -2539,6 +2539,7 @@ fn buildOutputType(
             &comp_destroyed,
             all_args,
             runtime_args_start,
+            link_libc,
         );
     }
 
@@ -2611,6 +2612,7 @@ fn buildOutputType(
                         &comp_destroyed,
                         all_args,
                         runtime_args_start,
+                        link_libc,
                     );
                 },
                 .update_and_run => {
@@ -2636,6 +2638,7 @@ fn buildOutputType(
                         &comp_destroyed,
                         all_args,
                         runtime_args_start,
+                        link_libc,
                     );
                 },
             }
@@ -2700,6 +2703,7 @@ fn runOrTest(
     comp_destroyed: *bool,
     all_args: []const []const u8,
     runtime_args_start: ?usize,
+    link_libc: bool,
 ) !void {
     const exe_loc = emit_bin_loc orelse return;
     const exe_directory = exe_loc.directory orelse comp.bin_file.options.emit.?.directory;
@@ -2740,7 +2744,7 @@ fn runOrTest(
     if (std.process.can_execv and arg_mode == .run and !watch) {
         // execv releases the locks; no need to destroy the Compilation here.
         const err = std.process.execv(gpa, argv.items);
-        try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info);
+        try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc);
         const cmd = try argvCmd(arena, argv.items);
         fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd });
     } else {
@@ -2759,7 +2763,7 @@ fn runOrTest(
         }
 
         const term = child.spawnAndWait() catch |err| {
-            try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info);
+            try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc);
             const cmd = try argvCmd(arena, argv.items);
             fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd });
         };
@@ -4662,34 +4666,108 @@ fn warnAboutForeignBinaries(
     arena: Allocator,
     arg_mode: ArgMode,
     target_info: std.zig.system.NativeTargetInfo,
+    link_libc: bool,
 ) !void {
     const host_cross_target: std.zig.CrossTarget = .{};
     const host_target_info = try detectNativeTargetInfo(gpa, host_cross_target);
 
-    if (!host_target_info.target.canExecBinariesOf(target_info.target)) {
-        const host_name = try host_target_info.target.zigTriple(arena);
-        const foreign_name = try target_info.target.zigTriple(arena);
-        const tip_suffix = switch (arg_mode) {
-            .zig_test => ". Consider using --test-no-exec or --test-cmd",
-            else => "",
-        };
-        warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}){s}", .{
-            host_name, foreign_name, tip_suffix,
-        });
-        return;
-    }
-
-    if (target_info.dynamic_linker.get()) |foreign_dl| {
-        std.fs.cwd().access(foreign_dl, .{}) catch {
+    switch (host_target_info.getExternalExecutor(target_info, .{ .link_libc = link_libc })) {
+        .native => return,
+        .rosetta => {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}). Consider installing Rosetta.", .{
+                host_name, foreign_name,
+            });
+        },
+        .qemu => |qemu| {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            switch (arg_mode) {
+                .zig_test => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++
+                        "to run the tests",
+                    .{ host_name, foreign_name, qemu },
+                ),
+                else => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '{s}' to run the binary",
+                    .{ host_name, foreign_name, qemu },
+                ),
+            }
+        },
+        .wine => |wine| {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            switch (arg_mode) {
+                .zig_test => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++
+                        "to run the tests",
+                    .{ host_name, foreign_name, wine },
+                ),
+                else => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '{s}' to run the binary",
+                    .{ host_name, foreign_name, wine },
+                ),
+            }
+        },
+        .wasmtime => |wasmtime| {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            switch (arg_mode) {
+                .zig_test => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++
+                        "to run the tests",
+                    .{ host_name, foreign_name, wasmtime },
+                ),
+                else => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '{s}' to run the binary",
+                    .{ host_name, foreign_name, wasmtime },
+                ),
+            }
+        },
+        .darling => |darling| {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            switch (arg_mode) {
+                .zig_test => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '--test-cmd {s} --test-cmd-bin' " ++
+                        "to run the tests",
+                    .{ host_name, foreign_name, darling },
+                ),
+                else => warn(
+                    "the host system ({s}) does not appear to be capable of executing binaries " ++
+                        "from the target ({s}). Consider using '{s}' to run the binary",
+                    .{ host_name, foreign_name, darling },
+                ),
+            }
+        },
+        .bad_dl => |foreign_dl| {
             const host_dl = host_target_info.dynamic_linker.get() orelse "(none)";
             const tip_suffix = switch (arg_mode) {
-                .zig_test => ", --test-no-exec, or --test-cmd",
+                .zig_test => ", '--test-no-exec', or '--test-cmd'",
                 else => "",
             };
-            warn("the host system does not appear to be capable of executing binaries from the target because the host dynamic linker is located at '{s}', while the target dynamic linker path is '{s}'. Consider using --dynamic-linker{s}", .{
+            warn("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 using '--dynamic-linker'{s}", .{
                 host_dl, foreign_dl, tip_suffix,
             });
-            return;
-        };
+        },
+        .bad_os_or_cpu => {
+            const host_name = try host_target_info.target.zigTriple(arena);
+            const foreign_name = try target_info.target.zigTriple(arena);
+            const tip_suffix = switch (arg_mode) {
+                .zig_test => ". Consider using '--test-no-exec' or '--test-cmd'",
+                else => "",
+            };
+            warn("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}){s}", .{
+                host_name, foreign_name, tip_suffix,
+            });
+        },
     }
 }
src/test.zig
@@ -611,6 +611,8 @@ pub const TestContext = struct {
     }
 
     fn run(self: *TestContext) !void {
+        const host = try std.zig.system.NativeTargetInfo.detect(std.testing.allocator, .{});
+
         var progress = std.Progress{};
         const root_node = try progress.start("compiler", self.cases.items.len);
         defer root_node.end();
@@ -669,6 +671,7 @@ pub const TestContext = struct {
                 zig_lib_directory,
                 &thread_pool,
                 global_cache_directory,
+                host,
             ) catch |err| {
                 fail_count += 1;
                 print("test '{s}' failed: {s}\n\n", .{ case.name, @errorName(err) });
@@ -687,6 +690,7 @@ pub const TestContext = struct {
         zig_lib_directory: Compilation.Directory,
         thread_pool: *ThreadPool,
         global_cache_directory: Compilation.Directory,
+        host: std.zig.system.NativeTargetInfo,
     ) !void {
         const target_info = try std.zig.system.NativeTargetInfo.detect(allocator, case.target);
         const target = target_info.target;
@@ -882,6 +886,7 @@ pub const TestContext = struct {
             .stage1 => true,
             else => null,
         };
+        const link_libc = case.backend == .llvm;
         const comp = try Compilation.create(allocator, .{
             .local_cache_directory = zig_cache_directory,
             .global_cache_directory = global_cache_directory,
@@ -903,7 +908,7 @@ pub const TestContext = struct {
             .is_native_os = case.target.isNativeOs(),
             .is_native_abi = case.target.isNativeAbi(),
             .dynamic_linker = target_info.dynamic_linker.get(),
-            .link_libc = case.backend == .llvm,
+            .link_libc = link_libc,
             .use_llvm = use_llvm,
             .use_stage1 = use_stage1,
             .self_exe_path = std.testing.zig_exe_path,
@@ -1113,7 +1118,7 @@ pub const TestContext = struct {
                         // child process.
                         const exe_path = try std.fmt.allocPrint(arena, "." ++ std.fs.path.sep_str ++ "{s}", .{bin_name});
                         if (case.object_format != null and case.object_format.? == .c) {
-                            if (case.target.getExternalExecutor() != .native) {
+                            if (host.getExternalExecutor(target_info, .{ .link_libc = true }) != .native) {
                                 // We wouldn't be able to run the compiled C code.
                                 return; // Pass test.
                             }
@@ -1129,9 +1134,9 @@ pub const TestContext = struct {
                                 "-lc",
                                 exe_path,
                             });
-                        } else switch (case.target.getExternalExecutor()) {
+                        } else switch (host.getExternalExecutor(target_info, .{ .link_libc = link_libc })) {
                             .native => try argv.append(exe_path),
-                            .unavailable => return, // Pass test.
+                            .bad_dl, .bad_os_or_cpu => return, // Pass test.
 
                             .rosetta => if (enable_rosetta) {
                                 try argv.append(exe_path);
CMakeLists.txt
@@ -540,6 +540,8 @@ set(ZIG_STAGE2_SOURCES
     "${CMAKE_SOURCE_DIR}/lib/std/zig/render.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/zig/string_literal.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/zig/system.zig"
+    "${CMAKE_SOURCE_DIR}/lib/std/zig/system/NativePaths.zig"
+    "${CMAKE_SOURCE_DIR}/lib/std/zig/system/NativeTargetInfo.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/zig/system/x86.zig"
     "${CMAKE_SOURCE_DIR}/lib/std/zig/tokenizer.zig"
     "${CMAKE_SOURCE_DIR}/src/Air.zig"