Commit dbdb87502d

Andrew Kelley <andrew@ziglang.org>
2023-12-04 23:26:57
std.Target: add DynamicLinker
1 parent 3179f58
lib/std/Build/Step/Compile.zig
@@ -9,7 +9,6 @@ const StringHashMap = std.StringHashMap;
 const Sha256 = std.crypto.hash.sha2.Sha256;
 const Allocator = mem.Allocator;
 const Step = std.Build.Step;
-const NativeTargetInfo = std.zig.system.NativeTargetInfo;
 const LazyPath = std.Build.LazyPath;
 const PkgConfigPkg = std.Build.PkgConfigPkg;
 const PkgConfigError = std.Build.PkgConfigError;
lib/std/Build/Step/Options.zig
@@ -294,11 +294,9 @@ test Options {
     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
     defer arena.deinit();
 
-    const detected = try std.zig.system.NativeTargetInfo.detect(.{});
     const host: std.Build.ResolvedTarget = .{
         .query = .{},
-        .target = detected.target,
-        .dynamic_linker = detected.dynamic_linker,
+        .target = try std.zig.system.resolveTargetQuery(.{}),
     };
 
     var cache: std.Build.Cache = .{
lib/std/Build/Step/Run.zig
@@ -678,8 +678,8 @@ fn runCommand(
 
             const need_cross_glibc = exe.rootModuleTarget().isGnuLibC() and
                 exe.is_linking_libc;
-            const other_target_info = exe.root_module.target.?.toNativeTargetInfo();
-            switch (b.host.toNativeTargetInfo().getExternalExecutor(&other_target_info, .{
+            const other_target = exe.root_module.target.?.target;
+            switch (std.zig.system.getExternalExecutor(b.host.target, &other_target, .{
                 .qemu_fixes_dl = need_cross_glibc and b.glibc_runtimes_dir != null,
                 .link_libc = exe.is_linking_libc,
             })) {
@@ -752,7 +752,7 @@ fn runCommand(
                 .bad_dl => |foreign_dl| {
                     if (allow_skip) return error.MakeSkipped;
 
-                    const host_dl = b.host.dynamic_linker.get() orelse "(none)";
+                    const host_dl = b.host.target.dynamic_linker.get() orelse "(none)";
 
                     return step.fail(
                         \\the host system is unable to execute binaries from the target
lib/std/Build/Module.zig
@@ -746,5 +746,4 @@ const Module = @This();
 const std = @import("std");
 const assert = std.debug.assert;
 const LazyPath = std.Build.LazyPath;
-const NativeTargetInfo = std.zig.system.NativeTargetInfo;
 const Step = std.Build.Step;
lib/std/Target/Query.zig
@@ -34,7 +34,7 @@ abi: ?Target.Abi = null,
 
 /// When `os_tag` is `null`, then `null` means native. Otherwise it means the standard path
 /// based on the `os_tag`.
-dynamic_linker: DynamicLinker = DynamicLinker{},
+dynamic_linker: Target.DynamicLinker = Target.DynamicLinker.none,
 
 /// `null` means default for the cpu/arch/os combo.
 ofmt: ?Target.ObjectFormat = null,
@@ -61,8 +61,6 @@ pub const OsVersion = union(enum) {
 
 pub const SemanticVersion = std.SemanticVersion;
 
-pub const DynamicLinker = Target.DynamicLinker;
-
 pub fn fromTarget(target: Target) Query {
     var result: Query = .{
         .cpu_arch = target.cpu.arch,
@@ -164,7 +162,7 @@ fn updateOsVersionRange(self: *Query, os: Target.Os) void {
     }
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn toTarget(self: Query) Target {
     return .{
         .cpu = self.getCpu(),
@@ -232,7 +230,7 @@ pub fn parse(args: ParseOptions) !Query {
     const diags = args.diagnostics orelse &dummy_diags;
 
     var result: Query = .{
-        .dynamic_linker = DynamicLinker.init(args.dynamic_linker),
+        .dynamic_linker = Target.DynamicLinker.init(args.dynamic_linker),
     };
 
     var it = mem.splitScalar(u8, args.arch_os_abi, '-');
@@ -379,13 +377,13 @@ test parseVersion {
     try std.testing.expectError(error.InvalidVersion, parseVersion("1.2.3.4"));
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn getCpu(self: Query) Target.Cpu {
     switch (self.cpu_model) {
         .native => {
             // This works when doing `zig build` because Zig generates a build executable using
             // native CPU model & features. However this will not be accurate otherwise, and
-            // will need to be integrated with `std.zig.system.NativeTargetInfo.detect`.
+            // will need to be integrated with `std.zig.system.resolveTargetQuery`.
             return builtin.cpu;
         },
         .baseline => {
@@ -396,7 +394,7 @@ pub fn getCpu(self: Query) Target.Cpu {
         .determined_by_cpu_arch => if (self.cpu_arch == null) {
             // This works when doing `zig build` because Zig generates a build executable using
             // native CPU model & features. However this will not be accurate otherwise, and
-            // will need to be integrated with `std.zig.system.NativeTargetInfo.detect`.
+            // will need to be integrated with `std.zig.system.resolveTargetQuery`.
             return builtin.cpu;
         } else {
             var adjusted_baseline = Target.Cpu.baseline(self.getCpuArch());
@@ -426,11 +424,11 @@ pub fn getCpuFeatures(self: Query) Target.Cpu.Feature.Set {
     return self.getCpu().features;
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn getOs(self: Query) Target.Os {
     // `builtin.os` works when doing `zig build` because Zig generates a build executable using
     // native OS version range. However this will not be accurate otherwise, and
-    // will need to be integrated with `std.zig.system.NativeTargetInfo.detect`.
+    // will need to be integrated with `std.zig.system.resolveTargetQuery`.
     var adjusted_os = if (self.os_tag) |os_tag| os_tag.defaultVersionRange(self.getCpuArch()) else builtin.os;
 
     if (self.os_version_min) |min| switch (min) {
@@ -463,7 +461,7 @@ pub fn getOsTag(self: Query) Target.Os.Tag {
     return self.os_tag orelse builtin.os.tag;
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn getOsVersionMin(self: Query) OsVersion {
     if (self.os_version_min) |version_min| return version_min;
     var tmp: Query = undefined;
@@ -471,7 +469,7 @@ pub fn getOsVersionMin(self: Query) OsVersion {
     return tmp.os_version_min.?;
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn getOsVersionMax(self: Query) OsVersion {
     if (self.os_version_max) |version_max| return version_max;
     var tmp: Query = undefined;
@@ -479,14 +477,14 @@ pub fn getOsVersionMax(self: Query) OsVersion {
     return tmp.os_version_max.?;
 }
 
-/// TODO deprecated, use `std.zig.system.NativeTargetInfo.detect`.
+/// TODO deprecated, use `std.zig.system.resolveTargetQuery`.
 pub fn getAbi(self: Query) Target.Abi {
     if (self.abi) |abi| return abi;
 
     if (self.os_tag == null) {
         // This works when doing `zig build` because Zig generates a build executable using
         // native CPU model & features. However this will not be accurate otherwise, and
-        // will need to be integrated with `std.zig.system.NativeTargetInfo.detect`.
+        // will need to be integrated with `std.zig.system.resolveTargetQuery`.
         return builtin.abi;
     }
 
lib/std/zig/system/NativePaths.zig
@@ -5,7 +5,6 @@ const process = std.process;
 const mem = std.mem;
 
 const NativePaths = @This();
-const NativeTargetInfo = std.zig.system.NativeTargetInfo;
 
 arena: Allocator,
 include_dirs: std.ArrayListUnmanaged([]const u8) = .{},
@@ -14,8 +13,7 @@ framework_dirs: std.ArrayListUnmanaged([]const u8) = .{},
 rpaths: std.ArrayListUnmanaged([]const u8) = .{},
 warnings: std.ArrayListUnmanaged([]const u8) = .{},
 
-pub fn detect(arena: Allocator, native_info: NativeTargetInfo) !NativePaths {
-    const native_target = native_info.target;
+pub fn detect(arena: Allocator, native_target: std.Target) !NativePaths {
     var self: NativePaths = .{ .arena = arena };
     var is_nix = false;
     if (process.getEnvVarOwned(arena, "NIX_CFLAGS_COMPILE")) |nix_cflags_compile| {
lib/std/zig/system/NativeTargetInfo.zig
@@ -1,1130 +0,0 @@
-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 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{
-    FileSystem,
-    SystemResources,
-    SymLinkLoop,
-    ProcessFdQuotaExceeded,
-    SystemFdQuotaExceeded,
-    DeviceBusy,
-    OSVersionDetectionFail,
-    Unexpected,
-};
-
-/// Given a `Target.Query`, 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.
-pub fn detect(query: Target.Query) DetectError!NativeTargetInfo {
-    var os = query.getOsTag().defaultVersionRange(query.getCpuArch());
-    if (query.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.SemanticVersion.parse(release)) |ver| {
-                    os.version_range.linux.range.min = ver;
-                    os.version_range.linux.range.max = ver;
-                } else |err| switch (err) {
-                    error.Overflow => {},
-                    error.InvalidVersion => {},
-                }
-            },
-            .solaris, .illumos => {
-                const uts = std.os.uname();
-                const release = mem.sliceTo(&uts.release, 0);
-                if (std.SemanticVersion.parse(release)) |ver| {
-                    os.version_range.semver.min = ver;
-                    os.version_range.semver.max = ver;
-                } else |err| switch (err) {
-                    error.Overflow => {},
-                    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;
-                // consider that sysctl result includes null-termination
-                // reserve 1 byte to ensure we never overflow when appending ".0"
-                var len: usize = buf.len - 1;
-
-                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,
-                };
-
-                // append ".0" to satisfy semver
-                buf[len - 1] = '.';
-                buf[len] = '0';
-                len += 1;
-
-                if (std.SemanticVersion.parse(buf[0..len])) |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 (query.os_version_min) |min| switch (min) {
-        .none => {},
-        .semver => |semver| switch (query.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 (query.os_version_max) |max| switch (max) {
-        .none => {},
-        .semver => |semver| switch (query.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 (query.glibc_version) |glibc| {
-        assert(query.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 = query.getCpuArch();
-
-    const cpu = switch (query.cpu_model) {
-        .native => detectNativeCpuAndFeatures(cpu_arch, os, query),
-        .baseline => Target.Cpu.baseline(cpu_arch),
-        .determined_by_cpu_arch => if (query.cpu_arch == null)
-            detectNativeCpuAndFeatures(cpu_arch, os, query)
-        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(cpu, os, query);
-    // 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 x86
-    // However, the "mode" flags can be used as overrides, so if the user explicitly
-    // sets one of them, that takes precedence.
-    switch (cpu_arch) {
-        .x86 => {
-            if (!Target.x86.featureSetHasAny(query.cpu_features_add, .{
-                .@"16bit_mode", .@"32bit_mode",
-            })) {
-                switch (result.target.abi) {
-                    .code16 => result.target.cpu.features.addFeature(
-                        @intFromEnum(Target.x86.Feature.@"16bit_mode"),
-                    ),
-                    else => result.target.cpu.features.addFeature(
-                        @intFromEnum(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(
-                @intFromEnum(Target.arm.Feature.thumb_mode),
-            );
-        },
-        else => {},
-    }
-    query.updateCpuFeatures(&result.target.cpu.features);
-    return result;
-}
-
-/// In the past, this function attempted to use the executable's own binary if it was dynamically
-/// linked to answer both the C ABI question and the dynamic linker question. However, this
-/// could be problematic on a system that uses a RUNPATH for the compiler binary, locking
-/// it to an older glibc version, while system binaries such as /usr/bin/env use a newer glibc
-/// version. The problem is that libc.so.6 glibc version will match that of the system while
-/// the dynamic linker will match that of the compiler binary. Executables with these versions
-/// mismatching will fail to run.
-///
-/// Therefore, this function works the same regardless of whether the compiler binary is
-/// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the
-/// answer to these questions, or if there is a shebang line, then it chases the referenced
-/// file recursively. If that does not provide the answer, then the function falls back to
-/// defaults.
-fn detectAbiAndDynamicLinker(
-    cpu: Target.Cpu,
-    os: Target.Os,
-    query: Target.Query,
-) DetectError!NativeTargetInfo {
-    const native_target_has_ld = comptime builtin.target.hasDynamicLinker();
-    const is_linux = builtin.target.os.tag == .linux;
-    const is_solarish = builtin.target.os.tag.isSolarish();
-    const have_all_info = query.dynamic_linker.get() != null and
-        query.abi != null and (!is_linux or query.abi.?.isGnu());
-    const os_is_non_native = query.os_tag != null;
-    // The Solaris/illumos environment is always the same.
-    if (!native_target_has_ld or have_all_info or os_is_non_native or is_solarish) {
-        return defaultAbiAndDynamicLinker(cpu, os, query);
-    }
-    if (query.abi) |abi| {
-        if (abi.isMusl()) {
-            // musl implies static linking.
-            return defaultAbiAndDynamicLinker(cpu, os, query);
-        }
-    }
-    // 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(@intFromEnum(Target.Abi.none) == 0);
-        const fields = std.meta.fields(Target.Abi)[1..];
-        var array: [fields.len]Target.Abi = undefined;
-        for (fields, 0..) |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;
-    const ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch);
-
-    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,
-            .ofmt = ofmt,
-        };
-        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.
-    const elf_file = blk: {
-        // This block looks for a shebang line in /usr/bin/env,
-        // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead,
-        // doing the same logic recursively in case it finds another shebang line.
-
-        // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a
-        // reasonably reliable path to start with.
-        var file_name: []const u8 = "/usr/bin/env";
-        // #! (2) + 255 (max length of shebang line since Linux 5.1) + \n (1)
-        var buffer: [258]u8 = undefined;
-        while (true) {
-            const file = fs.openFileAbsolute(file_name, .{}) 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.FileBusy => unreachable, // opened without write permissions
-
-                error.IsDir,
-                error.NotDir,
-                error.InvalidHandle,
-                error.AccessDenied,
-                error.NoDevice,
-                error.FileNotFound,
-                error.NetworkNotFound,
-                error.FileTooBig,
-                error.Unexpected,
-                => |e| {
-                    std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.\n", .{@errorName(e)});
-                    return defaultAbiAndDynamicLinker(cpu, os, query);
-                },
-
-                else => |e| return e,
-            };
-            errdefer file.close();
-
-            const len = preadMin(file, &buffer, 0, buffer.len) catch |err| switch (err) {
-                error.UnexpectedEndOfFile,
-                error.UnableToReadElfFile,
-                => break :blk file,
-
-                else => |e| return e,
-            };
-            const newline = mem.indexOfScalar(u8, buffer[0..len], '\n') orelse break :blk file;
-            const line = buffer[0..newline];
-            if (!mem.startsWith(u8, line, "#!")) break :blk file;
-            var it = mem.tokenizeScalar(u8, line[2..], ' ');
-            file_name = it.next() orelse return defaultAbiAndDynamicLinker(cpu, os, query);
-            file.close();
-        }
-    };
-    defer elf_file.close();
-
-    // If Zig is statically linked, such as via distributed binary static builds, the above
-    // trick (block self_exe) won't work. The next thing we fall back to is the same thing, but for elf_file.
-    // TODO: inline this function and combine the buffer we already read above to find
-    // the possible shebang line with the buffer we use for the ELF header.
-    return abiAndDynamicLinkerFromFile(elf_file, cpu, os, ld_info_list, query) 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.
-        => |e| {
-            std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.\n", .{@errorName(e)});
-            return defaultAbiAndDynamicLinker(cpu, os, query);
-        },
-    };
-}
-
-fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion {
-    var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) {
-        error.NameTooLong => unreachable,
-        error.InvalidUtf8 => unreachable,
-        error.BadPathName => unreachable,
-        error.DeviceBusy => unreachable,
-        error.NetworkNotFound => unreachable, // Windows-only
-
-        error.FileNotFound,
-        error.NotDir,
-        error.InvalidHandle,
-        error.AccessDenied,
-        error.NoDevice,
-        => return error.GLibCNotFound,
-
-        error.ProcessFdQuotaExceeded,
-        error.SystemFdQuotaExceeded,
-        error.SystemResources,
-        error.SymLinkLoop,
-        error.Unexpected,
-        => |e| return e,
-    };
-    defer dir.close();
-
-    // Now we have a candidate for the path to libc shared object. In
-    // the past, we used readlink() here because the link name would
-    // reveal the glibc version. However, in more recent GNU/Linux
-    // installations, there is no symlink. Thus we instead use a more
-    // robust check of opening the libc shared object and looking at the
-    // .dynstr section, and finding the max version number of symbols
-    // that start with "GLIBC_2.".
-    const glibc_so_basename = "libc.so.6";
-    var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) {
-        error.NameTooLong => unreachable,
-        error.InvalidUtf8 => unreachable, // Windows only
-        error.BadPathName => unreachable, // Windows only
-        error.PipeBusy => unreachable, // Windows-only
-        error.SharingViolation => unreachable, // Windows-only
-        error.NetworkNotFound => unreachable, // Windows-only
-        error.FileLocksNotSupported => unreachable, // No lock requested.
-        error.NoSpaceLeft => unreachable, // read-only
-        error.PathAlreadyExists => unreachable, // read-only
-        error.DeviceBusy => unreachable, // read-only
-        error.FileBusy => unreachable, // read-only
-        error.InvalidHandle => unreachable, // should not be in the error set
-        error.WouldBlock => unreachable, // not using O_NONBLOCK
-        error.NoDevice => unreachable, // not asking for a special device
-
-        error.AccessDenied,
-        error.FileNotFound,
-        error.NotDir,
-        error.IsDir,
-        => return error.GLibCNotFound,
-
-        error.FileTooBig => return error.Unexpected,
-
-        error.ProcessFdQuotaExceeded,
-        error.SystemFdQuotaExceeded,
-        error.SystemResources,
-        error.SymLinkLoop,
-        error.Unexpected,
-        => |e| return e,
-    };
-    defer f.close();
-
-    return glibcVerFromSoFile(f) catch |err| switch (err) {
-        error.InvalidElfMagic,
-        error.InvalidElfEndian,
-        error.InvalidElfClass,
-        error.InvalidElfFile,
-        error.InvalidElfVersion,
-        error.InvalidGnuLibCVersion,
-        error.UnexpectedEndOfFile,
-        => return error.GLibCNotFound,
-
-        error.SystemResources,
-        error.UnableToReadElfFile,
-        error.Unexpected,
-        error.FileSystem,
-        => |e| return e,
-    };
-}
-
-fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {
-    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 = @as(*elf.Elf32_Ehdr, @ptrCast(&hdr_buf));
-    const hdr64 = @as(*elf.Elf64_Ehdr, @ptrCast(&hdr_buf));
-    if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) 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,
-    };
-    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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf));
-    const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&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 = @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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
-            const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
-            const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
-            const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 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 return error.InvalidGnuLibCVersion;
-
-    // Here we loop over all the strings in the dynstr string table, assuming that any
-    // strings that start with "GLIBC_2." indicate the existence of such a glibc version,
-    // and furthermore, that the system-installed glibc is at minimum that version.
-
-    // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system.
-    // Here I use double this value plus some headroom. This makes it only need
-    // a single read syscall here.
-    var buf: [80000]u8 = undefined;
-    if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion;
-
-    const dynstr_size: usize = @intCast(dynstr.size);
-    const dynstr_bytes = buf[0..dynstr_size];
-    _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr_bytes.len);
-    var it = mem.splitScalar(u8, dynstr_bytes, 0);
-    var max_ver: std.SemanticVersion = .{ .major = 2, .minor = 2, .patch = 5 };
-    while (it.next()) |s| {
-        if (mem.startsWith(u8, s, "GLIBC_2.")) {
-            const chopped = s["GLIBC_".len..];
-            const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) {
-                error.Overflow => return error.InvalidGnuLibCVersion,
-                error.InvalidVersion => return error.InvalidGnuLibCVersion,
-            };
-            switch (ver.order(max_ver)) {
-                .gt => max_ver = ver,
-                .lt, .eq => continue,
-            }
-        }
-    }
-    return max_ver;
-}
-
-fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) error{ UnrecognizedGnuLibCFileName, InvalidGnuLibCVersion }!std.SemanticVersion {
-    // example: "libc-2.3.4.so"
-    // example: "libc-2.27.so"
-    // example: "ld-2.33.so"
-    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 Target.Query.parseVersion(link_name_chopped) catch |err| switch (err) {
-        error.Overflow => return error.InvalidGnuLibCVersion,
-        error.InvalidVersion => return error.InvalidGnuLibCVersion,
-    };
-}
-
-test glibcVerFromLinkName {
-    try std.testing.expectError(error.UnrecognizedGnuLibCFileName, glibcVerFromLinkName("ld-2.37.so", "this-prefix-does-not-exist"));
-    try std.testing.expectError(error.UnrecognizedGnuLibCFileName, glibcVerFromLinkName("libc-2.37.so-is-not-end", "libc-"));
-
-    try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.so", "ld-"));
-    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 0 }, try glibcVerFromLinkName("ld-2.37.so", "ld-"));
-    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 0 }, try glibcVerFromLinkName("ld-2.37.0.so", "ld-"));
-    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 1 }, try glibcVerFromLinkName("ld-2.37.1.so", "ld-"));
-    try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.37.4.5.so", "ld-"));
-}
-
-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,
-    query: Target.Query,
-) 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 = @as(*elf.Elf32_Ehdr, @ptrCast(&hdr_buf));
-    const hdr64 = @as(*elf.Elf64_Ehdr, @ptrCast(&hdr_buf));
-    if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) 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 = query.abi orelse Target.Abi.default(cpu.arch, os),
-            .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch),
-        },
-        .dynamic_linker = query.dynamic_linker,
-    };
-    var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC
-    const look_for_ld = query.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: *elf.Elf32_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i]));
-            const ph64: *elf.Elf64_Phdr = @ptrCast(@alignCast(&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 = @as(usize, @intCast(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 = @as(u8, @intCast(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
-                    query.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: *elf.Elf32_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i]));
-                            const dyn64: *elf.Elf64_Dyn = @ptrCast(@alignCast(&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
-        query.glibc_version == null)
-    {
-        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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf));
-        const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&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 = @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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
-                const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
-                const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
-                const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 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| {
-            if (rpath_offset) |rpoff| {
-                if (rpoff > ds.size) return error.InvalidElfFile;
-                const rpoff_file = ds.offset + rpoff;
-                const rp_max_size = ds.size - rpoff;
-
-                const strtab_len = @min(rp_max_size, strtab_buf.len);
-                const strtab_read_len = try preadMin(file, &strtab_buf, rpoff_file, strtab_len);
-                const strtab = strtab_buf[0..strtab_read_len];
-
-                const rpath_list = mem.sliceTo(strtab, 0);
-                var it = mem.tokenizeScalar(u8, rpath_list, ':');
-                while (it.next()) |rpath| {
-                    if (glibcVerFromRPath(rpath)) |ver| {
-                        result.target.os.version_range.linux.glibc = ver;
-                        return result;
-                    } else |err| switch (err) {
-                        error.GLibCNotFound => continue,
-                        else => |e| return e,
-                    }
-                }
-            }
-        }
-
-        if (result.dynamic_linker.get()) |dl_path| glibc_ver: {
-            // There is no DT_RUNPATH so we try to find libc.so.6 inside the same
-            // directory as the dynamic linker.
-            if (fs.path.dirname(dl_path)) |rpath| {
-                if (glibcVerFromRPath(rpath)) |ver| {
-                    result.target.os.version_range.linux.glibc = ver;
-                    return result;
-                } else |err| switch (err) {
-                    error.GLibCNotFound => {},
-                    else => |e| return e,
-                }
-            }
-
-            // So far, no luck. Next we try to see if the information is
-            // present in the symlink data for the dynamic linker path.
-            var link_buf: [std.os.PATH_MAX]u8 = undefined;
-            const link_name = std.os.readlink(dl_path, &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.NetworkNotFound => unreachable, // Windows only
-
-                error.AccessDenied,
-                error.FileNotFound,
-                error.NotLink,
-                error.NotDir,
-                => break :glibc_ver,
-
-                error.SystemResources,
-                error.FileSystem,
-                error.SymLinkLoop,
-                error.Unexpected,
-                => |e| return e,
-            };
-            result.target.os.version_range.linux.glibc = glibcVerFromLinkName(
-                fs.path.basename(link_name),
-                "ld-",
-            ) catch |err| switch (err) {
-                error.UnrecognizedGnuLibCFileName,
-                error.InvalidGnuLibCVersion,
-                => break :glibc_ver,
-            };
-            return result;
-        }
-
-        // Nothing worked so far. Finally we fall back to hard-coded search paths.
-        // Some distros such as Debian keep their libc.so.6 in `/lib/$triple/`.
-        var path_buf: [std.os.PATH_MAX]u8 = undefined;
-        var index: usize = 0;
-        const prefix = "/lib/";
-        const cpu_arch = @tagName(result.target.cpu.arch);
-        const os_tag = @tagName(result.target.os.tag);
-        const abi = @tagName(result.target.abi);
-        @memcpy(path_buf[index..][0..prefix.len], prefix);
-        index += prefix.len;
-        @memcpy(path_buf[index..][0..cpu_arch.len], cpu_arch);
-        index += cpu_arch.len;
-        path_buf[index] = '-';
-        index += 1;
-        @memcpy(path_buf[index..][0..os_tag.len], os_tag);
-        index += os_tag.len;
-        path_buf[index] = '-';
-        index += 1;
-        @memcpy(path_buf[index..][0..abi.len], abi);
-        index += abi.len;
-        const rpath = path_buf[0..index];
-        if (glibcVerFromRPath(rpath)) |ver| {
-            result.target.os.version_range.linux.glibc = ver;
-            return result;
-        } else |err| switch (err) {
-            error.GLibCNotFound => {},
-            else => |e| return e,
-        }
-    }
-
-    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.SocketNotConnected => return error.UnableToReadElfFile,
-            error.NetNameDeleted => 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, query: Target.Query) !NativeTargetInfo {
-    const target: Target = .{
-        .cpu = cpu,
-        .os = os,
-        .abi = query.abi orelse Target.Abi.default(cpu.arch, os),
-        .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch),
-    };
-    return NativeTargetInfo{
-        .target = target,
-        .dynamic_linker = if (query.dynamic_linker.get() == null)
-            target.standardDynamicLinkerPath()
-        else
-            query.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(int_64);
-        } else {
-            return int_64;
-        }
-    } else {
-        if (need_bswap) {
-            return @byteSwap(int_32);
-        } else {
-            return int_32;
-        }
-    }
-}
-
-fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?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, .x86 => {
-            return @import("x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, query);
-        },
-        else => {},
-    }
-
-    switch (builtin.os.tag) {
-        .linux => return linux.detectNativeCpuAndFeatures(),
-        .macos => return darwin.macos.detectNativeCpuAndFeatures(),
-        .windows => return windows.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 is capable of running executables of
-/// the other target.
-pub fn getExternalExecutor(
-    host: NativeTargetInfo,
-    candidate: *const 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 == .x86)
-            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" },
-            .x86 => 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" },
-            .sparc64 => Executor{ .qemu = "qemu-sparc64" },
-            .x86_64 => Executor{ .qemu = "qemu-x86_64" },
-            else => return bad_result,
-        };
-    }
-
-    switch (candidate.target.os.tag) {
-        .windows => {
-            if (options.allow_wine) {
-                // x86_64 wine does not support emulating aarch64-windows and
-                // vice versa.
-                if (candidate.target.cpu.arch != builtin.cpu.arch) {
-                    return bad_result;
-                }
-                switch (candidate.target.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.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/system.zig
@@ -1,13 +1,1127 @@
 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 linux = @import("system/linux.zig");
 
+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 is capable of running executables of
+/// the other target.
+pub fn getExternalExecutor(
+    host: std.Target,
+    candidate: *const std.Target,
+    options: GetExternalExecutorOptions,
+) Executor {
+    const os_match = host.os.tag == candidate.os.tag;
+    const cpu_ok = cpu_ok: {
+        if (host.cpu.arch == candidate.cpu.arch)
+            break :cpu_ok true;
+
+        if (host.cpu.arch == .x86_64 and candidate.cpu.arch == .x86)
+            break :cpu_ok true;
+
+        if (host.cpu.arch == .aarch64 and candidate.cpu.arch == .arm)
+            break :cpu_ok true;
+
+        if (host.cpu.arch == .aarch64_be and candidate.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.os.tag == .macos and host.cpu.arch == .aarch64)
+    {
+        switch (candidate.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.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" },
+            .x86 => 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" },
+            .sparc64 => Executor{ .qemu = "qemu-sparc64" },
+            .x86_64 => Executor{ .qemu = "qemu-x86_64" },
+            else => return bad_result,
+        };
+    }
+
+    switch (candidate.os.tag) {
+        .windows => {
+            if (options.allow_wine) {
+                // x86_64 wine does not support emulating aarch64-windows and
+                // vice versa.
+                if (candidate.cpu.arch != builtin.cpu.arch) {
+                    return bad_result;
+                }
+                switch (candidate.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.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.cpu.arch != builtin.cpu.arch) {
+                    return bad_result;
+                }
+                return Executor{ .darling = "darling" };
+            }
+            return bad_result;
+        },
+        else => return bad_result,
+    }
+}
+
+pub const DetectError = error{
+    FileSystem,
+    SystemResources,
+    SymLinkLoop,
+    ProcessFdQuotaExceeded,
+    SystemFdQuotaExceeded,
+    DeviceBusy,
+    OSVersionDetectionFail,
+    Unexpected,
+};
+
+/// Given a `Target.Query`, 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.
+pub fn resolveTargetQuery(query: Target.Query) DetectError!Target {
+    var os = query.getOsTag().defaultVersionRange(query.getCpuArch());
+    if (query.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.SemanticVersion.parse(release)) |ver| {
+                    os.version_range.linux.range.min = ver;
+                    os.version_range.linux.range.max = ver;
+                } else |err| switch (err) {
+                    error.Overflow => {},
+                    error.InvalidVersion => {},
+                }
+            },
+            .solaris, .illumos => {
+                const uts = std.os.uname();
+                const release = mem.sliceTo(&uts.release, 0);
+                if (std.SemanticVersion.parse(release)) |ver| {
+                    os.version_range.semver.min = ver;
+                    os.version_range.semver.max = ver;
+                } else |err| switch (err) {
+                    error.Overflow => {},
+                    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;
+                // consider that sysctl result includes null-termination
+                // reserve 1 byte to ensure we never overflow when appending ".0"
+                var len: usize = buf.len - 1;
+
+                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,
+                };
+
+                // append ".0" to satisfy semver
+                buf[len - 1] = '.';
+                buf[len] = '0';
+                len += 1;
+
+                if (std.SemanticVersion.parse(buf[0..len])) |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 (query.os_version_min) |min| switch (min) {
+        .none => {},
+        .semver => |semver| switch (query.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 (query.os_version_max) |max| switch (max) {
+        .none => {},
+        .semver => |semver| switch (query.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 (query.glibc_version) |glibc| {
+        assert(query.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 = query.getCpuArch();
+
+    const cpu = switch (query.cpu_model) {
+        .native => detectNativeCpuAndFeatures(cpu_arch, os, query),
+        .baseline => Target.Cpu.baseline(cpu_arch),
+        .determined_by_cpu_arch => if (query.cpu_arch == null)
+            detectNativeCpuAndFeatures(cpu_arch, os, query)
+        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(cpu, os, query);
+    // 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 x86
+    // However, the "mode" flags can be used as overrides, so if the user explicitly
+    // sets one of them, that takes precedence.
+    switch (cpu_arch) {
+        .x86 => {
+            if (!Target.x86.featureSetHasAny(query.cpu_features_add, .{
+                .@"16bit_mode", .@"32bit_mode",
+            })) {
+                switch (result.abi) {
+                    .code16 => result.cpu.features.addFeature(
+                        @intFromEnum(Target.x86.Feature.@"16bit_mode"),
+                    ),
+                    else => result.cpu.features.addFeature(
+                        @intFromEnum(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.cpu.features.addFeature(
+                @intFromEnum(Target.arm.Feature.thumb_mode),
+            );
+        },
+        else => {},
+    }
+    query.updateCpuFeatures(&result.cpu.features);
+    return result;
+}
+
+fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?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, .x86 => {
+            return @import("system/x86.zig").detectNativeCpuAndFeatures(cpu_arch, os, query);
+        },
+        else => {},
+    }
+
+    switch (builtin.os.tag) {
+        .linux => return linux.detectNativeCpuAndFeatures(),
+        .macos => return darwin.macos.detectNativeCpuAndFeatures(),
+        .windows => return windows.detectNativeCpuAndFeatures(),
+        else => {},
+    }
+
+    // This architecture does not have CPU model & feature detection yet.
+    // See https://github.com/ziglang/zig/issues/4591
+    return null;
+}
+
+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,
+    query: Target.Query,
+) AbiAndDynamicLinkerFromFileError!Target {
+    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 = @as(*elf.Elf32_Ehdr, @ptrCast(&hdr_buf));
+    const hdr64 = @as(*elf.Elf64_Ehdr, @ptrCast(&hdr_buf));
+    if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) 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: Target = .{
+        .cpu = cpu,
+        .os = os,
+        .abi = query.abi orelse Target.Abi.default(cpu.arch, os),
+        .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch),
+        .dynamic_linker = query.dynamic_linker,
+    };
+    var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC
+    const look_for_ld = query.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: *elf.Elf32_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i]));
+            const ph64: *elf.Elf64_Phdr = @ptrCast(@alignCast(&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 = @as(usize, @intCast(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 = @as(u8, @intCast(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.abi = ld_info.abi;
+                            break;
+                        }
+                    }
+                },
+                // We only need this for detecting glibc version.
+                elf.PT_DYNAMIC => if (builtin.target.os.tag == .linux and result.isGnuLibC() and
+                    query.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: *elf.Elf32_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i]));
+                            const dyn64: *elf.Elf64_Dyn = @ptrCast(@alignCast(&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.isGnuLibC() and
+        query.glibc_version == null)
+    {
+        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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf));
+        const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&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 = @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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
+                const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
+                const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
+                const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 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| {
+            if (rpath_offset) |rpoff| {
+                if (rpoff > ds.size) return error.InvalidElfFile;
+                const rpoff_file = ds.offset + rpoff;
+                const rp_max_size = ds.size - rpoff;
+
+                const strtab_len = @min(rp_max_size, strtab_buf.len);
+                const strtab_read_len = try preadMin(file, &strtab_buf, rpoff_file, strtab_len);
+                const strtab = strtab_buf[0..strtab_read_len];
+
+                const rpath_list = mem.sliceTo(strtab, 0);
+                var it = mem.tokenizeScalar(u8, rpath_list, ':');
+                while (it.next()) |rpath| {
+                    if (glibcVerFromRPath(rpath)) |ver| {
+                        result.os.version_range.linux.glibc = ver;
+                        return result;
+                    } else |err| switch (err) {
+                        error.GLibCNotFound => continue,
+                        else => |e| return e,
+                    }
+                }
+            }
+        }
+
+        if (result.dynamic_linker.get()) |dl_path| glibc_ver: {
+            // There is no DT_RUNPATH so we try to find libc.so.6 inside the same
+            // directory as the dynamic linker.
+            if (fs.path.dirname(dl_path)) |rpath| {
+                if (glibcVerFromRPath(rpath)) |ver| {
+                    result.os.version_range.linux.glibc = ver;
+                    return result;
+                } else |err| switch (err) {
+                    error.GLibCNotFound => {},
+                    else => |e| return e,
+                }
+            }
+
+            // So far, no luck. Next we try to see if the information is
+            // present in the symlink data for the dynamic linker path.
+            var link_buf: [std.os.PATH_MAX]u8 = undefined;
+            const link_name = std.os.readlink(dl_path, &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.NetworkNotFound => unreachable, // Windows only
+
+                error.AccessDenied,
+                error.FileNotFound,
+                error.NotLink,
+                error.NotDir,
+                => break :glibc_ver,
+
+                error.SystemResources,
+                error.FileSystem,
+                error.SymLinkLoop,
+                error.Unexpected,
+                => |e| return e,
+            };
+            result.os.version_range.linux.glibc = glibcVerFromLinkName(
+                fs.path.basename(link_name),
+                "ld-",
+            ) catch |err| switch (err) {
+                error.UnrecognizedGnuLibCFileName,
+                error.InvalidGnuLibCVersion,
+                => break :glibc_ver,
+            };
+            return result;
+        }
+
+        // Nothing worked so far. Finally we fall back to hard-coded search paths.
+        // Some distros such as Debian keep their libc.so.6 in `/lib/$triple/`.
+        var path_buf: [std.os.PATH_MAX]u8 = undefined;
+        var index: usize = 0;
+        const prefix = "/lib/";
+        const cpu_arch = @tagName(result.cpu.arch);
+        const os_tag = @tagName(result.os.tag);
+        const abi = @tagName(result.abi);
+        @memcpy(path_buf[index..][0..prefix.len], prefix);
+        index += prefix.len;
+        @memcpy(path_buf[index..][0..cpu_arch.len], cpu_arch);
+        index += cpu_arch.len;
+        path_buf[index] = '-';
+        index += 1;
+        @memcpy(path_buf[index..][0..os_tag.len], os_tag);
+        index += os_tag.len;
+        path_buf[index] = '-';
+        index += 1;
+        @memcpy(path_buf[index..][0..abi.len], abi);
+        index += abi.len;
+        const rpath = path_buf[0..index];
+        if (glibcVerFromRPath(rpath)) |ver| {
+            result.os.version_range.linux.glibc = ver;
+            return result;
+        } else |err| switch (err) {
+            error.GLibCNotFound => {},
+            else => |e| return e,
+        }
+    }
+
+    return result;
+}
+
+fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) error{ UnrecognizedGnuLibCFileName, InvalidGnuLibCVersion }!std.SemanticVersion {
+    // example: "libc-2.3.4.so"
+    // example: "libc-2.27.so"
+    // example: "ld-2.33.so"
+    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 Target.Query.parseVersion(link_name_chopped) catch |err| switch (err) {
+        error.Overflow => return error.InvalidGnuLibCVersion,
+        error.InvalidVersion => return error.InvalidGnuLibCVersion,
+    };
+}
+
+test glibcVerFromLinkName {
+    try std.testing.expectError(error.UnrecognizedGnuLibCFileName, glibcVerFromLinkName("ld-2.37.so", "this-prefix-does-not-exist"));
+    try std.testing.expectError(error.UnrecognizedGnuLibCFileName, glibcVerFromLinkName("libc-2.37.so-is-not-end", "libc-"));
+
+    try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.so", "ld-"));
+    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 0 }, try glibcVerFromLinkName("ld-2.37.so", "ld-"));
+    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 0 }, try glibcVerFromLinkName("ld-2.37.0.so", "ld-"));
+    try std.testing.expectEqual(std.SemanticVersion{ .major = 2, .minor = 37, .patch = 1 }, try glibcVerFromLinkName("ld-2.37.1.so", "ld-"));
+    try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.37.4.5.so", "ld-"));
+}
+
+fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion {
+    var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) {
+        error.NameTooLong => unreachable,
+        error.InvalidUtf8 => unreachable,
+        error.BadPathName => unreachable,
+        error.DeviceBusy => unreachable,
+        error.NetworkNotFound => unreachable, // Windows-only
+
+        error.FileNotFound,
+        error.NotDir,
+        error.InvalidHandle,
+        error.AccessDenied,
+        error.NoDevice,
+        => return error.GLibCNotFound,
+
+        error.ProcessFdQuotaExceeded,
+        error.SystemFdQuotaExceeded,
+        error.SystemResources,
+        error.SymLinkLoop,
+        error.Unexpected,
+        => |e| return e,
+    };
+    defer dir.close();
+
+    // Now we have a candidate for the path to libc shared object. In
+    // the past, we used readlink() here because the link name would
+    // reveal the glibc version. However, in more recent GNU/Linux
+    // installations, there is no symlink. Thus we instead use a more
+    // robust check of opening the libc shared object and looking at the
+    // .dynstr section, and finding the max version number of symbols
+    // that start with "GLIBC_2.".
+    const glibc_so_basename = "libc.so.6";
+    var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) {
+        error.NameTooLong => unreachable,
+        error.InvalidUtf8 => unreachable, // Windows only
+        error.BadPathName => unreachable, // Windows only
+        error.PipeBusy => unreachable, // Windows-only
+        error.SharingViolation => unreachable, // Windows-only
+        error.NetworkNotFound => unreachable, // Windows-only
+        error.FileLocksNotSupported => unreachable, // No lock requested.
+        error.NoSpaceLeft => unreachable, // read-only
+        error.PathAlreadyExists => unreachable, // read-only
+        error.DeviceBusy => unreachable, // read-only
+        error.FileBusy => unreachable, // read-only
+        error.InvalidHandle => unreachable, // should not be in the error set
+        error.WouldBlock => unreachable, // not using O_NONBLOCK
+        error.NoDevice => unreachable, // not asking for a special device
+
+        error.AccessDenied,
+        error.FileNotFound,
+        error.NotDir,
+        error.IsDir,
+        => return error.GLibCNotFound,
+
+        error.FileTooBig => return error.Unexpected,
+
+        error.ProcessFdQuotaExceeded,
+        error.SystemFdQuotaExceeded,
+        error.SystemResources,
+        error.SymLinkLoop,
+        error.Unexpected,
+        => |e| return e,
+    };
+    defer f.close();
+
+    return glibcVerFromSoFile(f) catch |err| switch (err) {
+        error.InvalidElfMagic,
+        error.InvalidElfEndian,
+        error.InvalidElfClass,
+        error.InvalidElfFile,
+        error.InvalidElfVersion,
+        error.InvalidGnuLibCVersion,
+        error.UnexpectedEndOfFile,
+        => return error.GLibCNotFound,
+
+        error.SystemResources,
+        error.UnableToReadElfFile,
+        error.Unexpected,
+        error.FileSystem,
+        => |e| return e,
+    };
+}
+
+fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {
+    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 = @as(*elf.Elf32_Ehdr, @ptrCast(&hdr_buf));
+    const hdr64 = @as(*elf.Elf64_Ehdr, @ptrCast(&hdr_buf));
+    if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) 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,
+    };
+    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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf));
+    const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&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 = @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: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
+            const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i]));
+            const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
+            const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 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 return error.InvalidGnuLibCVersion;
+
+    // Here we loop over all the strings in the dynstr string table, assuming that any
+    // strings that start with "GLIBC_2." indicate the existence of such a glibc version,
+    // and furthermore, that the system-installed glibc is at minimum that version.
+
+    // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system.
+    // Here I use double this value plus some headroom. This makes it only need
+    // a single read syscall here.
+    var buf: [80000]u8 = undefined;
+    if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion;
+
+    const dynstr_size: usize = @intCast(dynstr.size);
+    const dynstr_bytes = buf[0..dynstr_size];
+    _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr_bytes.len);
+    var it = mem.splitScalar(u8, dynstr_bytes, 0);
+    var max_ver: std.SemanticVersion = .{ .major = 2, .minor = 2, .patch = 5 };
+    while (it.next()) |s| {
+        if (mem.startsWith(u8, s, "GLIBC_2.")) {
+            const chopped = s["GLIBC_".len..];
+            const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) {
+                error.Overflow => return error.InvalidGnuLibCVersion,
+                error.InvalidVersion => return error.InvalidGnuLibCVersion,
+            };
+            switch (ver.order(max_ver)) {
+                .gt => max_ver = ver,
+                .lt, .eq => continue,
+            }
+        }
+    }
+    return max_ver;
+}
+
+/// In the past, this function attempted to use the executable's own binary if it was dynamically
+/// linked to answer both the C ABI question and the dynamic linker question. However, this
+/// could be problematic on a system that uses a RUNPATH for the compiler binary, locking
+/// it to an older glibc version, while system binaries such as /usr/bin/env use a newer glibc
+/// version. The problem is that libc.so.6 glibc version will match that of the system while
+/// the dynamic linker will match that of the compiler binary. Executables with these versions
+/// mismatching will fail to run.
+///
+/// Therefore, this function works the same regardless of whether the compiler binary is
+/// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the
+/// answer to these questions, or if there is a shebang line, then it chases the referenced
+/// file recursively. If that does not provide the answer, then the function falls back to
+/// defaults.
+fn detectAbiAndDynamicLinker(
+    cpu: Target.Cpu,
+    os: Target.Os,
+    query: Target.Query,
+) DetectError!Target {
+    const native_target_has_ld = comptime builtin.target.hasDynamicLinker();
+    const is_linux = builtin.target.os.tag == .linux;
+    const is_solarish = builtin.target.os.tag.isSolarish();
+    const have_all_info = query.dynamic_linker.get() != null and
+        query.abi != null and (!is_linux or query.abi.?.isGnu());
+    const os_is_non_native = query.os_tag != null;
+    // The Solaris/illumos environment is always the same.
+    if (!native_target_has_ld or have_all_info or os_is_non_native or is_solarish) {
+        return defaultAbiAndDynamicLinker(cpu, os, query);
+    }
+    if (query.abi) |abi| {
+        if (abi.isMusl()) {
+            // musl implies static linking.
+            return defaultAbiAndDynamicLinker(cpu, os, query);
+        }
+    }
+    // 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(@intFromEnum(Target.Abi.none) == 0);
+        const fields = std.meta.fields(Target.Abi)[1..];
+        var array: [fields.len]Target.Abi = undefined;
+        for (fields, 0..) |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;
+    const ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch);
+
+    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,
+            .ofmt = ofmt,
+        };
+        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.
+    const elf_file = blk: {
+        // This block looks for a shebang line in /usr/bin/env,
+        // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead,
+        // doing the same logic recursively in case it finds another shebang line.
+
+        // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a
+        // reasonably reliable path to start with.
+        var file_name: []const u8 = "/usr/bin/env";
+        // #! (2) + 255 (max length of shebang line since Linux 5.1) + \n (1)
+        var buffer: [258]u8 = undefined;
+        while (true) {
+            const file = fs.openFileAbsolute(file_name, .{}) 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.FileBusy => unreachable, // opened without write permissions
+
+                error.IsDir,
+                error.NotDir,
+                error.InvalidHandle,
+                error.AccessDenied,
+                error.NoDevice,
+                error.FileNotFound,
+                error.NetworkNotFound,
+                error.FileTooBig,
+                error.Unexpected,
+                => |e| {
+                    std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.\n", .{@errorName(e)});
+                    return defaultAbiAndDynamicLinker(cpu, os, query);
+                },
+
+                else => |e| return e,
+            };
+            errdefer file.close();
+
+            const len = preadMin(file, &buffer, 0, buffer.len) catch |err| switch (err) {
+                error.UnexpectedEndOfFile,
+                error.UnableToReadElfFile,
+                => break :blk file,
+
+                else => |e| return e,
+            };
+            const newline = mem.indexOfScalar(u8, buffer[0..len], '\n') orelse break :blk file;
+            const line = buffer[0..newline];
+            if (!mem.startsWith(u8, line, "#!")) break :blk file;
+            var it = mem.tokenizeScalar(u8, line[2..], ' ');
+            file_name = it.next() orelse return defaultAbiAndDynamicLinker(cpu, os, query);
+            file.close();
+        }
+    };
+    defer elf_file.close();
+
+    // If Zig is statically linked, such as via distributed binary static builds, the above
+    // trick (block self_exe) won't work. The next thing we fall back to is the same thing, but for elf_file.
+    // TODO: inline this function and combine the buffer we already read above to find
+    // the possible shebang line with the buffer we use for the ELF header.
+    return abiAndDynamicLinkerFromFile(elf_file, cpu, os, ld_info_list, query) 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.
+        => |e| {
+            std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.\n", .{@errorName(e)});
+            return defaultAbiAndDynamicLinker(cpu, os, query);
+        },
+    };
+}
+
+fn defaultAbiAndDynamicLinker(cpu: Target.Cpu, os: Target.Os, query: Target.Query) !Target {
+    const abi = query.abi orelse Target.Abi.default(cpu.arch, os);
+    return .{
+        .cpu = cpu,
+        .os = os,
+        .abi = abi,
+        .ofmt = query.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch),
+        .dynamic_linker = if (query.dynamic_linker.get() == null)
+            Target.standardDynamicLinkerPath_cpu_os_abi(cpu, os.tag, abi)
+        else
+            query.dynamic_linker,
+    };
+}
+
+const LdInfo = struct {
+    ld: Target.DynamicLinker,
+    abi: Target.Abi,
+};
+
+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.SocketNotConnected => return error.UnableToReadElfFile,
+            error.NetNameDeleted => 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 elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) {
+    if (is_64) {
+        if (need_bswap) {
+            return @byteSwap(int_64);
+        } else {
+            return int_64;
+        }
+    } else {
+        if (need_bswap) {
+            return @byteSwap(int_32);
+        } else {
+            return int_32;
+        }
+    }
+}
+
+const builtin = @import("builtin");
+const std = @import("../std.zig");
+const mem = std.mem;
+const elf = std.elf;
+const fs = std.fs;
+const assert = std.debug.assert;
+const Target = std.Target;
+const native_endian = builtin.cpu.arch.endian();
+
 test {
     _ = NativePaths;
-    _ = NativeTargetInfo;
 
     _ = darwin;
     _ = linux;
lib/std/Build.zig
@@ -2129,14 +2129,6 @@ pub fn hex64(x: u64) [16]u8 {
 pub const ResolvedTarget = struct {
     query: Target.Query,
     target: Target,
-    dynamic_linker: Target.DynamicLinker,
-
-    pub fn toNativeTargetInfo(self: ResolvedTarget) std.zig.system.NativeTargetInfo {
-        return .{
-            .target = self.target,
-            .dynamic_linker = self.dynamic_linker,
-        };
-    }
 };
 
 /// Converts a target query into a fully resolved target that can be passed to
@@ -2146,13 +2138,10 @@ pub fn resolveTargetQuery(b: *Build, query: Target.Query) ResolvedTarget {
     // resolved via a WASI API or via the build protocol.
     _ = b;
 
-    const result = std.zig.system.NativeTargetInfo.detect(query) catch
-        @panic("unable to resolve target query");
-
     return .{
         .query = query,
-        .target = result.target,
-        .dynamic_linker = result.dynamic_linker,
+        .target = std.zig.system.resolveTargetQuery(query) catch
+            @panic("unable to resolve target query"),
     };
 }
 
lib/std/Target.zig
@@ -1,7 +1,13 @@
+//! All the details about the machine that will be executing code.
+//! Unlike `Query` which might leave some things as "default" or "host", this
+//! data is fully resolved into a concrete set of OS versions, CPU features,
+//! etc.
+
 cpu: Cpu,
 os: Os,
 abi: Abi,
 ofmt: ObjectFormat,
+dynamic_linker: DynamicLinker = DynamicLinker.none,
 
 pub const Query = @import("Target/Query.zig");
 
@@ -1529,13 +1535,19 @@ pub inline fn hasDynamicLinker(self: Target) bool {
 }
 
 pub const DynamicLinker = struct {
-    /// Contains the memory used to store the dynamic linker path. This field should
-    /// not be used directly. See `get` and `set`. This field exists so that this API requires no allocator.
-    buffer: [255]u8 = undefined,
+    /// Contains the memory used to store the dynamic linker path. This field
+    /// should not be used directly. See `get` and `set`. This field exists so
+    /// that this API requires no allocator.
+    buffer: [255]u8,
 
     /// Used to construct the dynamic linker path. This field should not be used
     /// directly. See `get` and `set`.
-    max_byte: ?u8 = null,
+    max_byte: ?u8,
+
+    pub const none: DynamicLinker = .{
+        .buffer = undefined,
+        .max_byte = null,
+    };
 
     /// Asserts that the length is less than or equal to 255 bytes.
     pub fn init(dl_or_null: ?[]const u8) DynamicLinker {
@@ -1561,8 +1573,12 @@ pub const DynamicLinker = struct {
     }
 };
 
-pub fn standardDynamicLinkerPath(self: Target) DynamicLinker {
-    var result: DynamicLinker = .{};
+pub fn standardDynamicLinkerPath(target: Target) DynamicLinker {
+    return standardDynamicLinkerPath_cpu_os_abi(target.cpu, target.os.tag, target.abi);
+}
+
+pub fn standardDynamicLinkerPath_cpu_os_abi(cpu: Cpu, os_tag: Os.Tag, abi: Abi) DynamicLinker {
+    var result = DynamicLinker.none;
     const S = struct {
         fn print(r: *DynamicLinker, comptime fmt: []const u8, args: anytype) DynamicLinker {
             r.max_byte = @as(u8, @intCast((std.fmt.bufPrint(&r.buffer, fmt, args) catch unreachable).len - 1));
@@ -1577,32 +1593,32 @@ pub fn standardDynamicLinkerPath(self: Target) DynamicLinker {
     const print = S.print;
     const copy = S.copy;
 
-    if (self.abi == .android) {
-        const suffix = if (self.ptrBitWidth() == 64) "64" else "";
+    if (abi == .android) {
+        const suffix = if (ptrBitWidth_cpu_abi(cpu, abi) == 64) "64" else "";
         return print(&result, "/system/bin/linker{s}", .{suffix});
     }
 
-    if (self.abi.isMusl()) {
-        const is_arm = switch (self.cpu.arch) {
+    if (abi.isMusl()) {
+        const is_arm = switch (cpu.arch) {
             .arm, .armeb, .thumb, .thumbeb => true,
             else => false,
         };
-        const arch_part = switch (self.cpu.arch) {
+        const arch_part = switch (cpu.arch) {
             .arm, .thumb => "arm",
             .armeb, .thumbeb => "armeb",
             else => |arch| @tagName(arch),
         };
-        const arch_suffix = if (is_arm and self.abi.floatAbi() == .hard) "hf" else "";
+        const arch_suffix = if (is_arm and abi.floatAbi() == .hard) "hf" else "";
         return print(&result, "/lib/ld-musl-{s}{s}.so.1", .{ arch_part, arch_suffix });
     }
 
-    switch (self.os.tag) {
+    switch (os_tag) {
         .freebsd => return copy(&result, "/libexec/ld-elf.so.1"),
         .netbsd => return copy(&result, "/libexec/ld.elf_so"),
         .openbsd => return copy(&result, "/usr/libexec/ld.so"),
         .dragonfly => return copy(&result, "/libexec/ld-elf.so.2"),
         .solaris, .illumos => return copy(&result, "/lib/64/ld.so.1"),
-        .linux => switch (self.cpu.arch) {
+        .linux => switch (cpu.arch) {
             .x86,
             .sparc,
             .sparcel,
@@ -1616,7 +1632,7 @@ pub fn standardDynamicLinkerPath(self: Target) DynamicLinker {
             .armeb,
             .thumb,
             .thumbeb,
-            => return copy(&result, switch (self.abi.floatAbi()) {
+            => return copy(&result, switch (abi.floatAbi()) {
                 .hard => "/lib/ld-linux-armhf.so.3",
                 else => "/lib/ld-linux.so.3",
             }),
@@ -1626,12 +1642,12 @@ pub fn standardDynamicLinkerPath(self: Target) DynamicLinker {
             .mips64,
             .mips64el,
             => {
-                const lib_suffix = switch (self.abi) {
+                const lib_suffix = switch (abi) {
                     .gnuabin32, .gnux32 => "32",
                     .gnuabi64 => "64",
                     else => "",
                 };
-                const is_nan_2008 = mips.featureSetHas(self.cpu.features, .nan2008);
+                const is_nan_2008 = mips.featureSetHas(cpu.features, .nan2008);
                 const loader = if (is_nan_2008) "ld-linux-mipsn8.so.1" else "ld.so.1";
                 return print(&result, "/lib{s}/{s}", .{ lib_suffix, loader });
             },
@@ -1640,7 +1656,7 @@ pub fn standardDynamicLinkerPath(self: Target) DynamicLinker {
             .powerpc64, .powerpc64le => return copy(&result, "/lib64/ld64.so.2"),
             .s390x => return copy(&result, "/lib64/ld64.so.1"),
             .sparc64 => return copy(&result, "/lib64/ld-linux.so.2"),
-            .x86_64 => return copy(&result, switch (self.abi) {
+            .x86_64 => return copy(&result, switch (abi) {
                 .gnux32 => "/libx32/ld-linux-x32.so.2",
                 else => "/lib64/ld-linux-x86-64.so.2",
             }),
@@ -1862,17 +1878,17 @@ pub fn maxIntAlignment(target: Target) u16 {
     };
 }
 
-pub fn ptrBitWidth(target: Target) u16 {
-    switch (target.abi) {
+pub fn ptrBitWidth_cpu_abi(cpu: Cpu, abi: Abi) u16 {
+    switch (abi) {
         .gnux32, .muslx32, .gnuabin32, .gnuilp32 => return 32,
         .gnuabi64 => return 64,
         else => {},
     }
-    switch (target.cpu.arch) {
+    return switch (cpu.arch) {
         .avr,
         .msp430,
         .spu_2,
-        => return 16,
+        => 16,
 
         .arc,
         .arm,
@@ -1908,7 +1924,7 @@ pub fn ptrBitWidth(target: Target) u16 {
         .loongarch32,
         .dxil,
         .xtensa,
-        => return 32,
+        => 32,
 
         .aarch64,
         .aarch64_be,
@@ -1933,10 +1949,14 @@ pub fn ptrBitWidth(target: Target) u16 {
         .ve,
         .spirv64,
         .loongarch64,
-        => return 64,
+        => 64,
 
-        .sparc => return if (std.Target.sparc.featureSetHas(target.cpu.features, .v9)) 64 else 32,
-    }
+        .sparc => if (std.Target.sparc.featureSetHas(cpu.features, .v9)) 64 else 32,
+    };
+}
+
+pub fn ptrBitWidth(target: Target) u16 {
+    return ptrBitWidth_cpu_abi(target.cpu, target.abi);
 }
 
 pub fn stackAlignment(target: Target) u16 {
lib/build_runner.zig
@@ -46,11 +46,9 @@ pub fn main() !void {
         return error.InvalidArgs;
     };
 
-    const detected = try std.zig.system.NativeTargetInfo.detect(.{});
     const host: std.Build.ResolvedTarget = .{
         .query = .{},
-        .target = detected.target,
-        .dynamic_linker = detected.dynamic_linker,
+        .target = try std.zig.system.resolveTargetQuery(.{}),
     };
 
     const build_root_directory: std.Build.Cache.Directory = .{
src/Compilation.zig
@@ -6527,7 +6527,6 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca
             try buffer.writer().print("        .{},\n", .{std.zig.fmtId(feature.name)});
         }
     }
-
     try buffer.writer().print(
         \\    }}),
         \\}};
@@ -6607,15 +6606,31 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca
             .{ windows.min, windows.max },
         ),
     }
-    try buffer.appendSlice("};\n");
-
-    try buffer.writer().print(
-        \\pub const target = std.Target{{
+    try buffer.appendSlice(
+        \\};
+        \\pub const target: std.Target = .{
         \\    .cpu = cpu,
         \\    .os = os,
         \\    .abi = abi,
         \\    .ofmt = object_format,
-        \\}};
+        \\
+    );
+
+    if (target.dynamic_linker.get()) |dl| {
+        try buffer.writer().print(
+            \\    .dynamic_linker = std.Target.DynamicLinker.init("{s}"),
+            \\}};
+            \\
+        , .{dl});
+    } else {
+        try buffer.appendSlice(
+            \\    .dynamic_linker = std.Target.DynamicLinker.none,
+            \\};
+            \\
+        );
+    }
+
+    try buffer.writer().print(
         \\pub const object_format = std.Target.ObjectFormat.{};
         \\pub const mode = std.builtin.OptimizeMode.{};
         \\pub const link_libc = {};
src/main.zig
@@ -321,13 +321,14 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
     } else if (mem.eql(u8, cmd, "init")) {
         return cmdInit(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "targets")) {
-        const info = try detectNativeTargetInfo(.{});
+        const host = try std.zig.system.resolveTargetQuery(.{});
         const stdout = io.getStdOut().writer();
-        return @import("print_targets.zig").cmdTargets(arena, cmd_args, stdout, info.target);
+        return @import("print_targets.zig").cmdTargets(arena, cmd_args, stdout, host);
     } else if (mem.eql(u8, cmd, "version")) {
         try std.io.getStdOut().writeAll(build_options.version ++ "\n");
-        // Check libc++ linkage to make sure Zig was built correctly, but only for "env" and "version"
-        // to avoid affecting the startup time for build-critical commands (check takes about ~10 μs)
+        // Check libc++ linkage to make sure Zig was built correctly, but only
+        // for "env" and "version" to avoid affecting the startup time for
+        // build-critical commands (check takes about ~10 μs)
         return verifyLibcxxCorrectlyLinked();
     } else if (mem.eql(u8, cmd, "env")) {
         verifyLibcxxCorrectlyLinked();
@@ -2608,9 +2609,9 @@ fn buildOutputType(
     }
 
     const target_query = try parseTargetQueryOrReportFatalError(arena, target_parse_options);
-    const target_info = try detectNativeTargetInfo(target_query);
+    const target = try std.zig.system.resolveTargetQuery(target_query);
 
-    if (target_info.target.os.tag != .freestanding) {
+    if (target.os.tag != .freestanding) {
         if (ensure_libc_on_non_freestanding)
             link_libc = true;
         if (ensure_libcpp_on_non_freestanding)
@@ -2621,7 +2622,7 @@ fn buildOutputType(
         if (!force) {
             entry = null;
         } else if (entry == null and output_mode == .Exe) {
-            entry = switch (target_info.target.ofmt) {
+            entry = switch (target.ofmt) {
                 .coff => "wWinMainCRTStartup",
                 .macho => "_main",
                 .elf, .plan9 => "_start",
@@ -2629,12 +2630,12 @@ fn buildOutputType(
                 else => |tag| fatal("No default entry point available for output format {s}", .{@tagName(tag)}),
             };
         }
-    } else if (entry == null and target_info.target.isWasm() and output_mode == .Exe) {
+    } else if (entry == null and target.isWasm() and output_mode == .Exe) {
         // For WebAssembly the compiler defaults to setting the entry name when no flags are set.
         entry = defaultWasmEntryName(wasi_exec_model);
     }
 
-    if (target_info.target.ofmt == .coff) {
+    if (target.ofmt == .coff) {
         // Now that we know the target supports resources,
         // we can add the res files as link objects.
         for (res_files.items) |res_file| {
@@ -2652,7 +2653,7 @@ fn buildOutputType(
         }
     }
 
-    if (target_info.target.cpu.arch.isWasm()) blk: {
+    if (target.cpu.arch.isWasm()) blk: {
         if (single_threaded == null) {
             single_threaded = true;
         }
@@ -2678,8 +2679,8 @@ fn buildOutputType(
                 fatal("shared memory is not allowed in object files", .{});
             }
 
-            if (!target_info.target.cpu.features.isEnabled(@intFromEnum(std.Target.wasm.Feature.atomics)) or
-                !target_info.target.cpu.features.isEnabled(@intFromEnum(std.Target.wasm.Feature.bulk_memory)))
+            if (!target.cpu.features.isEnabled(@intFromEnum(std.Target.wasm.Feature.atomics)) or
+                !target.cpu.features.isEnabled(@intFromEnum(std.Target.wasm.Feature.bulk_memory)))
             {
                 fatal("'atomics' and 'bulk-memory' features must be enabled to use shared memory", .{});
             }
@@ -2777,15 +2778,15 @@ fn buildOutputType(
     }
 
     for (system_libs.keys(), system_libs.values()) |lib_name, info| {
-        if (target_info.target.is_libc_lib_name(lib_name)) {
+        if (target.is_libc_lib_name(lib_name)) {
             link_libc = true;
             continue;
         }
-        if (target_info.target.is_libcpp_lib_name(lib_name)) {
+        if (target.is_libcpp_lib_name(lib_name)) {
             link_libcpp = true;
             continue;
         }
-        switch (target_util.classifyCompilerRtLibName(target_info.target, lib_name)) {
+        switch (target_util.classifyCompilerRtLibName(target, lib_name)) {
             .none => {},
             .only_libunwind, .both => {
                 link_libunwind = true;
@@ -2797,8 +2798,8 @@ fn buildOutputType(
             },
         }
 
-        if (target_info.target.isMinGW()) {
-            const exists = mingw.libExists(arena, target_info.target, zig_lib_directory, lib_name) catch |err| {
+        if (target.isMinGW()) {
+            const exists = mingw.libExists(arena, target, zig_lib_directory, lib_name) catch |err| {
                 fatal("failed to check zig installation for DLL import libs: {s}", .{
                     @errorName(err),
                 });
@@ -2820,7 +2821,7 @@ fn buildOutputType(
             fatal("cannot use absolute path as a system library: {s}", .{lib_name});
         }
 
-        if (target_info.target.os.tag == .wasi) {
+        if (target.os.tag == .wasi) {
             if (wasi_libc.getEmulatedLibCRTFile(lib_name)) |crt_file| {
                 try wasi_emulated_libs.append(crt_file);
                 continue;
@@ -2838,7 +2839,7 @@ fn buildOutputType(
     if (sysroot == null and target_query.isNativeOs() and target_query.isNativeAbi() and
         (external_system_libs.len != 0 or want_native_include_dirs))
     {
-        const paths = std.zig.system.NativePaths.detect(arena, target_info) catch |err| {
+        const paths = std.zig.system.NativePaths.detect(arena, target) catch |err| {
             fatal("unable to detect native system paths: {s}", .{@errorName(err)});
         };
         for (paths.warnings.items) |warning| {
@@ -2857,7 +2858,7 @@ fn buildOutputType(
     }
 
     if (builtin.target.os.tag == .windows and
-        target_info.target.abi == .msvc and
+        target.abi == .msvc and
         external_system_libs.len != 0)
     {
         if (libc_installation == null) {
@@ -2902,7 +2903,7 @@ fn buildOutputType(
                             &checked_paths,
                             lib_dir_path,
                             lib_name,
-                            target_info.target,
+                            target,
                             info.preferred_mode,
                         )) {
                             const path = try arena.dupe(u8, test_path.items);
@@ -2936,7 +2937,7 @@ fn buildOutputType(
                             &checked_paths,
                             lib_dir_path,
                             lib_name,
-                            target_info.target,
+                            target,
                             info.fallbackMode(),
                         )) {
                             const path = try arena.dupe(u8, test_path.items);
@@ -2970,7 +2971,7 @@ fn buildOutputType(
                             &checked_paths,
                             lib_dir_path,
                             lib_name,
-                            target_info.target,
+                            target,
                             info.preferred_mode,
                         )) {
                             const path = try arena.dupe(u8, test_path.items);
@@ -2994,7 +2995,7 @@ fn buildOutputType(
                             &checked_paths,
                             lib_dir_path,
                             lib_name,
-                            target_info.target,
+                            target,
                             info.fallbackMode(),
                         )) {
                             const path = try arena.dupe(u8, test_path.items);
@@ -3089,15 +3090,13 @@ fn buildOutputType(
     }
     // After this point, resolved_frameworks is used instead of frameworks.
 
-    const object_format = target_info.target.ofmt;
-
-    if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) {
+    if (output_mode == .Obj and (target.ofmt == .coff or target.ofmt == .macho)) {
         const total_obj_count = c_source_files.items.len +
             @intFromBool(root_src_file != null) +
             rc_source_files.items.len +
             link_objects.items.len;
         if (total_obj_count > 1) {
-            fatal("{s} does not support linking multiple objects into one", .{@tagName(object_format)});
+            fatal("{s} does not support linking multiple objects into one", .{@tagName(target.ofmt)});
         }
     }
 
@@ -3110,7 +3109,7 @@ fn buildOutputType(
     const resolved_soname: ?[]const u8 = switch (soname) {
         .yes => |explicit| explicit,
         .no => null,
-        .yes_default_value => switch (object_format) {
+        .yes_default_value => switch (target.ofmt) {
             .elf => if (have_version)
                 try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ root_name, version.major })
             else
@@ -3119,7 +3118,7 @@ fn buildOutputType(
         },
     };
 
-    const a_out_basename = switch (object_format) {
+    const a_out_basename = switch (target.ofmt) {
         .coff => "a.exe",
         else => "a.out",
     };
@@ -3141,7 +3140,7 @@ fn buildOutputType(
             },
             .basename = try std.zig.binNameAlloc(arena, .{
                 .root_name = root_name,
-                .target = target_info.target,
+                .target = target,
                 .output_mode = output_mode,
                 .link_mode = link_mode,
                 .version = optional_version,
@@ -3269,7 +3268,7 @@ fn buildOutputType(
     // Note that cmake when targeting Windows will try to execute
     // zig cc to make an executable and output an implib too.
     const implib_eligible = is_exe_or_dyn_lib and
-        emit_bin_loc != null and target_info.target.os.tag == .windows;
+        emit_bin_loc != null and target.os.tag == .windows;
     if (!implib_eligible) {
         if (!emit_implib_arg_provided) {
             emit_implib = .no;
@@ -3419,7 +3418,7 @@ fn buildOutputType(
         // "-" is stdin. Dump it to a real file.
         const sep = fs.path.sep_str;
         const sub_path = try std.fmt.allocPrint(arena, "tmp" ++ sep ++ "{x}-stdin{s}", .{
-            std.crypto.random.int(u64), ext.canonicalName(target_info.target),
+            std.crypto.random.int(u64), ext.canonicalName(target),
         });
         try local_cache_directory.handle.makePath("tmp");
         // Note that in one of the happy paths, execve() is used to switch
@@ -3454,10 +3453,10 @@ fn buildOutputType(
         .local_cache_directory = local_cache_directory,
         .global_cache_directory = global_cache_directory,
         .root_name = root_name,
-        .target = target_info.target,
+        .target = target,
         .is_native_os = target_query.isNativeOs(),
         .is_native_abi = target_query.isNativeAbi(),
-        .dynamic_linker = target_info.dynamic_linker.get(),
+        .dynamic_linker = target.dynamic_linker.get(),
         .sysroot = sysroot,
         .output_mode = output_mode,
         .main_mod = main_mod,
@@ -3603,7 +3602,6 @@ fn buildOutputType(
         .want_structured_cfg = want_structured_cfg,
     }) catch |err| switch (err) {
         error.LibCUnavailable => {
-            const target = target_info.target;
             const triple_name = try target.zigTriple(arena);
             std.log.err("unable to find or provide libc for target '{s}'", .{triple_name});
 
@@ -3692,7 +3690,7 @@ fn buildOutputType(
     try comp.makeBinFileExecutable();
     saveState(comp, debug_incremental);
 
-    if (test_exec_args.items.len == 0 and object_format == .c) default_exec_args: {
+    if (test_exec_args.items.len == 0 and target.ofmt == .c) default_exec_args: {
         // Default to using `zig run` to execute the produced .c code from `zig test`.
         const c_code_loc = emit_bin_loc orelse break :default_exec_args;
         const c_code_directory = c_code_loc.directory orelse comp.bin_file.options.emit.?.directory;
@@ -3707,7 +3705,7 @@ fn buildOutputType(
 
         if (link_libc) {
             try test_exec_args.append("-lc");
-        } else if (target_info.target.os.tag == .windows) {
+        } else if (target.os.tag == .windows) {
             try test_exec_args.appendSlice(&.{
                 "--subsystem", "console",
                 "-lkernel32",  "-lntdll",
@@ -3741,7 +3739,7 @@ fn buildOutputType(
             test_exec_args.items,
             self_exe_path.?,
             arg_mode,
-            &target_info,
+            &target,
             &comp_destroyed,
             all_args,
             runtime_args_start,
@@ -3861,7 +3859,7 @@ fn serve(
                 //    test_exec_args,
                 //    self_exe_path.?,
                 //    arg_mode,
-                //    target_info,
+                //    target,
                 //    true,
                 //    &comp_destroyed,
                 //    all_args,
@@ -4071,7 +4069,7 @@ fn runOrTest(
     test_exec_args: []const ?[]const u8,
     self_exe_path: []const u8,
     arg_mode: ArgMode,
-    target_info: *const std.zig.system.NativeTargetInfo,
+    target: *const std.Target,
     comp_destroyed: *bool,
     all_args: []const []const u8,
     runtime_args_start: ?usize,
@@ -4105,7 +4103,7 @@ fn runOrTest(
     if (process.can_execv and arg_mode == .run) {
         // execv releases the locks; no need to destroy the Compilation here.
         const err = process.execve(gpa, argv.items, &env_map);
-        try warnAboutForeignBinaries(arena, arg_mode, target_info, link_libc);
+        try warnAboutForeignBinaries(arena, arg_mode, target, link_libc);
         const cmd = try std.mem.join(arena, " ", argv.items);
         fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd });
     } else if (process.can_spawn) {
@@ -4121,7 +4119,7 @@ fn runOrTest(
         comp_destroyed.* = true;
 
         const term = child.spawnAndWait() catch |err| {
-            try warnAboutForeignBinaries(arena, arg_mode, target_info, link_libc);
+            try warnAboutForeignBinaries(arena, arg_mode, target, link_libc);
             const cmd = try std.mem.join(arena, " ", argv.items);
             fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd });
         };
@@ -4820,12 +4818,10 @@ pub fn cmdLibC(gpa: Allocator, args: []const []const u8) !void {
         if (!target_query.isNative()) {
             fatal("unable to detect libc for non-native target", .{});
         }
-        const target_info = try detectNativeTargetInfo(target_query);
-
         var libc = LibCInstallation.findNative(.{
             .allocator = gpa,
             .verbose = true,
-            .target = target_info.target,
+            .target = try std.zig.system.resolveTargetQuery(target_query),
         }) catch |err| {
             fatal("unable to detect native libc: {s}", .{@errorName(err)});
         };
@@ -5114,11 +5110,11 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
         gimmeMoreOfThoseSweetSweetFileDescriptors();
 
         const target_query: std.Target.Query = .{};
-        const target_info = try detectNativeTargetInfo(target_query);
+        const target = try std.zig.system.resolveTargetQuery(target_query);
 
         const exe_basename = try std.zig.binNameAlloc(arena, .{
             .root_name = "build",
-            .target = target_info.target,
+            .target = target,
             .output_mode = .Exe,
         });
         const emit_bin: Compilation.EmitLoc = .{
@@ -5282,10 +5278,10 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
             .local_cache_directory = local_cache_directory,
             .global_cache_directory = global_cache_directory,
             .root_name = "build",
-            .target = target_info.target,
+            .target = target,
             .is_native_os = target_query.isNativeOs(),
             .is_native_abi = target_query.isNativeAbi(),
-            .dynamic_linker = target_info.dynamic_linker.get(),
+            .dynamic_linker = target.dynamic_linker.get(),
             .output_mode = .Exe,
             .main_mod = &main_mod,
             .emit_bin = emit_bin,
@@ -6269,10 +6265,6 @@ test "fds" {
     gimmeMoreOfThoseSweetSweetFileDescriptors();
 }
 
-fn detectNativeTargetInfo(target_query: std.Target.Query) !std.zig.system.NativeTargetInfo {
-    return std.zig.system.NativeTargetInfo.detect(target_query);
-}
-
 const usage_ast_check =
     \\Usage: zig ast-check [file]
     \\
@@ -6669,24 +6661,24 @@ fn parseIntSuffix(arg: []const u8, prefix_len: usize) u64 {
 fn warnAboutForeignBinaries(
     arena: Allocator,
     arg_mode: ArgMode,
-    target_info: *const std.zig.system.NativeTargetInfo,
+    target: *const std.Target,
     link_libc: bool,
 ) !void {
     const host_query: std.Target.Query = .{};
-    const host_target_info = try detectNativeTargetInfo(host_query);
+    const host_target = try std.zig.system.resolveTargetQuery(host_query);
 
-    switch (host_target_info.getExternalExecutor(target_info, .{ .link_libc = link_libc })) {
+    switch (std.zig.system.getExternalExecutor(host_target, target, .{ .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);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try 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);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try target.zigTriple(arena);
             switch (arg_mode) {
                 .zig_test => warn(
                     "the host system ({s}) does not appear to be capable of executing binaries " ++
@@ -6702,8 +6694,8 @@ fn warnAboutForeignBinaries(
             }
         },
         .wine => |wine| {
-            const host_name = try host_target_info.target.zigTriple(arena);
-            const foreign_name = try target_info.target.zigTriple(arena);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try target.zigTriple(arena);
             switch (arg_mode) {
                 .zig_test => warn(
                     "the host system ({s}) does not appear to be capable of executing binaries " ++
@@ -6719,8 +6711,8 @@ fn warnAboutForeignBinaries(
             }
         },
         .wasmtime => |wasmtime| {
-            const host_name = try host_target_info.target.zigTriple(arena);
-            const foreign_name = try target_info.target.zigTriple(arena);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try target.zigTriple(arena);
             switch (arg_mode) {
                 .zig_test => warn(
                     "the host system ({s}) does not appear to be capable of executing binaries " ++
@@ -6736,8 +6728,8 @@ fn warnAboutForeignBinaries(
             }
         },
         .darling => |darling| {
-            const host_name = try host_target_info.target.zigTriple(arena);
-            const foreign_name = try target_info.target.zigTriple(arena);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try target.zigTriple(arena);
             switch (arg_mode) {
                 .zig_test => warn(
                     "the host system ({s}) does not appear to be capable of executing binaries " ++
@@ -6753,7 +6745,7 @@ fn warnAboutForeignBinaries(
             }
         },
         .bad_dl => |foreign_dl| {
-            const host_dl = host_target_info.dynamic_linker.get() orelse "(none)";
+            const host_dl = host_target.dynamic_linker.get() orelse "(none)";
             const tip_suffix = switch (arg_mode) {
                 .zig_test => ", '--test-no-exec', or '--test-cmd'",
                 else => "",
@@ -6763,8 +6755,8 @@ fn warnAboutForeignBinaries(
             });
         },
         .bad_os_or_cpu => {
-            const host_name = try host_target_info.target.zigTriple(arena);
-            const foreign_name = try target_info.target.zigTriple(arena);
+            const host_name = try host_target.zigTriple(arena);
+            const foreign_name = try target.zigTriple(arena);
             const tip_suffix = switch (arg_mode) {
                 .zig_test => ". Consider using '--test-no-exec' or '--test-cmd'",
                 else => "",
src/print_env.zig
@@ -17,8 +17,8 @@ pub fn cmdEnv(arena: Allocator, args: []const []const u8, stdout: std.fs.File.Wr
 
     const global_cache_dir = try introspect.resolveGlobalCacheDir(arena);
 
-    const info = try std.zig.system.NativeTargetInfo.detect(.{});
-    const triple = try info.target.zigTriple(arena);
+    const host = try std.zig.system.resolveTargetQuery(.{});
+    const triple = try host.zigTriple(arena);
 
     var bw = std.io.bufferedWriter(stdout);
     const w = bw.writer();
test/src/Cases.zig
@@ -541,7 +541,7 @@ pub fn lowerToBuildSteps(
     cases_dir_path: []const u8,
     incremental_exe: *std.Build.Step.Compile,
 ) void {
-    const host = std.zig.system.NativeTargetInfo.detect(.{}) catch |err|
+    const host = std.zig.system.resolveTargetQuery(.{}) catch |err|
         std.debug.panic("unable to detect native host: {s}\n", .{@errorName(err)});
 
     for (self.incremental_cases.items) |incr_case| {
@@ -648,8 +648,7 @@ pub fn lowerToBuildSteps(
             },
             .Execution => |expected_stdout| no_exec: {
                 const run = if (case.target.target.ofmt == .c) run_step: {
-                    const target_info = case.target.toNativeTargetInfo();
-                    if (host.getExternalExecutor(&target_info, .{ .link_libc = true }) != .native) {
+                    if (getExternalExecutor(host, &case.target.target, .{ .link_libc = true }) != .native) {
                         // We wouldn't be able to run the compiled C code.
                         break :no_exec;
                     }
@@ -694,8 +693,7 @@ pub fn lowerToBuildSteps(
                 continue; // Pass test.
             }
 
-            const target_info = case.target.toNativeTargetInfo();
-            if (host.getExternalExecutor(&target_info, .{ .link_libc = true }) != .native) {
+            if (getExternalExecutor(host, &case.target.target, .{ .link_libc = true }) != .native) {
                 // We wouldn't be able to run the compiled C code.
                 continue; // Pass test.
             }
@@ -1199,6 +1197,8 @@ const builtin = @import("builtin");
 const std = @import("std");
 const assert = std.debug.assert;
 const Allocator = std.mem.Allocator;
+const getExternalExecutor = std.zig.system.getExternalExecutor;
+
 const Compilation = @import("../../src/Compilation.zig");
 const zig_h = @import("../../src/link.zig").File.C.zig_h;
 const introspect = @import("../../src/introspect.zig");
@@ -1386,18 +1386,15 @@ pub fn main() !void {
 }
 
 fn resolveTargetQuery(query: std.Target.Query) std.Build.ResolvedTarget {
-    const result = std.zig.system.NativeTargetInfo.detect(query) catch
-        @panic("unable to resolve target query");
-
     return .{
         .query = query,
-        .target = result.target,
-        .dynamic_linker = result.dynamic_linker,
+        .target = std.zig.system.resolveTargetQuery(query) catch
+            @panic("unable to resolve target query"),
     };
 }
 
 fn runCases(self: *Cases, zig_exe_path: []const u8) !void {
-    const host = try std.zig.system.NativeTargetInfo.detect(.{});
+    const host = try std.zig.system.resolveTargetQuery(.{});
 
     var progress = std.Progress{};
     const root_node = progress.start("compiler", self.cases.items.len);
@@ -1478,7 +1475,7 @@ fn runOneCase(
     zig_exe_path: []const u8,
     thread_pool: *ThreadPool,
     global_cache_directory: Compilation.Directory,
-    host: std.zig.system.NativeTargetInfo,
+    host: std.Target,
 ) !void {
     const tmp_src_path = "tmp.zig";
     const enable_rosetta = build_options.enable_rosetta;
@@ -1488,8 +1485,7 @@ fn runOneCase(
     const enable_darling = build_options.enable_darling;
     const glibc_runtimes_dir: ?[]const u8 = build_options.glibc_runtimes_dir;
 
-    const target_info = try std.zig.system.NativeTargetInfo.detect(case.target);
-    const target = target_info.target;
+    const target = try std.zig.system.resolveTargetQuery(case.target);
 
     var arena_allocator = std.heap.ArenaAllocator.init(allocator);
     defer arena_allocator.deinit();
@@ -1579,7 +1575,7 @@ fn runOneCase(
         .keep_source_files_loaded = true,
         .is_native_os = case.target.isNativeOs(),
         .is_native_abi = case.target.isNativeAbi(),
-        .dynamic_linker = target_info.dynamic_linker.get(),
+        .dynamic_linker = target.dynamic_linker.get(),
         .link_libc = case.link_libc,
         .use_llvm = use_llvm,
         .self_exe_path = zig_exe_path,
@@ -1715,7 +1711,7 @@ fn runOneCase(
                         .{ &tmp.sub_path, bin_name },
                     );
                     if (case.target.ofmt != null and case.target.ofmt.? == .c) {
-                        if (host.getExternalExecutor(target_info, .{ .link_libc = true }) != .native) {
+                        if (getExternalExecutor(host, &target, .{ .link_libc = true }) != .native) {
                             // We wouldn't be able to run the compiled C code.
                             continue :update; // Pass test.
                         }
@@ -1734,7 +1730,7 @@ fn runOneCase(
                         if (zig_lib_directory.path) |p| {
                             try argv.appendSlice(&.{ "-I", p });
                         }
-                    } else switch (host.getExternalExecutor(target_info, .{ .link_libc = case.link_libc })) {
+                    } else switch (getExternalExecutor(host, &target, .{ .link_libc = case.link_libc })) {
                         .native => {
                             if (case.backend == .stage2 and case.target.getCpuArch().isArmOrThumb()) {
                                 // https://github.com/ziglang/zig/issues/13623
CMakeLists.txt
@@ -516,7 +516,6 @@ set(ZIG_STAGE2_SOURCES
     "${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"