diff --git a/Cargo.lock b/Cargo.lock
index 2ee2c52b32ade..4680eb137484e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3460,6 +3460,7 @@ dependencies = [
  "rustc_serialize",
  "rustc_session",
  "rustc_span",
+ "rustc_target",
 ]
 
 [[package]]
diff --git a/compiler/rustc_attr_parsing/Cargo.toml b/compiler/rustc_attr_parsing/Cargo.toml
index 7ccedf40c3fa9..f3a70a7f38b3c 100644
--- a/compiler/rustc_attr_parsing/Cargo.toml
+++ b/compiler/rustc_attr_parsing/Cargo.toml
@@ -18,4 +18,5 @@ rustc_macros = { path = "../rustc_macros" }
 rustc_serialize = { path = "../rustc_serialize" }
 rustc_session = { path = "../rustc_session" }
 rustc_span = { path = "../rustc_span" }
+rustc_target = { path = "../rustc_target" }
 # tidy-alphabetical-end
diff --git a/compiler/rustc_attr_parsing/messages.ftl b/compiler/rustc_attr_parsing/messages.ftl
index faa2865cb9130..9ed0fe21dfd9b 100644
--- a/compiler/rustc_attr_parsing/messages.ftl
+++ b/compiler/rustc_attr_parsing/messages.ftl
@@ -1,3 +1,9 @@
+attr_parsing_apple_version_invalid =
+    failed parsing version: {$error}
+
+attr_parsing_apple_version_unnecessarily_low =
+    version is set unnecessarily low, the minimum supported by Rust on this platform is {$os_min}
+
 attr_parsing_cfg_predicate_identifier =
     `cfg` predicate key must be an identifier
 
@@ -9,6 +15,12 @@ attr_parsing_deprecated_item_suggestion =
 attr_parsing_expected_one_cfg_pattern =
     expected 1 cfg-pattern
 
+attr_parsing_expected_platform_and_version_literals =
+    expected two literals, a platform and a version
+
+attr_parsing_expected_platform_literal =
+    expected a platform literal
+
 attr_parsing_expected_single_version_literal =
     expected single version literal
 
@@ -104,6 +116,9 @@ attr_parsing_unknown_meta_item =
     unknown meta item '{$item}'
     .label = expected one of {$expected}
 
+attr_parsing_unknown_platform_literal =
+    unknown platform literal, expected values are: {$possibilities}
+
 attr_parsing_unknown_version_literal =
     unknown version literal format, assuming it refers to a future version
 
diff --git a/compiler/rustc_attr_parsing/src/attributes/cfg.rs b/compiler/rustc_attr_parsing/src/attributes/cfg.rs
index bb9aaaa2fea9e..b50e9957763ec 100644
--- a/compiler/rustc_attr_parsing/src/attributes/cfg.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/cfg.rs
@@ -10,6 +10,7 @@ use rustc_session::lint::BuiltinLintDiag;
 use rustc_session::lint::builtin::UNEXPECTED_CFGS;
 use rustc_session::parse::feature_err;
 use rustc_span::{Span, Symbol, kw, sym};
+use rustc_target::spec::apple;
 
 use crate::util::UnsupportedLiteralReason;
 use crate::{fluent_generated, parse_version, session_diagnostics};
@@ -150,6 +151,122 @@ pub fn eval_condition(
                 RustcVersion::CURRENT >= min_version
             }
         }
