diff --git a/compiler/rustc_target/src/lib.rs b/compiler/rustc_target/src/lib.rs
index ecc91ab9a310e..2962b0ab61e7f 100644
--- a/compiler/rustc_target/src/lib.rs
+++ b/compiler/rustc_target/src/lib.rs
@@ -12,11 +12,13 @@
 #![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
 #![doc(rust_logo)]
 #![feature(assert_matches)]
+#![feature(fn_traits)]
 #![feature(iter_intersperse)]
 #![feature(let_chains)]
 #![feature(min_exhaustive_patterns)]
 #![feature(rustc_attrs)]
 #![feature(rustdoc_internals)]
+#![feature(unboxed_closures)]
 // tidy-alphabetical-end
 
 use std::path::{Path, PathBuf};
diff --git a/compiler/rustc_target/src/spec/base/apple/mod.rs b/compiler/rustc_target/src/spec/base/apple/mod.rs
index 055420090835d..391ec6078c27b 100644
--- a/compiler/rustc_target/src/spec/base/apple/mod.rs
+++ b/compiler/rustc_target/src/spec/base/apple/mod.rs
@@ -1,6 +1,7 @@
 use std::{borrow::Cow, env};
 
-use crate::spec::{add_link_args, add_link_args_iter};
+use crate::spec::link_args::LazyLinkArgsState;
+use crate::spec::{add_link_args, add_link_args_iter, MaybeLazy};
 use crate::spec::{cvs, Cc, DebuginfoKind, FramePointer, LinkArgs, LinkerFlavor, Lld};
 use crate::spec::{SplitDebuginfo, StackProbeType, StaticCow, Target, TargetOptions};
 
@@ -94,7 +95,10 @@ impl TargetAbi {
     }
 }
 
