about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-09-07 01:37:52 +0000
committerbors <bors@rust-lang.org>2024-09-07 01:37:52 +0000
commit9afe7136958edaa403f0b0eb00f0353c125b7352 (patch)
treed45bc0fe5e2b864a75697a250603484301558d1e
parent26b5599e4d6ed2b45152c60493c1788c0a27533d (diff)
parentbd56857e31e9324d867a4d3184301a1ef510d316 (diff)
downloadrust-9afe7136958edaa403f0b0eb00f0353c125b7352.tar.gz
rust-9afe7136958edaa403f0b0eb00f0353c125b7352.zip
Auto merge of #129341 - madsmtm:refactor-deployment-target, r=petrochenkov
Apple: Refactor deployment target version parsing

Refactor deployment target parsing to make it easier to do https://github.com/rust-lang/rust/pull/129342 (I wanted to make sure of all the places that `std::env::var` is called).

Specifically, my goal was to minimize the amount of target-specific configuration, so to that end I renamed the `opts` function that generates the `TargetOptions` to `base`, and made it return the LLVM target and `target_arch` too. In the future, I would like to move even more out of the target files and into `spec::apple`, as it makes it easier for me to maintain.

For example, this fixed a bug in `aarch64-apple-watchos`, which wasn't passing the deployment target as part of the LLVM triple. This (probably) fixes https://github.com/rust-lang/rust/issues/123582 and fixes https://github.com/rust-lang/rust/issues/107630.

We also now parse the patch version of deployment targets, allowing the user to specify e.g. `MACOSX_DEPLOYMENT_TARGET=10.12.6`.

Finally, this fixes the LLVM target name for visionOS, it should be `*-apple-xros` and not `*-apple-visionos`.

Since I have changed all the Apple targets here, I smoke-tested my changes by running the following:
```console
# Build each target
./x build library --target="aarch64-apple-darwin,aarch64-apple-ios,aarch64-apple-ios-macabi,aarch64-apple-ios-sim,aarch64-apple-tvos,aarch64-apple-tvos-sim,aarch64-apple-visionos,aarch64-apple-visionos-sim,aarch64-apple-watchos,aarch64-apple-watchos-sim,arm64_32-apple-watchos,arm64e-apple-ios,armv7k-apple-watchos,armv7s-apple-ios,i386-apple-ios,x86_64-apple-darwin,x86_64-apple-ios,x86_64-apple-ios-macabi,x86_64-apple-tvos,x86_64-apple-watchos-sim,x86_64h-apple-darwin"

# Test that we can still at least link basic projects
cargo new foobar && cd foobar && cargo +stage1 build --target=aarch64-apple-darwin --target=aarch64-apple-ios --target=aarch64-apple-ios-macabi --target=aarch64-apple-ios-sim --target=aarch64-apple-tvos --target=aarch64-apple-tvos-sim --target=aarch64-apple-visionos --target=aarch64-apple-visionos-sim --target=aarch64-apple-watchos --target=aarch64-apple-watchos-sim --target=arm64_32-apple-watchos --target=armv7s-apple-ios --target=i386-apple-ios --target=x86_64-apple-darwin --target=x86_64-apple-ios --target=x86_64-apple-ios-macabi --target=x86_64-apple-tvos --target=x86_64-apple-watchos-sim --target=x86_64h-apple-darwin
```

I couldn't build for the `arm64e-apple-darwin` target, the `armv7k-apple-watchos` and `arm64e-apple-ios` targets failed to link, and I know that the `i686-apple-darwin` target requires a bit of setup, but all of this is as it was before this PR.

r? thomcc

CC `@BlackHoleFox`

I would recommend using `rollup=never` when merging this, in case we need to bisect this later.
-rw-r--r--compiler/rustc_codegen_ssa/src/back/metadata.rs33
-rw-r--r--compiler/rustc_driver_impl/src/lib.rs6
-rw-r--r--compiler/rustc_target/src/spec/base/apple/mod.rs329
-rw-r--r--compiler/rustc_target/src/spec/base/apple/tests.rs9
-rw-r--r--compiler/rustc_target/src/spec/mod.rs4
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs24
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs18
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_ios_macabi.rs14
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_ios_sim.rs18
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_tvos.rs10
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_tvos_sim.rs10
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_visionos.rs14
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_visionos_sim.rs14
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs10
-rw-r--r--compiler/rustc_target/src/spec/targets/aarch64_apple_watchos_sim.rs14
-rw-r--r--compiler/rustc_target/src/spec/targets/arm64_32_apple_watchos.rs11
-rw-r--r--compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs24
-rw-r--r--compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs18
-rw-r--r--compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs10
-rw-r--r--compiler/rustc_target/src/spec/targets/armv7s_apple_ios.rs10
-rw-r--r--compiler/rustc_target/src/spec/targets/i386_apple_ios.rs15
-rw-r--r--compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs26
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64_apple_darwin.rs30
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64_apple_ios.rs17
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64_apple_ios_macabi.rs17
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64_apple_tvos.rs11
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64_apple_watchos_sim.rs13
-rw-r--r--compiler/rustc_target/src/spec/targets/x86_64h_apple_darwin.rs26
28 files changed, 381 insertions, 374 deletions
diff --git a/compiler/rustc_codegen_ssa/src/back/metadata.rs b/compiler/rustc_codegen_ssa/src/back/metadata.rs
index 9b5a797ad51..0fd9d7fffe8 100644
--- a/compiler/rustc_codegen_ssa/src/back/metadata.rs
+++ b/compiler/rustc_codegen_ssa/src/back/metadata.rs
@@ -372,27 +372,42 @@ pub(crate) fn create_object_file(sess: &Session) -> Option<write::Object<'static
     Some(file)
 }
 