+        ast::MetaItemKind::List(mis) if cfg.name_or_empty() == sym::os_version_min => {
+            try_gate_cfg(sym::os_version_min, cfg.span, sess, features);
+
+            let (platform, version) = match &mis[..] {
+                [platform, version] => (platform, version),
+                [..] => {
+                    dcx.emit_err(session_diagnostics::ExpectedPlatformAndVersionLiterals {
+                        span: cfg.span,
+                    });
+                    return false;
+                }
+            };
+
+            let (platform_sym, platform_span) = match platform {
+                MetaItemInner::Lit(MetaItemLit {
+                    kind: LitKind::Str(platform_sym, ..),
+                    span: platform_span,
+                    ..
+                }) => (platform_sym, platform_span),
+                MetaItemInner::Lit(MetaItemLit { span, .. })
+                | MetaItemInner::MetaItem(MetaItem { span, .. }) => {
+                    dcx.emit_err(session_diagnostics::ExpectedPlatformLiteral { span: *span });
+                    return false;
+                }
+            };
+
+            let (version_sym, version_span) = match version {
+                MetaItemInner::Lit(MetaItemLit {
+                    kind: LitKind::Str(version_sym, ..),
+                    span: version_span,
+                    ..
+                }) => (version_sym, version_span),
+                MetaItemInner::Lit(MetaItemLit { span, .. })
+                | MetaItemInner::MetaItem(MetaItem { span, .. }) => {
+                    dcx.emit_err(session_diagnostics::ExpectedVersionLiteral { span: *span });
+                    return false;
+                }
+            };
+
+            // Always parse version, regardless of current target platform.
+            let version = match *platform_sym {
+                // Apple platforms follow the same versioning schema.
+                sym::macos | sym::ios | sym::tvos | sym::watchos | sym::visionos => {
+                    match version_sym.as_str().parse() {
+                        Ok(version) => {
+                            let os_min = apple::OSVersion::os_minimum_deployment_target(
+                                &platform_sym.as_str(),
+                            );
+
+                            // It's unnecessary to specify `cfg_target_os(...)` for a platform
+                            // version that is lower than the minimum targetted by `rustc` (instead,
+                            // make the item always available).
+                            //
+                            // This is correct _now_, but once we bump versions next time, we should
+                            // maybe make this a lint so that users can opt-in to supporting older
+                            // `rustc` versions? Or perhaps only fire the warning when Cargo's
+                            // `rust-version` field is above the version where the bump happened? Or
+                            // perhaps keep the version we check against low for a sufficiently long
+                            // time?
+                            if version <= os_min {
+                                sess.dcx().emit_warn(
+                                    session_diagnostics::AppleVersionUnnecessarilyLow {
+                                        span: *version_span,
+                                        os_min: os_min.fmt_pretty().to_string(),
+                                    },
+                                );
+                                // TODO(madsmtm): Add suggestion for replacing with `target_os = "..."`
+                            }
+
+                            PlatformVersion::Apple { os: *platform_sym, version }
+                        }
+                        Err(error) => {
+                            sess.dcx().emit_err(session_diagnostics::AppleVersionInvalid {
+                                span: *version_span,
+                                error,
+                            });
+                            return false;
+                        }
+                    }
+                }
+                // FIXME(madsmtm): Handle further platforms as specified in the RFC.
+                sym::windows | sym::libc => {
+                    #[allow(rustc::untranslatable_diagnostic)] // Temporary
+                    dcx.span_err(*platform_span, "unimplemented platform");
+                    return false;
+                }
+                _ => {
+                    // Unknown platform. This is intentionally a warning (and not an error) to be
+                    // future-compatible with later additions.
+                    let known_platforms = [
+                        sym::macos,
+                        sym::ios,
+                        sym::tvos,
+                        sym::watchos,
+                        sym::visionos,
+                        // sym::windows,
+                        // sym::libc,
+                    ];
+                    dcx.emit_warn(session_diagnostics::UnknownPlatformLiteral {
+                        span: *platform_span,
+                        possibilities: known_platforms.into_iter().collect(),
+                    });
+                    return false;
+                }
+            };
+
+            // Figure out actual cfg-status based on current platform.
+            match version {
+                PlatformVersion::Apple { os, version } if os.as_str() == sess.target.os => {
+                    let deployment_target = sess.apple_deployment_target();
+                    version <= deployment_target
+                }
+                // If a `cfg`-value does not apply to a specific platform, assume
+                _ => false,
+            }
+        }
         ast::MetaItemKind::List(mis) => {
             for mi in mis.iter() {
                 if mi.meta_item_or_bool().is_none() {
@@ -251,3 +368,8 @@ pub fn eval_condition(
         }
     }
 }
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+enum PlatformVersion {
+    Apple { os: Symbol, version: apple::OSVersion },
+}
diff --git a/compiler/rustc_attr_parsing/src/session_diagnostics.rs b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
index 92bc2a8aeb05e..12f38d7b1c957 100644
--- a/compiler/rustc_attr_parsing/src/session_diagnostics.rs
+++ b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
@@ -1,14 +1,32 @@
-use std::num::IntErrorKind;
+use std::num::{IntErrorKind, ParseIntError};
 
 use rustc_ast as ast;
 use rustc_errors::codes::*;
-use rustc_errors::{Applicability, Diag, DiagCtxtHandle, Diagnostic, EmissionGuarantee, Level};
+use rustc_errors::{
+    Applicability, Diag, DiagCtxtHandle, DiagSymbolList, Diagnostic, EmissionGuarantee, Level,
+};
 use rustc_macros::{Diagnostic, Subdiagnostic};
 use rustc_span::{Span, Symbol};
 
 use crate::attributes::util::UnsupportedLiteralReason;
 use crate::fluent_generated as fluent;
 
+#[derive(Diagnostic)]
+#[diag(attr_parsing_apple_version_invalid)]
+pub(crate) struct AppleVersionInvalid {
+    #[primary_span]
+    pub span: Span,
+    pub error: ParseIntError,
+}
+
+#[derive(Diagnostic)]
+#[diag(attr_parsing_apple_version_unnecessarily_low)]
+pub(crate) struct AppleVersionUnnecessarilyLow {
+    #[primary_span]
+    pub span: Span,
+    pub os_min: String,
+}
+
 #[derive(Diagnostic)]
 #[diag(attr_parsing_expected_one_cfg_pattern, code = E0536)]
 pub(crate) struct ExpectedOneCfgPattern {
@@ -371,6 +389,20 @@ pub(crate) struct DeprecatedItemSuggestion {
     pub details: (),
 }
 
+#[derive(Diagnostic)]
+#[diag(attr_parsing_expected_platform_and_version_literals)]
+pub(crate) struct ExpectedPlatformAndVersionLiterals {
+    #[primary_span]
+    pub span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(attr_parsing_expected_platform_literal)]
+pub(crate) struct ExpectedPlatformLiteral {
+    #[primary_span]
+    pub span: Span,
+}
+
 #[derive(Diagnostic)]
 #[diag(attr_parsing_expected_single_version_literal)]
 pub(crate) struct ExpectedSingleVersionLiteral {
@@ -417,6 +449,14 @@ pub(crate) struct SoftNoArgs {
     pub span: Span,
 }
 
+#[derive(Diagnostic)]
+#[diag(attr_parsing_unknown_platform_literal)]
+pub(crate) struct UnknownPlatformLiteral {
+    #[primary_span]
+    pub span: Span,
+    pub possibilities: DiagSymbolList,
+}
+
 #[derive(Diagnostic)]
 #[diag(attr_parsing_unknown_version_literal)]
 pub(crate) struct UnknownVersionLiteral {
diff --git a/compiler/rustc_codegen_ssa/messages.ftl b/compiler/rustc_codegen_ssa/messages.ftl
index 22e262546c3a7..fc3ed74f5e9fe 100644
--- a/compiler/rustc_codegen_ssa/messages.ftl
+++ b/compiler/rustc_codegen_ssa/messages.ftl
@@ -4,12 +4,6 @@ codegen_ssa_add_native_library = failed to add native library {$library_path}: {
 
 codegen_ssa_aix_strip_not_used = using host's `strip` binary to cross-compile to AIX which is not guaranteed to work
 
-codegen_ssa_apple_deployment_target_invalid =
-    failed to parse deployment target specified in {$env_var}: {$error}
-
-codegen_ssa_apple_deployment_target_too_low =
-    deployment target in {$env_var} was set to {$version}, but the minimum supported by `rustc` is {$os_min}
-
 codegen_ssa_apple_sdk_error_sdk_path = failed to get {$sdk_name} SDK path: {$error}
 
 codegen_ssa_archive_build_failure = failed to build archive at `{$path}`: {$error}
diff --git a/compiler/rustc_codegen_ssa/src/back/apple.rs b/compiler/rustc_codegen_ssa/src/back/apple.rs
index d9c5c3e5af96d..b41ec30451953 100644
--- a/compiler/rustc_codegen_ssa/src/back/apple.rs
+++ b/compiler/rustc_codegen_ssa/src/back/apple.rs
@@ -1,11 +1,5 @@
-use std::env;
-use std::fmt::{Display, from_fn};
-use std::num::ParseIntError;
-
-use rustc_session::Session;
 use rustc_target::spec::Target;
-
-use crate::errors::AppleDeploymentTarget;
+pub(super) use rustc_target::spec::apple::OSVersion;
 
 #[cfg(test)]
 mod tests;
@@ -26,124 +20,6 @@ pub(super) fn macho_platform(target: &Target) -> u32 {
     }
 }
 
-/// Deployment target or SDK version.
-///
-/// The size of the numbers in here are limited by Mach-O's `LC_BUILD_VERSION`.
-type OSVersion = (u16, u8, u8);
-
-/// Parse an OS version triple (SDK version or deployment target).
-fn parse_version(version: &str) -> Result<OSVersion, ParseIntError> {
-    if let Some((major, minor)) = version.split_once('.') {
-        let major = major.parse()?;
-        if let Some((minor, patch)) = minor.split_once('.') {
-            Ok((major, minor.parse()?, patch.parse()?))
-        } else {
-            Ok((major, minor.parse()?, 0))
-        }
-    } else {
-        Ok((version.parse()?, 0, 0))
-    }
-}
-
-pub fn pretty_version(version: OSVersion) -> impl Display {
-    let (major, minor, patch) = version;
-    from_fn(move |f| {
-        write!(f, "{major}.{minor}")?;
-        if patch != 0 {
-            write!(f, ".{patch}")?;
-        }
-        Ok(())
-    })
-}
-
-/// Minimum operating system versions currently supported by `rustc`.
-fn os_minimum_deployment_target(os: &str) -> OSVersion {
-    // When bumping a version in here, remember to update the platform-support docs too.
-    //
-    // NOTE: The defaults may change in future `rustc` versions, so if you are looking for the
-    // default deployment target, prefer:
-    // ```
-    // $ rustc --print deployment-target
-    // ```
-    match os {
-        "macos" => (10, 12, 0),
-        "ios" => (10, 0, 0),
-        "tvos" => (10, 0, 0),
-        "watchos" => (5, 0, 0),
-        "visionos" => (1, 0, 0),
-        _ => unreachable!("tried to get deployment target for non-Apple platform"),
-    }
-}
-
-/// The deployment target for the given target.
-///
-/// This is similar to `os_minimum_deployment_target`, except that on certain targets it makes sense
-/// to raise the minimum OS version.
-///
-/// This matches what LLVM does, see in part:
-/// <https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/TargetParser/Triple.cpp#L1900-L1932>
-fn minimum_deployment_target(target: &Target) -> OSVersion {
-    match (&*target.os, &*target.arch, &*target.abi) {
-        ("macos", "aarch64", _) => (11, 0, 0),
-        ("ios", "aarch64", "macabi") => (14, 0, 0),
-        ("ios", "aarch64", "sim") => (14, 0, 0),
-        ("ios", _, _) if target.llvm_target.starts_with("arm64e") => (14, 0, 0),
-        // Mac Catalyst defaults to 13.1 in Clang.
-        ("ios", _, "macabi") => (13, 1, 0),
-        ("tvos", "aarch64", "sim") => (14, 0, 0),
-        ("watchos", "aarch64", "sim") => (7, 0, 0),
-        (os, _, _) => os_minimum_deployment_target(os),
-    }
-}
-
-/// Name of the environment variable used to fetch the deployment target on the given OS.
-pub fn deployment_target_env_var(os: &str) -> &'static str {
-    match os {
-        "macos" => "MACOSX_DEPLOYMENT_TARGET",
-        "ios" => "IPHONEOS_DEPLOYMENT_TARGET",
-        "watchos" => "WATCHOS_DEPLOYMENT_TARGET",
-        "tvos" => "TVOS_DEPLOYMENT_TARGET",
-        "visionos" => "XROS_DEPLOYMENT_TARGET",
-        _ => unreachable!("tried to get deployment target env var for non-Apple platform"),
-    }
-}
-
-/// Get the deployment target based on the standard environment variables, or fall back to the
-/// minimum version supported by `rustc`.
-pub fn deployment_target(sess: &Session) -> OSVersion {
-    let min = minimum_deployment_target(&sess.target);
-    let env_var = deployment_target_env_var(&sess.target.os);
-
-    if let Ok(deployment_target) = env::var(env_var) {
-        match parse_version(&deployment_target) {
-            Ok(version) => {
-                let os_min = os_minimum_deployment_target(&sess.target.os);
-                // It is common that the deployment target is set a bit too low, for example on
-                // macOS Aarch64 to also target older x86_64. So we only want to warn when variable
-                // is lower than the minimum OS supported by rustc, not when the variable is lower
-                // than the minimum for a specific target.
-                if version < os_min {
-                    sess.dcx().emit_warn(AppleDeploymentTarget::TooLow {
-                        env_var,
-                        version: pretty_version(version).to_string(),
-                        os_min: pretty_version(os_min).to_string(),
-                    });
-                }
-
-                // Raise the deployment target to the minimum supported.
-                version.max(min)
-            }
-            Err(error) => {
-                sess.dcx().emit_err(AppleDeploymentTarget::Invalid { env_var, error });
-                min
-            }
-        }
-    } else {
-        // If no deployment target variable is set, default to the minimum found above.
-        min
-    }
-}
-
 pub(super) fn add_version_to_llvm_target(
     llvm_target: &str,
     deployment_target: OSVersion,
@@ -155,17 +31,16 @@ pub(super) fn add_version_to_llvm_target(
     let environment = components.next();
     assert_eq!(components.next(), None, "too many LLVM triple components");
 
-    let (major, minor, patch) = deployment_target;
-
     assert!(
         !os.contains(|c: char| c.is_ascii_digit()),
         "LLVM target must not already be versioned"
     );
 
+    let version = deployment_target.fmt_full();
     if let Some(env) = environment {
         // Insert version into OS, before environment
-        format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}-{env}")
+        format!("{arch}-{vendor}-{os}{version}-{env}")
     } else {
-        format!("{arch}-{vendor}-{os}{major}.{minor}.{patch}")
+        format!("{arch}-{vendor}-{os}{version}")
     }
 }
diff --git a/compiler/rustc_codegen_ssa/src/back/apple/tests.rs b/compiler/rustc_codegen_ssa/src/back/apple/tests.rs
index 7ccda5a8190c7..06695c0d59360 100644
--- a/compiler/rustc_codegen_ssa/src/back/apple/tests.rs
+++ b/compiler/rustc_codegen_ssa/src/back/apple/tests.rs
@@ -1,21 +1,13 @@
-use super::{add_version_to_llvm_target, parse_version};
+use super::{OSVersion, add_version_to_llvm_target};
 
 #[test]
 fn test_add_version_to_llvm_target() {
     assert_eq!(
-        add_version_to_llvm_target("aarch64-apple-macosx", (10, 14, 1)),
+        add_version_to_llvm_target("aarch64-apple-macosx", OSVersion::new(10, 14, 1)),
         "aarch64-apple-macosx10.14.1"
     );
     assert_eq!(
-        add_version_to_llvm_target("aarch64-apple-ios-simulator", (16, 1, 0)),
+        add_version_to_llvm_target("aarch64-apple-ios-simulator", OSVersion::new(16, 1, 0)),
         "aarch64-apple-ios16.1.0-simulator"
     );
 }
-
-#[test]
-fn test_parse_version() {
-    assert_eq!(parse_version("10"), Ok((10, 0, 0)));
-    assert_eq!(parse_version("10.12"), Ok((10, 12, 0)));
-    assert_eq!(parse_version("10.12.6"), Ok((10, 12, 6)));
-    assert_eq!(parse_version("9999.99.99"), Ok((9999, 99, 99)));
-}
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index 105a4cb81f0d1..aeed922d88274 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -53,7 +53,7 @@ use super::command::Command;
 use super::linker::{self, Linker};
 use super::metadata::{MetadataPosition, create_wrapper_file};
 use super::rpath::{self, RPathConfig};
-use super::{apple, versioned_llvm_target};
+use super::versioned_llvm_target;
 use crate::{
     CodegenResults, CompiledModule, CrateInfo, NativeLib, common, errors,
     looks_like_rust_object_file,
@@ -3114,8 +3114,7 @@ fn add_apple_link_args(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavo
             _ => bug!("invalid OS/ABI combination for Apple target: {target_os}, {target_abi}"),
         };
 
-        let (major, minor, patch) = apple::deployment_target(sess);
-        let min_version = format!("{major}.{minor}.{patch}");
+        let min_version = sess.apple_deployment_target().fmt_full().to_string();
 
         // The SDK version is used at runtime when compiling with a newer SDK / version of Xcode:
         // - By dyld to give extra warnings and errors, see e.g.:
@@ -3184,10 +3183,10 @@ fn add_apple_link_args(cmd: &mut dyn Linker, sess: &Session, flavor: LinkerFlavo
 
             // The presence of `-mmacosx-version-min` makes CC default to
             // macOS, and it sets the deployment target.
-            let (major, minor, patch) = apple::deployment_target(sess);
+            let version = sess.apple_deployment_target().fmt_full();
             // Intentionally pass this as a single argument, Clang doesn't
             // seem to like it otherwise.
-            cmd.cc_arg(&format!("-mmacosx-version-min={major}.{minor}.{patch}"));
+            cmd.cc_arg(&format!("-mmacosx-version-min={version}"));
 
             // macOS has no environment, so with these two, we've told CC the
             // four desired parameters.
diff --git a/compiler/rustc_codegen_ssa/src/back/metadata.rs b/compiler/rustc_codegen_ssa/src/back/metadata.rs
index d70413b8a4741..a032d860e8e39 100644
--- a/compiler/rustc_codegen_ssa/src/back/metadata.rs
+++ b/compiler/rustc_codegen_ssa/src/back/metadata.rs
@@ -426,13 +426,13 @@ pub(crate) fn create_object_file(sess: &Session) -> Option<write::Object<'static
 fn macho_object_build_version_for_target(sess: &Session) -> object::write::MachOBuildVersion {
     /// The `object` crate demands "X.Y.Z encoded in nibbles as xxxx.yy.zz"
     /// e.g. minOS 14.0 = 0x000E0000, or SDK 16.2 = 0x00100200
-    fn pack_version((major, minor, patch): (u16, u8, u8)) -> u32 {
+    fn pack_version(apple::OSVersion { major, minor, patch }: apple::OSVersion) -> u32 {
         let (major, minor, patch) = (major as u32, minor as u32, patch as u32);
         (major << 16) | (minor << 8) | patch
     }
 
     let platform = apple::macho_platform(&sess.target);
-    let min_os = apple::deployment_target(sess);
+    let min_os = sess.apple_deployment_target();
 
     let mut build_version = object::write::MachOBuildVersion::default();
     build_version.platform = platform;
diff --git a/compiler/rustc_codegen_ssa/src/back/mod.rs b/compiler/rustc_codegen_ssa/src/back/mod.rs
index 64b5d4569ecce..76971096a5820 100644
--- a/compiler/rustc_codegen_ssa/src/back/mod.rs
+++ b/compiler/rustc_codegen_ssa/src/back/mod.rs
@@ -20,7 +20,7 @@ pub mod write;
 /// Certain optimizations also depend on the deployment target.
 pub fn versioned_llvm_target(sess: &Session) -> Cow<'_, str> {
     if sess.target.is_like_osx {
-        apple::add_version_to_llvm_target(&sess.target.llvm_target, apple::deployment_target(sess))
+        apple::add_version_to_llvm_target(&sess.target.llvm_target, sess.apple_deployment_target())
             .into()
     } else {
         // FIXME(madsmtm): Certain other targets also include a version,
diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs
index 3ddbe4aeeec5d..ff8a08e2859bc 100644
--- a/compiler/rustc_codegen_ssa/src/errors.rs
+++ b/compiler/rustc_codegen_ssa/src/errors.rs
@@ -3,7 +3,6 @@
 use std::borrow::Cow;
 use std::ffi::OsString;
 use std::io::Error;
-use std::num::ParseIntError;
 use std::path::{Path, PathBuf};
 use std::process::ExitStatus;
 
@@ -642,14 +641,6 @@ pub(crate) struct UnsupportedArch<'a> {
     pub os: &'a str,
 }
 
-#[derive(Diagnostic)]
-pub(crate) enum AppleDeploymentTarget {
-    #[diag(codegen_ssa_apple_deployment_target_invalid)]
-    Invalid { env_var: &'static str, error: ParseIntError },
-    #[diag(codegen_ssa_apple_deployment_target_too_low)]
-    TooLow { env_var: &'static str, version: String, os_min: String },
-}
-
 #[derive(Diagnostic)]
 pub(crate) enum AppleSdkRootError<'a> {
     #[diag(codegen_ssa_apple_sdk_error_sdk_path)]
diff --git a/compiler/rustc_codegen_ssa/src/lib.rs b/compiler/rustc_codegen_ssa/src/lib.rs
index 428a45975f1ec..0bde08bf9110a 100644
--- a/compiler/rustc_codegen_ssa/src/lib.rs
+++ b/compiler/rustc_codegen_ssa/src/lib.rs
@@ -6,7 +6,6 @@
 #![doc(rust_logo)]
 #![feature(assert_matches)]
 #![feature(box_patterns)]
-#![feature(debug_closure_helpers)]
 #![feature(file_buffered)]
 #![feature(if_let_guard)]
 #![feature(let_chains)]
diff --git a/compiler/rustc_driver_impl/src/lib.rs b/compiler/rustc_driver_impl/src/lib.rs
index 4c47ce93dd56f..8deeb3659da36 100644
--- a/compiler/rustc_driver_impl/src/lib.rs
+++ b/compiler/rustc_driver_impl/src/lib.rs
@@ -34,7 +34,6 @@ use std::time::{Instant, SystemTime};
 use std::{env, str};
 
 use rustc_ast as ast;
-use rustc_codegen_ssa::back::apple;
 use rustc_codegen_ssa::traits::CodegenBackend;
 use rustc_codegen_ssa::{CodegenErrors, CodegenResults};
 use rustc_data_structures::profiling::{
@@ -775,8 +774,8 @@ fn print_crate_info(
                 if sess.target.is_like_osx {
                     println_info!(
                         "{}={}",
-                        apple::deployment_target_env_var(&sess.target.os),
-                        apple::pretty_version(apple::deployment_target(sess)),
+                        rustc_target::spec::apple::deployment_target_env_var(&sess.target.os),
+                        sess.apple_deployment_target().fmt_pretty(),
                     )
                 } else {
                     #[allow(rustc::diagnostic_outside_of_impl)]
diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs
index eb5fac96af270..24591515d0338 100644
--- a/compiler/rustc_feature/src/builtin_attrs.rs
+++ b/compiler/rustc_feature/src/builtin_attrs.rs
@@ -33,6 +33,7 @@ const GATED_CFGS: &[GatedCfg] = &[
     ),
     (sym::sanitize, sym::cfg_sanitize, Features::cfg_sanitize),
     (sym::version, sym::cfg_version, Features::cfg_version),
+    (sym::os_version_min, sym::cfg_os_version_min, Features::cfg_os_version_min),
     (sym::relocation_model, sym::cfg_relocation_model, Features::cfg_relocation_model),
     (sym::sanitizer_cfi_generalize_pointers, sym::cfg_sanitizer_cfi, Features::cfg_sanitizer_cfi),
     (sym::sanitizer_cfi_normalize_integers, sym::cfg_sanitizer_cfi, Features::cfg_sanitizer_cfi),
diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs
index 3a2e810dc6af7..41af2f11d2494 100644
--- a/compiler/rustc_feature/src/unstable.rs
+++ b/compiler/rustc_feature/src/unstable.rs
@@ -405,6 +405,8 @@ declare_features! (
     (unstable, cfg_boolean_literals, "1.83.0", Some(131204)),
     /// Allows the use of `#[cfg(contract_checks)` to check if contract checks are enabled.
     (unstable, cfg_contract_checks, "CURRENT_RUSTC_VERSION", Some(128044)),
+    /// Allow conditional compilation depending on target platform version.
+    (unstable, cfg_os_version_min, "CURRENT_RUSTC_VERSION", Some(136866)),
     /// Allows the use of `#[cfg(overflow_checks)` to check if integer overflow behaviour.
     (unstable, cfg_overflow_checks, "1.71.0", Some(111466)),
     /// Provides the relocation model information as cfg entry
diff --git a/compiler/rustc_session/messages.ftl b/compiler/rustc_session/messages.ftl
index e5fba8cc5a2d2..99cfb2a88064d 100644
--- a/compiler/rustc_session/messages.ftl
+++ b/compiler/rustc_session/messages.ftl
@@ -1,3 +1,9 @@
+session_apple_deployment_target_invalid =
+    failed to parse deployment target specified in {$env_var}: {$error}
+
+session_apple_deployment_target_too_low =
+    deployment target in {$env_var} was set to {$version}, but the minimum supported by `rustc` is {$os_min}
+
 session_binary_float_literal_not_supported = binary float literal is not supported
 session_branch_protection_requires_aarch64 = `-Zbranch-protection` is only supported on aarch64
 
diff --git a/compiler/rustc_session/src/errors.rs b/compiler/rustc_session/src/errors.rs
index 75c3b2c7a8592..6399531121ba9 100644
--- a/compiler/rustc_session/src/errors.rs
+++ b/compiler/rustc_session/src/errors.rs
@@ -1,4 +1,4 @@
-use std::num::NonZero;
+use std::num::{NonZero, ParseIntError};
 
 use rustc_ast::token;
 use rustc_ast::util::literal::LitError;
@@ -14,6 +14,14 @@ use rustc_target::spec::{SplitDebuginfo, StackProtector, TargetTuple};
 use crate::config::CrateType;
 use crate::parse::ParseSess;
 
+#[derive(Diagnostic)]
+pub(crate) enum AppleDeploymentTarget {
+    #[diag(session_apple_deployment_target_invalid)]
+    Invalid { env_var: &'static str, error: ParseIntError },
+    #[diag(session_apple_deployment_target_too_low)]
+    TooLow { env_var: &'static str, version: String, os_min: String },
+}
+
 pub(crate) struct FeatureGateError {
     pub(crate) span: MultiSpan,
     pub(crate) explain: DiagMessage,
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index f795ad1ee17d7..7d75fcf90fbc8 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -29,7 +29,7 @@ use rustc_target::asm::InlineAsmArch;
 use rustc_target::spec::{
     CodeModel, DebuginfoKind, PanicStrategy, RelocModel, RelroLevel, SanitizerSet,
     SmallDataThresholdSupport, SplitDebuginfo, StackProtector, SymbolVisibility, Target,
-    TargetTuple, TlsModel,
+    TargetTuple, TlsModel, apple,
 };
 
 use crate::code_stats::CodeStats;
@@ -877,6 +877,45 @@ impl Session {
             FileNameDisplayPreference::Local
         }
     }
+
+    /// Get the deployment target on Apple platforms based on the standard environment variables,
+    /// or fall back to the minimum version supported by `rustc`.
+    ///
+    /// This should be guarded behind `if sess.target.is_like_osx`.
+    pub fn apple_deployment_target(&self) -> apple::OSVersion {
+        let min = apple::OSVersion::minimum_deployment_target(&self.target);
+        let env_var = apple::deployment_target_env_var(&self.target.os);
+
+        // FIXME(madsmtm): Track changes to this.
+        if let Ok(deployment_target) = env::var(env_var) {
+            match apple::OSVersion::from_str(&deployment_target) {
+                Ok(version) => {
+                    let os_min = apple::OSVersion::os_minimum_deployment_target(&self.target.os);
+                    // It is common that the deployment target is set a bit too low, for example on
+                    // macOS Aarch64 to also target older x86_64. So we only want to warn when variable
+                    // is lower than the minimum OS supported by rustc, not when the variable is lower
+                    // than the minimum for a specific target.
+                    if version < os_min {
+                        self.dcx().emit_warn(errors::AppleDeploymentTarget::TooLow {
+                            env_var,
+                            version: version.fmt_pretty().to_string(),
+                            os_min: os_min.fmt_pretty().to_string(),
+                        });
+                    }
+
+                    // Raise the deployment target to the minimum supported.
+                    version.max(min)
+                }
+                Err(error) => {
+                    self.dcx().emit_err(errors::AppleDeploymentTarget::Invalid { env_var, error });
+                    min
+                }
+            }
+        } else {
+            // If no deployment target variable is set, default to the minimum found above.
+            min
+        }
+    }
 }
 
 // JUSTIFICATION: part of session construction
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index c819d43323583..4d17b986a8905 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -577,6 +577,7 @@ symbols! {
         cfg_eval,
         cfg_fmt_debug,
         cfg_hide,
+        cfg_os_version_min,
         cfg_overflow_checks,
         cfg_panic,
         cfg_relocation_model,
@@ -1139,6 +1140,7 @@ symbols! {
         intrinsics_unaligned_volatile_store,
         io_stderr,
         io_stdout,
+        ios,
         irrefutable_let_patterns,
         is,
         is_val_statically_known,
@@ -1224,6 +1226,7 @@ symbols! {
         loop_break_value,
         lt,
         m68k_target_feature,
+        macos,
         macro_at_most_once_rep,
         macro_attributes_in_derive_output,
         macro_escape,
@@ -1449,6 +1452,7 @@ symbols! {
         ord_cmp_method,
         os_str_to_os_string,
         os_string_as_os_str,
+        os_version_min,
         other,
         out,
         overflow_checks,
@@ -2064,6 +2068,7 @@ symbols! {
         tuple,
         tuple_indexing,
         tuple_trait,
+        tvos,
         two_phase,
         ty,
         type_alias_enum_variants,
@@ -2206,6 +2211,7 @@ symbols! {
         vfp2,
         vis,
         visible_private_types,
+        visionos,
         volatile,
         volatile_copy_memory,
         volatile_copy_nonoverlapping_memory,
@@ -2222,6 +2228,7 @@ symbols! {
         wasm_abi,
         wasm_import_module,
         wasm_target_feature,
+        watchos,
         while_let,
         windows,
         windows_subsystem,
diff --git a/compiler/rustc_target/src/lib.rs b/compiler/rustc_target/src/lib.rs
index bde4af643fa6c..7249f148e62ee 100644
--- a/compiler/rustc_target/src/lib.rs
+++ b/compiler/rustc_target/src/lib.rs
@@ -12,6 +12,7 @@
 #![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
 #![doc(rust_logo)]
 #![feature(assert_matches)]
+#![feature(debug_closure_helpers)]
 #![feature(iter_intersperse)]
 #![feature(let_chains)]
 #![feature(rustc_attrs)]
diff --git a/compiler/rustc_target/src/spec/base/apple/mod.rs b/compiler/rustc_target/src/spec/base/apple/mod.rs
index 497994a5998cb..cdd3a0c30dec7 100644
--- a/compiler/rustc_target/src/spec/base/apple/mod.rs
+++ b/compiler/rustc_target/src/spec/base/apple/mod.rs
@@ -1,9 +1,12 @@
 use std::borrow::Cow;
 use std::env;
+use std::fmt::{Display, from_fn};
+use std::num::ParseIntError;
+use std::str::FromStr;
 
 use crate::spec::{
     Cc, DebuginfoKind, FloatAbi, FramePointer, LinkerFlavor, Lld, SplitDebuginfo, StackProbeType,
-    StaticCow, TargetOptions, cvs,
+    StaticCow, Target, TargetOptions, cvs,
 };
 
 #[cfg(test)]
@@ -217,3 +220,107 @@ fn link_env_remove(os: &'static str) -> StaticCow<[StaticCow<str>]> {
         cvs!["MACOSX_DEPLOYMENT_TARGET"]
     }
 }
+
+/// Deployment target or SDK version.
+///
+/// The size of the numbers in here are limited by Mach-O's `LC_BUILD_VERSION`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct OSVersion {
+    pub major: u16,
+    pub minor: u8,
+    pub patch: u8,
+}
+
+impl FromStr for OSVersion {
+    type Err = ParseIntError;
+
+    /// Parse an OS version triple (SDK version or deployment target).
+    fn from_str(version: &str) -> Result<Self, ParseIntError> {
+        if let Some((major, minor)) = version.split_once('.') {
+            let major = major.parse()?;
+            if let Some((minor, patch)) = minor.split_once('.') {
+                Ok(Self { major, minor: minor.parse()?, patch: patch.parse()? })
+            } else {
+                Ok(Self { major, minor: minor.parse()?, patch: 0 })
+            }
+        } else {
+            Ok(Self { major: version.parse()?, minor: 0, patch: 0 })
+        }
+    }
+}
+
+impl OSVersion {
+    pub fn new(major: u16, minor: u8, patch: u8) -> Self {
+        Self { major, minor, patch }
+    }
+
+    pub fn fmt_pretty(self) -> impl Display {
+        let Self { major, minor, patch } = self;
+        from_fn(move |f| {
+            write!(f, "{major}.{minor}")?;
+            if patch != 0 {
+                write!(f, ".{patch}")?;
+            }
+            Ok(())
+        })
+    }
+
+    pub fn fmt_full(self) -> impl Display {
+        let Self { major, minor, patch } = self;
+        from_fn(move |f| write!(f, "{major}.{minor}.{patch}"))
+    }
+
+    /// Minimum operating system versions currently supported by `rustc`.
+    pub fn os_minimum_deployment_target(os: &str) -> Self {
+        // When bumping a version in here, remember to update the platform-support docs too.
+        //
+        // NOTE: The defaults may change in future `rustc` versions, so if you are looking for the
+        // default deployment target, prefer:
+        // ```
+        // $ rustc --print deployment-target
+        // ```
+        let (major, minor, patch) = match os {
+            "macos" => (10, 12, 0),
+            "ios" => (10, 0, 0),
+            "tvos" => (10, 0, 0),
+            "watchos" => (5, 0, 0),
+            "visionos" => (1, 0, 0),
+            _ => unreachable!("tried to get deployment target for non-Apple platform"),
+        };
+        Self { major, minor, patch }
+    }
+
+    /// The deployment target for the given target.
+    ///
+    /// This is similar to `os_minimum_deployment_target`, except that on certain targets it makes sense
+    /// to raise the minimum OS version.
+    ///
+    /// This matches what LLVM does, see in part:
+    /// <https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/TargetParser/Triple.cpp#L1900-L1932>
+    pub fn minimum_deployment_target(target: &Target) -> Self {
+        let (major, minor, patch) = match (&*target.os, &*target.arch, &*target.abi) {
+            ("macos", "aarch64", _) => (11, 0, 0),
+            ("ios", "aarch64", "macabi") => (14, 0, 0),
+            ("ios", "aarch64", "sim") => (14, 0, 0),
+            ("ios", _, _) if target.llvm_target.starts_with("arm64e") => (14, 0, 0),
+            // Mac Catalyst defaults to 13.1 in Clang.
+            ("ios", _, "macabi") => (13, 1, 0),
+            ("tvos", "aarch64", "sim") => (14, 0, 0),
+            ("watchos", "aarch64", "sim") => (7, 0, 0),
+            (os, _, _) => return Self::os_minimum_deployment_target(os),
+        };
+        Self { major, minor, patch }
+    }
+}
+
+/// Name of the environment variable used to fetch the deployment target on the given OS.
+pub fn deployment_target_env_var(os: &str) -> &'static str {
+    match os {
+        "macos" => "MACOSX_DEPLOYMENT_TARGET",
+        "ios" => "IPHONEOS_DEPLOYMENT_TARGET",
+        "watchos" => "WATCHOS_DEPLOYMENT_TARGET",
+        "tvos" => "TVOS_DEPLOYMENT_TARGET",
+        "visionos" => "XROS_DEPLOYMENT_TARGET",
+        _ => unreachable!("tried to get deployment target env var for non-Apple platform"),
+    }
+}
diff --git a/compiler/rustc_target/src/spec/base/apple/tests.rs b/compiler/rustc_target/src/spec/base/apple/tests.rs
index 7a985ad4dc056..391f347010436 100644
--- a/compiler/rustc_target/src/spec/base/apple/tests.rs
+++ b/compiler/rustc_target/src/spec/base/apple/tests.rs
@@ -1,3 +1,4 @@
+use super::OSVersion;
 use crate::spec::targets::{
     aarch64_apple_darwin, aarch64_apple_ios_sim, aarch64_apple_visionos_sim,
     aarch64_apple_watchos_sim, i686_apple_darwin, x86_64_apple_darwin, x86_64_apple_ios,
@@ -42,3 +43,11 @@ fn macos_link_environment_unmodified() {
         );
     }
 }
+
+#[test]
+fn test_parse_version() {
+    assert_eq!("10".parse(), Ok(OSVersion::new(10, 0, 0)));
+    assert_eq!("10.12".parse(), Ok(OSVersion::new(10, 12, 0)));
+    assert_eq!("10.12.6".parse(), Ok(OSVersion::new(10, 12, 6)));
+    assert_eq!("9999.99.99".parse(), Ok(OSVersion::new(9999, 99, 99)));
+}
diff --git a/compiler/rustc_target/src/spec/base/mod.rs b/compiler/rustc_target/src/spec/base/mod.rs
index 28d10dcf2ff3a..e8226d758c2a4 100644
--- a/compiler/rustc_target/src/spec/base/mod.rs
+++ b/compiler/rustc_target/src/spec/base/mod.rs
@@ -1,6 +1,6 @@
 pub(crate) mod aix;
 pub(crate) mod android;
-pub(crate) mod apple;
+pub mod apple;
 pub(crate) mod avr_gnu;
 pub(crate) mod bpf;
 pub(crate) mod dragonfly;
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index df1862ec27eee..dfb231fa42438 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -67,6 +67,7 @@ pub mod abi {
 mod base;
 mod json;
 
+pub use base::apple;
 pub use base::avr_gnu::ef_avr_arch;
 
 /// Linker is called through a C/C++ compiler.
diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs
index 954a4182fbd6c..6301e129886f6 100644
--- a/library/std/src/lib.rs
+++ b/library/std/src/lib.rs
@@ -281,6 +281,8 @@
 #![feature(cfg_sanitizer_cfi)]
 #![feature(cfg_target_thread_local)]
 #![feature(cfi_encoding)]
+#![feature(cfg_boolean_literals)]
+#![cfg_attr(not(bootstrap), feature(cfg_os_version_min))]
 #![feature(concat_idents)]
 #![feature(decl_macro)]
 #![feature(deprecated_suggestion)]
diff --git a/library/std/src/sys/pal/unix/fd.rs b/library/std/src/sys/pal/unix/fd.rs
index 2fc33bdfefbf5..ca6a97e296626 100644
--- a/library/std/src/sys/pal/unix/fd.rs
+++ b/library/std/src/sys/pal/unix/fd.rs
@@ -252,7 +252,15 @@ impl FileDesc {
 
     #[cfg(all(target_os = "android", target_pointer_width = "32"))]
     pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
-        super::weak::weak!(fn preadv64(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize);
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(false)]
+            fn preadv64(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
 
         match preadv64.get() {
             Some(preadv) => {
@@ -281,7 +289,33 @@ impl FileDesc {
     // use "weak" linking.
     #[cfg(target_vendor = "apple")]
     pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
-        super::weak::weak!(fn preadv(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize);
+        #[cfg(not(bootstrap))]
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(any(
+                os_version_min("macos", "11.0"),
+                os_version_min("ios", "14.0"),
+                os_version_min("tvos", "14.0"),
+                os_version_min("watchos", "7.0"),
+                os_version_min("visionos", "1.0"),
+            ))]
+            fn preadv(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        #[cfg(bootstrap)]
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(false)]
+            fn preadv(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
 
         match preadv.get() {
             Some(preadv) => {
@@ -445,7 +479,15 @@ impl FileDesc {
 
     #[cfg(all(target_os = "android", target_pointer_width = "32"))]
     pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
-        super::weak::weak!(fn pwritev64(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize);
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(false)]
+            fn pwritev64(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
 
         match pwritev64.get() {
             Some(pwritev) => {
@@ -463,18 +505,35 @@ impl FileDesc {
         }
     }
 
-    // We support old MacOS, iOS, watchOS, tvOS and visionOS. `pwritev` was added in the following
-    // Apple OS versions:
-    // ios 14.0
-    // tvos 14.0
-    // macos 11.0
-    // watchos 7.0
-    //
-    // These versions may be newer than the minimum supported versions of OS's we support so we must
-    // use "weak" linking.
     #[cfg(target_vendor = "apple")]
     pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
-        super::weak::weak!(fn pwritev(libc::c_int, *const libc::iovec, libc::c_int, off64_t) -> isize);
+        #[cfg(not(bootstrap))]
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(any(
+                os_version_min("macos", "11.0"),
+                os_version_min("ios", "14.0"),
+                os_version_min("tvos", "14.0"),
+                os_version_min("watchos", "7.0"),
+                os_version_min("visionos", "1.0"),
+            ))]
+            fn pwritev(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
+
+        #[cfg(bootstrap)]
+        super::weak::maybe_weak!(
+            #[cfg_always_available_on(false)]
+            fn pwritev(
+                fd: libc::c_int,
+                iov: *const libc::iovec,
+                iovcnt: libc::c_int,
+                offset: off64_t,
+            ) -> isize;
+        );
 
         match pwritev.get() {
             Some(pwritev) => {
diff --git a/library/std/src/sys/pal/unix/fs.rs b/library/std/src/sys/pal/unix/fs.rs
index 00cfa7a7fcfda..14191c0bff623 100644
--- a/library/std/src/sys/pal/unix/fs.rs
+++ b/library/std/src/sys/pal/unix/fs.rs
@@ -80,10 +80,10 @@ use crate::sync::Arc;
 use crate::sys::common::small_c_string::run_path_with_cstr;
 use crate::sys::fd::FileDesc;
 use crate::sys::time::SystemTime;
+#[cfg(target_os = "android")]
+use crate::sys::weak::maybe_weak;
 #[cfg(all(target_os = "linux", target_env = "gnu"))]
 use crate::sys::weak::syscall;
-#[cfg(target_os = "android")]
-use crate::sys::weak::weak;
 use crate::sys::{cvt, cvt_r};
 pub use crate::sys_common::fs::exists;
 use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner};
@@ -1508,9 +1508,13 @@ impl File {
                 Ok(())
             } else if #[cfg(target_os = "android")] {
                 let times = [to_timespec(times.accessed)?, to_timespec(times.modified)?];
-                // futimens requires Android API level 19
                 cvt(unsafe {
-                    weak!(fn futimens(c_int, *const libc::timespec) -> c_int);
+                    maybe_weak!(
+                        // FIXME: Once `os_version_min` supports Android, mark this as available on
+                        // Android API level 19.
+                        #[cfg_always_available_on(false)]
+                        fn futimens(c_int, *const libc::timespec) -> c_int;
+                    );
                     match futimens.get() {
                         Some(futimens) => futimens(self.as_raw_fd(), times.as_ptr()),
                         None => return Err(io::const_error!(
@@ -1523,10 +1527,14 @@ impl File {
             } else {
                 #[cfg(all(target_os = "linux", target_env = "gnu", target_pointer_width = "32", not(target_arch = "riscv32")))]
                 {
-                    use crate::sys::{time::__timespec64, weak::weak};
-
-                    // Added in glibc 2.34
-                    weak!(fn __futimens64(libc::c_int, *const __timespec64) -> libc::c_int);
+                    use crate::sys::{time::__timespec64, weak::maybe_weak};
+
+                    maybe_weak!(
+                        // FIXME: Once `os_version_min` supports glibc, mark this as availabe on
+                        // glibc 2.34.
+                        #[cfg_always_available_on(false)]
+                        fn __futimens64(libc::c_int, *const __timespec64) -> libc::c_int;
+                    );
 
                     if let Some(futimens64) = __futimens64.get() {
                         let to_timespec = |time: Option<SystemTime>| time.map(|time| time.t.to_timespec64())
diff --git a/library/std/src/sys/pal/unix/process/process_unix.rs b/library/std/src/sys/pal/unix/process/process_unix.rs
index 2bff192a5bd83..18c04a9150804 100644
--- a/library/std/src/sys/pal/unix/process/process_unix.rs
+++ b/library/std/src/sys/pal/unix/process/process_unix.rs
@@ -454,20 +454,24 @@ impl Command {
 
         cfg_if::cfg_if! {
             if #[cfg(target_os = "linux")] {
-                use crate::sys::weak::weak;
+                use crate::sys::weak::maybe_weak;
 
-                weak! {
+                maybe_weak!(
+                    #[cfg_always_available_on(false)]
                     fn pidfd_spawnp(
-                        *mut libc::c_int,
-                        *const libc::c_char,
-                        *const libc::posix_spawn_file_actions_t,
-                        *const libc::posix_spawnattr_t,
-                        *const *mut libc::c_char,
-                        *const *mut libc::c_char
-                    ) -> libc::c_int
-                }
+                        pidfd: *mut libc::c_int,
+                          path: *const libc::c_char,
+                        file_actions: *const libc::posix_spawn_file_actions_t,
+                        attrp: *const libc::posix_spawnattr_t,
+                        argv: *const *mut libc::c_char,
+                        envp: *const *mut libc::c_char,
+                    ) -> libc::c_int;
+                );
 
-                weak! { fn pidfd_getpid(libc::c_int) -> libc::c_int }
+                maybe_weak!(
+                    #[cfg_always_available_on(false)]
+                    fn pidfd_getpid(fd: libc::c_int) -> libc::c_int;
+                );
 
                 static PIDFD_SUPPORTED: AtomicU8 = AtomicU8::new(0);
                 const UNKNOWN: u8 = 0;
@@ -575,38 +579,30 @@ impl Command {
         ) -> libc::c_int;
 
         /// Get the function pointer for adding a chdir action to a
-        /// `posix_spawn_file_actions_t`, if available, assuming a dynamic libc.
+        /// `posix_spawn_file_actions_t`, if available.
         ///
         /// Some platforms can set a new working directory for a spawned process in the
         /// `posix_spawn` path. This function looks up the function pointer for adding
         /// such an action to a `posix_spawn_file_actions_t` struct.
-        #[cfg(not(all(target_os = "linux", target_env = "musl")))]
         fn get_posix_spawn_addchdir() -> Option<PosixSpawnAddChdirFn> {
-            use crate::sys::weak::weak;
-
-            weak! {
+            use crate::sys::weak::maybe_weak;
+
+            maybe_weak!(
+                // Weak symbol lookup doesn't work with statically linked libcs, so in cases
+                // where static linking is possible we need to either check for the presence
+                // of the symbol at compile time or know about it upfront.
+                //
+                // Our minimum required musl supports this function, so we can just use it.
+                #[cfg_always_available_on(all(target_os = "linux", target_env = "musl"))]
                 fn posix_spawn_file_actions_addchdir_np(
-                    *mut libc::posix_spawn_file_actions_t,
-                    *const libc::c_char
-                ) -> libc::c_int
-            }
+                    file_actions: *mut libc::posix_spawn_file_actions_t,
+                    path: *const libc::c_char,
+                ) -> libc::c_int;
+            );
 
             posix_spawn_file_actions_addchdir_np.get()
         }
 
-        /// Get the function pointer for adding a chdir action to a
-        /// `posix_spawn_file_actions_t`, if available, on platforms where the function
-        /// is known to exist.
-        ///
-        /// Weak symbol lookup doesn't work with statically linked libcs, so in cases
-        /// where static linking is possible we need to either check for the presence
-        /// of the symbol at compile time or know about it upfront.
-        #[cfg(all(target_os = "linux", target_env = "musl"))]
-        fn get_posix_spawn_addchdir() -> Option<PosixSpawnAddChdirFn> {
-            // Our minimum required musl supports this function, so we can just use it.
-            Some(libc::posix_spawn_file_actions_addchdir_np)
-        }
-
         let addchdir = match self.get_cwd() {
             Some(cwd) => {
                 if cfg!(target_vendor = "apple") {
diff --git a/library/std/src/sys/pal/unix/thread.rs b/library/std/src/sys/pal/unix/thread.rs
index 479021af040a7..129954de5063b 100644
--- a/library/std/src/sys/pal/unix/thread.rs
+++ b/library/std/src/sys/pal/unix/thread.rs
@@ -4,7 +4,7 @@ use crate::num::NonZero;
 #[cfg(all(target_os = "linux", target_env = "gnu"))]
 use crate::sys::weak::dlsym;
 #[cfg(any(target_os = "solaris", target_os = "illumos", target_os = "nto",))]
-use crate::sys::weak::weak;
+use crate::sys::weak::maybe_weak;
 use crate::sys::{os, stack_overflow};
 use crate::time::Duration;
 use crate::{cmp, io, ptr};
@@ -186,11 +186,13 @@ impl Thread {
 
     #[cfg(any(target_os = "solaris", target_os = "illumos", target_os = "nto"))]
     pub fn set_name(name: &CStr) {
-        weak! {
+        maybe_weak!(
+            #[cfg_always_available_on(false)]
             fn pthread_setname_np(
-                libc::pthread_t, *const libc::c_char
-            ) -> libc::c_int
-        }
+                thread: libc::pthread_t,
+                name: *const libc::c_char,
+            ) -> libc::c_int;
+        );
 
         if let Some(f) = pthread_setname_np.get() {
             #[cfg(target_os = "nto")]
diff --git a/library/std/src/sys/pal/unix/time.rs b/library/std/src/sys/pal/unix/time.rs
index e224980e95f31..f27542d640003 100644
--- a/library/std/src/sys/pal/unix/time.rs
+++ b/library/std/src/sys/pal/unix/time.rs
@@ -108,11 +108,18 @@ impl Timespec {
             not(target_arch = "riscv32")
         ))]
         {
-            use crate::sys::weak::weak;
+            use crate::sys::weak::maybe_weak;
 
             // __clock_gettime64 was added to 32-bit arches in glibc 2.34,
             // and it handles both vDSO calls and ENOSYS fallbacks itself.
-            weak!(fn __clock_gettime64(libc::clockid_t, *mut __timespec64) -> libc::c_int);
+            maybe_weak!(
+                // FIXME: Mark with `os_version_min` once that supports glibc.
+                #[cfg_always_available_on(false)]
+                fn __clock_gettime64(
+                    clock_id: libc::clockid_t,
+                    tp: *mut __timespec64,
+                ) -> libc::c_int;
+            );
 
             if let Some(clock_gettime64) = __clock_gettime64.get() {
                 let mut t = MaybeUninit::uninit();
diff --git a/library/std/src/sys/pal/unix/weak.rs b/library/std/src/sys/pal/unix/weak.rs
index 5a37598f43827..fd9a62bd2d2d6 100644
--- a/library/std/src/sys/pal/unix/weak.rs
+++ b/library/std/src/sys/pal/unix/weak.rs
@@ -19,7 +19,7 @@
 // There are a variety of `#[cfg]`s controlling which targets are involved in
 // each instance of `weak!` and `syscall!`. Rather than trying to unify all of
 // that, we'll just allow that some unix targets don't use this module at all.
-#![allow(dead_code, unused_macros)]
+#![allow(dead_code, unused_macros, unused_imports)]
 
 use crate::ffi::CStr;
 use crate::marker::PhantomData;
@@ -28,11 +28,12 @@ use crate::{mem, ptr};
 
 // We can use true weak linkage on ELF targets.
 #[cfg(all(unix, not(target_vendor = "apple")))]
-pub(crate) macro weak {
-    (fn $name:ident($($t:ty),*) -> $ret:ty) => (
+pub(crate) macro weak_impl {
+    (fn $name:ident($($t:ty),*) -> $ret:ty $(, $sym:expr)?) => (
         let ref $name: ExternWeak<unsafe extern "C" fn($($t),*) -> $ret> = {
             unsafe extern "C" {
                 #[linkage = "extern_weak"]
+                $(#[link_name = $sym])?
                 static $name: Option<unsafe extern "C" fn($($t),*) -> $ret>;
             }
             #[allow(unused_unsafe)]
@@ -41,9 +42,44 @@ pub(crate) macro weak {
     )
 }
 
-// On non-ELF targets, use the dlsym approximation of weak linkage.
+// On Apple targets, we use `dlsym` instead of real weak linkage, since that requires an Xcode SDK
+// with the item available in `*.tbd` files for the linker, and we support compiling and linking
+// the standard library with older Xcode versions.
 #[cfg(target_vendor = "apple")]
-pub(crate) use self::dlsym as weak;
+pub(crate) use dlsym as weak_impl;
+
+/// Try to use the symbol directly if always available, and fall back to weak linking if not.
+pub(crate) macro maybe_weak {
+    {
+        #[cfg_always_available_on($($cfg:tt)*)]
+        $(#[link_name = $sym:expr])?
+        fn $name:ident($($param:ident : $t:ty),* $(,)?) -> $ret:ty;
+    } => {
+        // If the symbol is known to be available at compile-time, use it directly.
+        #[cfg($($cfg)*)]
+        let $name = {
+            extern "C" {
+                $(#[link_name = $sym])?
+                fn $name($($param : $t),*) -> $ret;
+            }
+            Known($name)
+        };
+
+        // Otherwise it needs to be weakly linked.
+        #[cfg(not($($cfg)*))]
+        weak_impl!(fn $name($($t),*) -> $ret $(, $sym)?);
+    }
+}
+
+/// The function is statically known here.
+pub(crate) struct Known<F>(F);
+
+impl<F: Copy> Known<F> {
+    #[inline]
+    pub(crate) fn get(&self) -> Option<F> {
+        Some(self.0)
+    }
+}
 
 pub(crate) struct ExternWeak<F: Copy> {
     weak_ptr: Option<F>,
@@ -145,7 +181,7 @@ unsafe fn fetch(name: &str) -> *mut libc::c_void {
 pub(crate) macro syscall {
     (fn $name:ident($($arg_name:ident: $t:ty),*) -> $ret:ty) => (
         unsafe fn $name($($arg_name: $t),*) -> $ret {
-            weak! { fn $name($($t),*) -> $ret }
+            weak_impl! { fn $name($($t),*) -> $ret }
 
             if let Some(fun) = $name.get() {
                 fun($($arg_name),*)
@@ -161,7 +197,7 @@ pub(crate) macro syscall {
 pub(crate) macro syscall {
     (fn $name:ident($($arg_name:ident: $t:ty),*) -> $ret:ty) => (
         unsafe fn $name($($arg_name:$t),*) -> $ret {
-            weak! { fn $name($($t),*) -> $ret }
+            weak_impl! { fn $name($($t),*) -> $ret }
 
             // Use a weak symbol from libc when possible, allowing `LD_PRELOAD`
             // interposition, but if it's not found just use a raw syscall.
diff --git a/src/doc/unstable-book/src/language-features/cfg-os-version-min.md b/src/doc/unstable-book/src/language-features/cfg-os-version-min.md
new file mode 100644
index 0000000000000..e2119814271e9
--- /dev/null
+++ b/src/doc/unstable-book/src/language-features/cfg-os-version-min.md
@@ -0,0 +1,76 @@
+# `cfg_os_version_min`
+
+The tracking issue for this feature is: [#136866]
+
+[#136866]: https://github.com/rust-lang/rust/issues/136866
+
+------------------------
+
+The `cfg_os_version_min` feature makes it possible to conditionally compile
+code depending on the target platform version(s).
+
+As `cfg(os_version_min("platform", "version"))` is platform-specific, it
+internally contains relevant `cfg(target_os = "platform")`. So specifying
+`cfg(os_version_min("macos", "1.0"))` is equivalent to
+`cfg(target_os = "macos")`.
+
+Note that this only concerns the compile-time configured version; at runtime,
+the version may be higher.
+
+
+## Changing the version
+
+Each Rust target has a default set of targetted platform versions. Examples
+include the operating system version (`windows`, `macos`, Linux `kernel`) and
+system library version (`libc`).
+
+The mechanism for changing a targetted platform version is currently
+target-specific (a more general mechanism may be created in the future):
+- On macOS, you can select the minimum OS version using the
+  `MACOSX_DEPLOYMENT_TARGET` environment variable. Similarly for iOS, tvOS,
+  watchOS and visionOS, see the relevant [platform docs] for details.
+- Others: Unknown.
+
+[platform docs]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
+
+
+## Examples
+
+Statically link the `preadv` symbol if available, or fall back to weak linking if not.
+
+```rust
+#![feature(cfg_os_version_min)]
+use libc;
+
+// Always available under these conditions.
+#[cfg(any(
+    os_version_min("macos", "11.0"),
+    os_version_min("ios", "14.0"),
+    os_version_min("tvos", "14.0"),
+    os_version_min("watchos", "7.0"),
+    os_version_min("visionos", "1.0")
+))]
+let preadv = {
+    extern "C" {
+        fn preadv(libc::c_int, *const libc::iovec, libc::c_int, libc::off64_t) -> libc::ssize_t;
+    }
+    Some(preadv)
+};
+
+// Otherwise `preadv` needs to be weakly linked.
+// We do that using a `weak!` macro, defined elsewhere.
+#[cfg(not(any(
+    os_version_min("macos", "11.0"),
+    os_version_min("ios", "14.0"),
+    os_version_min("tvos", "14.0"),
+    os_version_min("watchos", "7.0"),
+    os_version_min("visionos", "1.0")
+)))]
+weak!(fn preadv(libc::c_int, *const libc::iovec, libc::c_int, libc::off64_t) -> libc::ssize_t);
+
+if let Some(preadv) = preadv {
+    preadv(...) // Use preadv, it's available
+} else {
+    // ... fallback impl
+}
+```
diff --git a/tests/ui/cfg/cfg-os-version-min-deployment-target.rs b/tests/ui/cfg/cfg-os-version-min-deployment-target.rs
new file mode 100644
index 0000000000000..3a80a2512a998
--- /dev/null
+++ b/tests/ui/cfg/cfg-os-version-min-deployment-target.rs
@@ -0,0 +1,21 @@
+//! Test the semantics of `cfg(os_version_min)` while setting deployment target.
+
+//@ only-macos
+//@ revisions: no_env env_low env_mid env_high
+//@[no_env] unset-rustc-env:MACOSX_DEPLOYMENT_TARGET
+//@[env_low] rustc-env:MACOSX_DEPLOYMENT_TARGET=10.14
+//@[env_mid] rustc-env:MACOSX_DEPLOYMENT_TARGET=14.0
+//@[env_high] rustc-env:MACOSX_DEPLOYMENT_TARGET=17.0
+//@ run-pass
+
+#![feature(cfg_os_version_min)]
+
+fn main() {
+    assert_eq!(cfg!(os_version_min("macos", "14.0")), cfg!(any(env_mid, env_high)));
+
+    // Aarch64 minimum is macOS 11.0, even if a lower env is requested.
+    assert_eq!(
+        cfg!(os_version_min("macos", "11.0")),
+        cfg!(any(env_mid, env_high, target_arch = "aarch64"))
+    );
+}
diff --git a/tests/ui/cfg/cfg-os-version-min-edges.rs b/tests/ui/cfg/cfg-os-version-min-edges.rs
new file mode 100644
index 0000000000000..35dfa85d22871
--- /dev/null
+++ b/tests/ui/cfg/cfg-os-version-min-edges.rs
@@ -0,0 +1,12 @@
+//! Test low and high version with `cfg(os_version_min)`.
+//@ run-pass
+#![feature(cfg_os_version_min)]
+
+fn main() {
+    // Always available on macOS
+    assert_eq!(cfg!(os_version_min("macos", "10.0")), cfg!(target_os = "macos"));
+    //~^ WARNING: version is set unnecessarily low
+
+    // Never available
+    assert!(cfg!(not(os_version_min("macos", "9999.99.99"))));
+}
diff --git a/tests/ui/cfg/cfg-os-version-min-edges.stderr b/tests/ui/cfg/cfg-os-version-min-edges.stderr
new file mode 100644
index 0000000000000..5e0b768cf25f8
--- /dev/null
+++ b/tests/ui/cfg/cfg-os-version-min-edges.stderr
@@ -0,0 +1,8 @@
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.12
+  --> $DIR/cfg-os-version-min-edges.rs:7:45
+   |
+LL |     assert_eq!(cfg!(os_version_min("macos", "10.0")), cfg!(target_os = "macos"));
+   |                                             ^^^^^^
+
+warning: 1 warning emitted
+
diff --git a/tests/ui/cfg/cfg-os-version-min-invalid.rs b/tests/ui/cfg/cfg-os-version-min-invalid.rs
new file mode 100644
index 0000000000000..c6da61e956e66
--- /dev/null
+++ b/tests/ui/cfg/cfg-os-version-min-invalid.rs
@@ -0,0 +1,57 @@
+//! Test invalid syntax for `cfg(os_version_min)`.
+#![feature(cfg_os_version_min)]
+
+#[cfg(os_version_min = "macos")] //~ WARNING: unexpected `cfg` condition name: `os_version_min`
+fn foo() {}
+
+#[cfg(os_version_min("macos"))] //~ ERROR: expected two literals, a platform and a version
+fn foo() {}
+#[cfg(os_version_min("1", "2", "3"))] //~ ERROR: expected two literals, a platform and a version
+fn foo() {}
+
+#[cfg(os_version_min(macos, "1.0.0"))] //~ ERROR: expected a platform literal
+fn foo() {}
+#[cfg(os_version_min(42, "1.0.0"))] //~ ERROR: expected a platform literal
+fn foo() {}
+
+#[cfg(os_version_min("macos", 42))] //~ ERROR: expected a version literal
+fn foo() {}
+#[cfg(os_version_min("macos", 10.10))] //~ ERROR: expected a version literal
+fn foo() {}
+#[cfg(os_version_min("macos", false))] //~ ERROR: expected a version literal
+fn foo() {}
+
+#[cfg(os_version_min("11.0", "macos"))] //~ WARNING: unknown platform literal
+fn foo() {}
+#[cfg(os_version_min("linux", "5.3"))] //~ WARNING: unknown platform literal
+fn foo() {}
+
+#[cfg(os_version_min("windows", "10.0.10240"))] //~ ERROR: unimplemented platform
+fn foo() {}
+
+#[cfg(os_version_min("macos", "99999"))] //~ ERROR: failed parsing version
+fn foo() {}
+#[cfg(os_version_min("macos", "-1"))] //~ ERROR: failed parsing version
+fn foo() {}
+#[cfg(os_version_min("macos", "65536"))] //~ ERROR: failed parsing version
+fn foo() {}
+#[cfg(os_version_min("macos", "1.2.3.4"))] //~ ERROR: failed parsing version
+fn foo() {}
+
+#[cfg(os_version_min("macos", "10.0"))] //~ WARNING: version is set unnecessarily low
+fn bar1() {}
+#[cfg(os_version_min("macos", "0"))] //~ WARNING: version is set unnecessarily low
+fn bar2() {}
+
+#[cfg(os_version_min("macos", "10.12"))] //~ WARNING: version is set unnecessarily low
+fn bar3() {}
+#[cfg(os_version_min("ios", "10.0"))] //~ WARNING: version is set unnecessarily low
+fn bar4() {}
+#[cfg(os_version_min("tvos", "10.0"))] //~ WARNING: version is set unnecessarily low
+fn bar5() {}
+#[cfg(os_version_min("watchos", "5.0"))] //~ WARNING: version is set unnecessarily low
+fn bar6() {}
+#[cfg(os_version_min("visionos", "1.0"))] //~ WARNING: version is set unnecessarily low
+fn bar7() {}
+
+fn main() {}
diff --git a/tests/ui/cfg/cfg-os-version-min-invalid.stderr b/tests/ui/cfg/cfg-os-version-min-invalid.stderr
new file mode 100644
index 0000000000000..7ef143993597e
--- /dev/null
+++ b/tests/ui/cfg/cfg-os-version-min-invalid.stderr
@@ -0,0 +1,139 @@
+error: expected two literals, a platform and a version
+  --> $DIR/cfg-os-version-min-invalid.rs:7:7
+   |
+LL | #[cfg(os_version_min("macos"))]
+   |       ^^^^^^^^^^^^^^^^^^^^^^^
+
+error: expected two literals, a platform and a version
+  --> $DIR/cfg-os-version-min-invalid.rs:9:7
+   |
+LL | #[cfg(os_version_min("1", "2", "3"))]
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: expected a platform literal
+  --> $DIR/cfg-os-version-min-invalid.rs:12:22
+   |
+LL | #[cfg(os_version_min(macos, "1.0.0"))]
+   |                      ^^^^^
+
+error: expected a platform literal
+  --> $DIR/cfg-os-version-min-invalid.rs:14:22
+   |
+LL | #[cfg(os_version_min(42, "1.0.0"))]
+   |                      ^^
+
+error: expected a version literal
+  --> $DIR/cfg-os-version-min-invalid.rs:17:31
+   |
+LL | #[cfg(os_version_min("macos", 42))]
+   |                               ^^
+
+error: expected a version literal
+  --> $DIR/cfg-os-version-min-invalid.rs:19:31
+   |
+LL | #[cfg(os_version_min("macos", 10.10))]
+   |                               ^^^^^
+
+error: expected a version literal
+  --> $DIR/cfg-os-version-min-invalid.rs:21:31
+   |
+LL | #[cfg(os_version_min("macos", false))]
+   |                               ^^^^^
+
+warning: unknown platform literal, expected values are: `macos`, `ios`, `tvos`, `watchos`, and `visionos`
+  --> $DIR/cfg-os-version-min-invalid.rs:24:22
+   |
+LL | #[cfg(os_version_min("11.0", "macos"))]
+   |                      ^^^^^^
+
+warning: unknown platform literal, expected values are: `macos`, `ios`, `tvos`, `watchos`, and `visionos`
+  --> $DIR/cfg-os-version-min-invalid.rs:26:22
+   |
+LL | #[cfg(os_version_min("linux", "5.3"))]
+   |                      ^^^^^^^
+
+error: unimplemented platform
+  --> $DIR/cfg-os-version-min-invalid.rs:29:22
+   |
+LL | #[cfg(os_version_min("windows", "10.0.10240"))]
+   |                      ^^^^^^^^^
+
+error: failed parsing version: number too large to fit in target type
+  --> $DIR/cfg-os-version-min-invalid.rs:32:31
+   |
+LL | #[cfg(os_version_min("macos", "99999"))]
+   |                               ^^^^^^^
+
+error: failed parsing version: invalid digit found in string
+  --> $DIR/cfg-os-version-min-invalid.rs:34:31
+   |
+LL | #[cfg(os_version_min("macos", "-1"))]
+   |                               ^^^^
+
+error: failed parsing version: number too large to fit in target type
+  --> $DIR/cfg-os-version-min-invalid.rs:36:31
+   |
+LL | #[cfg(os_version_min("macos", "65536"))]
+   |                               ^^^^^^^
+
+error: failed parsing version: invalid digit found in string
+  --> $DIR/cfg-os-version-min-invalid.rs:38:31
+   |
+LL | #[cfg(os_version_min("macos", "1.2.3.4"))]
+   |                               ^^^^^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.12
+  --> $DIR/cfg-os-version-min-invalid.rs:41:31
+   |
+LL | #[cfg(os_version_min("macos", "10.0"))]
+   |                               ^^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.12
+  --> $DIR/cfg-os-version-min-invalid.rs:43:31
+   |
+LL | #[cfg(os_version_min("macos", "0"))]
+   |                               ^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.12
+  --> $DIR/cfg-os-version-min-invalid.rs:46:31
+   |
+LL | #[cfg(os_version_min("macos", "10.12"))]
+   |                               ^^^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.0
+  --> $DIR/cfg-os-version-min-invalid.rs:48:29
+   |
+LL | #[cfg(os_version_min("ios", "10.0"))]
+   |                             ^^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 10.0
+  --> $DIR/cfg-os-version-min-invalid.rs:50:30
+   |
+LL | #[cfg(os_version_min("tvos", "10.0"))]
+   |                              ^^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 5.0
+  --> $DIR/cfg-os-version-min-invalid.rs:52:33
+   |
+LL | #[cfg(os_version_min("watchos", "5.0"))]
+   |                                 ^^^^^
+
+warning: version is set unnecessarily low, the minimum supported by Rust on this platform is 1.0
+  --> $DIR/cfg-os-version-min-invalid.rs:54:34
+   |
+LL | #[cfg(os_version_min("visionos", "1.0"))]
+   |                                  ^^^^^
+
+warning: unexpected `cfg` condition name: `os_version_min`
+  --> $DIR/cfg-os-version-min-invalid.rs:4:7
+   |
+LL | #[cfg(os_version_min = "macos")]
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: expected names are: `FALSE` and `test` and 31 more
+   = help: to expect this configuration use `--check-cfg=cfg(os_version_min, values("macos"))`
+   = note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg.html> for more information about checking conditional configuration
+   = note: `#[warn(unexpected_cfgs)]` on by default
+
+error: aborting due to 12 previous errors; 10 warnings emitted
+
diff --git a/tests/ui/feature-gates/feature-gate-cfg-os-version-min.rs b/tests/ui/feature-gates/feature-gate-cfg-os-version-min.rs
new file mode 100644
index 0000000000000..f14d6cb985601
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-cfg-os-version-min.rs
@@ -0,0 +1,18 @@
+#![feature(cfg_boolean_literals)]
+
+#[cfg(os_version_min("macos", "11.0"))]
+//~^ ERROR `cfg(os_version_min)` is experimental and subject to change
+fn foo1() {}
+
+#[cfg(os_version_min("macos", 1.20))] //~ ERROR: expected a version literal
+//~^ ERROR `cfg(os_version_min)` is experimental and subject to change
+fn foo2() {}
+
+// No warning if cfg'd away.
+#[cfg_attr(false, cfg(os_version_min("macos", false)))]
+fn foo3() {}
+
+fn main() {
+    if cfg!(os_version_min("macos", "11.0")) {}
+    //~^ ERROR `cfg(os_version_min)` is experimental and subject to change
+}
diff --git a/tests/ui/feature-gates/feature-gate-cfg-os-version-min.stderr b/tests/ui/feature-gates/feature-gate-cfg-os-version-min.stderr
new file mode 100644
index 0000000000000..9f2b738e2b4e2
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-cfg-os-version-min.stderr
@@ -0,0 +1,36 @@
+error[E0658]: `cfg(os_version_min)` is experimental and subject to change
+  --> $DIR/feature-gate-cfg-os-version-min.rs:3:7
+   |
+LL | #[cfg(os_version_min("macos", "11.0"))]
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: add `#![feature(cfg_os_version_min)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error[E0658]: `cfg(os_version_min)` is experimental and subject to change
+  --> $DIR/feature-gate-cfg-os-version-min.rs:7:7
+   |
+LL | #[cfg(os_version_min("macos", 1.20))]
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: add `#![feature(cfg_os_version_min)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: expected a version literal
+  --> $DIR/feature-gate-cfg-os-version-min.rs:7:31
+   |
+LL | #[cfg(os_version_min("macos", 1.20))]
+   |                               ^^^^
+
+error[E0658]: `cfg(os_version_min)` is experimental and subject to change
+  --> $DIR/feature-gate-cfg-os-version-min.rs:16:13
+   |
+LL |     if cfg!(os_version_min("macos", "11.0")) {}
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: add `#![feature(cfg_os_version_min)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 4 previous errors
+
+For more information about this error, try `rustc --explain E0658`.