From 82b4b6a501654ae66fc1c0106a5d3dee2c3131be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 17 Mar 2025 21:57:44 +0100
Subject: [PATCH 01/11] use LLD by default on x64 regardless of channel

---
 src/bootstrap/src/core/build_steps/compile.rs | 4 +---
 src/bootstrap/src/core/config/config.rs       | 6 +-----
 2 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/src/bootstrap/src/core/build_steps/compile.rs b/src/bootstrap/src/core/build_steps/compile.rs
index 2e5865e509695..0235045cf3533 100644
--- a/src/bootstrap/src/core/build_steps/compile.rs
+++ b/src/bootstrap/src/core/build_steps/compile.rs
@@ -1326,9 +1326,7 @@ pub fn rustc_cargo_env(
     }
 
     // Enable rustc's env var for `rust-lld` when requested.
-    if builder.config.lld_enabled
-        && (builder.config.channel == "dev" || builder.config.channel == "nightly")
-    {
+    if builder.config.lld_enabled {
         cargo.env("CFG_USE_SELF_CONTAINED_LINKER", "1");
     }
 
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index 65a3e7667e7f0..b71f18410edfc 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -2469,7 +2469,6 @@ impl Config {
         // build our internal lld and use it as the default linker, by setting the `rust.lld` config
         // to true by default:
         // - on the `x86_64-unknown-linux-gnu` target
-        // - on the `dev` and `nightly` channels
         // - when building our in-tree llvm (i.e. the target has not set an `llvm-config`), so that
         //   we're also able to build the corresponding lld
         // - or when using an external llvm that's downloaded from CI, which also contains our prebuilt
@@ -2478,10 +2477,7 @@ impl Config {
         //   thus, disabled
         // - similarly, lld will not be built nor used by default when explicitly asked not to, e.g.
         //   when the config sets `rust.lld = false`
-        if config.build.triple == "x86_64-unknown-linux-gnu"
-            && config.hosts == [config.build]
-            && (config.channel == "dev" || config.channel == "nightly")
-        {
+        if config.build.triple == "x86_64-unknown-linux-gnu" && config.hosts == [config.build] {
             let no_llvm_config = config
                 .target_config
                 .get(&config.build)

From 4f25e77c22cda68d2b38f4272c69fff455bb08e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Tue, 18 Mar 2025 08:17:38 +0100
Subject: [PATCH 02/11] add post-dist test for checking that we use LLD

And remove the previous beta/stable/nightly LLD tests.
---
 src/tools/opt-dist/src/tests.rs               |  3 +++
 .../rust-lld-by-default-beta-stable/main.rs   |  1 -
 .../rust-lld-by-default-beta-stable/rmake.rs  | 14 -------------
 .../main.rs                                   |  0
 .../rmake.rs                                  | 11 ++++------
 .../rust-lld-x86_64-unknown-linux-gnu/main.rs |  5 +++++
 .../rmake.rs                                  | 20 +++++++++++++++++++
 7 files changed, 32 insertions(+), 22 deletions(-)
 delete mode 100644 tests/run-make/rust-lld-by-default-beta-stable/main.rs
 delete mode 100644 tests/run-make/rust-lld-by-default-beta-stable/rmake.rs
 rename tests/run-make/{rust-lld-by-default-nightly => rust-lld-x86_64-unknown-linux-gnu-dist}/main.rs (100%)
 rename tests/run-make/{rust-lld-by-default-nightly => rust-lld-x86_64-unknown-linux-gnu-dist}/rmake.rs (59%)
 create mode 100644 tests/run-make/rust-lld-x86_64-unknown-linux-gnu/main.rs
 create mode 100644 tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs

diff --git a/src/tools/opt-dist/src/tests.rs b/src/tools/opt-dist/src/tests.rs
index 53ce772fa7792..6ee06e6069ac3 100644
--- a/src/tools/opt-dist/src/tests.rs
+++ b/src/tools/opt-dist/src/tests.rs
@@ -104,7 +104,10 @@ llvm-config = "{llvm_config}"
             "tests/incremental",
             "tests/mir-opt",
             "tests/pretty",
+            // Make sure that we don't use too new GLIBC symbols on x64
             "tests/run-make/glibc-symbols-x86_64-unknown-linux-gnu",
+            // Make sure that we use LLD by default on x64
+            "tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist",
             "tests/ui",
             "tests/crashes",
         ];
diff --git a/tests/run-make/rust-lld-by-default-beta-stable/main.rs b/tests/run-make/rust-lld-by-default-beta-stable/main.rs
deleted file mode 100644
index f328e4d9d04c3..0000000000000
--- a/tests/run-make/rust-lld-by-default-beta-stable/main.rs
+++ /dev/null
@@ -1 +0,0 @@
-fn main() {}
diff --git a/tests/run-make/rust-lld-by-default-beta-stable/rmake.rs b/tests/run-make/rust-lld-by-default-beta-stable/rmake.rs
deleted file mode 100644
index 9a08991c4b895..0000000000000
--- a/tests/run-make/rust-lld-by-default-beta-stable/rmake.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Ensure that rust-lld is *not* used as the default linker on `x86_64-unknown-linux-gnu` on stable
-// or beta.
-
-//@ ignore-nightly
-//@ only-x86_64-unknown-linux-gnu
-
-use run_make_support::linker::assert_rustc_doesnt_use_lld;
-use run_make_support::rustc;
-
-fn main() {
-    // A regular compilation should not use rust-lld by default. We'll check that by asking the
-    // linker to display its version number with a link-arg.
-    assert_rustc_doesnt_use_lld(rustc().input("main.rs"));
-}
diff --git a/tests/run-make/rust-lld-by-default-nightly/main.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/main.rs
similarity index 100%
rename from tests/run-make/rust-lld-by-default-nightly/main.rs
rename to tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/main.rs
diff --git a/tests/run-make/rust-lld-by-default-nightly/rmake.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
similarity index 59%
rename from tests/run-make/rust-lld-by-default-nightly/rmake.rs
rename to tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
index 3ff1e2770e65e..c26f82b7d3743 100644
--- a/tests/run-make/rust-lld-by-default-nightly/rmake.rs
+++ b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
@@ -1,17 +1,14 @@
-// Ensure that rust-lld is used as the default linker on `x86_64-unknown-linux-gnu` on the nightly
-// channel, and that it can also be turned off with a CLI flag.
+// Ensure that rust-lld is used as the default linker on `x86_64-unknown-linux-gnu`
+// dist artifacts and that it can also be turned off with a CLI flag.
 
-//@ needs-rust-lld
-//@ ignore-beta
-//@ ignore-stable
+//@ only-dist
 //@ only-x86_64-unknown-linux-gnu
 
 use run_make_support::linker::{assert_rustc_doesnt_use_lld, assert_rustc_uses_lld};
 use run_make_support::rustc;
 
 fn main() {
-    // A regular compilation should use rust-lld by default. We'll check that by asking the linker
-    // to display its version number with a link-arg.
+    // A regular compilation should use rust-lld by default.
     assert_rustc_uses_lld(rustc().input("main.rs"));
 
     // But it can still be disabled by turning the linker feature off.
diff --git a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/main.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/main.rs
new file mode 100644
index 0000000000000..e9f655fc09e4c
--- /dev/null
+++ b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/main.rs
@@ -0,0 +1,5 @@
+// Test linking using `cc` with `rust-lld`, which is on by default on the x86_64-unknown-linux-gnu
+// target.
+// See https://github.com/rust-lang/compiler-team/issues/510 for more info
+
+fn main() {}
diff --git a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs
new file mode 100644
index 0000000000000..e71a47f11e263
--- /dev/null
+++ b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs
@@ -0,0 +1,20 @@
+// Ensure that rust-lld is used as the default linker on `x86_64-unknown-linux-gnu`
+// and that it can also be turned off with a CLI flag.
+//
+// This version of the test checks that LLD is used by default when LLD is enabled in the
+// toolchain. There is a separate test that checks that LLD is used for dist artifacts
+// unconditionally.
+
+//@ needs-rust-lld
+//@ only-x86_64-unknown-linux-gnu
+
+use run_make_support::linker::{assert_rustc_doesnt_use_lld, assert_rustc_uses_lld};
+use run_make_support::rustc;
+
+fn main() {
+    // A regular compilation should use rust-lld by default.
+    assert_rustc_uses_lld(rustc().input("main.rs"));
+
+    // But it can still be disabled by turning the linker feature off.
+    assert_rustc_doesnt_use_lld(rustc().arg("-Zlinker-features=-lld").input("main.rs"));
+}

From b6c94400dc86cc8e586b65af1774d0049c2bcbbf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Wed, 19 Mar 2025 16:59:21 +0100
Subject: [PATCH 03/11] stabilize `-Zlinker-features` as `-Clinker-features`

---
 compiler/rustc_codegen_ssa/src/back/link.rs            |  2 +-
 compiler/rustc_session/src/config.rs                   |  2 +-
 compiler/rustc_session/src/options.rs                  |  4 ++--
 compiler/rustc_target/src/spec/mod.rs                  |  4 ++--
 tests/run-make/compressed-debuginfo-zstd/rmake.rs      |  2 +-
 tests/run-make/rust-lld-custom-target/rmake.rs         |  2 +-
 tests/run-make/rust-lld-link-script-provide/rmake.rs   |  3 +--
 .../rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs    |  2 +-
 .../rust-lld-x86_64-unknown-linux-gnu/rmake.rs         |  2 +-
 tests/run-make/rust-lld/rmake.rs                       | 10 +++++-----
 10 files changed, 16 insertions(+), 17 deletions(-)

diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index 159c17b0af757..146f0b6ec9742 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -1420,7 +1420,7 @@ pub fn linker_and_flavor(sess: &Session) -> (PathBuf, LinkerFlavor) {
         }
     }
 
-    let features = sess.opts.unstable_opts.linker_features;
+    let features = sess.opts.cg.linker_features;
 
     // linker and linker flavor specified via command line have precedence over what the target
     // specification specifies
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index a9d9236d3188c..2ee1ff31d0214 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -399,7 +399,7 @@ impl LinkSelfContained {
     }
 }
 
-/// The different values that `-Z linker-features` can take on the CLI: a list of individually
+/// The different values that `-C linker-features` can take on the CLI: a list of individually
 /// enabled or disabled features used during linking.
 ///
 /// There is no need to enable or disable them in bulk. Each feature is fine-grained, and can be
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 440e8f808c70a..1fc1648176983 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -1994,6 +1994,8 @@ options! {
         on a C toolchain or linker installed in the system"),
     linker: Option<PathBuf> = (None, parse_opt_pathbuf, [UNTRACKED],
         "system linker to link outputs with"),
+    linker_features: LinkerFeaturesCli = (LinkerFeaturesCli::default(), parse_linker_features, [UNTRACKED],
+        "a comma-separated list of linker features to enable (+) or disable (-): `lld`"),
     linker_flavor: Option<LinkerFlavorCli> = (None, parse_linker_flavor, [UNTRACKED],
         "linker flavor"),
     linker_plugin_lto: LinkerPluginLto = (LinkerPluginLto::Disabled,
@@ -2284,8 +2286,6 @@ options! {
         "link native libraries in the linker invocation (default: yes)"),
     link_only: bool = (false, parse_bool, [TRACKED],
         "link the `.rlink` file generated by `-Z no-link` (default: no)"),
-    linker_features: LinkerFeaturesCli = (LinkerFeaturesCli::default(), parse_linker_features, [UNTRACKED],
-        "a comma-separated list of linker features to enable (+) or disable (-): `lld`"),
     lint_llvm_ir: bool = (false, parse_bool, [TRACKED],
         "lint LLVM IR (default: no)"),
     lint_mir: bool = (false, parse_bool, [UNTRACKED],
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index 37ea0d6e7b58c..d9d381ca0742f 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -721,7 +721,7 @@ impl ToJson for LinkSelfContainedComponents {
 }
 
 bitflags::bitflags! {
-    /// The `-Z linker-features` components that can individually be enabled or disabled.
+    /// The `-C linker-features` components that can individually be enabled or disabled.
     ///
     /// They are feature flags intended to be a more flexible mechanism than linker flavors, and
     /// also to prevent a combinatorial explosion of flavors whenever a new linker feature is
@@ -752,7 +752,7 @@ bitflags::bitflags! {
 rustc_data_structures::external_bitflags_debug! { LinkerFeatures }
 
 impl LinkerFeatures {
-    /// Parses a single `-Z linker-features` well-known feature, not a set of flags.
+    /// Parses a single `-C linker-features` well-known feature, not a set of flags.
     pub fn from_str(s: &str) -> Option<LinkerFeatures> {
         Some(match s {
             "cc" => LinkerFeatures::CC,
diff --git a/tests/run-make/compressed-debuginfo-zstd/rmake.rs b/tests/run-make/compressed-debuginfo-zstd/rmake.rs
index cd8cf223047d3..8d7e5c089daa1 100644
--- a/tests/run-make/compressed-debuginfo-zstd/rmake.rs
+++ b/tests/run-make/compressed-debuginfo-zstd/rmake.rs
@@ -26,7 +26,7 @@ fn prepare_and_check<F: FnOnce(&mut Rustc) -> &mut Rustc>(to_find: &str, prepare
     run_in_tmpdir(|| {
         let mut rustc = Rustc::new();
         rustc
-            .arg("-Zlinker-features=+lld")
+            .arg("-Clinker-features=+lld")
             .arg("-Clink-self-contained=+linker")
             .arg("-Zunstable-options")
             .arg("-Cdebuginfo=full")
diff --git a/tests/run-make/rust-lld-custom-target/rmake.rs b/tests/run-make/rust-lld-custom-target/rmake.rs
index e2b065a10b170..d0db31408f7a3 100644
--- a/tests/run-make/rust-lld-custom-target/rmake.rs
+++ b/tests/run-make/rust-lld-custom-target/rmake.rs
@@ -23,7 +23,7 @@ fn main() {
         rustc()
             .crate_type("cdylib")
             .target("custom-target.json")
-            .arg("-Zlinker-features=-lld")
+            .arg("-Clinker-features=-lld")
             .input("lib.rs"),
     );
 }
diff --git a/tests/run-make/rust-lld-link-script-provide/rmake.rs b/tests/run-make/rust-lld-link-script-provide/rmake.rs
index e78a411bc15f0..0bd8f68116d01 100644
--- a/tests/run-make/rust-lld-link-script-provide/rmake.rs
+++ b/tests/run-make/rust-lld-link-script-provide/rmake.rs
@@ -10,9 +10,8 @@ use run_make_support::rustc;
 fn main() {
     rustc()
         .input("main.rs")
-        .arg("-Zlinker-features=+lld")
+        .arg("-Clinker-features=+lld")
         .arg("-Clink-self-contained=+linker")
-        .arg("-Zunstable-options")
         .link_arg("-Tscript.t")
         .run();
 }
diff --git a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
index c26f82b7d3743..c315d36a39d7c 100644
--- a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
+++ b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu-dist/rmake.rs
@@ -12,5 +12,5 @@ fn main() {
     assert_rustc_uses_lld(rustc().input("main.rs"));
 
     // But it can still be disabled by turning the linker feature off.
-    assert_rustc_doesnt_use_lld(rustc().arg("-Zlinker-features=-lld").input("main.rs"));
+    assert_rustc_doesnt_use_lld(rustc().arg("-Clinker-features=-lld").input("main.rs"));
 }
diff --git a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs
index e71a47f11e263..00415d27aaf33 100644
--- a/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs
+++ b/tests/run-make/rust-lld-x86_64-unknown-linux-gnu/rmake.rs
@@ -16,5 +16,5 @@ fn main() {
     assert_rustc_uses_lld(rustc().input("main.rs"));
 
     // But it can still be disabled by turning the linker feature off.
-    assert_rustc_doesnt_use_lld(rustc().arg("-Zlinker-features=-lld").input("main.rs"));
+    assert_rustc_doesnt_use_lld(rustc().arg("-Clinker-features=-lld").input("main.rs"));
 }
diff --git a/tests/run-make/rust-lld/rmake.rs b/tests/run-make/rust-lld/rmake.rs
index 9470f5d0be183..2b914f19ac089 100644
--- a/tests/run-make/rust-lld/rmake.rs
+++ b/tests/run-make/rust-lld/rmake.rs
@@ -12,14 +12,14 @@ fn main() {
     // asking the linker to display its version number with a link-arg.
     assert_rustc_uses_lld(
         rustc()
-            .arg("-Zlinker-features=+lld")
+            .arg("-Clinker-features=+lld")
             .arg("-Clink-self-contained=+linker")
             .arg("-Zunstable-options")
             .input("main.rs"),
     );
 
     // It should not be used when we explicitly opt out of lld.
-    assert_rustc_doesnt_use_lld(rustc().arg("-Zlinker-features=-lld").input("main.rs"));
+    assert_rustc_doesnt_use_lld(rustc().arg("-Clinker-features=-lld").input("main.rs"));
 
     // While we're here, also check that the last linker feature flag "wins" when passed multiple
     // times to rustc.
@@ -27,9 +27,9 @@ fn main() {
         rustc()
             .arg("-Clink-self-contained=+linker")
             .arg("-Zunstable-options")
-            .arg("-Zlinker-features=-lld")
-            .arg("-Zlinker-features=+lld")
-            .arg("-Zlinker-features=-lld,+lld")
+            .arg("-Clinker-features=-lld")
+            .arg("-Clinker-features=+lld")
+            .arg("-Clinker-features=-lld,+lld")
             .input("main.rs"),
     );
 }

From 31af605c87968b88d948d9616ca576241c8a51ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Wed, 19 Mar 2025 17:13:11 +0100
Subject: [PATCH 04/11] check that `-Clinker-features=[-+]lld` can only be used
 on x64 linux without `-Zunstable-options`

---
 compiler/rustc_session/src/config.rs          | 42 +++++++++++++++++--
 .../run-make/rust-lld-custom-target/rmake.rs  |  1 +
 .../linker-features-lld-disallowed-target.rs  |  8 ++++
 ...nker-features-lld-disallowed-target.stderr |  2 +
 .../ui/linking/linker-features-unstable-cc.rs |  8 ++++
 .../linker-features-unstable-cc.stderr        |  2 +
 6 files changed, 59 insertions(+), 4 deletions(-)
 create mode 100644 tests/ui/linking/linker-features-lld-disallowed-target.rs
 create mode 100644 tests/ui/linking/linker-features-lld-disallowed-target.stderr
 create mode 100644 tests/ui/linking/linker-features-unstable-cc.rs
 create mode 100644 tests/ui/linking/linker-features-unstable-cc.stderr

diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 2ee1ff31d0214..88f1de01646e3 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -439,6 +439,35 @@ impl LinkerFeaturesCli {
             _ => None,
         }
     }
+
+    /// Checks usage of unstable variants for linker features for the given `target_tuple`.
+    /// Returns `Ok` if no unstable variants are used.
+    pub(crate) fn check_unstable_variants(&self, target_tuple: &TargetTuple) -> Result<(), String> {
+        let mentioned_features = self.enabled.union(self.disabled);
+        let has_lld = mentioned_features.is_lld_enabled();
+
+        // Check that -Clinker-features=[-+]lld is not used anywhere else than on x64
+        // without -Zunstable-options.
+        if has_lld && target_tuple.tuple() != "x86_64-unknown-linux-gnu" {
+            return Err(format!(
+                "`-C linker-features` with lld are unstable for the `{target_tuple}` target, \
+the `-Z unstable-options` flag must also be passed to use it on this target",
+            ));
+        }
+
+        for feature in LinkerFeatures::all() {
+            // Check that no other features were enabled without -Zunstable-options
+            // Note that this should currently be unreachable, because the `-Clinker-features` parser
+            // currently only accepts lld.
+            if feature != LinkerFeatures::LLD && mentioned_features.contains(feature) {
+                return Err("`-C linker-features` is stable only for the lld feature, \
+the`-Z unstable-options` flag must also be passed to use it with other features"
+                    .to_string());
+            }
+        }
+
+        Ok(())
+    }
 }
 
 /// Used with `-Z assert-incr-state`.
@@ -2595,9 +2624,8 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
         }
     }
 
-    if !nightly_options::is_unstable_enabled(matches)
-        && cg.force_frame_pointers == FramePointer::NonLeaf
-    {
+    let unstable_options_enabled = nightly_options::is_unstable_enabled(matches);
+    if !unstable_options_enabled && cg.force_frame_pointers == FramePointer::NonLeaf {
         early_dcx.early_fatal(
             "`-Cforce-frame-pointers=non-leaf` or `always` also requires `-Zunstable-options` \
                 and a nightly compiler",
@@ -2607,7 +2635,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
     // For testing purposes, until we have more feedback about these options: ensure `-Z
     // unstable-options` is required when using the unstable `-C link-self-contained` and `-C
     // linker-flavor` options.
-    if !nightly_options::is_unstable_enabled(matches) {
+    if !unstable_options_enabled {
         let uses_unstable_self_contained_option =
             cg.link_self_contained.are_unstable_variants_set();
         if uses_unstable_self_contained_option {
@@ -2655,6 +2683,12 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
     let debuginfo = select_debuginfo(matches, &cg);
     let debuginfo_compression = unstable_opts.debuginfo_compression;
 
+    if !unstable_options_enabled {
+        if let Err(error) = cg.linker_features.check_unstable_variants(&target_triple) {
+            early_dcx.early_fatal(error);
+        }
+    }
+
     let crate_name = matches.opt_str("crate-name");
     let unstable_features = UnstableFeatures::from_environment(crate_name.as_deref());
     // Parse any `-l` flags, which link to native libraries.
diff --git a/tests/run-make/rust-lld-custom-target/rmake.rs b/tests/run-make/rust-lld-custom-target/rmake.rs
index d0db31408f7a3..90ba424ffe940 100644
--- a/tests/run-make/rust-lld-custom-target/rmake.rs
+++ b/tests/run-make/rust-lld-custom-target/rmake.rs
@@ -24,6 +24,7 @@ fn main() {
             .crate_type("cdylib")
             .target("custom-target.json")
             .arg("-Clinker-features=-lld")
+            .arg("-Zunstable-options")
             .input("lib.rs"),
     );
 }
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.rs b/tests/ui/linking/linker-features-lld-disallowed-target.rs
new file mode 100644
index 0000000000000..6ebafc4371c60
--- /dev/null
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.rs
@@ -0,0 +1,8 @@
+// Check that -CLinker-features=[+-]lld can only be used on x64.
+//
+//@ check-fail
+//@ compile-flags: --target=x86_64-unknown-linux-musl -C linker-features=-lld --crate-type=rlib
+//@ needs-llvm-components: x86
+
+#![feature(no_core)]
+#![no_core]
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.stderr b/tests/ui/linking/linker-features-lld-disallowed-target.stderr
new file mode 100644
index 0000000000000..292fadcd0f2ec
--- /dev/null
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.stderr
@@ -0,0 +1,2 @@
+error: `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl target, ` the `-Z unstable-options` flag must also be passed to use it on this target
+
diff --git a/tests/ui/linking/linker-features-unstable-cc.rs b/tests/ui/linking/linker-features-unstable-cc.rs
new file mode 100644
index 0000000000000..08431a2aa0045
--- /dev/null
+++ b/tests/ui/linking/linker-features-unstable-cc.rs
@@ -0,0 +1,8 @@
+// Check that -CLinker-features with anything else than lld requires -Zunstable-options.
+//
+//@ check-fail
+//@ compile-flags: --target=x86_64-unknown-linux-gnu -C linker-features=+cc --crate-type=rlib
+//@ needs-llvm-components: x86
+
+#![feature(no_core)]
+#![no_core]
diff --git a/tests/ui/linking/linker-features-unstable-cc.stderr b/tests/ui/linking/linker-features-unstable-cc.stderr
new file mode 100644
index 0000000000000..a69b419816098
--- /dev/null
+++ b/tests/ui/linking/linker-features-unstable-cc.stderr
@@ -0,0 +1,2 @@
+error: incorrect value `+cc` for codegen option `linker-features` - a list of enabled (`+` prefix) and disabled (`-` prefix) features: `lld` was expected
+

From 6a74c4d10f5febcc351c3dd3e772d61b3ae76464 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 24 Mar 2025 09:49:26 +0100
Subject: [PATCH 05/11] stabilize `-Clink-self-contained=[+-]linker`

---
 compiler/rustc_session/src/config.rs | 13 +++++++++----
 tests/run-make/rust-lld/rmake.rs     |  2 +-
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 88f1de01646e3..9a5aab4b029f8 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -370,9 +370,14 @@ impl LinkSelfContained {
     /// components was set individually. This would also require the `-Zunstable-options` flag, to
     /// be allowed.
     fn are_unstable_variants_set(&self) -> bool {
-        let any_component_set =
-            !self.enabled_components.is_empty() || !self.disabled_components.is_empty();
-        self.explicitly_set.is_none() && any_component_set
+        if self.explicitly_set.is_some() {
+            return false;
+        }
+
+        // Only the linker component is stable, anything else is thus unstable.
+        let mentioned_components = self.enabled_components.union(self.disabled_components);
+        let unstable_components = mentioned_components - LinkSelfContainedComponents::LINKER;
+        !unstable_components.is_empty()
     }
 
     /// Returns whether the self-contained linker component was enabled on the CLI, using the
@@ -2640,7 +2645,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
             cg.link_self_contained.are_unstable_variants_set();
         if uses_unstable_self_contained_option {
             early_dcx.early_fatal(
-                "only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off` are stable, \
+                "only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, \
                 the `-Z unstable-options` flag must also be passed to use the unstable values",
             );
         }
diff --git a/tests/run-make/rust-lld/rmake.rs b/tests/run-make/rust-lld/rmake.rs
index 2b914f19ac089..1e16bea50f253 100644
--- a/tests/run-make/rust-lld/rmake.rs
+++ b/tests/run-make/rust-lld/rmake.rs
@@ -14,7 +14,7 @@ fn main() {
         rustc()
             .arg("-Clinker-features=+lld")
             .arg("-Clink-self-contained=+linker")
-            .arg("-Zunstable-options")
+            .arg("-Zunstable-options") // needed for targets other than `x86_64-unknown-linux-gnu`
             .input("main.rs"),
     );
 

From 9d33272f578aee6276955135660966f9cbeb2a9f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 24 Mar 2025 12:18:24 +0100
Subject: [PATCH 06/11] add test for unstable values of `-Clink-self-contained`

---
 tests/run-make/rust-lld/rmake.rs                |  8 +++++---
 .../linking/link-self-contained-consistency.rs  |  1 -
 .../link-self-contained-unstable.crto.stderr    |  2 ++
 .../link-self-contained-unstable.libc.stderr    |  2 ++
 .../link-self-contained-unstable.mingw.stderr   |  2 ++
 .../ui/linking/link-self-contained-unstable.rs  | 17 +++++++++++++++++
 ...nk-self-contained-unstable.sanitizers.stderr |  2 ++
 .../link-self-contained-unstable.unwind.stderr  |  2 ++
 .../linker-features-lld-disallowed-target.rs    |  5 ++++-
 ...linker-features-lld-disallowed-target.stderr |  2 +-
 tests/ui/linking/linker-features-unstable-cc.rs |  9 +++++++--
 11 files changed, 44 insertions(+), 8 deletions(-)
 create mode 100644 tests/ui/linking/link-self-contained-unstable.crto.stderr
 create mode 100644 tests/ui/linking/link-self-contained-unstable.libc.stderr
 create mode 100644 tests/ui/linking/link-self-contained-unstable.mingw.stderr
 create mode 100644 tests/ui/linking/link-self-contained-unstable.rs
 create mode 100644 tests/ui/linking/link-self-contained-unstable.sanitizers.stderr
 create mode 100644 tests/ui/linking/link-self-contained-unstable.unwind.stderr

diff --git a/tests/run-make/rust-lld/rmake.rs b/tests/run-make/rust-lld/rmake.rs
index 1e16bea50f253..556580f066f3e 100644
--- a/tests/run-make/rust-lld/rmake.rs
+++ b/tests/run-make/rust-lld/rmake.rs
@@ -1,5 +1,5 @@
-// Test linking using `cc` with `rust-lld`, using the unstable CLI described in MCP 510
-// see https://github.com/rust-lang/compiler-team/issues/510 for more info
+// Test linking using `cc` with `rust-lld`, using the `-Clinker-features` and
+// `-Clink-self-contained` CLI flags.
 
 //@ needs-rust-lld
 //@ ignore-s390x lld does not yet support s390x as target
@@ -19,7 +19,9 @@ fn main() {
     );
 
     // It should not be used when we explicitly opt out of lld.
-    assert_rustc_doesnt_use_lld(rustc().arg("-Clinker-features=-lld").input("main.rs"));
+    assert_rustc_doesnt_use_lld(
+        rustc().arg("-Clinker-features=-lld").arg("-Zunstable-options").input("main.rs"),
+    );
 
     // While we're here, also check that the last linker feature flag "wins" when passed multiple
     // times to rustc.
diff --git a/tests/ui/linking/link-self-contained-consistency.rs b/tests/ui/linking/link-self-contained-consistency.rs
index 0822743389160..e3944fc0360cd 100644
--- a/tests/ui/linking/link-self-contained-consistency.rs
+++ b/tests/ui/linking/link-self-contained-consistency.rs
@@ -1,7 +1,6 @@
 // Checks that self-contained linking components cannot be both enabled and disabled at the same
 // time on the CLI.
 
-//@ check-fail
 //@ revisions: one many
 //@ [one] compile-flags: -Clink-self-contained=-linker -Clink-self-contained=+linker -Zunstable-options
 //@ [many] compile-flags: -Clink-self-contained=+linker,+crto -Clink-self-contained=-linker,-crto -Zunstable-options
diff --git a/tests/ui/linking/link-self-contained-unstable.crto.stderr b/tests/ui/linking/link-self-contained-unstable.crto.stderr
new file mode 100644
index 0000000000000..086b22736bb6d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.crto.stderr
@@ -0,0 +1,2 @@
+error: only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+
diff --git a/tests/ui/linking/link-self-contained-unstable.libc.stderr b/tests/ui/linking/link-self-contained-unstable.libc.stderr
new file mode 100644
index 0000000000000..086b22736bb6d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.libc.stderr
@@ -0,0 +1,2 @@
+error: only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+
diff --git a/tests/ui/linking/link-self-contained-unstable.mingw.stderr b/tests/ui/linking/link-self-contained-unstable.mingw.stderr
new file mode 100644
index 0000000000000..086b22736bb6d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.mingw.stderr
@@ -0,0 +1,2 @@
+error: only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+
diff --git a/tests/ui/linking/link-self-contained-unstable.rs b/tests/ui/linking/link-self-contained-unstable.rs
new file mode 100644
index 0000000000000..ad7570374751f
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.rs
@@ -0,0 +1,17 @@
+// Checks that values for `-Clink-self-contained` other than the blanket enable/disable and
+// `+/-linker` require `-Zunstable-options`.
+
+//@ revisions: crto libc unwind sanitizers mingw
+//@ [crto] compile-flags: -Clink-self-contained=+crto
+//@ [libc] compile-flags: -Clink-self-contained=-libc
+//@ [unwind] compile-flags: -Clink-self-contained=+unwind
+//@ [sanitizers] compile-flags: -Clink-self-contained=-sanitizers
+//@ [mingw] compile-flags: -Clink-self-contained=+mingw
+
+fn main() {}
+
+//[crto]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+//[libc]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+//[unwind]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+//[sanitizers]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+//[mingw]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
diff --git a/tests/ui/linking/link-self-contained-unstable.sanitizers.stderr b/tests/ui/linking/link-self-contained-unstable.sanitizers.stderr
new file mode 100644
index 0000000000000..086b22736bb6d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.sanitizers.stderr
@@ -0,0 +1,2 @@
+error: only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+
diff --git a/tests/ui/linking/link-self-contained-unstable.unwind.stderr b/tests/ui/linking/link-self-contained-unstable.unwind.stderr
new file mode 100644
index 0000000000000..086b22736bb6d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-unstable.unwind.stderr
@@ -0,0 +1,2 @@
+error: only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.rs b/tests/ui/linking/linker-features-lld-disallowed-target.rs
index 6ebafc4371c60..641f95a486440 100644
--- a/tests/ui/linking/linker-features-lld-disallowed-target.rs
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.rs
@@ -1,4 +1,5 @@
-// Check that -CLinker-features=[+-]lld can only be used on x64.
+// Check that `-C linker-features=[+-]lld` is only stable on x64 linux, and needs `-Z
+// unstable-options` elsewhere.
 //
 //@ check-fail
 //@ compile-flags: --target=x86_64-unknown-linux-musl -C linker-features=-lld --crate-type=rlib
@@ -6,3 +7,5 @@
 
 #![feature(no_core)]
 #![no_core]
+
+//~? ERROR `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl` target, the `-Z unstable-options` flag must also be passed to use it on this target
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.stderr b/tests/ui/linking/linker-features-lld-disallowed-target.stderr
index 292fadcd0f2ec..033b829e3a4c2 100644
--- a/tests/ui/linking/linker-features-lld-disallowed-target.stderr
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.stderr
@@ -1,2 +1,2 @@
-error: `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl target, ` the `-Z unstable-options` flag must also be passed to use it on this target
+error: `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl` target, the `-Z unstable-options` flag must also be passed to use it on this target
 
diff --git a/tests/ui/linking/linker-features-unstable-cc.rs b/tests/ui/linking/linker-features-unstable-cc.rs
index 08431a2aa0045..da7651699fb9a 100644
--- a/tests/ui/linking/linker-features-unstable-cc.rs
+++ b/tests/ui/linking/linker-features-unstable-cc.rs
@@ -1,8 +1,13 @@
-// Check that -CLinker-features with anything else than lld requires -Zunstable-options.
+// Check that only `-C linker-features=[+-]lld` is stable on x64 linux, and that other linker
+// features require using `-Z unstable-options`.
+//
+// Note that, currently, only `lld` is parsed on the CLI, but that other linker features can exist
+// internally (`cc`).
 //
-//@ check-fail
 //@ compile-flags: --target=x86_64-unknown-linux-gnu -C linker-features=+cc --crate-type=rlib
 //@ needs-llvm-components: x86
 
 #![feature(no_core)]
 #![no_core]
+
+//~? ERROR incorrect value `+cc` for codegen option `linker-features` - a list of enabled (`+` prefix) and disabled (`-` prefix) features: `lld` was expected

From 84aed65408367893c635b2dab30b63d677e4cfe2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 24 Mar 2025 12:29:55 +0100
Subject: [PATCH 07/11] update documentation

---
 src/doc/rustc/src/codegen-options/index.md    | 34 +++++++++++++++---
 .../src/compiler-flags/codegen-options.md     |  4 +--
 .../src/compiler-flags/linker-features.md     | 35 -------------------
 3 files changed, 32 insertions(+), 41 deletions(-)
 delete mode 100644 src/doc/unstable-book/src/compiler-flags/linker-features.md

diff --git a/src/doc/rustc/src/codegen-options/index.md b/src/doc/rustc/src/codegen-options/index.md
index a3b70e7f97711..195b02fa21276 100644
--- a/src/doc/rustc/src/codegen-options/index.md
+++ b/src/doc/rustc/src/codegen-options/index.md
@@ -231,15 +231,22 @@ coverage measurement. Its use is not recommended.
 
 ## link-self-contained
 
-On `windows-gnu`, `linux-musl`, and `wasi` targets, this flag controls whether the
-linker will use libraries and objects shipped with Rust instead of those in the system.
-It takes one of the following values:
+This flag controls whether the linker will use libraries and objects shipped with Rust instead of
+those in the system.  It also controls which binary is used for the linker itself. This allows
+overriding cases when detection fails or the user wants to use shipped libraries.
+
+You can enable or disable the usage of any self-contained components using one of the following values:
 
 * no value: rustc will use heuristic to disable self-contained mode if system has necessary tools.
 * `y`, `yes`, `on`, `true`: use only libraries/objects shipped with Rust.
 * `n`, `no`, `off` or `false`: rely on the user or the linker to provide non-Rust libraries/objects.
 
-This allows overriding cases when detection fails or user wants to use shipped libraries.
+It is also possible to enable or disable specific self-contained components in a more granular way.
+You can pass a comma-separated list of self-contained components, individually enabled
+(`+component`) or disabled (`-component`).
+
+Currently, only the `linker` granular option is stabilized:
+- `linker`: toggle the usage of self-contained linker binaries (linker, dlltool, and their necessary libraries)
 
 ## linker
 
@@ -248,6 +255,25 @@ path to the linker executable. If this flag is not specified, the linker will
 be inferred based on the target. See also the [linker-flavor](#linker-flavor)
 flag for another way to specify the linker.
 
+## linker-features
+
+The `-Clinker-features` flag allows enabling or disabling specific features used during linking.
+
+These feature flags are a flexible extension mechanism that is complementary to linker flavors,
+designed to avoid the combinatorial explosion of having to create a new set of flavors for each
+linker feature we'd want to use.
+
+The flag accepts a comma-separated list of features, individually enabled (`+feature`) or disabled
+(`-feature`).
+
+Currently only one is stable, and only on the `x86_64-unknown-linux-gnu` target:
+- `lld`: to toggle using the lld linker, either the system-installed binary, or the self-contained
+  `rust-lld` linker (via the `-Clink-self-contained=+linker` flag).
+
+For example, use:
+- `-Clinker-features=+lld` to opt in to using the `lld` linker
+- `-Clinker-features=-lld` to opt out instead, for targets where it is configured as the default linker
+
 ## linker-flavor
 
 This flag controls the linker flavor used by `rustc`. If a linker is given with
diff --git a/src/doc/unstable-book/src/compiler-flags/codegen-options.md b/src/doc/unstable-book/src/compiler-flags/codegen-options.md
index cc51554706d07..6a6e14a5c1ddc 100644
--- a/src/doc/unstable-book/src/compiler-flags/codegen-options.md
+++ b/src/doc/unstable-book/src/compiler-flags/codegen-options.md
@@ -51,10 +51,10 @@ instead of those in the system. The stable boolean values for this flag are coar
 - `mingw`: other MinGW libs and Windows import libs
 
 Out of the above self-contained linking components, `linker` is the only one currently implemented
-(beyond parsing the CLI options).
+(beyond parsing the CLI options) and stabilized.
 
 It refers to the LLD linker, built from the same LLVM revision used by rustc (named `rust-lld` to
 avoid naming conflicts), that is distributed via `rustup` with the compiler (and is used by default
-for the wasm targets). One can also opt-in to use it by combining this flag with an appropriate
+for the wasm targets). One can also opt in to use it by combining this flag with an appropriate
 linker flavor: for example, `-Clinker-flavor=gnu-lld-cc -Clink-self-contained=+linker` will use the
 toolchain's `rust-lld` as the linker.
diff --git a/src/doc/unstable-book/src/compiler-flags/linker-features.md b/src/doc/unstable-book/src/compiler-flags/linker-features.md
deleted file mode 100644
index 643fcf7c6d7e0..0000000000000
--- a/src/doc/unstable-book/src/compiler-flags/linker-features.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# `linker-features`
-
---------------------
-
-The `-Zlinker-features` compiler flag allows enabling or disabling specific features used during
-linking, and is intended to be stabilized under the codegen options as `-Clinker-features`.
-
-These feature flags are a flexible extension mechanism that is complementary to linker flavors,
-designed to avoid the combinatorial explosion of having to create a new set of flavors for each
-linker feature we'd want to use.
-
-For example, this design allows:
-- default feature sets for principal flavors, or for specific targets.
-- flavor-specific features: for example, clang offers automatic cross-linking with `--target`, which
-  gcc-style compilers don't support. The *flavor* is still a C/C++ compiler, and we don't want to
-  multiply the number of flavors for this use-case. Instead, we can have a single `+target` feature.
-- umbrella features: for example, if clang accumulates more features in the future than just the
-  `+target` above. That could be modeled as `+clang`.
-- niche features for resolving specific issues: for example, on Apple targets the linker flag
-  implementing the `as-needed` native link modifier (#99424) is only possible on sufficiently recent
-  linker versions.
-- still allows for discovery and automation, for example via feature detection. This can be useful
-  in exotic environments/build systems.
-
-The flag accepts a comma-separated list of features, individually enabled (`+features`) or disabled
-(`-features`), though currently only one is exposed on the CLI:
-- `lld`: to toggle using the lld linker, either the system-installed binary, or the self-contained
-  `rust-lld` linker.
-
-As described above, this list is intended to grow in the future.
-
-One of the most common uses of this flag will be to toggle self-contained linking with `rust-lld` on
-and off: `-Clinker-features=+lld -Clink-self-contained=+linker` will use the toolchain's `rust-lld`
-as the linker. Inversely, `-Clinker-features=-lld` would opt out of that, if the current target had
-self-contained linking enabled by default.

From 374724872a513814e87f7b17a6c99359d5bbadb7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Fri, 4 Apr 2025 14:59:27 +0000
Subject: [PATCH 08/11] update `check_unstable_variants`'s handling of `-C
 linker-features=[-+]lld`

- separate enabling and disabling the feature in the error
- add both polarities to the dedicated test
- update documentation and precondition
---
 compiler/rustc_session/src/config.rs          | 51 +++++++++++--------
 compiler/rustc_target/src/spec/mod.rs         | 11 ++++
 ...ures-lld-disallowed-target.negative.stderr |  2 +
 ...ures-lld-disallowed-target.positive.stderr |  2 +
 .../linker-features-lld-disallowed-target.rs  | 15 ++++--
 ...nker-features-lld-disallowed-target.stderr |  2 -
 6 files changed, 55 insertions(+), 28 deletions(-)
 create mode 100644 tests/ui/linking/linker-features-lld-disallowed-target.negative.stderr
 create mode 100644 tests/ui/linking/linker-features-lld-disallowed-target.positive.stderr
 delete mode 100644 tests/ui/linking/linker-features-lld-disallowed-target.stderr

diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 9a5aab4b029f8..51622f2f15f78 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -445,32 +445,41 @@ impl LinkerFeaturesCli {
         }
     }
 
-    /// Checks usage of unstable variants for linker features for the given `target_tuple`.
-    /// Returns `Ok` if no unstable variants are used.
+    /// When *not* using `-Z unstable-options` on the CLI, ensure only stable linker features are
+    /// used, for the given `TargetTuple`. Returns `Ok` if no unstable variants are used.
+    /// The caller should ensure that e.g. `nightly_options::is_unstable_enabled()`
+    /// returns false.
     pub(crate) fn check_unstable_variants(&self, target_tuple: &TargetTuple) -> Result<(), String> {
-        let mentioned_features = self.enabled.union(self.disabled);
-        let has_lld = mentioned_features.is_lld_enabled();
-
-        // Check that -Clinker-features=[-+]lld is not used anywhere else than on x64
-        // without -Zunstable-options.
-        if has_lld && target_tuple.tuple() != "x86_64-unknown-linux-gnu" {
+        // `-C linker-features=[-+]lld` is only stable on x64 linux.
+        let check_lld = |features: LinkerFeatures, polarity: &str| {
+            let has_lld = features.is_lld_enabled();
+            if has_lld && target_tuple.tuple() != "x86_64-unknown-linux-gnu" {
+                return Err(format!(
+                    "`-C linker-features={polarity}lld` is unstable on the `{target_tuple}` \
+                    target. The `-Z unstable-options` flag must also be passed to use it on this target",
+                ));
+            }
+            Ok(())
+        };
+        check_lld(self.enabled, "+")?;
+        check_lld(self.disabled, "-")?;
+
+        // Since only lld is stable, any non-lld feature used is unstable, and that's an error.
+        let unstable_enabled = self.enabled - LinkerFeatures::LLD;
+        let unstable_disabled = self.disabled - LinkerFeatures::LLD;
+        if !unstable_enabled.union(unstable_disabled).is_empty() {
+            let unstable_features: Vec<_> = unstable_enabled
+                .iter()
+                .map(|f| format!("+{}", f.as_str().unwrap()))
+                .chain(unstable_disabled.iter().map(|f| format!("-{}", f.as_str().unwrap())))
+                .collect();
             return Err(format!(
-                "`-C linker-features` with lld are unstable for the `{target_tuple}` target, \
-the `-Z unstable-options` flag must also be passed to use it on this target",
+                "the requested `-C linker-features={}` are unstable, and also require the \
+                `-Z unstable-options` flag to be usable",
+                unstable_features.join(","),
             ));
         }
 
-        for feature in LinkerFeatures::all() {
-            // Check that no other features were enabled without -Zunstable-options
-            // Note that this should currently be unreachable, because the `-Clinker-features` parser
-            // currently only accepts lld.
-            if feature != LinkerFeatures::LLD && mentioned_features.contains(feature) {
-                return Err("`-C linker-features` is stable only for the lld feature, \
-the`-Z unstable-options` flag must also be passed to use it with other features"
-                    .to_string());
-            }
-        }
-
         Ok(())
     }
 }
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index d9d381ca0742f..79c1c209802e8 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -761,6 +761,17 @@ impl LinkerFeatures {
         })
     }
 
+    /// Return the linker feature name, as would be passed on the CLI.
+    ///
+    /// Returns `None` if the bitflags aren't a singular component (but a mix of multiple flags).
+    pub fn as_str(self) -> Option<&'static str> {
+        Some(match self {
+            LinkerFeatures::CC => "cc",
+            LinkerFeatures::LLD => "lld",
+            _ => return None,
+        })
+    }
+
     /// Returns whether the `lld` linker feature is enabled.
     pub fn is_lld_enabled(self) -> bool {
         self.contains(LinkerFeatures::LLD)
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.negative.stderr b/tests/ui/linking/linker-features-lld-disallowed-target.negative.stderr
new file mode 100644
index 0000000000000..205082b072626
--- /dev/null
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.negative.stderr
@@ -0,0 +1,2 @@
+error: `-C linker-features=-lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.positive.stderr b/tests/ui/linking/linker-features-lld-disallowed-target.positive.stderr
new file mode 100644
index 0000000000000..2b10e6c714850
--- /dev/null
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.positive.stderr
@@ -0,0 +1,2 @@
+error: `-C linker-features=+lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.rs b/tests/ui/linking/linker-features-lld-disallowed-target.rs
index 641f95a486440..b3ae7e7ecf401 100644
--- a/tests/ui/linking/linker-features-lld-disallowed-target.rs
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.rs
@@ -1,11 +1,16 @@
 // Check that `-C linker-features=[+-]lld` is only stable on x64 linux, and needs `-Z
 // unstable-options` elsewhere.
-//
-//@ check-fail
-//@ compile-flags: --target=x86_64-unknown-linux-musl -C linker-features=-lld --crate-type=rlib
-//@ needs-llvm-components: x86
+
+// ignore-tidy-linelength
+
+//@ revisions: positive negative
+//@ [negative] compile-flags: --target=x86_64-unknown-linux-musl -C linker-features=-lld --crate-type=rlib
+//@ [negative] needs-llvm-components: x86
+//@ [positive] compile-flags: --target=x86_64-unknown-linux-musl -C linker-features=+lld --crate-type=rlib
+//@ [positive] needs-llvm-components: x86
 
 #![feature(no_core)]
 #![no_core]
 
-//~? ERROR `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl` target, the `-Z unstable-options` flag must also be passed to use it on this target
+//[negative]~? ERROR `-C linker-features=-lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+//[positive]~? ERROR `-C linker-features=+lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.stderr b/tests/ui/linking/linker-features-lld-disallowed-target.stderr
deleted file mode 100644
index 033b829e3a4c2..0000000000000
--- a/tests/ui/linking/linker-features-lld-disallowed-target.stderr
+++ /dev/null
@@ -1,2 +0,0 @@
-error: `-C linker-features` with lld are unstable for the `x86_64-unknown-linux-musl` target, the `-Z unstable-options` flag must also be passed to use it on this target
-

From 9ac95810640eaae68784f9c4b3d54708914a1c7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Fri, 25 Apr 2025 15:27:29 +0000
Subject: [PATCH 09/11] update bootstrap mcp510 handling

now that it's been stabilized, beta and stage1 need to use different
flags (-C vs -Z)
---
 src/bootstrap/src/core/build_steps/test.rs | 20 +++++++++++++----
 src/bootstrap/src/core/builder/cargo.rs    | 18 +++++++++++-----
 src/bootstrap/src/core/builder/mod.rs      |  2 +-
 src/bootstrap/src/utils/helpers.rs         | 25 ++++++++++++++++------
 4 files changed, 49 insertions(+), 16 deletions(-)

diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs
index a7a3b5a878c31..5560cf928859a 100644
--- a/src/bootstrap/src/core/build_steps/test.rs
+++ b/src/bootstrap/src/core/build_steps/test.rs
@@ -261,7 +261,13 @@ impl Step for Cargotest {
             .args(builder.config.test_args())
             .env("RUSTC", builder.rustc(compiler))
             .env("RUSTDOC", builder.rustdoc(compiler));
-        add_rustdoc_cargo_linker_args(&mut cmd, builder, compiler.host, LldThreads::No);
+        add_rustdoc_cargo_linker_args(
+            &mut cmd,
+            builder,
+            compiler.host,
+            LldThreads::No,
+            compiler.stage,
+        );
         cmd.delay_failure().run(builder);
     }
 }
@@ -839,7 +845,7 @@ impl Step for RustdocTheme {
             .env("CFG_RELEASE_CHANNEL", &builder.config.channel)
             .env("RUSTDOC_REAL", builder.rustdoc(self.compiler))
             .env("RUSTC_BOOTSTRAP", "1");
-        cmd.args(linker_args(builder, self.compiler.host, LldThreads::No));
+        cmd.args(linker_args(builder, self.compiler.host, LldThreads::No, self.compiler.stage));
 
         cmd.delay_failure().run(builder);
     }
@@ -1015,7 +1021,13 @@ impl Step for RustdocGUI {
         cmd.env("RUSTDOC", builder.rustdoc(self.compiler))
             .env("RUSTC", builder.rustc(self.compiler));
 
-        add_rustdoc_cargo_linker_args(&mut cmd, builder, self.compiler.host, LldThreads::No);
+        add_rustdoc_cargo_linker_args(
+            &mut cmd,
+            builder,
+            self.compiler.host,
+            LldThreads::No,
+            self.compiler.stage,
+        );
 
         for path in &builder.paths {
             if let Some(p) = helpers::is_valid_test_suite_arg(path, "tests/rustdoc-gui", builder) {
@@ -1784,7 +1796,7 @@ NOTE: if you're sure you want to do this, please open an issue as to why. In the
         }
 
         let mut hostflags = flags.clone();
-        hostflags.extend(linker_flags(builder, compiler.host, LldThreads::No));
+        hostflags.extend(linker_flags(builder, compiler.host, LldThreads::No, compiler.stage));
 
         let mut targetflags = flags;
 
diff --git a/src/bootstrap/src/core/builder/cargo.rs b/src/bootstrap/src/core/builder/cargo.rs
index 36b3c95d638cc..76d064bb39f5b 100644
--- a/src/bootstrap/src/core/builder/cargo.rs
+++ b/src/bootstrap/src/core/builder/cargo.rs
@@ -116,7 +116,7 @@ impl Cargo {
             // as they don't invoke rustc at all.
             Kind::Clean | Kind::Suggest | Kind::Format | Kind::Setup => {}
             _ => {
-                cargo.configure_linker(builder);
+                cargo.configure_linker(builder, mode);
             }
         }
 
@@ -205,7 +205,7 @@ impl Cargo {
         self
     }
 
-    fn configure_linker(&mut self, builder: &Builder<'_>) -> &mut Cargo {
+    fn configure_linker(&mut self, builder: &Builder<'_>, mode: Mode) -> &mut Cargo {
         let target = self.target;
         let compiler = self.compiler;
 
@@ -260,7 +260,15 @@ impl Cargo {
             }
         }
 
-        for arg in linker_args(builder, compiler.host, LldThreads::Yes) {
+        // When determining flags for the host (build scripts/proc macros),
+        // we use the snapshot compiler when building `Mode::Std` tools, and
+        // the current compiler when building anything else.
+        // We need to determine the current stage here to pass proper linker args (e.g. -C vs -Z)
+        // to the compiler used to compile build scripts.
+        // This should stay synchronized with the [cargo] function.
+        let host_stage = if mode == Mode::Std { 0 } else { compiler.stage };
+
+        for arg in linker_args(builder, compiler.host, LldThreads::Yes, host_stage) {
             self.hostflags.arg(&arg);
         }
 
@@ -270,10 +278,10 @@ impl Cargo {
         }
         // We want to set -Clinker using Cargo, therefore we only call `linker_flags` and not
         // `linker_args` here.
-        for flag in linker_flags(builder, target, LldThreads::Yes) {
+        for flag in linker_flags(builder, target, LldThreads::Yes, compiler.stage) {
             self.rustflags.arg(&flag);
         }
-        for arg in linker_args(builder, target, LldThreads::Yes) {
+        for arg in linker_args(builder, target, LldThreads::Yes, compiler.stage) {
             self.rustdocflags.arg(&arg);
         }
 
diff --git a/src/bootstrap/src/core/builder/mod.rs b/src/bootstrap/src/core/builder/mod.rs
index c32d9c2870cfa..8aae5255d7e1c 100644
--- a/src/bootstrap/src/core/builder/mod.rs
+++ b/src/bootstrap/src/core/builder/mod.rs
@@ -1479,7 +1479,7 @@ impl<'a> Builder<'a> {
             cmd.arg("-Dwarnings");
         }
         cmd.arg("-Znormalize-docs");
-        cmd.args(linker_args(self, compiler.host, LldThreads::Yes));
+        cmd.args(linker_args(self, compiler.host, LldThreads::Yes, compiler.stage));
         cmd
     }
 
diff --git a/src/bootstrap/src/utils/helpers.rs b/src/bootstrap/src/utils/helpers.rs
index 1299fbb7d6291..be30eed0dd206 100644
--- a/src/bootstrap/src/utils/helpers.rs
+++ b/src/bootstrap/src/utils/helpers.rs
@@ -445,8 +445,9 @@ pub fn linker_args(
     builder: &Builder<'_>,
     target: TargetSelection,
     lld_threads: LldThreads,
+    stage: u32,
 ) -> Vec<String> {
-    let mut args = linker_flags(builder, target, lld_threads);
+    let mut args = linker_flags(builder, target, lld_threads, stage);
 
     if let Some(linker) = builder.linker(target) {
         args.push(format!("-Clinker={}", linker.display()));
@@ -461,19 +462,30 @@ pub fn linker_flags(
     builder: &Builder<'_>,
     target: TargetSelection,
     lld_threads: LldThreads,
+    stage: u32,
 ) -> Vec<String> {
     let mut args = vec![];
     if !builder.is_lld_direct_linker(target) && builder.config.lld_mode.is_used() {
         match builder.config.lld_mode {
             LldMode::External => {
-                args.push("-Zlinker-features=+lld".to_string());
-                // FIXME(kobzol): remove this flag once MCP510 gets stabilized
+                // cfg(bootstrap) - remove the stage 0 check after updating the bootstrap compiler:
+                // `-Clinker-features` has been stabilized.
+                if stage == 0 {
+                    args.push("-Zlinker-features=+lld".to_string());
+                } else {
+                    args.push("-Clinker-features=+lld".to_string());
+                }
                 args.push("-Zunstable-options".to_string());
             }
             LldMode::SelfContained => {
-                args.push("-Zlinker-features=+lld".to_string());
+                // cfg(bootstrap) - remove the stage 0 check after updating the bootstrap compiler:
+                // `-Clinker-features` has been stabilized.
+                if stage == 0 {
+                    args.push("-Zlinker-features=+lld".to_string());
+                } else {
+                    args.push("-Clinker-features=+lld".to_string());
+                }
                 args.push("-Clink-self-contained=+linker".to_string());
-                // FIXME(kobzol): remove this flag once MCP510 gets stabilized
                 args.push("-Zunstable-options".to_string());
             }
             LldMode::Unused => unreachable!(),
@@ -494,8 +506,9 @@ pub fn add_rustdoc_cargo_linker_args(
     builder: &Builder<'_>,
     target: TargetSelection,
     lld_threads: LldThreads,
+    stage: u32,
 ) {
-    let args = linker_args(builder, target, lld_threads);
+    let args = linker_args(builder, target, lld_threads, stage);
     let mut flags = cmd
         .get_envs()
         .find_map(|(k, v)| if k == OsStr::new("RUSTDOCFLAGS") { v } else { None })

From 1438da8f0359791236bc2fbc87bed8e21e4a9eb3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 5 May 2025 15:43:17 +0000
Subject: [PATCH 10/11] simplify test annotations

---
 tests/ui/linking/link-self-contained-unstable.rs       | 10 +++++-----
 .../linking/linker-features-lld-disallowed-target.rs   |  4 ++--
 tests/ui/linking/linker-features-unstable-cc.rs        |  2 +-
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/tests/ui/linking/link-self-contained-unstable.rs b/tests/ui/linking/link-self-contained-unstable.rs
index ad7570374751f..73ffaeac42b18 100644
--- a/tests/ui/linking/link-self-contained-unstable.rs
+++ b/tests/ui/linking/link-self-contained-unstable.rs
@@ -10,8 +10,8 @@
 
 fn main() {}
 
-//[crto]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
-//[libc]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
-//[unwind]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
-//[sanitizers]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
-//[mingw]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, the `-Z unstable-options` flag must also be passed to use the unstable values
+//[crto]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable
+//[libc]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable
+//[unwind]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable
+//[sanitizers]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable
+//[mingw]~? ERROR only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable
diff --git a/tests/ui/linking/linker-features-lld-disallowed-target.rs b/tests/ui/linking/linker-features-lld-disallowed-target.rs
index b3ae7e7ecf401..9d4555c64e710 100644
--- a/tests/ui/linking/linker-features-lld-disallowed-target.rs
+++ b/tests/ui/linking/linker-features-lld-disallowed-target.rs
@@ -12,5 +12,5 @@
 #![feature(no_core)]
 #![no_core]
 
-//[negative]~? ERROR `-C linker-features=-lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
-//[positive]~? ERROR `-C linker-features=+lld` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+//[negative]~? ERROR `-C linker-features=-lld` is unstable on the `x86_64-unknown-linux-musl` target
+//[positive]~? ERROR `-C linker-features=+lld` is unstable on the `x86_64-unknown-linux-musl` target
diff --git a/tests/ui/linking/linker-features-unstable-cc.rs b/tests/ui/linking/linker-features-unstable-cc.rs
index da7651699fb9a..8ea84117c10c7 100644
--- a/tests/ui/linking/linker-features-unstable-cc.rs
+++ b/tests/ui/linking/linker-features-unstable-cc.rs
@@ -10,4 +10,4 @@
 #![feature(no_core)]
 #![no_core]
 
-//~? ERROR incorrect value `+cc` for codegen option `linker-features` - a list of enabled (`+` prefix) and disabled (`-` prefix) features: `lld` was expected
+//~? ERROR incorrect value `+cc` for codegen option `linker-features`

From 2b829ba23376c02350f0ea5e6a0f7f06454e009d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Rakic?= <remy.rakic+github@gmail.com>
Date: Mon, 5 May 2025 17:09:40 +0000
Subject: [PATCH 11/11] check that `-Clink-self-contained=[-+]linker` can only
 be used on x64 linux without `-Zunstable-options`

---
 compiler/rustc_session/src/config.rs          | 56 ++++++++++++-------
 ...d-linker-disallowed-target.negative.stderr |  2 +
 ...d-linker-disallowed-target.positive.stderr |  2 +
 ...self-contained-linker-disallowed-target.rs | 16 ++++++
 4 files changed, 57 insertions(+), 19 deletions(-)
 create mode 100644 tests/ui/linking/link-self-contained-linker-disallowed-target.negative.stderr
 create mode 100644 tests/ui/linking/link-self-contained-linker-disallowed-target.positive.stderr
 create mode 100644 tests/ui/linking/link-self-contained-linker-disallowed-target.rs

diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 51622f2f15f78..8231630ac6fb1 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -367,17 +367,40 @@ impl LinkSelfContained {
     }
 
     /// To help checking CLI usage while some of the values are unstable: returns whether one of the
-    /// components was set individually. This would also require the `-Zunstable-options` flag, to
-    /// be allowed.
-    fn are_unstable_variants_set(&self) -> bool {
+    /// unstable components was set individually, for the given `TargetTuple`. This would also
+    /// require the `-Zunstable-options` flag, to be allowed.
+    fn check_unstable_variants(&self, target_tuple: &TargetTuple) -> Result<(), String> {
         if self.explicitly_set.is_some() {
-            return false;
+            return Ok(());
         }
 
-        // Only the linker component is stable, anything else is thus unstable.
-        let mentioned_components = self.enabled_components.union(self.disabled_components);
-        let unstable_components = mentioned_components - LinkSelfContainedComponents::LINKER;
-        !unstable_components.is_empty()
+        // `-C link-self-contained=[-+]linker` is only stable on x64 linux.
+        let check_linker = |components: LinkSelfContainedComponents, polarity: &str| {
+            let has_linker = components.is_linker_enabled();
+            if has_linker && target_tuple.tuple() != "x86_64-unknown-linux-gnu" {
+                return Err(format!(
+                    "`-C link-self-contained={polarity}linker` is unstable on the `{target_tuple}` \
+                    target. The `-Z unstable-options` flag must also be passed to use it on this target",
+                ));
+            }
+            Ok(())
+        };
+        check_linker(self.enabled_components, "+")?;
+        check_linker(self.disabled_components, "-")?;
+
+        // Since only the linker component is stable, any other component used is unstable, and
+        // that's an error.
+        let unstable_enabled = self.enabled_components - LinkSelfContainedComponents::LINKER;
+        let unstable_disabled = self.disabled_components - LinkSelfContainedComponents::LINKER;
+        if !unstable_enabled.union(unstable_disabled).is_empty() {
+            return Err(String::from(
+                "only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`\
+                /`+linker` are stable, the `-Z unstable-options` flag must also be passed to use \
+                the unstable values",
+            ));
+        }
+
+        Ok(())
     }
 
     /// Returns whether the self-contained linker component was enabled on the CLI, using the
@@ -2646,17 +2669,13 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
         )
     }
 
-    // For testing purposes, until we have more feedback about these options: ensure `-Z
-    // unstable-options` is required when using the unstable `-C link-self-contained` and `-C
-    // linker-flavor` options.
+    let target_triple = parse_target_triple(early_dcx, matches);
+
+    // Ensure `-Z unstable-options` is required when using the unstable `-C link-self-contained` and
+    // `-C linker-flavor` options.
     if !unstable_options_enabled {
-        let uses_unstable_self_contained_option =
-            cg.link_self_contained.are_unstable_variants_set();
-        if uses_unstable_self_contained_option {
-            early_dcx.early_fatal(
-                "only `-C link-self-contained` values `y`/`yes`/`on`/`n`/`no`/`off`/`-linker`/`+linker` are stable, \
-                the `-Z unstable-options` flag must also be passed to use the unstable values",
-            );
+        if let Err(error) = cg.link_self_contained.check_unstable_variants(&target_triple) {
+            early_dcx.early_fatal(error);
         }
 
         if let Some(flavor) = cg.linker_flavor {
@@ -2688,7 +2707,6 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
     let cg = cg;
 
     let sysroot_opt = matches.opt_str("sysroot").map(|m| PathBuf::from(&m));
-    let target_triple = parse_target_triple(early_dcx, matches);
     let opt_level = parse_opt_level(early_dcx, matches, &cg);
     // The `-g` and `-C debuginfo` flags specify the same setting, so we want to be able
     // to use them interchangeably. See the note above (regarding `-O` and `-C opt-level`)
diff --git a/tests/ui/linking/link-self-contained-linker-disallowed-target.negative.stderr b/tests/ui/linking/link-self-contained-linker-disallowed-target.negative.stderr
new file mode 100644
index 0000000000000..8bf71941c4454
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-linker-disallowed-target.negative.stderr
@@ -0,0 +1,2 @@
+error: `-C link-self-contained=-linker` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+
diff --git a/tests/ui/linking/link-self-contained-linker-disallowed-target.positive.stderr b/tests/ui/linking/link-self-contained-linker-disallowed-target.positive.stderr
new file mode 100644
index 0000000000000..3424ecc2f077d
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-linker-disallowed-target.positive.stderr
@@ -0,0 +1,2 @@
+error: `-C link-self-contained=+linker` is unstable on the `x86_64-unknown-linux-musl` target. The `-Z unstable-options` flag must also be passed to use it on this target
+
diff --git a/tests/ui/linking/link-self-contained-linker-disallowed-target.rs b/tests/ui/linking/link-self-contained-linker-disallowed-target.rs
new file mode 100644
index 0000000000000..2bf332756815b
--- /dev/null
+++ b/tests/ui/linking/link-self-contained-linker-disallowed-target.rs
@@ -0,0 +1,16 @@
+// Check that `-C link-self-contained=[+-]linker` is only stable on x64 linux, and needs `-Z
+// unstable-options` elsewhere.
+
+// ignore-tidy-linelength
+
+//@ revisions: positive negative
+//@ [negative] compile-flags: --target=x86_64-unknown-linux-musl -C link-self-contained=-linker --crate-type=rlib
+//@ [negative] needs-llvm-components: x86
+//@ [positive] compile-flags: --target=x86_64-unknown-linux-musl -C link-self-contained=+linker --crate-type=rlib
+//@ [positive] needs-llvm-components: x86
+
+#![feature(no_core)]
+#![no_core]
+
+//[negative]~? ERROR `-C link-self-contained=-linker` is unstable on the `x86_64-unknown-linux-musl` target
+//[positive]~? ERROR `-C link-self-contained=+linker` is unstable on the `x86_64-unknown-linux-musl` target