-/// Since Xcode 15 Apple's LD requires object files to contain information about what they were
-/// built for (LC_BUILD_VERSION): the platform (macOS/watchOS etc), minimum OS version, and SDK
-/// version. This returns a `MachOBuildVersion` for the target.
+/// Mach-O files contain information about:
+/// - The platform/OS they were built for (macOS/watchOS/Mac Catalyst/iOS simulator etc).
+/// - The minimum OS version / deployment target.
+/// - The version of the SDK they were targetting.
+///
+/// In the past, this was accomplished using the LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_IPHONEOS,
+/// LC_VERSION_MIN_TVOS or LC_VERSION_MIN_WATCHOS load commands, which each contain information
+/// about the deployment target and SDK version, and implicitly, by their presence, which OS they
+/// target. Simulator targets were determined if the architecture was x86_64, but there was e.g. a
+/// LC_VERSION_MIN_IPHONEOS present.
+///
+/// This is of course brittle and limited, so modern tooling emit the LC_BUILD_VERSION load
+/// command (which contains all three pieces of information in one) when the deployment target is
+/// high enough, or the target is something that wouldn't be encodable with the old load commands
+/// (such as Mac Catalyst, or Aarch64 iOS simulator).
+///
+/// Since Xcode 15, Apple's LD apparently requires object files to use this load command, so this
+/// returns the `MachOBuildVersion` for the target to do so.
 fn macho_object_build_version_for_target(target: &Target) -> object::write::MachOBuildVersion {
     /// The `object` crate demands "X.Y.Z encoded in nibbles as xxxx.yy.zz"
     /// e.g. minOS 14.0 = 0x000E0000, or SDK 16.2 = 0x00100200
-    fn pack_version((major, minor): (u32, u32)) -> u32 {
-        (major << 16) | (minor << 8)
+    fn pack_version((major, minor, patch): (u16, u8, u8)) -> u32 {
+        let (major, minor, patch) = (major as u32, minor as u32, patch as u32);
+        (major << 16) | (minor << 8) | patch
     }
 
     let platform =
         rustc_target::spec::current_apple_platform(target).expect("unknown Apple target OS");
-    let min_os = rustc_target::spec::current_apple_deployment_target(target)
-        .expect("unknown Apple target OS");
-    let sdk =
+    let min_os = rustc_target::spec::current_apple_deployment_target(target);
+    let (sdk_major, sdk_minor) =
         rustc_target::spec::current_apple_sdk_version(platform).expect("unknown Apple target OS");
 
     let mut build_version = object::write::MachOBuildVersion::default();
     build_version.platform = platform;
     build_version.minos = pack_version(min_os);
-    build_version.sdk = pack_version(sdk);
+    build_version.sdk = pack_version((sdk_major, sdk_minor, 0));
     build_version
 }
 
diff --git a/compiler/rustc_driver_impl/src/lib.rs b/compiler/rustc_driver_impl/src/lib.rs
index cb2fa6e9d74..e7d0fab3f0d 100644
--- a/compiler/rustc_driver_impl/src/lib.rs
+++ b/compiler/rustc_driver_impl/src/lib.rs
@@ -862,9 +862,9 @@ fn print_crate_info(
                 use rustc_target::spec::current_apple_deployment_target;
 
                 if sess.target.is_like_osx {
-                    let (major, minor) = current_apple_deployment_target(&sess.target)
-                        .expect("unknown Apple target OS");
-                    println_info!("deployment_target={}", format!("{major}.{minor}"))
+                    let (major, minor, patch) = current_apple_deployment_target(&sess.target);
+                    let patch = if patch != 0 { format!(".{patch}") } else { String::new() };
+                    println_info!("deployment_target={major}.{minor}{patch}")
                 } else {
                     #[allow(rustc::diagnostic_outside_of_impl)]
                     sess.dcx().fatal("only Apple targets currently support deployment version info")
diff --git a/compiler/rustc_target/src/spec/base/apple/mod.rs b/compiler/rustc_target/src/spec/base/apple/mod.rs
index b752a344395..b1fe49f76ca 100644
--- a/compiler/rustc_target/src/spec/base/apple/mod.rs
+++ b/compiler/rustc_target/src/spec/base/apple/mod.rs
@@ -1,5 +1,6 @@
 use std::borrow::Cow;
 use std::env;
+use std::num::ParseIntError;
 
 use crate::spec::{
     add_link_args, add_link_args_iter, cvs, Cc, DebuginfoKind, FramePointer, LinkArgs,
@@ -39,6 +40,25 @@ impl Arch {
         }
     }
 
+    /// The architecture name to forward to the linker.
+    fn ld_arch(self) -> &'static str {
+        // Supported architecture names can be found in the source:
+        // https://github.com/apple-oss-distributions/ld64/blob/ld64-951.9/src/abstraction/MachOFileAbstraction.hpp#L578-L648
+        match self {
+            Armv7k => "armv7k",
+            Armv7s => "armv7s",
+            Arm64 => "arm64",
+            Arm64e => "arm64e",
+            Arm64_32 => "arm64_32",
+            // ld64 doesn't understand i686, so fall back to i386 instead
+            //
+            // Same story when linking with cc, since that ends up invoking ld64.
+            I386 | I686 => "i386",
+            X86_64 => "x86_64",
+            X86_64h => "x86_64h",
+        }
+    }
+
     pub(crate) fn target_arch(self) -> Cow<'static, str> {
         Cow::Borrowed(match self {
             Armv7k | Armv7s => "arm",
@@ -97,53 +117,85 @@ impl TargetAbi {
 }
 
 fn pre_link_args(os: &'static str, arch: Arch, abi: TargetAbi) -> LinkArgs {
+    // From the man page for ld64 (`man ld`):
+    // > The linker accepts universal (multiple-architecture) input files,
+    // > but always creates a "thin" (single-architecture), standard Mach-O
+    // > output file. The architecture for the output file is specified using
+    // > the -arch option.
+    //
+    // The linker has heuristics to determine the desired architecture, but to
+    // be safe, and to avoid a warning, we set the architecture explicitly.
+    let mut args =
+        TargetOptions::link_args(LinkerFlavor::Darwin(Cc::No, Lld::No), &["-arch", arch.ld_arch()]);
+
+    // From the man page for ld64 (`man ld`):
+    // > This is set to indicate the platform, oldest supported version of
+    // > that platform that output is to be used on, and the SDK that the
+    // > output was built against. platform [...] may be one of the following
+    // > strings:
+    // > - macos
+    // > - ios
+    // > - tvos
+    // > - watchos
+    // > - bridgeos
+    // > - visionos
+    // > - xros
+    // > - mac-catalyst
+    // > - ios-simulator
+    // > - tvos-simulator
+    // > - watchos-simulator
+    // > - visionos-simulator
+    // > - xros-simulator
+    // > - driverkit
+    //
+    // Like with `-arch`, the linker can figure out the platform versions
+    // itself from the binaries being linked, but to be safe, we specify the
+    // desired versions here explicitly.
     let platform_name: StaticCow<str> = match abi {
         TargetAbi::Normal => os.into(),
         TargetAbi::Simulator => format!("{os}-simulator").into(),
         TargetAbi::MacCatalyst => "mac-catalyst".into(),
     };
-
     let min_version: StaticCow<str> = {
-        let (major, minor) = match os {
-            "ios" => ios_deployment_target(arch, abi.target_abi()),
-            "tvos" => tvos_deployment_target(),
-            "watchos" => watchos_deployment_target(),
-            "visionos" => visionos_deployment_target(),
-            "macos" => macos_deployment_target(arch),
-            _ => unreachable!(),
-        };
-        format!("{major}.{minor}").into()
+        let (major, minor, patch) = deployment_target(os, arch, abi);
+        format!("{major}.{minor}.{patch}").into()
     };
+    // Lie about the SDK version, we don't know it here
     let sdk_version = min_version.clone();
-
-    let mut args = TargetOptions::link_args(
-        LinkerFlavor::Darwin(Cc::No, Lld::No),
-        &["-arch", arch.target_name(), "-platform_version"],
-    );
     add_link_args_iter(
         &mut args,
         LinkerFlavor::Darwin(Cc::No, Lld::No),
-        [platform_name, min_version, sdk_version].into_iter(),
+        ["-platform_version".into(), platform_name, min_version, sdk_version].into_iter(),
     );
+
     if abi != TargetAbi::MacCatalyst {
+        // CC forwards the `-arch` to the linker, so we use the same value
+        // here intentionally.
         add_link_args(
             &mut args,
             LinkerFlavor::Darwin(Cc::Yes, Lld::No),
-            &["-arch", arch.target_name()],
+            &["-arch", arch.ld_arch()],
         );
     } else {
         add_link_args_iter(
             &mut args,
             LinkerFlavor::Darwin(Cc::Yes, Lld::No),
-            ["-target".into(), mac_catalyst_llvm_target(arch).into()].into_iter(),
+            ["-target".into(), llvm_target(os, arch, abi)].into_iter(),
         );
     }
 
     args
 }
 
-pub(crate) fn opts(os: &'static str, arch: Arch, abi: TargetAbi) -> TargetOptions {
-    TargetOptions {
+/// Get the base target options, LLVM target and `target_arch` from the three
+/// things that uniquely identify Rust's Apple targets: The OS, the
+/// architecture, and the ABI.
+pub(crate) fn base(
+    os: &'static str,
+    arch: Arch,
+    abi: TargetAbi,
+) -> (TargetOptions, StaticCow<str>, StaticCow<str>) {
+    let opts = TargetOptions {
         abi: abi.target_abi().into(),
         os: os.into(),
         cpu: arch.target_cpu(abi).into(),
@@ -193,10 +245,11 @@ pub(crate) fn opts(os: &'static str, arch: Arch, abi: TargetAbi) -> TargetOption
         link_env: Cow::Borrowed(&[(Cow::Borrowed("ZERO_AR_DATE"), Cow::Borrowed("1"))]),
 
         ..Default::default()
-    }
+    };
+    (opts, llvm_target(os, arch, abi), arch.target_arch())
 }
 
-pub fn sdk_version(platform: u32) -> Option<(u32, u32)> {
+pub fn sdk_version(platform: u32) -> Option<(u16, u8)> {
     // NOTE: These values are from an arbitrary point in time but shouldn't make it into the final
     // binary since the final link command will have the current SDK version passed to it.
     match platform {
@@ -230,58 +283,108 @@ pub fn platform(target: &Target) -> Option<u32> {
     })
 }
 
-pub fn deployment_target(target: &Target) -> Option<(u32, u32)> {
-    let (major, minor) = match &*target.os {
-        "macos" => {
-            // This does not need to be specific. It just needs to handle x86 vs M1.
-            let arch = match target.arch.as_ref() {
-                "x86" | "x86_64" => X86_64,
-                "arm64e" => Arm64e,
-                _ => Arm64,
-            };
-            macos_deployment_target(arch)
-        }
-        "ios" => {
-            let arch = match target.arch.as_ref() {
-                "arm64e" => Arm64e,
-                _ => Arm64,
-            };
-            ios_deployment_target(arch, &target.abi)
-        }
-        "watchos" => watchos_deployment_target(),
-        "tvos" => tvos_deployment_target(),
-        "visionos" => visionos_deployment_target(),
-        _ => return None,
+/// Hack for calling `deployment_target` outside of this module.
+pub fn deployment_target_for_target(target: &Target) -> (u16, u8, u8) {
+    let arch = if target.llvm_target.starts_with("arm64e") {
+        Arch::Arm64e
+    } else if target.arch == "aarch64" {
+        Arch::Arm64
+    } else {
+        // Dummy architecture, only used by `deployment_target` anyhow
+        Arch::X86_64
     };
-
-    Some((major, minor))
+    let abi = match &*target.abi {
+        "macabi" => TargetAbi::MacCatalyst,
+        "sim" => TargetAbi::Simulator,
+        "" => TargetAbi::Normal,
+        abi => unreachable!("invalid abi '{abi}' for Apple target"),
+    };
+    deployment_target(&target.os, arch, abi)
 }
 
-fn from_set_deployment_target(var_name: &str) -> Option<(u32, u32)> {
-    let deployment_target = env::var(var_name).ok()?;
-    let (unparsed_major, unparsed_minor) = deployment_target.split_once('.')?;
-    let (major, minor) = (unparsed_major.parse().ok()?, unparsed_minor.parse().ok()?);
+/// Get the deployment target based on the standard environment variables, or
+/// fall back to a sane default.
+fn deployment_target(os: &str, arch: Arch, abi: TargetAbi) -> (u16, u8, u8) {
+    // When bumping a version in here, remember to update the platform-support
+    // docs too.
+    //
+    // NOTE: If you are looking for the default deployment target, prefer
+    // `rustc --print deployment-target`, as the default here may change in
+    // future `rustc` versions.
+
+    // Minimum operating system versions currently supported by `rustc`.
+    let os_min = match os {
+        "macos" => (10, 12, 0),
+        "ios" => (10, 0, 0),
+        "tvos" => (10, 0, 0),
+        "watchos" => (5, 0, 0),
+        "visionos" => (1, 0, 0),
+        _ => unreachable!("tried to get deployment target for non-Apple platform"),
+    };
 
-    Some((major, minor))
-}
+    // On certain targets it makes sense to raise the minimum OS version.
+    let min = match (os, arch, abi) {
+        // Use 11.0 on Aarch64 as that's the earliest version with M1 support.
+        ("macos", Arch::Arm64 | Arch::Arm64e, _) => (11, 0, 0),
+        ("ios", Arch::Arm64e, _) => (14, 0, 0),
+        // Mac Catalyst defaults to 13.1 in Clang.
+        ("ios", _, TargetAbi::MacCatalyst) => (13, 1, 0),
+        _ => os_min,
+    };
 
-fn macos_default_deployment_target(arch: Arch) -> (u32, u32) {
-    match arch {
-        Arm64 | Arm64e => (11, 0),
-        _ => (10, 12),
-    }
-}
+    // The environment variable used to fetch the deployment target.
+    let env_var = match os {
+        "macos" => "MACOSX_DEPLOYMENT_TARGET",
+        "ios" => "IPHONEOS_DEPLOYMENT_TARGET",
+        "watchos" => "WATCHOS_DEPLOYMENT_TARGET",
+        "tvos" => "TVOS_DEPLOYMENT_TARGET",
+        "visionos" => "XROS_DEPLOYMENT_TARGET",
+        _ => unreachable!("tried to get deployment target env var for non-Apple platform"),
+    };
 
-fn macos_deployment_target(arch: Arch) -> (u32, u32) {
-    // If you are looking for the default deployment target, prefer `rustc --print deployment-target`.
-    // Note: If bumping this version, remember to update it in the rustc/platform-support docs.
-    from_set_deployment_target("MACOSX_DEPLOYMENT_TARGET")
-        .unwrap_or_else(|| macos_default_deployment_target(arch))
+    if let Ok(deployment_target) = env::var(env_var) {
+        match parse_version(&deployment_target) {
+            // It is common that the deployment target is set too low, e.g. on
+            // macOS Aarch64 to also target older x86_64, the user may set a
+            // lower deployment target than supported.
+            //
+            // To avoid such issues, we silently raise the deployment target
+            // here.
+            // FIXME: We want to show a warning when `version < os_min`.
+            Ok(version) => version.max(min),
+            // FIXME: Report erroneous environment variable to user.
+            Err(_) => min,
+        }
+    } else {
+        min
+    }
 }
 
-pub(crate) fn macos_llvm_target(arch: Arch) -> String {
-    let (major, minor) = macos_deployment_target(arch);
-    format!("{}-apple-macosx{}.{}.0", arch.target_name(), major, minor)
+/// Generate the target triple that we need to pass to LLVM and/or Clang.
+fn llvm_target(os: &str, arch: Arch, abi: TargetAbi) -> StaticCow<str> {
+    // The target triple depends on the deployment target, and is required to
+    // enable features such as cross-language LTO, and for picking the right
+    // Mach-O commands.
+    //
+    // Certain optimizations also depend on the deployment target.
+    let (major, minor, patch) = deployment_target(os, arch, abi);
+    let arch = arch.target_name();
+    // Convert to the "canonical" OS name used by LLVM:
+    // https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/TargetParser/Triple.cpp#L236-L282
+    let os = match os {
+        "macos" => "macosx",
+        "ios" => "ios",
+        "watchos" => "watchos",
+        "tvos" => "tvos",
+        "visionos" => "xros",
+        _ => unreachable!("tried to get LLVM target OS for non-Apple platform"),
+    };
+    let environment = match abi {
+        TargetAbi::Normal => "",
+        TargetAbi::MacCatalyst => "-macabi",
+        TargetAbi::Simulator => "-simulator",
+    };
+    format!("{arch}-apple-{os}{major}.{minor}.{patch}{environment}").into()
 }
 
 fn link_env_remove(os: &'static str) -> StaticCow<[StaticCow<str>]> {
@@ -321,83 +424,19 @@ fn link_env_remove(os: &'static str) -> StaticCow<[StaticCow<str>]> {
     }
 }
 
-fn ios_deployment_target(arch: Arch, abi: &str) -> (u32, u32) {
-    // If you are looking for the default deployment target, prefer `rustc --print deployment-target`.
-    // Note: If bumping this version, remember to update it in the rustc/platform-support docs.
-    let (major, minor) = match (arch, abi) {
-        (Arm64e, _) => (14, 0),
-        // Mac Catalyst defaults to 13.1 in Clang.
-        (_, "macabi") => (13, 1),
-        _ => (10, 0),
-    };
-    from_set_deployment_target("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or((major, minor))
-}
-
-pub(crate) fn ios_llvm_target(arch: Arch) -> String {
-    // Modern iOS tooling extracts information about deployment target
-    // from LC_BUILD_VERSION. This load command will only be emitted when
-    // we build with a version specific `llvm_target`, with the version
-    // set high enough. Luckily one LC_BUILD_VERSION is enough, for Xcode
-    // to pick it up (since std and core are still built with the fallback
-    // of version 7.0 and hence emit the old LC_IPHONE_MIN_VERSION).
-    let (major, minor) = ios_deployment_target(arch, "");
-    format!("{}-apple-ios{}.{}.0", arch.target_name(), major, minor)
-}
-
-pub(crate) fn mac_catalyst_llvm_target(arch: Arch) -> String {
-    let (major, minor) = ios_deployment_target(arch, "macabi");
-    format!("{}-apple-ios{}.{}.0-macabi", arch.target_name(), major, minor)
-}
-
-pub(crate) fn ios_sim_llvm_target(arch: Arch) -> String {
-    let (major, minor) = ios_deployment_target(arch, "sim");
-    format!("{}-apple-ios{}.{}.0-simulator", arch.target_name(), major, minor)
-}
-
-fn tvos_deployment_target() -> (u32, u32) {
-    // If you are looking for the default deployment target, prefer `rustc --print deployment-target`.
-    // Note: If bumping this version, remember to update it in the rustc platform-support docs.
-    from_set_deployment_target("TVOS_DEPLOYMENT_TARGET").unwrap_or((10, 0))
-}
-
-pub(crate) fn tvos_llvm_target(arch: Arch) -> String {
-    let (major, minor) = tvos_deployment_target();
-    format!("{}-apple-tvos{}.{}.0", arch.target_name(), major, minor)
-}
-
-pub(crate) fn tvos_sim_llvm_target(arch: Arch) -> String {
-    let (major, minor) = tvos_deployment_target();
-    format!("{}-apple-tvos{}.{}.0-simulator", arch.target_name(), major, minor)
-}
-
-fn watchos_deployment_target() -> (u32, u32) {
-    // If you are looking for the default deployment target, prefer `rustc --print deployment-target`.
-    // Note: If bumping this version, remember to update it in the rustc platform-support docs.
-    from_set_deployment_target("WATCHOS_DEPLOYMENT_TARGET").unwrap_or((5, 0))
-}
-
-pub(crate) fn watchos_llvm_target(arch: Arch) -> String {
-    let (major, minor) = watchos_deployment_target();
-    format!("{}-apple-watchos{}.{}.0", arch.target_name(), major, minor)
-}
-
-pub(crate) fn watchos_sim_llvm_target(arch: Arch) -> String {
-    let (major, minor) = watchos_deployment_target();
-    format!("{}-apple-watchos{}.{}.0-simulator", arch.target_name(), major, minor)
-}
-
-fn visionos_deployment_target() -> (u32, u32) {
-    // If you are looking for the default deployment target, prefer `rustc --print deployment-target`.
-    // Note: If bumping this version, remember to update it in the rustc platform-support docs.
-    from_set_deployment_target("XROS_DEPLOYMENT_TARGET").unwrap_or((1, 0))
-}
-
-pub(crate) fn visionos_llvm_target(arch: Arch) -> String {
-    let (major, minor) = visionos_deployment_target();
-    format!("{}-apple-visionos{}.{}.0", arch.target_name(), major, minor)
-}
-
-pub(crate) fn visionos_sim_llvm_target(arch: Arch) -> String {
-    let (major, minor) = visionos_deployment_target();
-    format!("{}-apple-visionos{}.{}.0-simulator", arch.target_name(), major, minor)
+/// Parse an OS version triple (SDK version or deployment target).
+///
+/// The size of the returned numbers here are limited by Mach-O's
+/// `LC_BUILD_VERSION`.
+fn parse_version(version: &str) -> Result<(u16, u8, u8), ParseIntError> {
+    if let Some((major, minor)) = version.split_once('.') {
+        let major = major.parse()?;
+        if let Some((minor, patch)) = minor.split_once('.') {
+            Ok((major, minor.parse()?, patch.parse()?))
+        } else {
+            Ok((major, minor.parse()?, 0))
+        }
+    } else {
+        Ok((version.parse()?, 0, 0))
+    }
 }
diff --git a/compiler/rustc_target/src/spec/base/apple/tests.rs b/compiler/rustc_target/src/spec/base/apple/tests.rs
index 7a985ad4dc0..d55b44e3c9e 100644
--- a/compiler/rustc_target/src/spec/base/apple/tests.rs
+++ b/compiler/rustc_target/src/spec/base/apple/tests.rs
@@ -1,3 +1,4 @@
+use super::parse_version;
 use crate::spec::targets::{
     aarch64_apple_darwin, aarch64_apple_ios_sim, aarch64_apple_visionos_sim,
     aarch64_apple_watchos_sim, i686_apple_darwin, x86_64_apple_darwin, x86_64_apple_ios,
@@ -42,3 +43,11 @@ fn macos_link_environment_unmodified() {
         );
     }
 }
+
+#[test]
+fn test_parse_version() {
+    assert_eq!(parse_version("10"), Ok((10, 0, 0)));
+    assert_eq!(parse_version("10.12"), Ok((10, 12, 0)));
+    assert_eq!(parse_version("10.12.6"), Ok((10, 12, 6)));
+    assert_eq!(parse_version("9999.99.99"), Ok((9999, 99, 99)));
+}
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index 6abd8a0d6b7..58d47c201f0 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -60,8 +60,8 @@ pub mod crt_objects;
 
 mod base;
 pub use base::apple::{
-    deployment_target as current_apple_deployment_target, platform as current_apple_platform,
-    sdk_version as current_apple_sdk_version,
+    deployment_target_for_target as current_apple_deployment_target,
+    platform as current_apple_platform, sdk_version as current_apple_sdk_version,
 };
 pub use base::avr_gnu::ef_avr_arch;
 
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs
index 912392c9fef..1061633be17 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs
@@ -1,20 +1,10 @@
-use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
-    base.cpu = "apple-m1".into();
-    base.max_atomic_width = Some(128);
-
-    // FIXME: The leak sanitizer currently fails the tests, see #88132.
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("macos", Arch::Arm64, TargetAbi::Normal);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // MACOSX_DEPLOYMENT_TARGET. To enable cross-language LTO to work
-        // correctly, we do too.
-        llvm_target: macos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 macOS (11.0+, Big Sur+)".into()),
             tier: Some(1),
@@ -23,11 +13,15 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             mcount: "\u{1}mcount".into(),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            cpu: "apple-m1".into(),
+            max_atomic_width: Some(128),
+            // FIXME: The leak sanitizer currently fails the tests, see #88132.
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs
index 7f3a86f3256..caeb9a121e4 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs
@@ -1,17 +1,10 @@
-use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::Normal);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::Arm64, TargetAbi::Normal);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // IPHONEOS_DEPLOYMENT_TARGET.
-        // This is required for the target to pick the right
-        // MACH-O commands, so we do too.
-        llvm_target: ios_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 iOS".into()),
             tier: Some(2),
@@ -20,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_macabi.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_macabi.rs
index e60eeda4295..eee9eca3bcb 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_macabi.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_macabi.rs
@@ -1,13 +1,10 @@
-use crate::spec::base::apple::{mac_catalyst_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::MacCatalyst);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::Arm64, TargetAbi::MacCatalyst);
     Target {
-        llvm_target: mac_catalyst_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Apple Catalyst on ARM64".into()),
             tier: Some(2),
@@ -16,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a12".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_sim.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_sim.rs
index d605e22701a..ee58038301d 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_sim.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios_sim.rs
@@ -1,17 +1,10 @@
-use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::Simulator);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::Arm64, TargetAbi::Simulator);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // IPHONEOS_DEPLOYMENT_TARGET.
-        // This is required for the simulator target to pick the right
-        // MACH-O commands, so we do too.
-        llvm_target: ios_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Apple iOS Simulator on ARM64".into()),
             tier: Some(2),
@@ -20,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos.rs
index fc774ab94c8..baca863d442 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{opts, tvos_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
+    let (opts, llvm_target, arch) = base("tvos", Arch::Arm64, TargetAbi::Normal);
     Target {
-        llvm_target: tvos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 tvOS".into()),
             tier: Some(3),
@@ -13,12 +13,12 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..opts("tvos", arch, TargetAbi::Normal)
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos_sim.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos_sim.rs
index 8e8bb1efc9d..1a48f8c5acf 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos_sim.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_tvos_sim.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{opts, tvos_sim_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
+    let (opts, llvm_target, arch) = base("tvos", Arch::Arm64, TargetAbi::Simulator);
     Target {
-        llvm_target: tvos_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 tvOS Simulator".into()),
             tier: Some(3),
@@ -13,12 +13,12 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..opts("tvos", arch, TargetAbi::Simulator)
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos.rs
index 16c7d72daed..1424126134d 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos.rs
@@ -1,13 +1,10 @@
-use crate::spec::base::apple::{opts, visionos_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("visionos", arch, TargetAbi::Normal);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("visionos", Arch::Arm64, TargetAbi::Normal);
     Target {
-        llvm_target: visionos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple visionOS".into()),
             tier: Some(3),
@@ -16,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a16".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos_sim.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos_sim.rs
index 42973e0268e..d7226e02ecb 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos_sim.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_visionos_sim.rs
@@ -1,13 +1,10 @@
-use crate::spec::base::apple::{opts, visionos_sim_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("visionos", arch, TargetAbi::Simulator);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("visionos", Arch::Arm64, TargetAbi::Simulator);
     Target {
-        llvm_target: visionos_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple visionOS simulator".into()),
             tier: Some(3),
@@ -16,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a16".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs
index 03fc2518d1d..1940a568b39 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let base = opts("watchos", Arch::Arm64, TargetAbi::Normal);
+    let (opts, llvm_target, arch) = base("watchos", Arch::Arm64, TargetAbi::Normal);
     Target {
-        llvm_target: "aarch64-apple-watchos".into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple WatchOS".into()),
             tier: Some(3),
@@ -13,13 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: "aarch64".into(),
+        arch,
         options: TargetOptions {
             features: "+v8a,+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             dynamic_linking: false,
             position_independent_executables: true,
-            ..base
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos_sim.rs b/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos_sim.rs
index 13e2b961794..6beef11c504 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos_sim.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos_sim.rs
@@ -1,14 +1,10 @@
-use crate::spec::base::apple::{opts, watchos_sim_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64;
+    let (opts, llvm_target, arch) = base("watchos", Arch::Arm64, TargetAbi::Simulator);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // WATCHOS_DEPLOYMENT_TARGET.
-        // This is required for the simulator target to pick the right
-        // MACH-O commands, so we do too.
-        llvm_target: watchos_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple WatchOS Simulator".into()),
             tier: Some(3),
@@ -17,12 +13,12 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..opts("watchos", arch, TargetAbi::Simulator)
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/arm64_32_apple_watchos.rs b/compiler/rustc_target/src/spec/targets/arm64_32_apple_watchos.rs
index f292ee7f563..e9f4d9330d5 100644
--- a/compiler/rustc_target/src/spec/targets/arm64_32_apple_watchos.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64_32_apple_watchos.rs
@@ -1,11 +1,10 @@
-use crate::spec::base::apple::{opts, watchos_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64_32;
-    let base = opts("watchos", arch, TargetAbi::Normal);
+    let (opts, llvm_target, arch) = base("watchos", Arch::Arm64_32, TargetAbi::Normal);
     Target {
-        llvm_target: watchos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Arm Apple WatchOS 64-bit with 32-bit pointers".into()),
             tier: Some(3),
@@ -14,13 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 32,
         data_layout: "e-m:o-p:32:32-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: "aarch64".into(),
+        arch,
         options: TargetOptions {
             features: "+v8a,+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
             dynamic_linking: false,
             position_independent_executables: true,
-            ..base
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs
index e86b8358a9c..4f7c945b9fe 100644
--- a/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs
@@ -1,20 +1,10 @@
-use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64e;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
-    base.cpu = "apple-m1".into();
-    base.max_atomic_width = Some(128);
-
-    // FIXME: The leak sanitizer currently fails the tests, see #88132.
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("macos", Arch::Arm64e, TargetAbi::Normal);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // MACOSX_DEPLOYMENT_TARGET. To enable cross-language LTO to work
-        // correctly, we do too.
-        llvm_target: macos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64e Apple Darwin".into()),
             tier: Some(3),
@@ -23,11 +13,15 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             mcount: "\u{1}mcount".into(),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            cpu: "apple-m1".into(),
+            max_atomic_width: Some(128),
+            // FIXME: The leak sanitizer currently fails the tests, see #88132.
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs b/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs
index fcb850f44bc..8f192fe2886 100644
--- a/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs
@@ -1,17 +1,10 @@
-use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Arm64e;
-    let mut base = opts("ios", arch, TargetAbi::Normal);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::Arm64e, TargetAbi::Normal);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // IPHONEOS_DEPLOYMENT_TARGET.
-        // This is required for the target to pick the right
-        // MACH-O commands, so we do too.
-        llvm_target: ios_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64e Apple iOS".into()),
             tier: Some(3),
@@ -20,12 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 64,
         data_layout: "e-m:o-i64:64-i128:128-n32:64-S128-Fn32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a12,+v8.3a,+pauth".into(),
             max_atomic_width: Some(128),
             frame_pointer: FramePointer::NonLeaf,
-            ..base
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs b/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs
index 35de6b35eaf..8fe27f60e0a 100644
--- a/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs
+++ b/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Armv7k;
+    let (opts, llvm_target, arch) = base("watchos", Arch::Armv7k, TargetAbi::Normal);
     Target {
-        llvm_target: "armv7k-apple-watchos".into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Armv7-A Apple WatchOS".into()),
             tier: Some(3),
@@ -13,13 +13,13 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 32,
         data_layout: "e-m:o-p:32:32-Fi8-i64:64-a:0:32-n32-S128".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+v7,+vfp4,+neon".into(),
             max_atomic_width: Some(64),
             dynamic_linking: false,
             position_independent_executables: true,
-            ..opts("watchos", arch, TargetAbi::Normal)
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/armv7s_apple_ios.rs b/compiler/rustc_target/src/spec/targets/armv7s_apple_ios.rs
index 41eaa79d3eb..deee6985f1a 100644
--- a/compiler/rustc_target/src/spec/targets/armv7s_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/armv7s_apple_ios.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::Armv7s;
+    let (opts, llvm_target, arch) = base("ios", Arch::Armv7s, TargetAbi::Normal);
     Target {
-        llvm_target: ios_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Armv7-A Apple-A6 Apple iOS".into()),
             tier: Some(3),
@@ -13,11 +13,11 @@ pub(crate) fn target() -> Target {
         },
         pointer_width: 32,
         data_layout: "e-m:o-p:32:32-Fi8-f64:32:64-v64:32:64-v128:32:128-a:0:32-n32-S32".into(),
-        arch: arch.target_arch(),
+        arch,
         options: TargetOptions {
             features: "+v7,+vfp4,+neon".into(),
             max_atomic_width: Some(64),
-            ..opts("ios", arch, TargetAbi::Normal)
+            ..opts
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/i386_apple_ios.rs b/compiler/rustc_target/src/spec/targets/i386_apple_ios.rs
index 263393fd536..dc14cb3ec76 100644
--- a/compiler/rustc_target/src/spec/targets/i386_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/i386_apple_ios.rs
@@ -1,17 +1,12 @@
-use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::I386;
     // i386-apple-ios is a simulator target, even though it isn't declared
     // that way in the target name like the other ones...
-    let abi = TargetAbi::Simulator;
+    let (opts, llvm_target, arch) = base("ios", Arch::I386, TargetAbi::Simulator);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // IPHONEOS_DEPLOYMENT_TARGET.
-        // This is required for the target to pick the right
-        // MACH-O commands, so we do too.
-        llvm_target: ios_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("32-bit x86 iOS".into()),
             tier: Some(3),
@@ -22,7 +17,7 @@ pub(crate) fn target() -> Target {
         data_layout: "e-m:o-p:32:32-p270:32:32-p271:32:32-p272:64:64-\
             i128:128-f64:32:64-f80:128-n8:16:32-S128"
             .into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { max_atomic_width: Some(64), ..opts("ios", arch, abi) },
+        arch,
+        options: TargetOptions { max_atomic_width: Some(64), ..opts },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs
index f173e8b7e4a..67afe35bee4 100644
--- a/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs
@@ -1,21 +1,12 @@
-use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    // ld64 only understands i386 and not i686
-    let arch = Arch::I386;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
-    base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m32"]);
-    base.frame_pointer = FramePointer::Always;
+    let (mut opts, llvm_target, arch) = base("macos", Arch::I686, TargetAbi::Normal);
+    opts.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m32"]);
 
     Target {
-        // Clang automatically chooses a more specific target based on
-        // MACOSX_DEPLOYMENT_TARGET. To enable cross-language LTO to work
-        // correctly, we do too.
-        //
-        // While ld64 doesn't understand i686, LLVM does.
-        llvm_target: macos_llvm_target(Arch::I686).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("32-bit macOS (10.12+, Sierra+)".into()),
             tier: Some(3),
@@ -26,7 +17,12 @@ pub(crate) fn target() -> Target {
         data_layout: "e-m:o-p:32:32-p270:32:32-p271:32:32-p272:64:64-\
             i128:128-f64:32:64-f80:128-n8:16:32-S128"
             .into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
+        arch,
+        options: TargetOptions {
+            mcount: "\u{1}mcount".into(),
+            max_atomic_width: Some(64),
+            frame_pointer: FramePointer::Always,
+            ..opts
+        },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/x86_64_apple_darwin.rs
index 7bf18026735..e7f14aa9209 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_apple_darwin.rs
@@ -1,20 +1,11 @@
-use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
-    base.max_atomic_width = Some(128); // penryn+ supports cmpxchg16b
-    base.frame_pointer = FramePointer::Always;
-    base.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
-    base.supported_sanitizers =
-        SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::LEAK | SanitizerSet::THREAD;
-
+    let (mut opts, llvm_target, arch) = base("macos", Arch::X86_64, TargetAbi::Normal);
+    opts.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
     Target {
-        // Clang automatically chooses a more specific target based on
-        // MACOSX_DEPLOYMENT_TARGET. To enable cross-language LTO to work
-        // correctly, we do too.
-        llvm_target: macos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("64-bit macOS (10.12+, Sierra+)".into()),
             tier: Some(1),
@@ -24,7 +15,16 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
+        arch,
+        options: TargetOptions {
+            mcount: "\u{1}mcount".into(),
+            max_atomic_width: Some(128), // penryn+ supports cmpxchg16b
+            frame_pointer: FramePointer::Always,
+            supported_sanitizers: SanitizerSet::ADDRESS
+                | SanitizerSet::CFI
+                | SanitizerSet::LEAK
+                | SanitizerSet::THREAD,
+            ..opts
+        },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_apple_ios.rs b/compiler/rustc_target/src/spec/targets/x86_64_apple_ios.rs
index 76a0bb1d91e..847c4f011f9 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_apple_ios.rs
@@ -1,15 +1,12 @@
-use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64;
     // x86_64-apple-ios is a simulator target, even though it isn't declared
     // that way in the target name like the other ones...
-    let mut base = opts("ios", arch, TargetAbi::Simulator);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::X86_64, TargetAbi::Simulator);
     Target {
-        llvm_target: ios_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("64-bit x86 iOS".into()),
             tier: Some(2),
@@ -19,7 +16,11 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { max_atomic_width: Some(128), ..base },
+        arch,
+        options: TargetOptions {
+            max_atomic_width: Some(128),
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD,
+            ..opts
+        },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_apple_ios_macabi.rs b/compiler/rustc_target/src/spec/targets/x86_64_apple_ios_macabi.rs
index f66efbbf709..042079f800b 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_apple_ios_macabi.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_apple_ios_macabi.rs
@@ -1,13 +1,10 @@
-use crate::spec::base::apple::{mac_catalyst_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64;
-    let mut base = opts("ios", arch, TargetAbi::MacCatalyst);
-    base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD;
-
+    let (opts, llvm_target, arch) = base("ios", Arch::X86_64, TargetAbi::MacCatalyst);
     Target {
-        llvm_target: mac_catalyst_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("Apple Catalyst on x86_64".into()),
             tier: Some(2),
@@ -17,7 +14,11 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { max_atomic_width: Some(128), ..base },
+        arch,
+        options: TargetOptions {
+            max_atomic_width: Some(128),
+            supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD,
+            ..opts
+        },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_apple_tvos.rs b/compiler/rustc_target/src/spec/targets/x86_64_apple_tvos.rs
index f9d5f6e77d9..94708609790 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_apple_tvos.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_apple_tvos.rs
@@ -1,13 +1,12 @@
-use crate::spec::base::apple::{opts, tvos_sim_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64;
     // x86_64-apple-tvos is a simulator target, even though it isn't declared
     // that way in the target name like the other ones...
-    let abi = TargetAbi::Simulator;
+    let (opts, llvm_target, arch) = base("tvos", Arch::X86_64, TargetAbi::Simulator);
     Target {
-        llvm_target: tvos_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("x86 64-bit tvOS".into()),
             tier: Some(3),
@@ -17,7 +16,7 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { max_atomic_width: Some(128), ..opts("tvos", arch, abi) },
+        arch,
+        options: TargetOptions { max_atomic_width: Some(128), ..opts },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_apple_watchos_sim.rs b/compiler/rustc_target/src/spec/targets/x86_64_apple_watchos_sim.rs
index 1fced10c895..1dab9598860 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_apple_watchos_sim.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_apple_watchos_sim.rs
@@ -1,10 +1,10 @@
-use crate::spec::base::apple::{opts, watchos_sim_llvm_target, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64;
+    let (opts, llvm_target, arch) = base("watchos", Arch::X86_64, TargetAbi::Simulator);
     Target {
-        llvm_target: watchos_sim_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("x86 64-bit Apple WatchOS simulator".into()),
             tier: Some(3),
@@ -14,10 +14,7 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions {
-            max_atomic_width: Some(128),
-            ..opts("watchos", arch, TargetAbi::Simulator)
-        },
+        arch,
+        options: TargetOptions { max_atomic_width: Some(128), ..opts },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64h_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/x86_64h_apple_darwin.rs
index 9b9ffe4e536..f44bc660a62 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64h_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64h_apple_darwin.rs
@@ -1,13 +1,12 @@
-use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
+use crate::spec::base::apple::{base, Arch, TargetAbi};
 use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, SanitizerSet, Target, TargetOptions};
 
 pub(crate) fn target() -> Target {
-    let arch = Arch::X86_64h;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
-    base.max_atomic_width = Some(128);
-    base.frame_pointer = FramePointer::Always;
-    base.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
-    base.supported_sanitizers =
+    let (mut opts, llvm_target, arch) = base("macos", Arch::X86_64h, TargetAbi::Normal);
+    opts.max_atomic_width = Some(128);
+    opts.frame_pointer = FramePointer::Always;
+    opts.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
+    opts.supported_sanitizers =
         SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::LEAK | SanitizerSet::THREAD;
 
     // x86_64h is core2-avx without a few of the features which would otherwise
@@ -20,19 +19,16 @@ pub(crate) fn target() -> Target {
     // It would be nice if this were not the case, but fixing it seems tricky
     // (and given that the main use-case for this target is for use in universal
     // binaries, probably not that important).
-    base.features = "-rdrnd,-aes,-pclmul,-rtm,-fsgsbase".into();
+    opts.features = "-rdrnd,-aes,-pclmul,-rtm,-fsgsbase".into();
     // Double-check that the `cpu` is what we expect (if it's not the list above
     // may need updating).
     assert_eq!(
-        base.cpu, "core-avx2",
+        opts.cpu, "core-avx2",
         "you need to adjust the feature list in x86_64h-apple-darwin if you change this",
     );
 
     Target {
-        // Clang automatically chooses a more specific target based on
-        // MACOSX_DEPLOYMENT_TARGET. To enable cross-language LTO to work
-        // correctly, we do too.
-        llvm_target: macos_llvm_target(arch).into(),
+        llvm_target,
         metadata: crate::spec::TargetMetadata {
             description: Some("macOS with late-gen Intel (at least Haswell)".into()),
             tier: Some(3),
@@ -42,7 +38,7 @@ pub(crate) fn target() -> Target {
         pointer_width: 64,
         data_layout:
             "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128".into(),
-        arch: arch.target_arch(),
-        options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
+        arch,
+        options: TargetOptions { mcount: "\u{1}mcount".into(), ..opts },
     }
 }