diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 9dcedaa13f661..dbb66797eee29 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -347,6 +347,7 @@ #![feature(hasher_prefixfree_extras)] #![feature(hashmap_internals)] #![feature(hint_must_use)] +#![feature(int_from_ascii)] #![feature(ip)] #![feature(lazy_get)] #![feature(maybe_uninit_slice)] @@ -363,6 +364,7 @@ #![feature(slice_internals)] #![feature(slice_ptr_get)] #![feature(slice_range)] +#![feature(slice_split_once)] #![feature(std_internals)] #![feature(str_internals)] #![feature(strict_provenance_atomic_ptr)] diff --git a/library/std/src/sys/mod.rs b/library/std/src/sys/mod.rs index f0cfb9b277366..a79d9019c390b 100644 --- a/library/std/src/sys/mod.rs +++ b/library/std/src/sys/mod.rs @@ -17,6 +17,7 @@ pub mod io; pub mod net; pub mod os_str; pub mod path; +pub mod platform_version; pub mod process; pub mod random; pub mod stdio; diff --git a/library/std/src/sys/platform_version/darwin/mod.rs b/library/std/src/sys/platform_version/darwin/mod.rs new file mode 100644 index 0000000000000..58293de02cd7f --- /dev/null +++ b/library/std/src/sys/platform_version/darwin/mod.rs @@ -0,0 +1,446 @@ +use crate::ffi::{CStr, c_char, c_void}; +use crate::num::{NonZero, ParseIntError}; +use crate::path::{Path, PathBuf}; +use crate::ptr::null_mut; +use crate::sync::atomic::{AtomicU32, Ordering}; +use crate::sys::common::small_c_string::run_path_with_cstr; +use crate::{env, fs}; + +mod public_extern; +#[cfg(test)] +mod tests; + +/// The version of the operating system. +/// +/// We use a packed u32 here to allow for fast comparisons and to match Mach-O's `LC_BUILD_VERSION`. +type OSVersion = u32; + +/// Combine parts of a version into an [`OSVersion`]. +/// +/// The size of the parts are inherently limited by Mach-O's `LC_BUILD_VERSION`. +#[inline] +const fn pack_os_version(major: u16, minor: u8, patch: u8) -> OSVersion { + let (major, minor, patch) = (major as u32, minor as u32, patch as u32); + (major << 16) | (minor << 8) | patch +} + +/// Get the current OS version, packed according to [`pack_os_version`]. +/// +/// # Semantics +/// +/// The reported version on macOS might be 10.16 if the SDK version of the binary is less than 11.0. +/// This is a workaround that Apple implemented to handle applications that assumed that macOS +/// versions would always start with "10", see: +/// +/// +/// It _is_ possible to get the real version regardless of the SDK version of the binary, this is +/// what Zig does: +/// +/// +/// We choose to not do that, and instead follow Apple's behaviour here, and return 10.16 when +/// compiled with an older SDK; the user should instead upgrade their tooling. +/// +/// NOTE: `rustc` currently doesn't set the right SDK version when linking with ld64, so this will +/// have the wrong behaviour with `-Clinker=ld` on x86_64. But that's a `rustc` bug: +/// +#[inline] +fn current_version() -> OSVersion { + // Cache the lookup for performance. + // + // 0.0.0 is never going to be a valid version ("vtool" reports "n/a" on 0 versions), so we use + // that as our sentinel value. + static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0); + + // We use relaxed atomics instead of e.g. a `Once`, it doesn't matter if multiple threads end up + // racing to read or write the version, `lookup_version` should be idempotent and always return + // the same value. + // + // `compiler-rt` uses `dispatch_once`, but that's overkill for the reasons above. + let version = CURRENT_VERSION.load(Ordering::Relaxed); + if version == 0 { + let version = lookup_version().get(); + CURRENT_VERSION.store(version, Ordering::Relaxed); + version + } else { + version + } +} + +/// Look up the os version. +/// +/// # Aborts +/// +/// Aborts if reading or parsing the version fails (or if the system was out of memory). +/// +/// We deliberately choose to abort, as having this silently return an invalid OS version would be +/// impossible for a user to debug. +// The lookup is costly and should be on the cold path because of the cache in `current_version`. +#[cold] +// Micro-optimization: We use `extern "C"` to abort on panic, allowing `current_version` (inlined) +// to be free of unwind handling. Aborting is required for `__isPlatformVersionAtLeast` anyhow. +extern "C" fn lookup_version() -> NonZero { + // Try to read from `sysctl` first (faster), but if that fails, fall back to reading the + // property list (this is roughly what `_availability_version_check` does internally). + let version = version_from_sysctl().unwrap_or_else(version_from_plist); + + // Use `NonZero` to try to make it clearer to the optimizer that this will never return 0. + NonZero::new(version).expect("version cannot be 0.0.0") +} + +/// Read the version from `kern.osproductversion` or `kern.iossupportversion`. +/// +/// This is faster than `version_from_plist`, since it doesn't need to invoke `dlsym`. +fn version_from_sysctl() -> Option { + // This won't work in the simulator, as `kern.osproductversion` returns the host macOS version, + // and `kern.iossupportversion` returns the host macOS' iOSSupportVersion (while you can run + // simulators with many different iOS versions). + if cfg!(target_abi = "sim") { + // Fall back to `version_from_plist` on these targets. + return None; + } + + let sysctl_version = |name: &CStr| { + let mut buf: [u8; 32] = [0; 32]; + let mut size = buf.len(); + let ptr = buf.as_mut_ptr().cast(); + let ret = unsafe { libc::sysctlbyname(name.as_ptr(), ptr, &mut size, null_mut(), 0) }; + if ret != 0 { + // This sysctl is not available. + return None; + } + let buf = &buf[..(size - 1)]; + + if buf.is_empty() { + // The buffer may be empty when using `kern.iossupportversion` on an actual iOS device, + // or on visionOS when running under "Designed for iPad". + // + // In that case, fall back to `kern.osproductversion`. + return None; + } + + Some(parse_os_version(buf).expect("failed parsing version from sysctl")) + }; + + // When `target_os = "ios"`, we may be in many different states: + // - Native iOS device. + // - iOS Simulator. + // - Mac Catalyst. + // - Mac + "Designed for iPad". + // - Native visionOS device + "Designed for iPad". + // - visionOS simulator + "Designed for iPad". + // + // Of these, only native, Mac Catalyst and simulators can be differentiated at compile-time + // (with `target_abi = ""`, `target_abi = "macabi"` and `target_abi = "sim"` respectively). + // + // That is, "Designed for iPad" will act as iOS at compile-time, but the `ProductVersion` will + // still be the host macOS or visionOS version. + // + // Furthermore, we can't even reliably differentiate between these at runtime, since + // `dyld_get_active_platform` isn't publicly available. + // + // Fortunately, we won't need to know any of that; we can simply attempt to get the + // `iOSSupportVersion` (which may be set on native iOS too, but then it will be set to the host + // iOS version), and if that fails, fall back to the `ProductVersion`. + if cfg!(target_os = "ios") { + // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2077-L2100 + if let Some(ios_support_version) = sysctl_version(c"kern.iossupportversion") { + return Some(ios_support_version); + } + + // On Mac Catalyst, if we failed looking up `iOSSupportVersion`, we don't want to + // accidentally fall back to `ProductVersion`. + if cfg!(target_abi = "macabi") { + return None; + } + } + + // Introduced in macOS 10.13.4. + // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2015-L2051 + sysctl_version(c"kern.osproductversion") +} + +/// Look up the current OS version(s) from `/System/Library/CoreServices/SystemVersion.plist`. +/// +/// More specifically, from the `ProductVersion` and `iOSSupportVersion` keys, and from +/// `$IPHONE_SIMULATOR_ROOT/System/Library/CoreServices/SystemVersion.plist` on the simulator. +/// +/// This file was introduced in macOS 10.3, which is well below the minimum supported version by +/// `rustc`, which is (at the time of writing) macOS 10.12. +/// +/// # Implementation +/// +/// We do roughly the same thing in here as `compiler-rt`, and dynamically look up CoreFoundation +/// utilities for parsing PLists (to avoid having to re-implement that in here, as pulling in a full +/// PList parser into `std` seems costly). +/// +/// If this is found to be undesirable, we _could_ possibly hack it by parsing the PList manually +/// (it seems to use the plain-text "xml1" encoding/format in all versions), but that seems brittle. +fn version_from_plist() -> OSVersion { + // The root directory relative to where all files are located. + let root = if cfg!(target_abi = "sim") { + PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").expect( + "environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator", + )) + } else { + PathBuf::from("/") + }; + + // Read `SystemVersion.plist`. Always present on Apple platforms, reading it cannot fail. + let path = root.join("System/Library/CoreServices/SystemVersion.plist"); + let plist_buffer = fs::read(&path).unwrap_or_else(|e| panic!("failed reading {path:?}: {e}")); + parse_version_from_plist(&root, &plist_buffer) +} + +/// Split out from [`version_from_plist`] to allow for testing. +#[allow(non_upper_case_globals, non_snake_case)] +fn parse_version_from_plist(root: &Path, plist_buffer: &[u8]) -> OSVersion { + // Link to the CoreFoundation dylib, and look up symbols from that. + // We explicitly use non-versioned path here, to allow this to work on older iOS devices. + let cf_path = root.join("System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"); + + let cf_handle = run_path_with_cstr(&cf_path, &|path| unsafe { + Ok(libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL)) + }) + .expect("failed allocating string"); + if cf_handle.is_null() { + let err = unsafe { CStr::from_ptr(libc::dlerror()) }; + panic!("could not open CoreFoundation.framework: {err:?}"); + } + let _cf_handle_free = Deferred(|| { + // Ignore errors when closing. This is also what `libloading` does: + // https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374 + let _ = unsafe { libc::dlclose(cf_handle) }; + }); + + macro_rules! dlsym { + ( + unsafe fn $name:ident($($param:ident: $param_ty:ty),* $(,)?) $(-> $ret:ty)?; + ) => {{ + let ptr = unsafe { + libc::dlsym( + cf_handle, + concat!(stringify!($name), '\0').as_bytes().as_ptr().cast(), + ) + }; + if ptr.is_null() { + let err = unsafe { CStr::from_ptr(libc::dlerror()) }; + panic!("could not find function {}: {err:?}", stringify!($name)); + } + // SAFETY: Just checked that the symbol isn't NULL, and caller verifies that the + // signature is correct. + unsafe { + crate::mem::transmute::< + *mut c_void, + unsafe extern "C" fn($($param_ty),*) $(-> $ret)?, + >(ptr) + } + }}; + } + + // MacTypes.h + type Boolean = u8; + // CoreFoundation/CFBase.h + type CFTypeID = usize; + type CFOptionFlags = usize; + type CFIndex = isize; + type CFTypeRef = *mut c_void; + type CFAllocatorRef = CFTypeRef; + const kCFAllocatorDefault: CFAllocatorRef = null_mut(); + // Available: in all CF versions. + let allocator_null = unsafe { libc::dlsym(cf_handle, c"kCFAllocatorNull".as_ptr()) }; + if allocator_null.is_null() { + let err = unsafe { CStr::from_ptr(libc::dlerror()) }; + panic!("could not find kCFAllocatorNull: {err:?}"); + } + let kCFAllocatorNull = unsafe { *allocator_null.cast::() }; + let CFRelease = dlsym!( + // Available: in all CF versions. + unsafe fn CFRelease(cf: CFTypeRef); + ); + let CFGetTypeID = dlsym!( + // Available: in all CF versions. + unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID; + ); + // CoreFoundation/CFError.h + type CFErrorRef = CFTypeRef; + // CoreFoundation/CFData.h + type CFDataRef = CFTypeRef; + let CFDataCreateWithBytesNoCopy = dlsym!( + // Available: in all CF versions. + unsafe fn CFDataCreateWithBytesNoCopy( + allocator: CFAllocatorRef, + bytes: *const u8, + length: CFIndex, + bytes_deallocator: CFAllocatorRef, + ) -> CFDataRef; + ); + // CoreFoundation/CFPropertyList.h + const kCFPropertyListImmutable: CFOptionFlags = 0; + type CFPropertyListFormat = CFIndex; + type CFPropertyListRef = CFTypeRef; + let CFPropertyListCreateWithData = dlsym!( + // Available: since macOS 10.6. + unsafe fn CFPropertyListCreateWithData( + allocator: CFAllocatorRef, + data: CFDataRef, + options: CFOptionFlags, + format: *mut CFPropertyListFormat, + error: *mut CFErrorRef, + ) -> CFPropertyListRef; + ); + // CoreFoundation/CFString.h + type CFStringRef = CFTypeRef; + type CFStringEncoding = u32; + const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100; + let CFStringGetTypeID = dlsym!( + // Available: in all CF versions. + unsafe fn CFStringGetTypeID() -> CFTypeID; + ); + let CFStringCreateWithCStringNoCopy = dlsym!( + // Available: in all CF versions. + unsafe fn CFStringCreateWithCStringNoCopy( + alloc: CFAllocatorRef, + c_str: *const c_char, + encoding: CFStringEncoding, + contents_deallocator: CFAllocatorRef, + ) -> CFStringRef; + ); + let CFStringGetCString = dlsym!( + // Available: in all CF versions. + unsafe fn CFStringGetCString( + the_string: CFStringRef, + buffer: *mut c_char, + buffer_size: CFIndex, + encoding: CFStringEncoding, + ) -> Boolean; + ); + // CoreFoundation/CFDictionary.h + type CFDictionaryRef = CFTypeRef; + let CFDictionaryGetTypeID = dlsym!( + // Available: in all CF versions. + unsafe fn CFDictionaryGetTypeID() -> CFTypeID; + ); + let CFDictionaryGetValue = dlsym!( + // Available: in all CF versions. + unsafe fn CFDictionaryGetValue( + the_dict: CFDictionaryRef, + key: *const c_void, + ) -> *const c_void; + ); + + // MARK: Done declaring symbols. + + let plist_data = unsafe { + CFDataCreateWithBytesNoCopy( + kCFAllocatorDefault, + plist_buffer.as_ptr(), + plist_buffer.len() as CFIndex, + kCFAllocatorNull, + ) + }; + assert!(!plist_data.is_null(), "failed creating data"); + let _plist_data_release = Deferred(|| unsafe { CFRelease(plist_data) }); + + let plist = unsafe { + CFPropertyListCreateWithData( + kCFAllocatorDefault, + plist_data, + kCFPropertyListImmutable, + null_mut(), // Don't care about the format of the PList. + null_mut(), // Don't care about the error data. + ) + }; + assert!(!plist.is_null(), "failed reading PList in SystemVersion.plist"); + let _plist_release = Deferred(|| unsafe { CFRelease(plist) }); + + assert_eq!( + unsafe { CFGetTypeID(plist) }, + unsafe { CFDictionaryGetTypeID() }, + "SystemVersion.plist did not contain a dictionary at the top level" + ); + let plist = plist as CFDictionaryRef; + + let get_string_key = |plist, lookup_key: &CStr| { + let cf_lookup_key = unsafe { + CFStringCreateWithCStringNoCopy( + kCFAllocatorDefault, + lookup_key.as_ptr(), + kCFStringEncodingUTF8, + kCFAllocatorNull, + ) + }; + assert!(!cf_lookup_key.is_null(), "failed creating CFString"); + let _lookup_key_release = Deferred(|| unsafe { CFRelease(cf_lookup_key) }); + + let value = unsafe { CFDictionaryGetValue(plist, cf_lookup_key) as CFTypeRef }; + // ^ getter, so don't release. + if value.is_null() { + return None; + } + + assert_eq!( + unsafe { CFGetTypeID(value) }, + unsafe { CFStringGetTypeID() }, + "key in SystemVersion.plist must be a string" + ); + let value = value as CFStringRef; + + let mut version_str = [0u8; 32]; + let ret = unsafe { + CFStringGetCString( + value, + version_str.as_mut_ptr().cast::(), + version_str.len() as CFIndex, + kCFStringEncodingUTF8, + ) + }; + assert_ne!(ret, 0, "failed getting string from CFString"); + + let version_str = + CStr::from_bytes_until_nul(&version_str).expect("failed converting to CStr"); + + Some(parse_os_version(version_str.to_bytes()).expect("failed parsing version")) + }; + + // Same logic as in `version_from_sysctl`. + if cfg!(target_os = "ios") { + if let Some(ios_support_version) = get_string_key(plist, c"iOSSupportVersion") { + return ios_support_version; + } + + // Force Mac Catalyst to use iOSSupportVersion (do not fall back to ProductVersion). + if cfg!(target_abi = "macabi") { + panic!("expected iOSSupportVersion in SystemVersion.plist"); + } + } + + // On all other platforms, we can find the OS version by simply looking at `ProductVersion`. + get_string_key(plist, c"ProductVersion") + .expect("expected ProductVersion in SystemVersion.plist") +} + +/// Parse an OS version from a bytestring like b"10.1" or b"14.3.7". +fn parse_os_version(version: &[u8]) -> Result { + if let Some((major, minor)) = version.split_once(|&b| b == b'.') { + let major = u16::from_ascii(major)?; + if let Some((minor, patch)) = minor.split_once(|&b| b == b'.') { + let minor = u8::from_ascii(minor)?; + let patch = u8::from_ascii(patch)?; + Ok(pack_os_version(major, minor, patch)) + } else { + let minor = u8::from_ascii(minor)?; + Ok(pack_os_version(major, minor, 0)) + } + } else { + let major = u16::from_ascii(version)?; + Ok(pack_os_version(major, 0, 0)) + } +} + +struct Deferred(F); + +impl Drop for Deferred { + fn drop(&mut self) { + (self.0)(); + } +} diff --git a/library/std/src/sys/platform_version/darwin/public_extern.rs b/library/std/src/sys/platform_version/darwin/public_extern.rs new file mode 100644 index 0000000000000..6a69687f48f46 --- /dev/null +++ b/library/std/src/sys/platform_version/darwin/public_extern.rs @@ -0,0 +1,157 @@ +//! # Runtime version checking ABI for other compilers. +//! +//! The symbols in this file are useful for us to expose to allow linking code written in the +//! following languages when using their version checking functionality: +//! - Clang's `__builtin_available` macro. +//! - Objective-C's `@available`. +//! - Swift's `#available`, +//! +//! Without Rust exposing these symbols, the user would encounter a linker error when linking to +//! C/Objective-C/Swift libraries using these features. +//! +//! The presence of these are mostly considered a quality-of-implementation detail, and should not +//! be relied upon to be available. +//! +//! ## Background +//! +//! The original discussion of this feature can be found at: +//! - +//! - +//! - +//! +//! And the upstream implementation of these can be found in `compiler-rt`: +//! +//! +//! Ideally, these symbols should probably have been a part of Apple's `libSystem.dylib`, both +//! because their implementation is quite complex, using allocation, environment variables, file +//! access and dynamic library loading (and emitting all of this into every binary). +//! +//! The reason why Apple chose to not do that originally is lost to the sands of time, but a good +//! reason would be that implementing it as part of `compiler-rt` allowed them to back-deploy this +//! to older OSes immediately. +//! +//! In Rust's case, while we may provide a feature similar to `@available` in the future, we will +//! probably do so as a macro exposed by `std` (and not as a compiler builtin). So implementing this +//! in `std` makes sense, since then we can implement it using `std` utilities, and we can avoid +//! having `compiler-builtins` depend on `libSystem.dylib`. +//! +//! This does mean that users that attempt to link C/Objective-C/Swift code _and_ use `#![no_std]` +//! in all their crates may get a linker error because these symbols are missing. Using `no_std` is +//! quite uncommon on Apple systems though, so it's probably fine to not support this use-case. +//! +//! The workaround would be to link `libclang_rt.osx.a` or otherwise use Clang's `compiler-rt`. +//! +//! See also discussion in . +//! +//! ## Implementation details +//! +//! NOTE: Since macOS 10.15, `libSystem.dylib` _has_ actually provided the undocumented +//! `_availability_version_check` via `libxpc` for doing the version lookup (zippered, which is why +//! it requires a platform parameter to differentiate between macOS and Mac Catalyst), though its +//! usage may be a bit dangerous, see: +//! - https://reviews.llvm.org/D150397 +//! - https://github.com/llvm/llvm-project/issues/64227 +//! +//! Besides, we'd need to implement the version lookup via. PList to support older versions anyhow, +//! so we might as well use that everywhere (since it can also be optimized more after inlining). + +#![allow(non_snake_case)] + +use super::{OSVersion, current_version, pack_os_version}; + +/// Whether the current platform's OS version is higher than or equal to the given version. +/// +/// The first argument is the _base_ Mach-O platform (i.e. `PLATFORM_MACOS`, `PLATFORM_IOS`, etc., +/// but not `PLATFORM_IOSSIMULATOR` or `PLATFORM_MACCATALYST`) of the invoking binary. +/// +/// Arguments are specified statically by Clang. Inlining with LTO should allow the versions to be +/// combined into a single `u32`, which should make comparisons faster, and should make the +/// `BASE_TARGET_PLATFORM` check a no-op. +// +// SAFETY: The signature is the same as what Clang expects, and we export weakly to allow linking +// both this and `libclang_rt.*.a`, similar to how `compiler-builtins` does it: +// https://github.com/rust-lang/compiler-builtins/blob/0.1.113/src/macros.rs#L494 +#[cfg_attr(not(feature = "compiler-builtins-mangled-names"), unsafe(no_mangle), linkage = "weak")] +// extern "C" is correct, Clang assumes the function cannot unwind: +// https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/clang/lib/CodeGen/CGObjC.cpp#L3980 +// +// If an error happens in this, we instead abort the process. +pub(super) extern "C" fn __isPlatformVersionAtLeast( + platform: i32, + major: u32, + minor: u32, + subminor: u32, +) -> i32 { + let version = pack_u32_os_version(major, minor, subminor); + + // Mac Catalyst is a technology that allows macOS to run in a different "mode" that closely + // resembles iOS (and has iOS libraries like UIKit available). + // + // (Apple has added a "Designed for iPad" mode later on that allows running iOS apps + // natively, but we don't need to think too much about those, since they link to + // iOS-specific system binaries as well). + // + // To support Mac Catalyst, Apple added the concept of a "zippered" binary, which is a single + // binary that can be run on both macOS and Mac Catalyst (has two `LC_BUILD_VERSION` Mach-O + // commands, one set to `PLATFORM_MACOS` and one to `PLATFORM_MACCATALYST`). + // + // Most system libraries are zippered, which allows re-use across macOS and Mac Catalyst. + // This includes the `libclang_rt.osx.a` shipped with Xcode! This means that `compiler-rt` + // can't statically know whether it's compiled for macOS or Mac Catalyst, and thus this new + // API (which replaces `__isOSVersionAtLeast`) is needed. + // + // In short: + // normal binary calls normal compiler-rt --> `__isOSVersionAtLeast` was enough + // normal binary calls zippered compiler-rt --> `__isPlatformVersionAtLeast` required + // zippered binary calls zippered compiler-rt --> `__isPlatformOrVariantPlatformVersionAtLeast` called + + // FIXME(madsmtm): `rustc` doesn't support zippered binaries yet, see rust-lang/rust#131216. + // But once it does, we need the pre-compiled `std` shipped with rustup to be zippered, and thus + // we also need to handle the `platform` difference here: + // + // if cfg!(target_os = "macos") && platform == 2 /* PLATFORM_IOS */ && cfg!(zippered) { + // return (version.to_u32() <= current_ios_version()) as i32; + // } + // + // `__isPlatformOrVariantPlatformVersionAtLeast` would also need to be implemented. + + // The base Mach-O platform for the current target. + const BASE_TARGET_PLATFORM: i32 = if cfg!(target_os = "macos") { + 1 // PLATFORM_MACOS + } else if cfg!(target_os = "ios") { + 2 // PLATFORM_IOS + } else if cfg!(target_os = "tvos") { + 3 // PLATFORM_TVOS + } else if cfg!(target_os = "watchos") { + 4 // PLATFORM_WATCHOS + } else if cfg!(target_os = "visionos") { + 11 // PLATFORM_VISIONOS + } else { + 0 // PLATFORM_UNKNOWN + }; + debug_assert_eq!( + platform, BASE_TARGET_PLATFORM, + "invalid platform provided to __isPlatformVersionAtLeast", + ); + + (version <= current_version()) as i32 +} + +/// Old entry point for availability. Used when compiling with older Clang versions. +// SAFETY: Same as for `__isPlatformVersionAtLeast`. +#[cfg_attr(not(feature = "compiler-builtins-mangled-names"), unsafe(no_mangle), linkage = "weak")] +pub(super) extern "C" fn __isOSVersionAtLeast(major: u32, minor: u32, subminor: u32) -> i32 { + let version = pack_u32_os_version(major, minor, subminor); + (version <= current_version()) as i32 +} + +/// [`pack_os_version`], but takes `u32` and saturates. +/// +/// Instead of using e.g. `major as u16`, which truncates. +#[inline] +fn pack_u32_os_version(major: u32, minor: u32, patch: u32) -> OSVersion { + let major: u16 = major.try_into().unwrap_or(u16::MAX); + let minor: u8 = minor.try_into().unwrap_or(u8::MAX); + let patch: u8 = patch.try_into().unwrap_or(u8::MAX); + pack_os_version(major, minor, patch) +} diff --git a/library/std/src/sys/platform_version/darwin/tests.rs b/library/std/src/sys/platform_version/darwin/tests.rs new file mode 100644 index 0000000000000..0c1e4adbaa103 --- /dev/null +++ b/library/std/src/sys/platform_version/darwin/tests.rs @@ -0,0 +1,390 @@ +use super::public_extern::*; +use super::*; +use crate::process::Command; + +#[test] +fn test_general_available() { + // Lowest version always available. + assert_eq!(__isOSVersionAtLeast(0, 0, 0), 1); + // This high version never available. + assert_eq!(__isOSVersionAtLeast(9999, 99, 99), 0); +} + +#[test] +fn test_saturating() { + // Higher version than supported by OSVersion -> make sure we saturate. + assert_eq!(__isOSVersionAtLeast(0x10000, 0, 0), 0); +} + +#[test] +#[cfg_attr(not(target_os = "macos"), ignore = "`sw_vers` is only available on host macOS")] +fn compare_against_sw_vers() { + let sw_vers = Command::new("sw_vers").arg("-productVersion").output().unwrap().stdout; + let sw_vers = String::from_utf8(sw_vers).unwrap(); + let mut sw_vers = sw_vers.trim().split('.'); + + let major: u32 = sw_vers.next().unwrap().parse().unwrap(); + let minor: u32 = sw_vers.next().unwrap_or("0").parse().unwrap(); + let subminor: u32 = sw_vers.next().unwrap_or("0").parse().unwrap(); + assert_eq!(sw_vers.count(), 0); + + // Current version is available + assert_eq!(__isOSVersionAtLeast(major, minor, subminor), 1); + + // One lower is available + assert_eq!(__isOSVersionAtLeast(major, minor, subminor.saturating_sub(1)), 1); + assert_eq!(__isOSVersionAtLeast(major, minor.saturating_sub(1), subminor), 1); + assert_eq!(__isOSVersionAtLeast(major.saturating_sub(1), minor, subminor), 1); + + // One higher isn't available + assert_eq!(__isOSVersionAtLeast(major, minor, subminor + 1), 0); + assert_eq!(__isOSVersionAtLeast(major, minor + 1, subminor), 0); + assert_eq!(__isOSVersionAtLeast(major + 1, minor, subminor), 0); + + // Test directly against the lookup + assert_eq!(lookup_version().get(), pack_os_version(major as _, minor as _, subminor as _)); +} + +#[test] +fn sysctl_same_as_in_plist() { + if let Some(version) = version_from_sysctl() { + assert_eq!(version, version_from_plist()); + } +} + +#[test] +fn lookup_idempotent() { + let version = lookup_version(); + for _ in 0..10 { + assert_eq!(version, lookup_version()); + } +} + +/// Test parsing a bunch of different PLists found in the wild, to ensure that +/// if `rustc` decides to parse it without CoreFoundation in the future, that +/// it would continue to work, even on older platforms. +#[test] +fn parse_plist() { + #[track_caller] + fn check( + (major, minor, patch): (u16, u8, u8), + ios_version: Option<(u16, u8, u8)>, + plist: &str, + ) { + let root = if cfg!(target_abi = "sim") { + PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").unwrap()) + } else { + PathBuf::from("/") + }; + let expected = if cfg!(target_os = "ios") { + if let Some((ios_major, ios_minor, ios_patch)) = ios_version { + pack_os_version(ios_major, ios_minor, ios_patch) + } else if cfg!(target_abi = "macabi") { + // Skip checking iOS version on Mac Catalyst. + return; + } else { + // iOS version will be parsed from ProductVersion + pack_os_version(major, minor, patch) + } + } else { + pack_os_version(major, minor, patch) + }; + assert_eq!(expected, parse_version_from_plist(&root, plist.as_bytes())); + } + + // macOS 10.3.0 + let plist = r#" + + + + ProductBuildVersion + 7B85 + ProductCopyright + Apple Computer, Inc. 1983-2003 + ProductName + Mac OS X + ProductUserVisibleVersion + 10.3 + ProductVersion + 10.3 + + + "#; + check((10, 3, 0), None, plist); + + // macOS 10.7.5 + let plist = r#" + + + + ProductBuildVersion + 11G63 + ProductCopyright + 1983-2012 Apple Inc. + ProductName + Mac OS X + ProductUserVisibleVersion + 10.7.5 + ProductVersion + 10.7.5 + + + "#; + check((10, 7, 5), None, plist); + + // macOS 14.7.4 + let plist = r#" + + + + BuildID + 6A558D8A-E2EA-11EF-A1D3-6222CAA672A8 + ProductBuildVersion + 23H420 + ProductCopyright + 1983-2025 Apple Inc. + ProductName + macOS + ProductUserVisibleVersion + 14.7.4 + ProductVersion + 14.7.4 + iOSSupportVersion + 17.7 + + + "#; + check((14, 7, 4), Some((17, 7, 0)), plist); + + // SystemVersionCompat.plist on macOS 14.7.4 + let plist = r#" + + + + BuildID + 6A558D8A-E2EA-11EF-A1D3-6222CAA672A8 + ProductBuildVersion + 23H420 + ProductCopyright + 1983-2025 Apple Inc. + ProductName + Mac OS X + ProductUserVisibleVersion + 10.16 + ProductVersion + 10.16 + iOSSupportVersion + 17.7 + + + "#; + check((10, 16, 0), Some((17, 7, 0)), plist); + + // macOS 15.4 Beta 24E5238a + let plist = r#" + + + + BuildID + 67A50F62-00DA-11F0-BDB6-F99BB8310D2A + ProductBuildVersion + 24E5238a + ProductCopyright + 1983-2025 Apple Inc. + ProductName + macOS + ProductUserVisibleVersion + 15.4 + ProductVersion + 15.4 + iOSSupportVersion + 18.4 + + + "#; + check((15, 4, 0), Some((18, 4, 0)), plist); + + // iOS Simulator 17.5 + let plist = r#" + + + + BuildID + 210B8A2C-09C3-11EF-9DB8-273A64AEFA1C + ProductBuildVersion + 21F79 + ProductCopyright + 1983-2024 Apple Inc. + ProductName + iPhone OS + ProductVersion + 17.5 + + + "#; + check((17, 5, 0), None, plist); + + // visionOS Simulator 2.3 + let plist = r#" + + + + BuildID + 57CEFDE6-D079-11EF-837C-8B8C7961D0AC + ProductBuildVersion + 22N895 + ProductCopyright + 1983-2025 Apple Inc. + ProductName + xrOS + ProductVersion + 2.3 + SystemImageID + D332C7F1-08DF-4DD9-8122-94EF39A1FB92 + iOSSupportVersion + 18.3 + + + "#; + check((2, 3, 0), Some((18, 3, 0)), plist); + + // tvOS Simulator 18.2 + let plist = r#" + + + + BuildID + 617587B0-B059-11EF-BE70-4380EDE44645 + ProductBuildVersion + 22K154 + ProductCopyright + 1983-2024 Apple Inc. + ProductName + Apple TVOS + ProductVersion + 18.2 + SystemImageID + 8BB5A425-33F0-4821-9F93-40E7ED92F4E0 + + + "#; + check((18, 2, 0), None, plist); + + // watchOS Simulator 11.2 + let plist = r#" + + + + BuildID + BAAE2D54-B122-11EF-BF78-C6C6836B724A + ProductBuildVersion + 22S99 + ProductCopyright + 1983-2024 Apple Inc. + ProductName + Watch OS + ProductVersion + 11.2 + SystemImageID + 79F773E2-2041-43B4-98EE-FAE52402AE95 + + + "#; + check((11, 2, 0), None, plist); + + // iOS 9.3.6 + let plist = r#" + + + + ProductBuildVersion + 13G37 + ProductCopyright + 1983-2019 Apple Inc. + ProductName + iPhone OS + ProductVersion + 9.3.6 + + + "#; + check((9, 3, 6), None, plist); +} + +#[test] +#[should_panic = "SystemVersion.plist did not contain a dictionary at the top level"] +fn invalid_plist() { + let root = if cfg!(target_abi = "sim") { + PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").unwrap()) + } else { + PathBuf::from("/") + }; + let _ = parse_version_from_plist(&root, b"INVALID"); +} + +#[test] +#[cfg_attr( + target_abi = "macabi", + should_panic = "expected iOSSupportVersion in SystemVersion.plist" +)] +#[cfg_attr( + not(target_abi = "macabi"), + should_panic = "expected ProductVersion in SystemVersion.plist" +)] +fn empty_plist() { + let root = if cfg!(target_abi = "sim") { + PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").unwrap()) + } else { + PathBuf::from("/") + }; + let plist = r#" + + + + +"#; + let _ = parse_version_from_plist(&root, plist.as_bytes()); +} + +#[test] +fn parse_version() { + #[track_caller] + fn check(major: u16, minor: u8, patch: u8, version: &str) { + assert_eq!( + pack_os_version(major, minor, patch), + parse_os_version(version.as_bytes()).unwrap() + ) + } + + check(0, 0, 0, "0"); + check(0, 0, 0, "0.0.0"); + check(1, 0, 0, "1"); + check(1, 2, 0, "1.2"); + check(1, 2, 3, "1.2.3"); + check(9999, 99, 99, "9999.99.99"); + + // Check leading zeroes + check(10, 0, 0, "010"); + check(10, 20, 0, "010.020"); + check(10, 20, 30, "010.020.030"); + check(10000, 100, 100, "000010000.00100.00100"); + + // Too many parts + assert!(parse_os_version(b"1.2.3.4").is_err()); + + // Empty + assert!(parse_os_version(b"").is_err()); + + // Invalid digit + assert!(parse_os_version(b"A.B").is_err()); + + // Missing digits + assert!(parse_os_version(b".").is_err()); + assert!(parse_os_version(b".1").is_err()); + assert!(parse_os_version(b"1.").is_err()); + + // Too large + assert!(parse_os_version(b"100000").is_err()); + assert!(parse_os_version(b"1.1000").is_err()); + assert!(parse_os_version(b"1.1.1000").is_err()); +} diff --git a/library/std/src/sys/platform_version/mod.rs b/library/std/src/sys/platform_version/mod.rs new file mode 100644 index 0000000000000..db4b5ab0ab544 --- /dev/null +++ b/library/std/src/sys/platform_version/mod.rs @@ -0,0 +1,12 @@ +//! Runtime lookup of operating system / platform version. +//! +//! Related to [RFC 3750](https://github.com/rust-lang/rfcs/pull/3750), which +//! does version detection at compile-time. +//! +//! See also the `os_info` crate. + +#[cfg(target_vendor = "apple")] +mod darwin; + +// FIXME(madsmtm): Use `RtlGetVersion` for version lookup on Windows. +// FIXME(madsmtm): Use `__system_property_get` for version lookup on Android. diff --git a/tests/run-make/apple-c-available-links/foo.c b/tests/run-make/apple-c-available-links/foo.c new file mode 100644 index 0000000000000..6df3dea32219a --- /dev/null +++ b/tests/run-make/apple-c-available-links/foo.c @@ -0,0 +1,16 @@ +int foo(void) { + // Use some API that's a lot newer than the deployment target. + // This forces Clang to insert a call to __isPlatformVersionAtLeast. + if (__builtin_available( + macos 1000.0, + ios 1000.0, + tvos 1000.0, + watchos 1000.0, + visionos 1000.0, + * + )) { + return 1; + } else { + return 0; + } +} diff --git a/tests/run-make/apple-c-available-links/main.rs b/tests/run-make/apple-c-available-links/main.rs new file mode 100644 index 0000000000000..4ffada43c1b2b --- /dev/null +++ b/tests/run-make/apple-c-available-links/main.rs @@ -0,0 +1,7 @@ +unsafe extern "C" { + safe fn foo() -> core::ffi::c_int; +} + +fn main() { + assert_eq!(foo(), 0); +} diff --git a/tests/run-make/apple-c-available-links/rmake.rs b/tests/run-make/apple-c-available-links/rmake.rs new file mode 100644 index 0000000000000..6e6769e358a73 --- /dev/null +++ b/tests/run-make/apple-c-available-links/rmake.rs @@ -0,0 +1,17 @@ +//! Test that using `__builtin_available` in C (`@available` in Objective-C) +//! successfully links (because `std` provides the required symbols). + +//@ only-apple __builtin_available is (mostly) specific to Apple platforms. + +use run_make_support::{cc, rustc, target}; + +fn main() { + // Invoke the C compiler to generate an object file. + // + // (We cheat a bit here, and use the `rustc` target tuple directly as the + // Clang tuple, though that might not work for older Clang versions). + cc().arg("-target").arg(target()).arg("-c").input("foo.c").output("foo.o").run(); + + // Link the object file together with a Rust program. + rustc().target(target()).input("main.rs").link_arg("foo.o").run(); +} diff --git a/triagebot.toml b/triagebot.toml index 4abd164080b09..3f0078166a68d 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -356,6 +356,7 @@ trigger_files = [ [autolabel."O-apple"] trigger_files = [ "library/std/src/os/darwin", + "library/std/src/sys/platform_version/darwin", "library/std/src/sys/sync/thread_parking/darwin.rs", "compiler/rustc_target/src/spec/base/apple", ]