-fn pre_link_args(os: &'static str, arch: Arch, abi: TargetAbi) -> LinkArgs {
+pub(crate) type ApplePreLinkArgs =
+    (/*os:*/ &'static str, /*arch:*/ Arch, /*abi:*/ TargetAbi);
+
+pub(crate) fn pre_link_args((os, arch, abi): ApplePreLinkArgs) -> LinkArgs {
     let platform_name: StaticCow<str> = match abi {
         TargetAbi::Normal => os.into(),
         TargetAbi::Simulator => format!("{os}-simulator").into(),
@@ -114,7 +118,9 @@ fn pre_link_args(os: &'static str, arch: Arch, abi: TargetAbi) -> LinkArgs {
     };
     let sdk_version = min_version.clone();
 
-    let mut args = TargetOptions::link_args(
+    let mut args = LinkArgs::new();
+    add_link_args(
+        &mut args,
         LinkerFlavor::Darwin(Cc::No, Lld::No),
         &["-arch", arch.target_name(), "-platform_version"],
     );
@@ -151,7 +157,7 @@ pub fn opts(os: &'static str, arch: Arch, abi: TargetAbi) -> TargetOptions {
         // macOS has -dead_strip, which doesn't rely on function_sections
         function_sections: false,
         dynamic_linking: true,
-        pre_link_args: pre_link_args(os, arch, abi),
+        pre_link_args: MaybeLazy::lazied(LazyLinkArgsState::Apple((os, arch, abi))),
         families: cvs!["unix"],
         is_like_osx: true,
         // LLVM notes that macOS 10.11+ and iOS 9+ default
diff --git a/compiler/rustc_target/src/spec/base/avr_gnu.rs b/compiler/rustc_target/src/spec/base/avr_gnu.rs
index 211d52f5b07ed..27bc5a1c9ea8a 100644
--- a/compiler/rustc_target/src/spec/base/avr_gnu.rs
+++ b/compiler/rustc_target/src/spec/base/avr_gnu.rs
@@ -4,8 +4,7 @@ use object::elf;
 /// A base target for AVR devices using the GNU toolchain.
 ///
 /// Requires GNU avr-gcc and avr-binutils on the host system.
-/// FIXME: Remove the second parameter when const string concatenation is possible.
-pub fn target(target_cpu: &'static str, mmcu: &'static str) -> Target {
+pub fn target(target_cpu: &'static str) -> Target {
     Target {
         arch: "avr".into(),
         metadata: crate::spec::TargetMetadata {
@@ -24,7 +23,10 @@ pub fn target(target_cpu: &'static str, mmcu: &'static str) -> Target {
 
             linker: Some("avr-gcc".into()),
             eh_frame_header: false,
-            pre_link_args: TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &[mmcu]),
+            pre_link_args: TargetOptions::link_args(
+                LinkerFlavor::Gnu(Cc::Yes, Lld::No),
+                &["-mmcu=atmega328"],
+            ),
             late_link_args: TargetOptions::link_args(
                 LinkerFlavor::Gnu(Cc::Yes, Lld::No),
                 &["-lgcc"],
diff --git a/compiler/rustc_target/src/spec/base/teeos.rs b/compiler/rustc_target/src/spec/base/teeos.rs
index 38d0a6d73140a..d50cf9c0db419 100644
--- a/compiler/rustc_target/src/spec/base/teeos.rs
+++ b/compiler/rustc_target/src/spec/base/teeos.rs
@@ -1,11 +1,15 @@
-use crate::spec::{add_link_args, Cc, LinkerFlavor, Lld, PanicStrategy, RelroLevel, TargetOptions};
+use crate::spec::{Cc, LinkerFlavor, Lld, PanicStrategy, RelroLevel, TargetOptions};
 
 pub fn opts() -> TargetOptions {
-    let lld_args = &["-zmax-page-size=4096", "-znow", "-ztext", "--execute-only"];
-    let cc_args = &["-Wl,-zmax-page-size=4096", "-Wl,-znow", "-Wl,-ztext", "-mexecute-only"];
-
-    let mut pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::No, Lld::No), lld_args);
-    add_link_args(&mut pre_link_args, LinkerFlavor::Gnu(Cc::Yes, Lld::No), cc_args);
+    let pre_link_args = {
+        const LLD_ARGS: &[&str] = &["-zmax-page-size=4096", "-znow", "-ztext", "--execute-only"];
+        const CC_ARGS: &[&str] =
+            &["-Wl,-zmax-page-size=4096", "-Wl,-znow", "-Wl,-ztext", "-mexecute-only"];
+        TargetOptions::link_args_list(&[
+            (LinkerFlavor::Gnu(Cc::No, Lld::No), LLD_ARGS),
+            (LinkerFlavor::Gnu(Cc::Yes, Lld::No), CC_ARGS),
+        ])
+    };
 
     TargetOptions {
         os: "teeos".into(),
diff --git a/compiler/rustc_target/src/spec/base/uefi_msvc.rs b/compiler/rustc_target/src/spec/base/uefi_msvc.rs
index e8acd6078e2ad..8d908144d58c2 100644
--- a/compiler/rustc_target/src/spec/base/uefi_msvc.rs
+++ b/compiler/rustc_target/src/spec/base/uefi_msvc.rs
@@ -9,12 +9,13 @@
 // the timer-interrupt. Device-drivers are required to use polling-based models. Furthermore, all
 // code runs in the same environment, no process separation is supported.
 
-use crate::spec::{base, LinkerFlavor, Lld, PanicStrategy, StackProbeType, TargetOptions};
+use crate::spec::{base, LinkerFlavor, Lld};
+use crate::spec::{PanicStrategy, StackProbeType, TargetOptions};
 
 pub fn opts() -> TargetOptions {
     let mut base = base::msvc::opts();
 
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Msvc(Lld::No),
         &[
             // Non-standard subsystems have no default entry-point in PE+ files. We have to define
diff --git a/compiler/rustc_target/src/spec/base/wasm.rs b/compiler/rustc_target/src/spec/base/wasm.rs
index f237391016e77..26d795834fb0b 100644
--- a/compiler/rustc_target/src/spec/base/wasm.rs
+++ b/compiler/rustc_target/src/spec/base/wasm.rs
@@ -1,7 +1,5 @@
-use crate::spec::{
-    add_link_args, cvs, Cc, LinkSelfContainedDefault, LinkerFlavor, PanicStrategy, RelocModel,
-    TargetOptions, TlsModel,
-};
+use crate::spec::{cvs, Cc, LinkSelfContainedDefault, LinkerFlavor, PanicStrategy};
+use crate::spec::{RelocModel, TargetOptions, TlsModel};
 
 pub fn options() -> TargetOptions {
     macro_rules! args {
@@ -48,8 +46,10 @@ pub fn options() -> TargetOptions {
         };
     }
 
-    let mut pre_link_args = TargetOptions::link_args(LinkerFlavor::WasmLld(Cc::No), args!(""));
-    add_link_args(&mut pre_link_args, LinkerFlavor::WasmLld(Cc::Yes), args!("-Wl,"));
+    let pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::WasmLld(Cc::No), args!("")),
+        (LinkerFlavor::WasmLld(Cc::Yes), args!("-Wl,")),
+    ]);
 
     TargetOptions {
         is_like_wasm: true,
diff --git a/compiler/rustc_target/src/spec/base/windows_gnu.rs b/compiler/rustc_target/src/spec/base/windows_gnu.rs
index 1357de2dad126..4d8e2c361db59 100644
--- a/compiler/rustc_target/src/spec/base/windows_gnu.rs
+++ b/compiler/rustc_target/src/spec/base/windows_gnu.rs
@@ -1,78 +1,80 @@
+use crate::spec::crt_objects;
 use crate::spec::LinkSelfContainedDefault;
-use crate::spec::{add_link_args, crt_objects};
 use crate::spec::{cvs, Cc, DebuginfoKind, LinkerFlavor, Lld, SplitDebuginfo, TargetOptions};
 use std::borrow::Cow;
 
 pub fn opts() -> TargetOptions {
-    let mut pre_link_args = TargetOptions::link_args(
-        LinkerFlavor::Gnu(Cc::No, Lld::No),
-        &[
-            // Enable ASLR
-            "--dynamicbase",
-            // ASLR will rebase it anyway so leaving that option enabled only leads to confusion
-            "--disable-auto-image-base",
-        ],
-    );
-    add_link_args(
-        &mut pre_link_args,
-        LinkerFlavor::Gnu(Cc::Yes, Lld::No),
-        &[
-            // Tell GCC to avoid linker plugins, because we are not bundling
-            // them with Windows installer, and Rust does its own LTO anyways.
-            "-fno-use-linker-plugin",
-            "-Wl,--dynamicbase",
-            "-Wl,--disable-auto-image-base",
-        ],
-    );
+    let pre_link_args = TargetOptions::link_args_list(&[
+        (
+            LinkerFlavor::Gnu(Cc::No, Lld::No),
+            &[
+                // Enable ASLR
+                "--dynamicbase",
+                // ASLR will rebase it anyway so leaving that option enabled only leads to confusion
+                "--disable-auto-image-base",
+            ],
+        ),
+        (
+            LinkerFlavor::Gnu(Cc::Yes, Lld::No),
+            &[
+                // Tell GCC to avoid linker plugins, because we are not bundling
+                // them with Windows installer, and Rust does its own LTO anyways.
+                "-fno-use-linker-plugin",
+                "-Wl,--dynamicbase",
+                "-Wl,--disable-auto-image-base",
+            ],
+        ),
+    ]);
 
-    // Order of `late_link_args*` was found through trial and error to work with various
-    // mingw-w64 versions (not tested on the CI). It's expected to change from time to time.
-    let mingw_libs = &[
-        "-lmsvcrt",
-        "-lmingwex",
-        "-lmingw32",
-        "-lgcc", // alas, mingw* libraries above depend on libgcc
-        // mingw's msvcrt is a weird hybrid import library and static library.
-        // And it seems that the linker fails to use import symbols from msvcrt
-        // that are required from functions in msvcrt in certain cases. For example
-        // `_fmode` that is used by an implementation of `__p__fmode` in x86_64.
-        // The library is purposely listed twice to fix that.
-        //
-        // See https://github.com/rust-lang/rust/pull/47483 for some more details.
-        "-lmsvcrt",
-        // Math functions missing in MSVCRT (they are present in UCRT) require
-        // this dependency cycle: `libmingwex.a` -> `libmsvcrt.a` -> `libmingwex.a`.
-        "-lmingwex",
-        "-luser32",
-        "-lkernel32",
-    ];
-    let mut late_link_args =
-        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::No, Lld::No), mingw_libs);
-    add_link_args(&mut late_link_args, LinkerFlavor::Gnu(Cc::Yes, Lld::No), mingw_libs);
+    let late_link_args = {
+        // Order of `late_link_args*` was found through trial and error to work with various
+        // mingw-w64 versions (not tested on the CI). It's expected to change from time to time.
+        const MINGW_LIBS: &[&str] = &[
+            "-lmsvcrt",
+            "-lmingwex",
+            "-lmingw32",
+            "-lgcc", // alas, mingw* libraries above depend on libgcc
+            // mingw's msvcrt is a weird hybrid import library and static library.
+            // And it seems that the linker fails to use import symbols from msvcrt
+            // that are required from functions in msvcrt in certain cases. For example
+            // `_fmode` that is used by an implementation of `__p__fmode` in x86_64.
+            // The library is purposely listed twice to fix that.
+            //
+            // See https://github.com/rust-lang/rust/pull/47483 for some more details.
+            "-lmsvcrt",
+            // Math functions missing in MSVCRT (they are present in UCRT) require
+            // this dependency cycle: `libmingwex.a` -> `libmsvcrt.a` -> `libmingwex.a`.
+            "-lmingwex",
+            "-luser32",
+            "-lkernel32",
+        ];
+        TargetOptions::link_args_list(&[
+            (LinkerFlavor::Gnu(Cc::No, Lld::No), MINGW_LIBS),
+            (LinkerFlavor::Gnu(Cc::Yes, Lld::No), MINGW_LIBS),
+        ])
+    };
     // If any of our crates are dynamically linked then we need to use
     // the shared libgcc_s-dw2-1.dll. This is required to support
     // unwinding across DLL boundaries.
-    let dynamic_unwind_libs = &["-lgcc_s"];
-    let mut late_link_args_dynamic =
-        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::No, Lld::No), dynamic_unwind_libs);
-    add_link_args(
-        &mut late_link_args_dynamic,
-        LinkerFlavor::Gnu(Cc::Yes, Lld::No),
-        dynamic_unwind_libs,
-    );
+    let late_link_args_dynamic = {
+        const DYNAMIC_UNWIND_LIBS: &[&str] = &["-lgcc_s"];
+        TargetOptions::link_args_list(&[
+            (LinkerFlavor::Gnu(Cc::No, Lld::No), DYNAMIC_UNWIND_LIBS),
+            (LinkerFlavor::Gnu(Cc::Yes, Lld::No), DYNAMIC_UNWIND_LIBS),
+        ])
+    };
     // If all of our crates are statically linked then we can get away
     // with statically linking the libgcc unwinding code. This allows
     // binaries to be redistributed without the libgcc_s-dw2-1.dll
     // dependency, but unfortunately break unwinding across DLL
     // boundaries when unwinding across FFI boundaries.
-    let static_unwind_libs = &["-lgcc_eh", "-l:libpthread.a"];
-    let mut late_link_args_static =
-        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::No, Lld::No), static_unwind_libs);
-    add_link_args(
-        &mut late_link_args_static,
-        LinkerFlavor::Gnu(Cc::Yes, Lld::No),
-        static_unwind_libs,
-    );
+    let late_link_args_static = {
+        const STATIC_UNWIND_LIBS: &[&str] = &["-lgcc_eh", "-l:libpthread.a"];
+        TargetOptions::link_args_list(&[
+            (LinkerFlavor::Gnu(Cc::No, Lld::No), STATIC_UNWIND_LIBS),
+            (LinkerFlavor::Gnu(Cc::Yes, Lld::No), STATIC_UNWIND_LIBS),
+        ])
+    };
 
     TargetOptions {
         os: "windows".into(),
diff --git a/compiler/rustc_target/src/spec/base/windows_uwp_gnu.rs b/compiler/rustc_target/src/spec/base/windows_uwp_gnu.rs
index 17256e18e24e3..a6bd247cf606f 100644
--- a/compiler/rustc_target/src/spec/base/windows_uwp_gnu.rs
+++ b/compiler/rustc_target/src/spec/base/windows_uwp_gnu.rs
@@ -1,26 +1,29 @@
-use crate::spec::{add_link_args, base, Cc, LinkArgs, LinkerFlavor, Lld, TargetOptions};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, TargetOptions};
 
 pub fn opts() -> TargetOptions {
     let base = base::windows_gnu::opts();
 
-    // FIXME: This should be updated for the exception machinery changes from #67502
-    // and inherit from `windows_gnu_base`, at least partially.
-    let mingw_libs = &[
-        "-lwinstorecompat",
-        "-lruntimeobject",
-        "-lsynchronization",
-        "-lvcruntime140_app",
-        "-lucrt",
-        "-lwindowsapp",
-        "-lmingwex",
-        "-lmingw32",
-    ];
-    let mut late_link_args =
-        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::No, Lld::No), mingw_libs);
-    add_link_args(&mut late_link_args, LinkerFlavor::Gnu(Cc::Yes, Lld::No), mingw_libs);
+    let late_link_args = {
+        // FIXME: This should be updated for the exception machinery changes from #67502
+        // and inherit from `windows_gnu_base`, at least partially.
+        const MINGW_LIBS: &[&str] = &[
+            "-lwinstorecompat",
+            "-lruntimeobject",
+            "-lsynchronization",
+            "-lvcruntime140_app",
+            "-lucrt",
+            "-lwindowsapp",
+            "-lmingwex",
+            "-lmingw32",
+        ];
+        TargetOptions::link_args_list(&[
+            (LinkerFlavor::Gnu(Cc::No, Lld::No), MINGW_LIBS),
+            (LinkerFlavor::Gnu(Cc::Yes, Lld::No), MINGW_LIBS),
+        ])
+    };
     // Reset the flags back to empty until the FIXME above is addressed.
-    let late_link_args_dynamic = LinkArgs::new();
-    let late_link_args_static = LinkArgs::new();
+    let late_link_args_dynamic = Default::default();
+    let late_link_args_static = Default::default();
 
     TargetOptions {
         abi: "uwp".into(),
diff --git a/compiler/rustc_target/src/spec/base/windows_uwp_msvc.rs b/compiler/rustc_target/src/spec/base/windows_uwp_msvc.rs
index 59a7616712541..a4140f4ea4fe7 100644
--- a/compiler/rustc_target/src/spec/base/windows_uwp_msvc.rs
+++ b/compiler/rustc_target/src/spec/base/windows_uwp_msvc.rs
@@ -5,7 +5,8 @@ pub fn opts() -> TargetOptions {
 
     opts.abi = "uwp".into();
     opts.vendor = "uwp".into();
-    opts.add_pre_link_args(LinkerFlavor::Msvc(Lld::No), &["/APPCONTAINER", "mincore.lib"]);
+    opts.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Msvc(Lld::No), &["/APPCONTAINER", "mincore.lib"]);
 
     opts
 }
diff --git a/compiler/rustc_target/src/spec/crt_objects.rs b/compiler/rustc_target/src/spec/crt_objects.rs
index 53f710b8f9e14..c542c690e8f72 100644
--- a/compiler/rustc_target/src/spec/crt_objects.rs
+++ b/compiler/rustc_target/src/spec/crt_objects.rs
@@ -40,28 +40,42 @@
 //! but not gcc's. As a result rustc cannot link with C++ static libraries (#36710)
 //! when linking in self-contained mode.
 
-use crate::spec::LinkOutputKind;
+use crate::spec::{LinkOutputKind, MaybeLazy};
 use std::borrow::Cow;
 use std::collections::BTreeMap;
 
+type LazyCrtObjectsArgs = &'static [(LinkOutputKind, &'static [&'static str])];
+pub struct LazyCrtObjectsState(LazyCrtObjectsArgs);
+
+impl FnOnce<()> for LazyCrtObjectsState {
+    type Output = CrtObjects;
+    extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
+        self.0.iter().map(|(z, k)| (*z, k.iter().map(|b| (*b).into()).collect())).collect()
+    }
+}
+
 pub type CrtObjects = BTreeMap<LinkOutputKind, Vec<Cow<'static, str>>>;
+pub type LazyCrtObjects = MaybeLazy<CrtObjects, LazyCrtObjectsState>;
 
-pub(super) fn new(obj_table: &[(LinkOutputKind, &[&'static str])]) -> CrtObjects {
-    obj_table.iter().map(|(z, k)| (*z, k.iter().map(|b| (*b).into()).collect())).collect()
+#[inline]
+pub(super) fn new(obj_table: LazyCrtObjectsArgs) -> LazyCrtObjects {
+    MaybeLazy::lazied(LazyCrtObjectsState(obj_table))
 }
 
-pub(super) fn all(obj: &'static str) -> CrtObjects {
-    new(&[
-        (LinkOutputKind::DynamicNoPicExe, &[obj]),
-        (LinkOutputKind::DynamicPicExe, &[obj]),
-        (LinkOutputKind::StaticNoPicExe, &[obj]),
-        (LinkOutputKind::StaticPicExe, &[obj]),
-        (LinkOutputKind::DynamicDylib, &[obj]),
-        (LinkOutputKind::StaticDylib, &[obj]),
-    ])
+macro_rules! all {
+    ($obj: literal) => {
+        new(&[
+            (LinkOutputKind::DynamicNoPicExe, &[$obj]),
+            (LinkOutputKind::DynamicPicExe, &[$obj]),
+            (LinkOutputKind::StaticNoPicExe, &[$obj]),
+            (LinkOutputKind::StaticPicExe, &[$obj]),
+            (LinkOutputKind::DynamicDylib, &[$obj]),
+            (LinkOutputKind::StaticDylib, &[$obj]),
+        ])
+    };
 }
 
-pub(super) fn pre_musl_self_contained() -> CrtObjects {
+pub(super) fn pre_musl_self_contained() -> LazyCrtObjects {
     new(&[
         (LinkOutputKind::DynamicNoPicExe, &["crt1.o", "crti.o", "crtbegin.o"]),
         (LinkOutputKind::DynamicPicExe, &["Scrt1.o", "crti.o", "crtbeginS.o"]),
@@ -72,7 +86,7 @@ pub(super) fn pre_musl_self_contained() -> CrtObjects {
     ])
 }
 
-pub(super) fn post_musl_self_contained() -> CrtObjects {
+pub(super) fn post_musl_self_contained() -> LazyCrtObjects {
     new(&[
         (LinkOutputKind::DynamicNoPicExe, &["crtend.o", "crtn.o"]),
         (LinkOutputKind::DynamicPicExe, &["crtendS.o", "crtn.o"]),
@@ -83,7 +97,7 @@ pub(super) fn post_musl_self_contained() -> CrtObjects {
     ])
 }
 
-pub(super) fn pre_mingw_self_contained() -> CrtObjects {
+pub(super) fn pre_mingw_self_contained() -> LazyCrtObjects {
     new(&[
         (LinkOutputKind::DynamicNoPicExe, &["crt2.o", "rsbegin.o"]),
         (LinkOutputKind::DynamicPicExe, &["crt2.o", "rsbegin.o"]),
@@ -94,19 +108,19 @@ pub(super) fn pre_mingw_self_contained() -> CrtObjects {
     ])
 }
 
-pub(super) fn post_mingw_self_contained() -> CrtObjects {
-    all("rsend.o")
+pub(super) fn post_mingw_self_contained() -> LazyCrtObjects {
+    all!("rsend.o")
 }
 
-pub(super) fn pre_mingw() -> CrtObjects {
-    all("rsbegin.o")
+pub(super) fn pre_mingw() -> LazyCrtObjects {
+    all!("rsbegin.o")
 }
 
-pub(super) fn post_mingw() -> CrtObjects {
-    all("rsend.o")
+pub(super) fn post_mingw() -> LazyCrtObjects {
+    all!("rsend.o")
 }
 
-pub(super) fn pre_wasi_self_contained() -> CrtObjects {
+pub(super) fn pre_wasi_self_contained() -> LazyCrtObjects {
     // Use crt1-command.o instead of crt1.o to enable support for new-style
     // commands. See https://reviews.llvm.org/D81689 for more info.
     new(&[
@@ -118,6 +132,6 @@ pub(super) fn pre_wasi_self_contained() -> CrtObjects {
     ])
 }
 
-pub(super) fn post_wasi_self_contained() -> CrtObjects {
+pub(super) fn post_wasi_self_contained() -> LazyCrtObjects {
     new(&[])
 }
diff --git a/compiler/rustc_target/src/spec/link_args.rs b/compiler/rustc_target/src/spec/link_args.rs
new file mode 100644
index 0000000000000..53233ebfa593d
--- /dev/null
+++ b/compiler/rustc_target/src/spec/link_args.rs
@@ -0,0 +1,41 @@
+//! Linker arguments
+
+use crate::spec::add_link_args;
+use crate::spec::{LinkerFlavor, LinkerFlavorCli};
+use crate::spec::{MaybeLazy, StaticCow};
+
+use std::collections::BTreeMap;
+
+pub type LinkArgs = BTreeMap<LinkerFlavor, Vec<StaticCow<str>>>;
+pub type LinkArgsCli = BTreeMap<LinkerFlavorCli, Vec<StaticCow<str>>>;
+
+pub type LazyLinkArgs = MaybeLazy<LinkArgs, LazyLinkArgsState>;
+
+pub enum LazyLinkArgsState {
+    Simple(LinkerFlavor, &'static [&'static str]),
+    List(&'static [(LinkerFlavor, &'static [&'static str])]),
+    Apple(super::base::apple::ApplePreLinkArgs),
+}
+
+impl FnOnce<()> for LazyLinkArgsState {
+    type Output = LinkArgs;
+
+    #[inline]
+    extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
+        match self {
+            LazyLinkArgsState::Simple(flavor, args) => {
+                let mut link_args = LinkArgs::new();
+                add_link_args(&mut link_args, flavor, args);
+                link_args
+            }
+            LazyLinkArgsState::List(l) => {
+                let mut link_args = LinkArgs::new();
+                for (flavor, args) in l {
+                    add_link_args(&mut link_args, *flavor, args)
+                }
+                link_args
+            }
+            LazyLinkArgsState::Apple(args) => super::base::apple::pre_link_args(args),
+        }
+    }
+}
diff --git a/compiler/rustc_target/src/spec/maybe_lazy.rs b/compiler/rustc_target/src/spec/maybe_lazy.rs
new file mode 100644
index 0000000000000..3694679b01f1c
--- /dev/null
+++ b/compiler/rustc_target/src/spec/maybe_lazy.rs
@@ -0,0 +1,144 @@
+//! A custom LazyLock+Cow suitable for holding borrowed, owned or lazy data.
+
+use std::borrow::{Borrow, Cow};
+use std::fmt::{Debug, Display};
+use std::ops::Deref;
+use std::sync::LazyLock;
+
+enum MaybeLazyInner<T: 'static + ToOwned + ?Sized, F> {
+    Lazy(LazyLock<T::Owned, F>),
+    Cow(Cow<'static, T>),
+}
+
+/// A custom LazyLock+Cow suitable for holding borrowed, owned or lazy data.
+///
+/// Technically this structure has 3 states: borrowed, owned and lazy
+/// They can all be constructed from the [`MaybeLazy::borrowed`], [`MaybeLazy::owned`] and
+/// [`MaybeLazy::lazy`] methods.
+#[repr(transparent)]
+pub struct MaybeLazy<T: 'static + ToOwned + ?Sized, F = fn() -> <T as ToOwned>::Owned> {
+    // Inner state.
+    //
+    // Not to be inlined since we may want in the future to
+    // make this struct usable to statics and we might need to
+    // workaround const-eval limitation (particulary around drop).
+    inner: MaybeLazyInner<T, F>,
+}
+
+impl<T: 'static + ?Sized + ToOwned, F: FnOnce() -> T::Owned> MaybeLazy<T, F> {
+    /// Create a [`MaybeLazy`] from an borrowed `T`.
+    #[inline]
+    pub const fn borrowed(a: &'static T) -> Self {
+        MaybeLazy { inner: MaybeLazyInner::Cow(Cow::Borrowed(a)) }
+    }
+
+    /// Create a [`MaybeLazy`] from an borrowed `T`.
+    #[inline]
+    pub const fn owned(a: T::Owned) -> Self {
+        MaybeLazy { inner: MaybeLazyInner::Cow(Cow::Owned(a)) }
+    }
+
+    /// Create a [`MaybeLazy`] from a function-able `F`.
+    #[inline]
+    pub const fn lazied(f: F) -> Self {
+        MaybeLazy { inner: MaybeLazyInner::Lazy(LazyLock::new(f)) }
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned> MaybeLazy<T> {
+    /// Create a [`MaybeLazy`] from a function pointer.
+    #[inline]
+    pub const fn lazy(a: fn() -> T::Owned) -> Self {
+        Self::lazied(a)
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned<Owned: Clone>, F: FnOnce() -> T::Owned> Clone
+    for MaybeLazy<T, F>
+{
+    #[inline]
+    fn clone(&self) -> Self {
+        MaybeLazy {
+            inner: MaybeLazyInner::Cow(match &self.inner {
+                MaybeLazyInner::Lazy(f) => Cow::Owned((*f).to_owned()),
+                MaybeLazyInner::Cow(c) => c.clone(),
+            }),
+        }
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned<Owned: Default>, F: FnOnce() -> T::Owned> Default
+    for MaybeLazy<T, F>
+{
+    #[inline]
+    fn default() -> MaybeLazy<T, F> {
+        MaybeLazy::owned(T::Owned::default())
+    }
+}
+
+// `Debug`, `Display` and other traits below are implemented in terms of this `Deref`
+impl<T: 'static + ?Sized + ToOwned<Owned: Borrow<T>>, F: FnOnce() -> T::Owned> Deref
+    for MaybeLazy<T, F>
+{
+    type Target = T;
+
+    #[inline]
+    fn deref(&self) -> &T {
+        match &self.inner {
+            MaybeLazyInner::Lazy(f) => (&**f).borrow(),
+            MaybeLazyInner::Cow(c) => &*c,
+        }
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned<Owned: Debug> + Debug, F: FnOnce() -> T::Owned> Debug
+    for MaybeLazy<T, F>
+{
+    #[inline]
+    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        Debug::fmt(&**self, fmt)
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned<Owned: Display> + Display, F: FnOnce() -> T::Owned> Display
+    for MaybeLazy<T, F>
+{
+    #[inline]
+    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(&**self, fmt)
+    }
+}
+
+impl<T: 'static + ?Sized + ToOwned, F: FnOnce() -> T::Owned> AsRef<T> for MaybeLazy<T, F> {
+    #[inline]
+    fn as_ref(&self) -> &T {
+        &**self
+    }
+}
+
+impl<
+    T1: ?Sized + PartialEq<T2> + ToOwned,
+    T2: ?Sized + ToOwned,
+    F1: FnOnce() -> T1::Owned,
+    F2: FnOnce() -> T2::Owned,
+> PartialEq<MaybeLazy<T2, F2>> for MaybeLazy<T1, F1>
+{
+    #[inline]
+    fn eq(&self, other: &MaybeLazy<T2, F2>) -> bool {
+        PartialEq::eq(&**self, &**other)
+    }
+}
+
+impl<F: FnOnce() -> String> PartialEq<&str> for MaybeLazy<str, F> {
+    #[inline]
+    fn eq(&self, other: &&str) -> bool {
+        &**self == *other
+    }
+}
+
+impl<F: FnOnce() -> String> From<&'static str> for MaybeLazy<str, F> {
+    #[inline]
+    fn from(s: &'static str) -> MaybeLazy<str, F> {
+        MaybeLazy::borrowed(s)
+    }
+}
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index 42860b1059ed7..19b58d4c7f726 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -38,7 +38,8 @@ use crate::abi::call::Conv;
 use crate::abi::{Endian, Integer, Size, TargetDataLayout, TargetDataLayoutErrors};
 use crate::json::{Json, ToJson};
 use crate::spec::abi::Abi;
-use crate::spec::crt_objects::CrtObjects;
+use crate::spec::crt_objects::{CrtObjects, LazyCrtObjects};
+use crate::spec::link_args::{LazyLinkArgs, LinkArgs, LinkArgsCli};
 use rustc_fs_util::try_canonicalize;
 use rustc_macros::{Decodable, Encodable, HashStable_Generic};
 use rustc_serialize::{Decodable, Decoder, Encodable, Encoder};
@@ -55,6 +56,8 @@ use tracing::debug;
 
 pub mod abi;
 pub mod crt_objects;
+pub mod link_args;
+pub mod maybe_lazy;
 
 mod base;
 pub use base::apple::deployment_target as current_apple_deployment_target;
@@ -62,6 +65,8 @@ pub use base::apple::platform as current_apple_platform;
 pub use base::apple::sdk_version as current_apple_sdk_version;
 pub use base::avr_gnu::ef_avr_arch;
 
+use maybe_lazy::MaybeLazy;
+
 /// Linker is called through a C/C++ compiler.
 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
 pub enum Cc {
@@ -1102,9 +1107,6 @@ impl fmt::Display for LinkOutputKind {
     }
 }
 
-pub type LinkArgs = BTreeMap<LinkerFlavor, Vec<StaticCow<str>>>;
-pub type LinkArgsCli = BTreeMap<LinkerFlavorCli, Vec<StaticCow<str>>>;
-
 /// Which kind of debuginfo does the target use?
 ///
 /// Useful in determining whether a target supports Split DWARF (a target with
@@ -1879,7 +1881,7 @@ impl TargetWarnings {
 #[derive(PartialEq, Clone, Debug)]
 pub struct Target {
     /// Target triple to pass to LLVM.
-    pub llvm_target: StaticCow<str>,
+    pub llvm_target: MaybeLazy<str>,
     /// Metadata about a target, for example the description or tier.
     /// Used for generating target documentation.
     pub metadata: TargetMetadata,
@@ -2014,34 +2016,34 @@ pub struct TargetOptions {
     linker_is_gnu_json: bool,
 
     /// Objects to link before and after all other object code.
-    pub pre_link_objects: CrtObjects,
-    pub post_link_objects: CrtObjects,
+    pub pre_link_objects: LazyCrtObjects,
+    pub post_link_objects: LazyCrtObjects,
     /// Same as `(pre|post)_link_objects`, but when self-contained linking mode is enabled.
-    pub pre_link_objects_self_contained: CrtObjects,
-    pub post_link_objects_self_contained: CrtObjects,
+    pub pre_link_objects_self_contained: LazyCrtObjects,
+    pub post_link_objects_self_contained: LazyCrtObjects,
     /// Behavior for the self-contained linking mode: inferred for some targets, or explicitly
     /// enabled (in bulk, or with individual components).
     pub link_self_contained: LinkSelfContainedDefault,
 
     /// Linker arguments that are passed *before* any user-defined libraries.
-    pub pre_link_args: LinkArgs,
+    pub pre_link_args: LazyLinkArgs,
     pre_link_args_json: LinkArgsCli,
     /// Linker arguments that are unconditionally passed after any
     /// user-defined but before post-link objects. Standard platform
     /// libraries that should be always be linked to, usually go here.
-    pub late_link_args: LinkArgs,
+    pub late_link_args: LazyLinkArgs,
     late_link_args_json: LinkArgsCli,
     /// Linker arguments used in addition to `late_link_args` if at least one
     /// Rust dependency is dynamically linked.
-    pub late_link_args_dynamic: LinkArgs,
+    pub late_link_args_dynamic: LazyLinkArgs,
     late_link_args_dynamic_json: LinkArgsCli,
     /// Linker arguments used in addition to `late_link_args` if all Rust
     /// dependencies are statically linked.
-    pub late_link_args_static: LinkArgs,
+    pub late_link_args_static: LazyLinkArgs,
     late_link_args_static_json: LinkArgsCli,
     /// Linker arguments that are unconditionally passed *after* any
     /// user-defined libraries.
-    pub post_link_args: LinkArgs,
+    pub post_link_args: LazyLinkArgs,
     post_link_args_json: LinkArgsCli,
 
     /// Optional link script applied to `dylib` and `executable` crate types.
@@ -2388,14 +2390,14 @@ fn add_link_args(link_args: &mut LinkArgs, flavor: LinkerFlavor, args: &[&'stati
 }
 
 impl TargetOptions {
-    fn link_args(flavor: LinkerFlavor, args: &[&'static str]) -> LinkArgs {
-        let mut link_args = LinkArgs::new();
-        add_link_args(&mut link_args, flavor, args);
-        link_args
+    #[inline]
+    fn link_args(flavor: LinkerFlavor, args: &'static [&'static str]) -> LazyLinkArgs {
+        MaybeLazy::lazied(link_args::LazyLinkArgsState::Simple(flavor, args))
     }
 
-    fn add_pre_link_args(&mut self, flavor: LinkerFlavor, args: &[&'static str]) {
-        add_link_args(&mut self.pre_link_args, flavor, args);
+    #[inline]
+    fn link_args_list(list: &'static [(LinkerFlavor, &'static [&'static str])]) -> LazyLinkArgs {
+        MaybeLazy::lazied(link_args::LazyLinkArgsState::List(list))
     }
 
     fn update_from_cli(&mut self) {
@@ -2404,14 +2406,14 @@ impl TargetOptions {
             self.lld_flavor_json,
             self.linker_is_gnu_json,
         );
-        for (args, args_json) in [
+        for (real_args, args_json) in [
             (&mut self.pre_link_args, &self.pre_link_args_json),
             (&mut self.late_link_args, &self.late_link_args_json),
             (&mut self.late_link_args_dynamic, &self.late_link_args_dynamic_json),
             (&mut self.late_link_args_static, &self.late_link_args_static_json),
             (&mut self.post_link_args, &self.post_link_args_json),
         ] {
-            args.clear();
+            let mut args = LinkArgs::new();
             for (flavor, args_json) in args_json {
                 let linker_flavor = self.linker_flavor.with_cli_hints(*flavor);
                 // Normalize to no lld to avoid asserts.
@@ -2422,9 +2424,10 @@ impl TargetOptions {
                     _ => linker_flavor,
                 };
                 if !args.contains_key(&linker_flavor) {
-                    add_link_args_iter(args, linker_flavor, args_json.iter().cloned());
+                    add_link_args_iter(&mut args, linker_flavor, args_json.iter().cloned());
                 }
             }
+            *real_args = MaybeLazy::owned(args);
         }
     }
 
@@ -2506,15 +2509,15 @@ impl Default for TargetOptions {
             pre_link_objects_self_contained: Default::default(),
             post_link_objects_self_contained: Default::default(),
             link_self_contained: LinkSelfContainedDefault::False,
-            pre_link_args: LinkArgs::new(),
+            pre_link_args: Default::default(),
             pre_link_args_json: LinkArgsCli::new(),
-            late_link_args: LinkArgs::new(),
+            late_link_args: Default::default(),
             late_link_args_json: LinkArgsCli::new(),
-            late_link_args_dynamic: LinkArgs::new(),
+            late_link_args_dynamic: Default::default(),
             late_link_args_dynamic_json: LinkArgsCli::new(),
-            late_link_args_static: LinkArgs::new(),
+            late_link_args_static: Default::default(),
             late_link_args_static_json: LinkArgsCli::new(),
-            post_link_args: LinkArgs::new(),
+            post_link_args: Default::default(),
             post_link_args_json: LinkArgsCli::new(),
             link_env: cvs![],
             link_env_remove: cvs![],
@@ -2734,7 +2737,7 @@ impl Target {
         };
 
         let mut base = Target {
-            llvm_target: get_req_field("llvm-target")?.into(),
+            llvm_target: MaybeLazy::owned(get_req_field("llvm-target")?),
             metadata: Default::default(),
             pointer_width: get_req_field("target-pointer-width")?
                 .parse::<u32>()
@@ -3095,7 +3098,7 @@ impl Target {
 
                         args.insert(kind, v);
                     }
-                    base.$key_name = args;
+                    base.$key_name = MaybeLazy::owned(args);
                 }
             } );
             ($key_name:ident = $json_name:expr, link_args) => ( {
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 4e2964174f925..4dd4592478010 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_darwin.rs
@@ -1,9 +1,12 @@
 use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "macos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.cpu = "apple-m1".into();
     base.max_atomic_width = Some(128);
 
@@ -14,7 +17,7 @@ pub fn target() -> 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: MaybeLazy::lazy(|| macos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -23,7 +26,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             mcount: "\u{1}mcount".into(),
             frame_pointer: FramePointer::NonLeaf,
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 20655689772d8..ebf28292551d1 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_ios.rs
@@ -1,9 +1,12 @@
 use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
@@ -11,7 +14,7 @@ pub fn target() -> Target {
         // 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: MaybeLazy::lazy(|| ios_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -20,7 +23,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
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 4c008f7985e6f..b2c5b1c4527b8 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,16 @@
 use crate::spec::base::apple::{mac_catalyst_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::MacCatalyst);
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::MacCatalyst;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD;
 
     Target {
-        llvm_target: mac_catalyst_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| mac_catalyst_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -16,7 +19,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a12".into(),
             max_atomic_width: Some(128),
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 4a63abdf5419f..a444993553d12 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,9 +1,12 @@
 use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("ios", arch, TargetAbi::Simulator);
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::Simulator;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
@@ -11,7 +14,7 @@ pub fn target() -> Target {
         // 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: MaybeLazy::lazy(|| ios_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -20,7 +23,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a7".into(),
             max_atomic_width: Some(128),
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 3310e6c9e8a41..3434789425727 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,13 @@
 use crate::spec::base::apple::{opts, tvos_llvm_target, Arch, TargetAbi};
-use crate::spec::{FramePointer, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "tvos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
     Target {
-        llvm_target: tvos_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| tvos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -13,12 +16,12 @@ pub 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: ARCH.target_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(OS, ARCH, ABI)
         },
     }
 }
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 b901c663afaef..3d71d01785b07 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,13 @@
 use crate::spec::base::apple::{opts, tvos_sim_llvm_target, Arch, TargetAbi};
-use crate::spec::{FramePointer, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "tvos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
     Target {
-        llvm_target: tvos_sim_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| tvos_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -13,12 +16,12 @@ pub 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: ARCH.target_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(OS, ARCH, ABI)
         },
     }
 }
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 b0798e5e4f580..0e6590ea4c445 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,16 @@
 use crate::spec::base::apple::{opts, visionos_llvm_target, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("visionos", arch, TargetAbi::Normal);
+    const OS: &str = "visionos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+    const ARCH: Arch = Arch::Arm64;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
-        llvm_target: visionos_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| visionos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple visionOS".into()),
             tier: Some(3),
@@ -16,7 +19,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a16".into(),
             max_atomic_width: Some(128),
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 7b2d2b6a8e442..152a2487cec04 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,16 @@
 use crate::spec::base::apple::{opts, visionos_sim_llvm_target, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
-    let mut base = opts("visionos", arch, TargetAbi::Simulator);
+    const OS: &str = "visionos";
+    const ABI: TargetAbi = TargetAbi::Simulator;
+    const ARCH: Arch = Arch::Arm64;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
-        llvm_target: visionos_sim_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| visionos_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: Some("ARM64 Apple visionOS simulator".into()),
             tier: Some(3),
@@ -16,7 +19,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a16".into(),
             max_atomic_width: Some(128),
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 a00a97a133f60..ae5920b0ea819 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_apple_watchos.rs
@@ -2,7 +2,11 @@ use crate::spec::base::apple::{opts, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub fn target() -> Target {
-    let base = opts("watchos", Arch::Arm64, TargetAbi::Normal);
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "watchos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let base = opts(OS, ARCH, ABI);
     Target {
         llvm_target: "aarch64-apple-watchos".into(),
         metadata: crate::spec::TargetMetadata {
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 e2f80b7b7a888..17e0ea966feb9 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,17 @@
 use crate::spec::base::apple::{opts, watchos_sim_llvm_target, Arch, TargetAbi};
-use crate::spec::{FramePointer, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64;
+    const ARCH: Arch = Arch::Arm64;
+    const OS: &'static str = "watchos";
+    const ABI: TargetAbi = 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: MaybeLazy::lazy(|| watchos_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -17,12 +20,12 @@ pub 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: ARCH.target_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(OS, ARCH, ABI)
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_unknown_illumos.rs b/compiler/rustc_target/src/spec/targets/aarch64_unknown_illumos.rs
index 6f253c2a22393..14337f4d8e83c 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_unknown_illumos.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_unknown_illumos.rs
@@ -1,8 +1,8 @@
-use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, Target};
+use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::illumos::opts();
-    base.add_pre_link_args(LinkerFlavor::Unix(Cc::Yes), &["-std=c99"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Unix(Cc::Yes), &["-std=c99"]);
     base.max_atomic_width = Some(128);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::CFI;
     base.features = "+v8a".into();
diff --git a/compiler/rustc_target/src/spec/targets/aarch64_unknown_uefi.rs b/compiler/rustc_target/src/spec/targets/aarch64_unknown_uefi.rs
index de4a56ae03da3..2f35a3e6ec92c 100644
--- a/compiler/rustc_target/src/spec/targets/aarch64_unknown_uefi.rs
+++ b/compiler/rustc_target/src/spec/targets/aarch64_unknown_uefi.rs
@@ -1,13 +1,13 @@
 // This defines the aarch64 target for UEFI systems as described in the UEFI specification. See the
 // uefi-base module for generic UEFI options.
 
-use crate::spec::{base, LinkerFlavor, Lld, Target};
+use crate::spec::{base, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::uefi_msvc::opts();
 
     base.max_atomic_width = Some(128);
-    base.add_pre_link_args(LinkerFlavor::Msvc(Lld::No), &["/machine:arm64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Msvc(Lld::No), &["/machine:arm64"]);
     base.features = "+v8a".into();
 
     Target {
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 3ca8c9969c2d5..682ddc5c2c67d 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,15 @@
 use crate::spec::base::apple::{opts, watchos_llvm_target, Arch, TargetAbi};
-use crate::spec::{Target, TargetOptions};
+use crate::spec::{MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64_32;
-    let base = opts("watchos", arch, TargetAbi::Normal);
+    const OS: &str = "watchos";
+    const ARCH: Arch = Arch::Arm64_32;
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let base = opts(OS, ARCH, ABI);
+
     Target {
-        llvm_target: watchos_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| watchos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
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 90be518638e93..c0795dcba6be6 100644
--- a/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs
@@ -1,9 +1,12 @@
 use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64e;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::Arm64e;
+    const OS: &'static str = "macos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.cpu = "apple-m1".into();
     base.max_atomic_width = Some(128);
 
@@ -14,7 +17,7 @@ pub fn target() -> 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: MaybeLazy::lazy(|| macos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -23,7 +26,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             mcount: "\u{1}mcount".into(),
             frame_pointer: FramePointer::NonLeaf,
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 56470d29eae00..42f74d53c305d 100644
--- a/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_ios.rs
@@ -1,9 +1,12 @@
 use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{FramePointer, SanitizerSet, Target, TargetOptions};
+use crate::spec::{FramePointer, MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Arm64e;
-    let mut base = opts("ios", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::Arm64e;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
@@ -11,7 +14,7 @@ pub fn target() -> Target {
         // 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: MaybeLazy::lazy(|| ios_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -20,7 +23,7 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+neon,+fp-armv8,+apple-a12,+v8.3a,+pauth".into(),
             max_atomic_width: Some(128),
diff --git a/compiler/rustc_target/src/spec/targets/arm64ec_pc_windows_msvc.rs b/compiler/rustc_target/src/spec/targets/arm64ec_pc_windows_msvc.rs
index aedfb88102462..cd329bd2c68d1 100644
--- a/compiler/rustc_target/src/spec/targets/arm64ec_pc_windows_msvc.rs
+++ b/compiler/rustc_target/src/spec/targets/arm64ec_pc_windows_msvc.rs
@@ -1,11 +1,10 @@
-use crate::spec::{add_link_args, base, LinkerFlavor, Lld, Target};
+use crate::spec::{base, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_msvc::opts();
     base.max_atomic_width = Some(128);
     base.features = "+v8a,+neon,+fp-armv8".into();
-    add_link_args(
-        &mut base.late_link_args,
+    base.late_link_args = TargetOptions::link_args(
         LinkerFlavor::Msvc(Lld::No),
         &["/machine:arm64ec", "softintrin.lib"],
     );
diff --git a/compiler/rustc_target/src/spec/targets/armv7_linux_androideabi.rs b/compiler/rustc_target/src/spec/targets/armv7_linux_androideabi.rs
index e798ef4735436..6a1194c3ece7d 100644
--- a/compiler/rustc_target/src/spec/targets/armv7_linux_androideabi.rs
+++ b/compiler/rustc_target/src/spec/targets/armv7_linux_androideabi.rs
@@ -10,7 +10,8 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, Target, TargetOptio
 
 pub fn target() -> Target {
     let mut base = base::android::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-march=armv7-a"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-march=armv7-a"]);
     Target {
         llvm_target: "armv7-none-linux-android".into(),
         metadata: crate::spec::TargetMetadata {
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 5c675c22ef511..0dbd06e587a9a 100644
--- a/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs
+++ b/compiler/rustc_target/src/spec/targets/armv7k_apple_watchos.rs
@@ -2,7 +2,10 @@ use crate::spec::base::apple::{opts, Arch, TargetAbi};
 use crate::spec::{Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Armv7k;
+    const ARCH: Arch = Arch::Armv7k;
+    const OS: &'static str = "watchos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
     Target {
         llvm_target: "armv7k-apple-watchos".into(),
         metadata: crate::spec::TargetMetadata {
@@ -13,13 +16,13 @@ pub 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: ARCH.target_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(OS, ARCH, ABI)
         },
     }
 }
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 4dd475e3a82da..f5693647040ca 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,13 @@
 use crate::spec::base::apple::{ios_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{Target, TargetOptions};
+use crate::spec::{MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::Armv7s;
+    const ARCH: Arch = Arch::Armv7s;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
     Target {
-        llvm_target: ios_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| ios_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -13,11 +16,11 @@ pub 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: ARCH.target_arch(),
         options: TargetOptions {
             features: "+v7,+vfp4,+neon".into(),
             max_atomic_width: Some(64),
-            ..opts("ios", arch, TargetAbi::Normal)
+            ..opts(OS, ARCH, ABI)
         },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/avr_unknown_gnu_atmega328.rs b/compiler/rustc_target/src/spec/targets/avr_unknown_gnu_atmega328.rs
index bf01413a80adf..2afe598c9f08a 100644
--- a/compiler/rustc_target/src/spec/targets/avr_unknown_gnu_atmega328.rs
+++ b/compiler/rustc_target/src/spec/targets/avr_unknown_gnu_atmega328.rs
@@ -1,5 +1,5 @@
 use crate::spec::{base, Target};
 
 pub fn target() -> Target {
-    base::avr_gnu::target("atmega328", "-mmcu=atmega328")
+    base::avr_gnu::target("atmega328")
 }
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 c03a0974bc1cb..747c881524621 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,19 @@
 use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{Target, TargetOptions};
+use crate::spec::{MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::I386;
+    const ARCH: Arch = Arch::I386;
+    const OS: &'static str = "ios";
     // 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;
+    const ABI: TargetAbi = 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: MaybeLazy::lazy(|| ios_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -22,7 +24,7 @@ pub 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: ARCH.target_arch(),
+        options: TargetOptions { max_atomic_width: Some(64), ..opts(OS, ARCH, ABI) },
     }
 }
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 aea6a1ac4ecea..adface04134a3 100644
--- a/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_apple_darwin.rs
@@ -1,12 +1,16 @@
 use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, Target, TargetOptions};
+use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
     // ld64 only understands i386 and not i686
-    let arch = Arch::I386;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::I386;
+    const OS: &'static str = "macos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m32"]);
     base.frame_pointer = FramePointer::Always;
 
     Target {
@@ -15,7 +19,7 @@ pub fn target() -> Target {
         // correctly, we do too.
         //
         // While ld64 doesn't understand i686, LLVM does.
-        llvm_target: macos_llvm_target(Arch::I686).into(),
+        llvm_target: MaybeLazy::lazy(|| macos_llvm_target(Arch::I686)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -26,7 +30,7 @@ pub 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(),
+        arch: ARCH.target_arch(),
         options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnu.rs b/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnu.rs
index 66e09416dde90..ae298fc9a4c41 100644
--- a/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnu.rs
@@ -1,4 +1,5 @@
-use crate::spec::{base, Cc, FramePointer, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, FramePointer};
+use crate::spec::{LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_gnu::opts();
@@ -9,11 +10,10 @@ pub fn target() -> Target {
 
     // Mark all dynamic libraries and executables as compatible with the larger 4GiB address
     // space available to x86 Windows binaries on x86_64.
-    base.add_pre_link_args(
-        LinkerFlavor::Gnu(Cc::No, Lld::No),
-        &["-m", "i386pe", "--large-address-aware"],
-    );
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-Wl,--large-address-aware"]);
+    base.pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::Gnu(Cc::No, Lld::No), &["-m", "i386pe", "--large-address-aware"]),
+        (LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-Wl,--large-address-aware"]),
+    ]);
 
     Target {
         llvm_target: "i686-pc-windows-gnu".into(),
diff --git a/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnullvm.rs b/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnullvm.rs
index 7a2d28aec9c08..ba9a20251577d 100644
--- a/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnullvm.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_pc_windows_gnullvm.rs
@@ -1,4 +1,4 @@
-use crate::spec::{base, Cc, FramePointer, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, FramePointer, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_gnullvm::opts();
@@ -9,7 +9,7 @@ pub fn target() -> Target {
 
     // Mark all dynamic libraries and executables as compatible with the larger 4GiB address
     // space available to x86 Windows binaries on x86_64.
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Gnu(Cc::No, Lld::No),
         &["-m", "i386pe", "--large-address-aware"],
     );
diff --git a/compiler/rustc_target/src/spec/targets/i686_pc_windows_msvc.rs b/compiler/rustc_target/src/spec/targets/i686_pc_windows_msvc.rs
index 970b43ad109ba..b27c7dd09ab8b 100644
--- a/compiler/rustc_target/src/spec/targets/i686_pc_windows_msvc.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_pc_windows_msvc.rs
@@ -1,4 +1,4 @@
-use crate::spec::{base, LinkerFlavor, Lld, SanitizerSet, Target};
+use crate::spec::{base, LinkerFlavor, Lld, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_msvc::opts();
@@ -6,7 +6,7 @@ pub fn target() -> Target {
     base.max_atomic_width = Some(64);
     base.supported_sanitizers = SanitizerSet::ADDRESS;
 
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Msvc(Lld::No),
         &[
             // Mark all dynamic libraries and executables as compatible with the larger 4GiB address
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_freebsd.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_freebsd.rs
index 5826906e9d8ed..97454046b2f21 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_freebsd.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_freebsd.rs
@@ -1,10 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::freebsd::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-Wl,-znotext"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-Wl,-znotext"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_haiku.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_haiku.rs
index 5f66911b39a14..35960eb7f082d 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_haiku.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_haiku.rs
@@ -1,10 +1,10 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::haiku::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_hurd_gnu.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_hurd_gnu.rs
index a67105f24ca2e..83136d600ba2b 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_hurd_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_hurd_gnu.rs
@@ -1,10 +1,10 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::hurd_gnu::opts();
     base.cpu = "pentiumpro".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_linux_gnu.rs
index 1d4916cabfdf3..a908994845729 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_linux_gnu.rs
@@ -1,11 +1,12 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld};
+use crate::spec::{SanitizerSet, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
     base.supported_sanitizers = SanitizerSet::ADDRESS;
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_linux_musl.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_linux_musl.rs
index c3b9b71802b18..f39eba801cca5 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_linux_musl.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_linux_musl.rs
@@ -1,10 +1,13 @@
-use crate::spec::{base, Cc, FramePointer, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{
+    base, Cc, FramePointer, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut base = base::linux_musl::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-Wl,-melf_i386"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-Wl,-melf_i386"]);
     base.stack_probes = StackProbeType::Inline;
 
     // The unwinder used by i686-unknown-linux-musl, the LLVM libunwind
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_netbsd.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_netbsd.rs
index 87eba1fb856f0..ac7ea5ff15d62 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_netbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_netbsd.rs
@@ -4,7 +4,7 @@ pub fn target() -> Target {
     let mut base = base::netbsd::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_openbsd.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_openbsd.rs
index 0436f39f5b11c..02b87aa919ccc 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_openbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_openbsd.rs
@@ -1,10 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::openbsd::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-fuse-ld=lld"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "-fuse-ld=lld"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/i686_unknown_redox.rs b/compiler/rustc_target/src/spec/targets/i686_unknown_redox.rs
index 83252fadb78ea..0f6307ea35d61 100644
--- a/compiler/rustc_target/src/spec/targets/i686_unknown_redox.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_unknown_redox.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::redox::opts();
     base.cpu = "pentiumpro".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     // don't use probe-stack=inline-asm until rust#83139 and rust#84667 are resolved
     base.stack_probes = StackProbeType::Call;
 
diff --git a/compiler/rustc_target/src/spec/targets/i686_uwp_windows_gnu.rs b/compiler/rustc_target/src/spec/targets/i686_uwp_windows_gnu.rs
index 77dcd645728f1..c88433537bb6f 100644
--- a/compiler/rustc_target/src/spec/targets/i686_uwp_windows_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_uwp_windows_gnu.rs
@@ -1,4 +1,5 @@
-use crate::spec::{base, Cc, FramePointer, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, FramePointer};
+use crate::spec::{LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_uwp_gnu::opts();
@@ -8,11 +9,10 @@ pub fn target() -> Target {
 
     // Mark all dynamic libraries and executables as compatible with the larger 4GiB address
     // space available to x86 Windows binaries on x86_64.
-    base.add_pre_link_args(
-        LinkerFlavor::Gnu(Cc::No, Lld::No),
-        &["-m", "i386pe", "--large-address-aware"],
-    );
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-Wl,--large-address-aware"]);
+    base.pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::Gnu(Cc::No, Lld::No), &["-m", "i386pe", "--large-address-aware"]),
+        (LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-Wl,--large-address-aware"]),
+    ]);
 
     Target {
         llvm_target: "i686-pc-windows-gnu".into(),
diff --git a/compiler/rustc_target/src/spec/targets/i686_win7_windows_msvc.rs b/compiler/rustc_target/src/spec/targets/i686_win7_windows_msvc.rs
index ae1a44e44a85a..d97900b4ab929 100644
--- a/compiler/rustc_target/src/spec/targets/i686_win7_windows_msvc.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_win7_windows_msvc.rs
@@ -1,4 +1,4 @@
-use crate::spec::{base, LinkerFlavor, Lld, Target};
+use crate::spec::{base, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_msvc::opts();
@@ -6,7 +6,7 @@ pub fn target() -> Target {
     base.max_atomic_width = Some(64);
     base.vendor = "win7".into();
 
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Msvc(Lld::No),
         &[
             // Mark all dynamic libraries and executables as compatible with the larger 4GiB address
diff --git a/compiler/rustc_target/src/spec/targets/i686_wrs_vxworks.rs b/compiler/rustc_target/src/spec/targets/i686_wrs_vxworks.rs
index e4d0b674cc4c3..9e23ce188ad4c 100644
--- a/compiler/rustc_target/src/spec/targets/i686_wrs_vxworks.rs
+++ b/compiler/rustc_target/src/spec/targets/i686_wrs_vxworks.rs
@@ -1,10 +1,10 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::vxworks::opts();
     base.cpu = "pentium4".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_ibm_aix.rs b/compiler/rustc_target/src/spec/targets/powerpc64_ibm_aix.rs
index 481df71c1a65a..1cab3395a8f0a 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_ibm_aix.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_ibm_aix.rs
@@ -1,9 +1,9 @@
-use crate::spec::{base, Cc, LinkerFlavor, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::aix::opts();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Unix(Cc::No),
         &["-b64", "-bpT:0x100000000", "-bpD:0x110000000", "-bcdtors:all:0:s"],
     );
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_freebsd.rs b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_freebsd.rs
index b1b981823b89d..12c2c9e247dfd 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_freebsd.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_freebsd.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::freebsd::opts();
     base.cpu = "ppc64".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_gnu.rs
index ac10630d94404..b44ba19b2b2df 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_gnu.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.cpu = "ppc64".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_musl.rs b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_musl.rs
index 663f06cf0c67b..b250315213fb4 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_musl.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_linux_musl.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::linux_musl::opts();
     base.cpu = "ppc64".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_openbsd.rs b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_openbsd.rs
index 5611352c951c7..ccd829f53ec43 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_unknown_openbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_unknown_openbsd.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::openbsd::opts();
     base.cpu = "ppc64".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64_wrs_vxworks.rs b/compiler/rustc_target/src/spec/targets/powerpc64_wrs_vxworks.rs
index 22b45042aa667..2dfe1b90d04ad 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64_wrs_vxworks.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64_wrs_vxworks.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::vxworks::opts();
     base.cpu = "ppc64".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_freebsd.rs b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_freebsd.rs
index 812b5928966f6..7deb248aa9fc7 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_freebsd.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_freebsd.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::freebsd::opts();
     base.cpu = "ppc64le".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_gnu.rs
index e3c4b3b585c2a..2515f011f24f8 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_gnu.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.cpu = "ppc64le".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_musl.rs b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_musl.rs
index 497a40ade81e8..7d25e1359afba 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_musl.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc64le_unknown_linux_musl.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::linux_musl::opts();
     base.cpu = "ppc64le".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_unknown_freebsd.rs b/compiler/rustc_target/src/spec/targets/powerpc_unknown_freebsd.rs
index 194bb0566f18f..7b2acb20ec51d 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_unknown_freebsd.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_unknown_freebsd.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 pub fn target() -> Target {
     let mut base = base::freebsd::opts();
     // Extra hint to linker that we are generating secure-PLT code.
-    base.add_pre_link_args(
+    base.pre_link_args = TargetOptions::link_args(
         LinkerFlavor::Gnu(Cc::Yes, Lld::No),
         &["-m32", "--target=powerpc-unknown-freebsd13.0"],
     );
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnu.rs
index b88b2fbf80948..fd2a367a85844 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnu.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnuspe.rs b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnuspe.rs
index b09c4cd21e046..fe8c7aad73079 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnuspe.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_gnuspe.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mspe"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mspe"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_musl.rs b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_musl.rs
index 67b19e9048916..5fa90e242ad48 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_musl.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_unknown_linux_musl.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::linux_musl::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_unknown_netbsd.rs b/compiler/rustc_target/src/spec/targets/powerpc_unknown_netbsd.rs
index c592cd3f6fd2d..a13a1fbd8c0bb 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_unknown_netbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_unknown_netbsd.rs
@@ -3,7 +3,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::netbsd::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks.rs b/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks.rs
index 91925ce151dfd..a89ea8f5fda4c 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks.rs
@@ -3,7 +3,8 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::vxworks::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "--secure-plt"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m32", "--secure-plt"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks_spe.rs b/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks_spe.rs
index 7640feb28e3c9..2affe53a05c33 100644
--- a/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks_spe.rs
+++ b/compiler/rustc_target/src/spec/targets/powerpc_wrs_vxworks_spe.rs
@@ -3,7 +3,8 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOpt
 
 pub fn target() -> Target {
     let mut base = base::vxworks::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mspe", "--secure-plt"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mspe", "--secure-plt"]);
     base.max_atomic_width = Some(32);
     base.stack_probes = StackProbeType::Inline;
 
diff --git a/compiler/rustc_target/src/spec/targets/sparc64_unknown_netbsd.rs b/compiler/rustc_target/src/spec/targets/sparc64_unknown_netbsd.rs
index 42944367cf666..3e4bbf538af5e 100644
--- a/compiler/rustc_target/src/spec/targets/sparc64_unknown_netbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/sparc64_unknown_netbsd.rs
@@ -4,7 +4,7 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 pub fn target() -> Target {
     let mut base = base::netbsd::opts();
     base.cpu = "v9".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/sparc64_unknown_openbsd.rs b/compiler/rustc_target/src/spec/targets/sparc64_unknown_openbsd.rs
index f0bf55d33e684..d9c3e63da1a65 100644
--- a/compiler/rustc_target/src/spec/targets/sparc64_unknown_openbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/sparc64_unknown_openbsd.rs
@@ -1,11 +1,11 @@
 use crate::abi::Endian;
-use crate::spec::{base, Cc, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::openbsd::opts();
     base.endian = Endian::Big;
     base.cpu = "v9".into();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(64);
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/sparc_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/sparc_unknown_linux_gnu.rs
index c10f9d82d4636..eff9d4deea9b8 100644
--- a/compiler/rustc_target/src/spec/targets/sparc_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/sparc_unknown_linux_gnu.rs
@@ -1,12 +1,13 @@
 use crate::abi::Endian;
-use crate::spec::{base, Cc, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.endian = Endian::Big;
     base.cpu = "v9".into();
     base.max_atomic_width = Some(32);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mv8plus"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mv8plus"]);
 
     Target {
         llvm_target: "sparc-unknown-linux-gnu".into(),
diff --git a/compiler/rustc_target/src/spec/targets/sparcv9_sun_solaris.rs b/compiler/rustc_target/src/spec/targets/sparcv9_sun_solaris.rs
index a42243f59dc4d..99a163af92709 100644
--- a/compiler/rustc_target/src/spec/targets/sparcv9_sun_solaris.rs
+++ b/compiler/rustc_target/src/spec/targets/sparcv9_sun_solaris.rs
@@ -1,10 +1,10 @@
 use crate::abi::Endian;
-use crate::spec::{base, Cc, LinkerFlavor, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::solaris::opts();
     base.endian = Endian::Big;
-    base.add_pre_link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64"]);
     // llvm calls this "v9"
     base.cpu = "v9".into();
     base.vendor = "sun".into();
diff --git a/compiler/rustc_target/src/spec/targets/thumbv7a_pc_windows_msvc.rs b/compiler/rustc_target/src/spec/targets/thumbv7a_pc_windows_msvc.rs
index 13e1e349b0402..836740144e67e 100644
--- a/compiler/rustc_target/src/spec/targets/thumbv7a_pc_windows_msvc.rs
+++ b/compiler/rustc_target/src/spec/targets/thumbv7a_pc_windows_msvc.rs
@@ -9,7 +9,7 @@ pub fn target() -> Target {
     // should be smart enough to insert branch islands only
     // where necessary, but this is not the observed behavior.
     // Disabling the LBR optimization works around the issue.
-    base.add_pre_link_args(LinkerFlavor::Msvc(Lld::No), &["/OPT:NOLBR"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Msvc(Lld::No), &["/OPT:NOLBR"]);
 
     Target {
         llvm_target: "thumbv7a-pc-windows-msvc".into(),
diff --git a/compiler/rustc_target/src/spec/targets/thumbv7neon_linux_androideabi.rs b/compiler/rustc_target/src/spec/targets/thumbv7neon_linux_androideabi.rs
index d50e63b92175d..8f07873826ba6 100644
--- a/compiler/rustc_target/src/spec/targets/thumbv7neon_linux_androideabi.rs
+++ b/compiler/rustc_target/src/spec/targets/thumbv7neon_linux_androideabi.rs
@@ -10,7 +10,8 @@ use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::android::opts();
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-march=armv7-a"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-march=armv7-a"]);
     Target {
         llvm_target: "armv7-none-linux-android".into(),
         metadata: crate::spec::TargetMetadata {
diff --git a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs
index 195ff46cf9d25..d61ce747aca1a 100644
--- a/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs
+++ b/compiler/rustc_target/src/spec/targets/wasm32_unknown_emscripten.rs
@@ -1,10 +1,9 @@
-use crate::spec::{
-    base, cvs, LinkArgs, LinkerFlavor, PanicStrategy, RelocModel, Target, TargetOptions,
-};
+use crate::spec::{base, cvs, LinkerFlavor, PanicStrategy};
+use crate::spec::{RelocModel, Target, TargetOptions};
 
 pub fn target() -> Target {
     // Reset flags for non-Em flavors back to empty to satisfy sanity checking tests.
-    let pre_link_args = LinkArgs::new();
+    let pre_link_args = Default::default();
     let post_link_args = TargetOptions::link_args(LinkerFlavor::EmCc, &["-sABORTING_MALLOC=0"]);
 
     let opts = TargetOptions {
diff --git a/compiler/rustc_target/src/spec/targets/wasm32_unknown_unknown.rs b/compiler/rustc_target/src/spec/targets/wasm32_unknown_unknown.rs
index 23f4772c39cb5..62a8794212d60 100644
--- a/compiler/rustc_target/src/spec/targets/wasm32_unknown_unknown.rs
+++ b/compiler/rustc_target/src/spec/targets/wasm32_unknown_unknown.rs
@@ -9,30 +9,31 @@
 //!
 //! This target is more or less managed by the Rust and WebAssembly Working
 //! Group nowadays at <https://github.com/rustwasm>.
-
-use crate::spec::{base, Cc, LinkerFlavor, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut options = base::wasm::options();
     options.os = "unknown".into();
 
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::No),
-        &[
-            // For now this target just never has an entry symbol no matter the output
-            // type, so unconditionally pass this.
-            "--no-entry",
-        ],
-    );
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::Yes),
-        &[
-            // Make sure clang uses LLD as its linker and is configured appropriately
-            // otherwise
-            "--target=wasm32-unknown-unknown",
-            "-Wl,--no-entry",
-        ],
-    );
+    options.pre_link_args = TargetOptions::link_args_list(&[
+        (
+            LinkerFlavor::WasmLld(Cc::No),
+            &[
+                // For now this target just never has an entry symbol no matter the output
+                // type, so unconditionally pass this.
+                "--no-entry",
+            ],
+        ),
+        (
+            LinkerFlavor::WasmLld(Cc::Yes),
+            &[
+                // Make sure clang uses LLD as its linker and is configured appropriately
+                // otherwise
+                "--target=wasm32-unknown-unknown",
+                "-Wl,--no-entry",
+            ],
+        ),
+    ]);
 
     Target {
         llvm_target: "wasm32-unknown-unknown".into(),
diff --git a/compiler/rustc_target/src/spec/targets/wasm32_wasip1.rs b/compiler/rustc_target/src/spec/targets/wasm32_wasip1.rs
index 4c2d222b590e4..2fe82cb571540 100644
--- a/compiler/rustc_target/src/spec/targets/wasm32_wasip1.rs
+++ b/compiler/rustc_target/src/spec/targets/wasm32_wasip1.rs
@@ -12,6 +12,7 @@
 
 use crate::spec::crt_objects;
 use crate::spec::LinkSelfContainedDefault;
+use crate::spec::TargetOptions;
 use crate::spec::{base, Cc, LinkerFlavor, Target};
 
 pub fn target() -> Target {
@@ -19,7 +20,8 @@ pub fn target() -> Target {
 
     options.os = "wasi".into();
     options.env = "p1".into();
-    options.add_pre_link_args(LinkerFlavor::WasmLld(Cc::Yes), &["--target=wasm32-wasi"]);
+    options.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::WasmLld(Cc::Yes), &["--target=wasm32-wasi"]);
 
     options.pre_link_objects_self_contained = crt_objects::pre_wasi_self_contained();
     options.post_link_objects_self_contained = crt_objects::post_wasi_self_contained();
diff --git a/compiler/rustc_target/src/spec/targets/wasm32_wasip1_threads.rs b/compiler/rustc_target/src/spec/targets/wasm32_wasip1_threads.rs
index 38af48ab2665a..72e4e03bac707 100644
--- a/compiler/rustc_target/src/spec/targets/wasm32_wasip1_threads.rs
+++ b/compiler/rustc_target/src/spec/targets/wasm32_wasip1_threads.rs
@@ -7,7 +7,9 @@
 //!
 //! Historically this target was known as `wasm32-wasi-preview1-threads`.
 
-use crate::spec::{base, crt_objects, Cc, LinkSelfContainedDefault, LinkerFlavor, Target};
+use crate::spec::{
+    base, crt_objects, Cc, LinkSelfContainedDefault, LinkerFlavor, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut options = base::wasm::options();
@@ -15,19 +17,18 @@ pub fn target() -> Target {
     options.os = "wasi".into();
     options.env = "p1".into();
 
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::No),
-        &["--import-memory", "--export-memory", "--shared-memory"],
-    );
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::Yes),
-        &[
-            "--target=wasm32-wasip1-threads",
-            "-Wl,--import-memory",
-            "-Wl,--export-memory,",
-            "-Wl,--shared-memory",
-        ],
-    );
+    options.pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::WasmLld(Cc::No), &["--import-memory", "--export-memory", "--shared-memory"]),
+        (
+            LinkerFlavor::WasmLld(Cc::Yes),
+            &[
+                "--target=wasm32-wasip1-threads",
+                "-Wl,--import-memory",
+                "-Wl,--export-memory,",
+                "-Wl,--shared-memory",
+            ],
+        ),
+    ]);
 
     options.pre_link_objects_self_contained = crt_objects::pre_wasi_self_contained();
     options.post_link_objects_self_contained = crt_objects::post_wasi_self_contained();
diff --git a/compiler/rustc_target/src/spec/targets/wasm64_unknown_unknown.rs b/compiler/rustc_target/src/spec/targets/wasm64_unknown_unknown.rs
index 8edde36dac623..28883ee3c77a7 100644
--- a/compiler/rustc_target/src/spec/targets/wasm64_unknown_unknown.rs
+++ b/compiler/rustc_target/src/spec/targets/wasm64_unknown_unknown.rs
@@ -7,30 +7,32 @@
 //! the standard library is available, most of it returns an error immediately
 //! (e.g. trying to create a TCP stream or something like that).
 
-use crate::spec::{base, Cc, LinkerFlavor, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut options = base::wasm::options();
     options.os = "unknown".into();
 
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::No),
-        &[
-            // For now this target just never has an entry symbol no matter the output
-            // type, so unconditionally pass this.
-            "--no-entry",
-            "-mwasm64",
-        ],
-    );
-    options.add_pre_link_args(
-        LinkerFlavor::WasmLld(Cc::Yes),
-        &[
-            // Make sure clang uses LLD as its linker and is configured appropriately
-            // otherwise
-            "--target=wasm64-unknown-unknown",
-            "-Wl,--no-entry",
-        ],
-    );
+    options.pre_link_args = TargetOptions::link_args_list(&[
+        (
+            LinkerFlavor::WasmLld(Cc::No),
+            &[
+                // For now this target just never has an entry symbol no matter the output
+                // type, so unconditionally pass this.
+                "--no-entry",
+                "-mwasm64",
+            ],
+        ),
+        (
+            LinkerFlavor::WasmLld(Cc::Yes),
+            &[
+                // Make sure clang uses LLD as its linker and is configured appropriately
+                // otherwise
+                "--target=wasm64-unknown-unknown",
+                "-Wl,--no-entry",
+            ],
+        ),
+    ]);
 
     // Any engine that implements wasm64 will surely implement the rest of these
     // features since they were all merged into the official spec by the time
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 21acd750df2dc..bd59dc3f813bd 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,13 +1,17 @@
 use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, SanitizerSet};
+use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, MaybeLazy, SanitizerSet};
 use crate::spec::{Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::X86_64;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::X86_64;
+    const OS: &'static str = "macos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     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.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
     base.supported_sanitizers =
         SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::LEAK | SanitizerSet::THREAD;
 
@@ -15,7 +19,7 @@ pub fn target() -> 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: MaybeLazy::lazy(|| macos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -25,7 +29,7 @@ pub 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(),
+        arch: ARCH.target_arch(),
         options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
     }
 }
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 ec61b7967646e..e59dbe0c6c761 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,18 @@
 use crate::spec::base::apple::{ios_sim_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{SanitizerSet, Target, TargetOptions};
+use crate::spec::{MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::X86_64;
+    const ARCH: Arch = Arch::X86_64;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::Simulator;
+
     // 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);
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::THREAD;
 
     Target {
-        llvm_target: ios_sim_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| ios_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -19,7 +22,7 @@ pub 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(),
+        arch: ARCH.target_arch(),
         options: TargetOptions { max_atomic_width: Some(128), ..base },
     }
 }
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 bd967ee972b32..04d0bc7775200 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,16 @@
 use crate::spec::base::apple::{mac_catalyst_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{SanitizerSet, Target, TargetOptions};
+use crate::spec::{MaybeLazy, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::X86_64;
-    let mut base = opts("ios", arch, TargetAbi::MacCatalyst);
+    const ARCH: Arch = Arch::X86_64;
+    const OS: &'static str = "ios";
+    const ABI: TargetAbi = TargetAbi::MacCatalyst;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.supported_sanitizers = SanitizerSet::ADDRESS | SanitizerSet::LEAK | SanitizerSet::THREAD;
 
     Target {
-        llvm_target: mac_catalyst_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| mac_catalyst_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -17,7 +20,7 @@ pub 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(),
+        arch: ARCH.target_arch(),
         options: TargetOptions { max_atomic_width: Some(128), ..base },
     }
 }
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 55b2e1afcd392..d6495f83c8ff9 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,16 @@
 use crate::spec::base::apple::{opts, tvos_sim_llvm_target, Arch, TargetAbi};
-use crate::spec::{Target, TargetOptions};
+use crate::spec::{MaybeLazy, Target, TargetOptions};
 
 pub 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;
+
+    const ARCH: Arch = Arch::X86_64;
+    const OS: &'static str = "tvos";
+    const ABI: TargetAbi = TargetAbi::Simulator;
+
     Target {
-        llvm_target: tvos_sim_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| tvos_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -17,7 +20,7 @@ pub 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: ARCH.target_arch(),
+        options: TargetOptions { max_atomic_width: Some(128), ..opts(OS, ARCH, ABI) },
     }
 }
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 a783eff15b261..a00e94a1a713c 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,13 @@
 use crate::spec::base::apple::{opts, watchos_sim_llvm_target, Arch, TargetAbi};
-use crate::spec::{Target, TargetOptions};
+use crate::spec::{MaybeLazy, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::X86_64;
+    const ARCH: Arch = Arch::X86_64;
+    const OS: &'static str = "watchos";
+    const ABI: TargetAbi = TargetAbi::Simulator;
+
     Target {
-        llvm_target: watchos_sim_llvm_target(arch).into(),
+        llvm_target: MaybeLazy::lazy(|| watchos_sim_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -14,10 +17,7 @@ pub 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: ARCH.target_arch(),
+        options: TargetOptions { max_atomic_width: Some(128), ..opts(OS, ARCH, ABI) },
     }
 }
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_linux_android.rs b/compiler/rustc_target/src/spec/targets/x86_64_linux_android.rs
index 92711bbe246b7..7fa499c29657b 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_linux_android.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_linux_android.rs
@@ -9,7 +9,7 @@ pub fn target() -> Target {
     // https://developer.android.com/ndk/guides/abis.html#86-64
     base.features = "+mmx,+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+popcnt".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.supports_xray = true;
 
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_pc_solaris.rs b/compiler/rustc_target/src/spec/targets/x86_64_pc_solaris.rs
index 4dbe049a4b782..df7b124f81008 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_pc_solaris.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_pc_solaris.rs
@@ -1,8 +1,8 @@
-use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::solaris::opts();
-    base.add_pre_link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64"]);
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.vendor = "pc".into();
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnu.rs b/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnu.rs
index de0f17246c3dc..cfc158eadf284 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnu.rs
@@ -1,4 +1,4 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_gnu::opts();
@@ -6,11 +6,10 @@ pub fn target() -> Target {
     base.features = "+cx16,+sse3,+sahf".into();
     base.plt_by_default = false;
     // Use high-entropy 64 bit address space for ASLR
-    base.add_pre_link_args(
-        LinkerFlavor::Gnu(Cc::No, Lld::No),
-        &["-m", "i386pep", "--high-entropy-va"],
-    );
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64", "-Wl,--high-entropy-va"]);
+    base.pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::Gnu(Cc::No, Lld::No), &["-m", "i386pep", "--high-entropy-va"]),
+        (LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64", "-Wl,--high-entropy-va"]),
+    ]);
     base.max_atomic_width = Some(128);
     base.linker = Some("x86_64-w64-mingw32-gcc".into());
 
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnullvm.rs b/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnullvm.rs
index b485970bb416d..a19e53ce93dc1 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnullvm.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_pc_windows_gnullvm.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_gnullvm::opts();
     base.cpu = "x86-64".into();
     base.features = "+cx16,+sse3,+sahf".into();
     base.plt_by_default = false;
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.max_atomic_width = Some(128);
     base.linker = Some("x86_64-w64-mingw32-clang".into());
 
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_dragonfly.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_dragonfly.rs
index aef95e373cbfa..38d0eb6bbb5f4 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_dragonfly.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_dragonfly.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::dragonfly::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_freebsd.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_freebsd.rs
index 15146a5ef7299..2cbf9d91f2717 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_freebsd.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_freebsd.rs
@@ -1,11 +1,13 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target};
+use crate::spec::{
+    base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut base = base::freebsd::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.supported_sanitizers =
         SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::MEMORY | SanitizerSet::THREAD;
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_haiku.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_haiku.rs
index 9f62eb1fa270d..6956fc788e12d 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_haiku.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_haiku.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::haiku::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     // This option is required to build executables on Haiku x86_64
     base.position_independent_executables = true;
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_illumos.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_illumos.rs
index c52cdf466abe7..d861b6e81d392 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_illumos.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_illumos.rs
@@ -1,8 +1,9 @@
-use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, Target};
+use crate::spec::{base, Cc, LinkerFlavor, SanitizerSet, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::illumos::opts();
-    base.add_pre_link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64", "-std=c99"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Unix(Cc::Yes), &["-m64", "-std=c99"]);
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnu.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnu.rs
index bd12d4d8af0e0..55eb001b4995f 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnu.rs
@@ -1,11 +1,13 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target};
+use crate::spec::{
+    base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.static_position_independent_executables = true;
     base.supported_sanitizers = SanitizerSet::ADDRESS
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnux32.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnux32.rs
index f6e0b051e8f54..2853b78fe6615 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnux32.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnux32.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::linux_gnu::opts();
     base.cpu = "x86-64".into();
     base.abi = "x32".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mx32"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-mx32"]);
     base.stack_probes = StackProbeType::Inline;
     base.has_thread_local = false;
     // BUG(GabrielMajeri): disabling the PLT on x86_64 Linux with x32 ABI
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_musl.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_musl.rs
index 66237f071028d..669de753c01e6 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_musl.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_musl.rs
@@ -1,11 +1,13 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target};
+use crate::spec::{
+    base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut base = base::linux_musl::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.static_position_independent_executables = true;
     base.supported_sanitizers = SanitizerSet::ADDRESS
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_ohos.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_ohos.rs
index db8db1d253824..dbe6433d543bb 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_ohos.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_ohos.rs
@@ -1,10 +1,12 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target};
+use crate::spec::{
+    base, Cc, LinkerFlavor, Lld, SanitizerSet, StackProbeType, Target, TargetOptions,
+};
 
 pub fn target() -> Target {
     let mut base = base::linux_ohos::opts();
     base.cpu = "x86-64".into();
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.static_position_independent_executables = true;
     base.supported_sanitizers = SanitizerSet::ADDRESS
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_netbsd.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_netbsd.rs
index 38ae3a4fe4248..222f90bc147e6 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_netbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_netbsd.rs
@@ -7,7 +7,7 @@ pub fn target() -> Target {
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.supported_sanitizers = SanitizerSet::ADDRESS
         | SanitizerSet::CFI
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_openbsd.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_openbsd.rs
index 4d7eba2421394..cd076fc15d931 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_openbsd.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_openbsd.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::openbsd::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.supports_xray = true;
 
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_unknown_redox.rs b/compiler/rustc_target/src/spec/targets/x86_64_unknown_redox.rs
index 99f5d9dc41d29..7b85a05f53ecd 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_unknown_redox.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_unknown_redox.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::redox::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_uwp_windows_gnu.rs b/compiler/rustc_target/src/spec/targets/x86_64_uwp_windows_gnu.rs
index aef6fd1a7814e..2b9ea5a295728 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_uwp_windows_gnu.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_uwp_windows_gnu.rs
@@ -1,4 +1,4 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::windows_uwp_gnu::opts();
@@ -6,11 +6,10 @@ pub fn target() -> Target {
     base.features = "+cx16,+sse3,+sahf".into();
     base.plt_by_default = false;
     // Use high-entropy 64 bit address space for ASLR
-    base.add_pre_link_args(
-        LinkerFlavor::Gnu(Cc::No, Lld::No),
-        &["-m", "i386pep", "--high-entropy-va"],
-    );
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64", "-Wl,--high-entropy-va"]);
+    base.pre_link_args = TargetOptions::link_args_list(&[
+        (LinkerFlavor::Gnu(Cc::No, Lld::No), &["-m", "i386pep", "--high-entropy-va"]),
+        (LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64", "-Wl,--high-entropy-va"]),
+    ]);
     base.max_atomic_width = Some(128);
 
     Target {
diff --git a/compiler/rustc_target/src/spec/targets/x86_64_wrs_vxworks.rs b/compiler/rustc_target/src/spec/targets/x86_64_wrs_vxworks.rs
index b956d228c17fe..adb23c8c3ab91 100644
--- a/compiler/rustc_target/src/spec/targets/x86_64_wrs_vxworks.rs
+++ b/compiler/rustc_target/src/spec/targets/x86_64_wrs_vxworks.rs
@@ -1,11 +1,11 @@
-use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target};
+use crate::spec::{base, Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
     let mut base = base::vxworks::opts();
     base.cpu = "x86-64".into();
     base.plt_by_default = false;
     base.max_atomic_width = Some(64);
-    base.add_pre_link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args = TargetOptions::link_args(LinkerFlavor::Gnu(Cc::Yes, Lld::No), &["-m64"]);
     base.stack_probes = StackProbeType::Inline;
     base.disable_redzone = true;
 
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 fe6cbca32c748..1be5856b4ca32 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,17 @@
 use crate::spec::base::apple::{macos_llvm_target, opts, Arch, TargetAbi};
-use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, SanitizerSet};
+use crate::spec::{Cc, FramePointer, LinkerFlavor, Lld, MaybeLazy, SanitizerSet};
 use crate::spec::{Target, TargetOptions};
 
 pub fn target() -> Target {
-    let arch = Arch::X86_64h;
-    let mut base = opts("macos", arch, TargetAbi::Normal);
+    const ARCH: Arch = Arch::X86_64h;
+    const OS: &'static str = "macos";
+    const ABI: TargetAbi = TargetAbi::Normal;
+
+    let mut base = opts(OS, ARCH, ABI);
     base.max_atomic_width = Some(128);
     base.frame_pointer = FramePointer::Always;
-    base.add_pre_link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
+    base.pre_link_args =
+        TargetOptions::link_args(LinkerFlavor::Darwin(Cc::Yes, Lld::No), &["-m64"]);
     base.supported_sanitizers =
         SanitizerSet::ADDRESS | SanitizerSet::CFI | SanitizerSet::LEAK | SanitizerSet::THREAD;
 
@@ -33,7 +37,7 @@ pub fn target() -> 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: MaybeLazy::lazy(|| macos_llvm_target(ARCH)),
         metadata: crate::spec::TargetMetadata {
             description: None,
             tier: None,
@@ -43,7 +47,7 @@ pub 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(),
+        arch: ARCH.target_arch(),
         options: TargetOptions { mcount: "\u{1}mcount".into(), ..base },
     }
 }
diff --git a/compiler/rustc_target/src/spec/tests/tests_impl.rs b/compiler/rustc_target/src/spec/tests/tests_impl.rs
index 3be18ef3127d5..e84aa598c20ed 100644
--- a/compiler/rustc_target/src/spec/tests/tests_impl.rs
+++ b/compiler/rustc_target/src/spec/tests/tests_impl.rs
@@ -37,7 +37,7 @@ impl Target {
             &self.late_link_args_static,
             &self.post_link_args,
         ] {
-            for (&flavor, flavor_args) in args {
+            for (&flavor, flavor_args) in &**args {
                 assert!(!flavor_args.is_empty());
                 // Check that flavors mentioned in link args are compatible with the default flavor.
                 match self.linker_flavor {