objc2/__macro_helpers/os_version/
apple.rs

1//! Heavily copied from:
2//! <https://github.com/rust-lang/rust/pull/138944>
3//!
4//! Once in MSRV, we should be able to replace this with just using that
5//! symbol.
6use core::ffi::{c_char, c_int, c_uint, c_void, CStr};
7use core::ptr::null_mut;
8use core::sync::atomic::{AtomicU32, Ordering};
9use std::env;
10use std::ffi::CString;
11use std::fs;
12use std::os::unix::ffi::OsStringExt;
13use std::path::{Path, PathBuf};
14
15use super::OSVersion;
16
17/// The deployment target for the current OS.
18pub(crate) const DEPLOYMENT_TARGET: OSVersion = {
19    // Intentionally use `#[cfg]` guards instead of `cfg!` here, to avoid
20    // recompiling when unrelated environment variables change.
21    #[cfg(target_os = "macos")]
22    let var = option_env!("MACOSX_DEPLOYMENT_TARGET");
23    #[cfg(target_os = "ios")] // Also used on Mac Catalyst.
24    let var = option_env!("IPHONEOS_DEPLOYMENT_TARGET");
25    #[cfg(target_os = "tvos")]
26    let var = option_env!("TVOS_DEPLOYMENT_TARGET");
27    #[cfg(target_os = "watchos")]
28    let var = option_env!("WATCHOS_DEPLOYMENT_TARGET");
29    #[cfg(target_os = "visionos")]
30    let var = option_env!("XROS_DEPLOYMENT_TARGET");
31
32    if let Some(var) = var {
33        OSVersion::from_str(var)
34    } else {
35        // Default operating system version.
36        // See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L207-L215>
37        //
38        // Note that we cannot do as they suggest, and use
39        // `rustc --print=deployment-target`, as this has to work at `const`
40        // time.
41        #[allow(clippy::if_same_then_else)]
42        let os_min = if cfg!(target_os = "macos") {
43            (10, 12, 0)
44        } else if cfg!(target_os = "ios") {
45            (10, 0, 0)
46        } else if cfg!(target_os = "tvos") {
47            (10, 0, 0)
48        } else if cfg!(target_os = "watchos") {
49            (5, 0, 0)
50        } else if cfg!(target_os = "visionos") {
51            (1, 0, 0)
52        } else {
53            panic!("unknown Apple OS")
54        };
55
56        // On certain targets it makes sense to raise the minimum OS version.
57        //
58        // See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L217-L231>
59        //
60        // Note that we cannot do all the same checks as `rustc` does, because
61        // we have no way of knowing if the architecture is `arm64e` without
62        // reading the target triple itself (and we want to get rid of build
63        // scripts).
64        #[allow(clippy::if_same_then_else)]
65        let min = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
66            (11, 0, 0)
67        } else if cfg!(all(
68            target_os = "ios",
69            target_arch = "aarch64",
70            target_abi_macabi
71        )) {
72            (14, 0, 0)
73        } else if cfg!(all(
74            target_os = "ios",
75            target_arch = "aarch64",
76            target_simulator
77        )) {
78            (14, 0, 0)
79        } else if cfg!(all(target_os = "tvos", target_arch = "aarch64")) {
80            (14, 0, 0)
81        } else if cfg!(all(target_os = "watchos", target_arch = "aarch64")) {
82            (7, 0, 0)
83        } else {
84            os_min
85        };
86
87        OSVersion {
88            major: min.0,
89            minor: min.1,
90            patch: min.2,
91        }
92    }
93};
94
95/// Get the current OS version.
96///
97/// # Semantics
98///
99/// The reported version on macOS might be 10.16 if the SDK version of the binary is less than 11.0.
100/// This is a workaround that Apple implemented to handle applications that assumed that macOS
101/// versions would always start with "10", see:
102/// <https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/libsyscall/wrappers/system-version-compat.c>
103///
104/// It _is_ possible to get the real version regardless of the SDK version of the binary, this is
105/// what Zig does:
106/// <https://github.com/ziglang/zig/blob/0.13.0/lib/std/zig/system/darwin/macos.zig>
107///
108/// We choose to not do that, and instead follow Apple's behaviour here, and return 10.16 when
109/// compiled with an older SDK; the user should instead upgrade their tooling.
110///
111/// NOTE: `rustc` currently doesn't set the right SDK version when linking with ld64, so this will
112/// have the wrong behaviour with `-Clinker=ld` on x86_64. But that's a `rustc` bug:
113/// <https://github.com/rust-lang/rust/issues/129432>
114#[inline]
115pub(crate) fn current_version() -> OSVersion {
116    // Cache the lookup for performance.
117    //
118    // 0.0.0 is never going to be a valid version ("vtool" reports "n/a" on 0 versions), so we use
119    // that as our sentinel value.
120    static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
121
122    // We use relaxed atomics instead of e.g. a `Once`, it doesn't matter if multiple threads end up
123    // racing to read or write the version, `lookup_version` should be idempotent and always return
124    // the same value.
125    //
126    // `compiler-rt` uses `dispatch_once`, but that's overkill for the reasons above.
127    let version = CURRENT_VERSION.load(Ordering::Relaxed);
128    OSVersion::from_u32(if version == 0 {
129        let version = lookup_version();
130        CURRENT_VERSION.store(version, Ordering::Relaxed);
131        version
132    } else {
133        version
134    })
135}
136
137/// Look up the os version.
138///
139/// # Aborts
140///
141/// Aborts if reading or parsing the version fails (or if the system was out of memory).
142///
143/// We deliberately choose to abort, as having this silently return an invalid OS version would be
144/// impossible for a user to debug.
145// The lookup is costly and should be on the cold path because of the cache in `current_version`.
146#[cold]
147// Micro-optimization: We use `extern "C"` to abort on panic, allowing `current_version` (inlined)
148// to be free of unwind handling.
149extern "C" fn lookup_version() -> u32 {
150    // Try to read from `sysctl` first (faster), but if that fails, fall back to reading the
151    // property list (this is roughly what `_availability_version_check` does internally).
152    let version = version_from_sysctl().unwrap_or_else(version_from_plist);
153
154    // Try to make it clearer to the optimizer that this will never return 0.
155    assert_ne!(version, OSVersion::MIN, "version cannot be 0.0.0");
156    version.to_u32()
157}
158
159/// Read the version from `kern.osproductversion` or `kern.iossupportversion`.
160///
161/// This is faster than `version_from_plist`, since it doesn't need to invoke `dlsym`.
162fn version_from_sysctl() -> Option<OSVersion> {
163    // This won't work in the simulator, as `kern.osproductversion` returns the host macOS version,
164    // and `kern.iossupportversion` returns the host macOS' iOSSupportVersion (while you can run
165    // simulators with many different iOS versions).
166    if cfg!(target_simulator) {
167        // Fall back to `version_from_plist` on these targets.
168        return None;
169    }
170
171    // SAFETY: Same signatures as in `libc`.
172    //
173    // NOTE: We do not need to link this, that will be done by `std` by linking `libSystem`
174    // (which is required on macOS/Darwin).
175    extern "C" {
176        fn sysctlbyname(
177            name: *const c_char,
178            oldp: *mut c_void,
179            oldlenp: *mut usize,
180            newp: *mut c_void,
181            newlen: usize,
182        ) -> c_uint;
183    }
184
185    let sysctl_version = |name: &[u8]| {
186        let mut buf: [u8; 32] = [0; 32];
187        let mut size = buf.len();
188        let ptr = buf.as_mut_ptr().cast();
189        let ret = unsafe { sysctlbyname(name.as_ptr().cast(), ptr, &mut size, null_mut(), 0) };
190        if ret != 0 {
191            // This sysctl is not available.
192            return None;
193        }
194        let buf = &buf[..(size - 1)];
195
196        if buf.is_empty() {
197            // The buffer may be empty when using `kern.iossupportversion` on an actual iOS device,
198            // or on visionOS when running under "Designed for iPad".
199            //
200            // In that case, fall back to `kern.osproductversion`.
201            return None;
202        }
203
204        Some(OSVersion::from_bytes(buf))
205    };
206
207    // When `target_os = "ios"`, we may be in many different states:
208    // - Native iOS device.
209    // - iOS Simulator.
210    // - Mac Catalyst.
211    // - Mac + "Designed for iPad".
212    // - Native visionOS device + "Designed for iPad".
213    // - visionOS simulator + "Designed for iPad".
214    //
215    // Of these, only native, Mac Catalyst and simulators can be differentiated at compile-time
216    // (with `target_abi = ""`, `target_abi = "macabi"` and `target_abi = "sim"` respectively).
217    //
218    // That is, "Designed for iPad" will act as iOS at compile-time, but the `ProductVersion` will
219    // still be the host macOS or visionOS version.
220    //
221    // Furthermore, we can't even reliably differentiate between these at runtime, since
222    // `dyld_get_active_platform` isn't publicly available.
223    //
224    // Fortunately, we won't need to know any of that; we can simply attempt to get the
225    // `iOSSupportVersion` (which may be set on native iOS too, but then it will be set to the host
226    // iOS version), and if that fails, fall back to the `ProductVersion`.
227    if cfg!(target_os = "ios") {
228        // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2077-L2100
229        if let Some(ios_support_version) = sysctl_version(b"kern.iossupportversion\0") {
230            return Some(ios_support_version);
231        }
232
233        // On Mac Catalyst, if we failed looking up `iOSSupportVersion`, we don't want to
234        // accidentally fall back to `ProductVersion`.
235        if cfg!(target_abi_macabi) {
236            return None;
237        }
238    }
239
240    // Introduced in macOS 10.13.4.
241    // https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2015-L2051
242    sysctl_version(b"kern.osproductversion\0")
243}
244
245/// Look up the current OS version(s) from `/System/Library/CoreServices/SystemVersion.plist`.
246///
247/// More specifically, from the `ProductVersion` and `iOSSupportVersion` keys, and from
248/// `$IPHONE_SIMULATOR_ROOT/System/Library/CoreServices/SystemVersion.plist` on the simulator.
249///
250/// This file was introduced in macOS 10.3, which is well below the minimum supported version by
251/// `rustc`, which is (at the time of writing) macOS 10.12.
252///
253/// # Implementation
254///
255/// We do roughly the same thing in here as `compiler-rt`, and dynamically look up CoreFoundation
256/// utilities for parsing PLists (to avoid having to re-implement that in here, as pulling in a full
257/// PList parser into `std` seems costly).
258///
259/// If this is found to be undesirable, we _could_ possibly hack it by parsing the PList manually
260/// (it seems to use the plain-text "xml1" encoding/format in all versions), but that seems brittle.
261fn version_from_plist() -> OSVersion {
262    // The root directory relative to where all files are located.
263    let root = if cfg!(target_simulator) {
264        PathBuf::from(env::var_os("IPHONE_SIMULATOR_ROOT").expect(
265            "environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator",
266        ))
267    } else {
268        PathBuf::from("/")
269    };
270
271    // Read `SystemVersion.plist`. Always present on Apple platforms, reading it cannot fail.
272    let path = root.join("System/Library/CoreServices/SystemVersion.plist");
273    let plist_buffer = fs::read(&path).unwrap_or_else(|e| panic!("failed reading {path:?}: {e}"));
274    parse_version_from_plist(&root, &plist_buffer)
275}
276
277/// Split out from [`version_from_plist`] to allow for testing.
278#[allow(non_upper_case_globals, non_snake_case)]
279fn parse_version_from_plist(root: &Path, plist_buffer: &[u8]) -> OSVersion {
280    const RTLD_LAZY: c_int = 0x1;
281    const RTLD_LOCAL: c_int = 0x4;
282
283    extern "C" {
284        fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
285        fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
286        fn dlerror() -> *mut c_char;
287        fn dlclose(handle: *mut c_void) -> c_int;
288    }
289
290    // Link to the CoreFoundation dylib, and look up symbols from that.
291    // We explicitly use non-versioned path here, to allow this to work on older iOS devices.
292    let cf_path = root.join("System/Library/Frameworks/CoreFoundation.framework/CoreFoundation");
293
294    let cf_path =
295        CString::new(cf_path.into_os_string().into_vec()).expect("failed allocating string");
296    let cf_handle = unsafe { dlopen(cf_path.as_ptr(), RTLD_LAZY | RTLD_LOCAL) };
297    if cf_handle.is_null() {
298        let err = unsafe { CStr::from_ptr(dlerror()) };
299        panic!("could not open CoreFoundation.framework: {err:?}");
300    }
301    let _cf_handle_free = Deferred(|| {
302        // Ignore errors when closing. This is also what `libloading` does:
303        // https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374
304        let _ = unsafe { dlclose(cf_handle) };
305    });
306
307    macro_rules! dlsym {
308        (
309            unsafe fn $name:ident($($param:ident: $param_ty:ty),* $(,)?) $(-> $ret:ty)?;
310        ) => {{
311            let ptr = unsafe {
312                dlsym(
313                    cf_handle,
314                    concat!(stringify!($name), '\0').as_bytes().as_ptr().cast(),
315                )
316            };
317            if ptr.is_null() {
318                let err = unsafe { CStr::from_ptr(dlerror()) };
319                panic!("could not find function {}: {err:?}", stringify!($name));
320            }
321            // SAFETY: Just checked that the symbol isn't NULL, and caller verifies that the
322            // signature is correct.
323            unsafe {
324                core::mem::transmute::<
325                    *mut c_void,
326                    unsafe extern "C" fn($($param_ty),*) $(-> $ret)?,
327                >(ptr)
328            }
329        }};
330    }
331
332    // MacTypes.h
333    type Boolean = u8;
334    // CoreFoundation/CFBase.h
335    type CFTypeID = usize;
336    type CFOptionFlags = usize;
337    type CFIndex = isize;
338    type CFTypeRef = *mut c_void;
339    type CFAllocatorRef = CFTypeRef;
340    const kCFAllocatorDefault: CFAllocatorRef = null_mut();
341    // Available: in all CF versions.
342    let allocator_null = unsafe { dlsym(cf_handle, b"kCFAllocatorNull\0".as_ptr().cast()) };
343    if allocator_null.is_null() {
344        let err = unsafe { CStr::from_ptr(dlerror()) };
345        panic!("could not find kCFAllocatorNull: {err:?}");
346    }
347    let kCFAllocatorNull = unsafe { *allocator_null.cast::<CFAllocatorRef>() };
348    let CFRelease = dlsym!(
349        // Available: in all CF versions.
350        unsafe fn CFRelease(cf: CFTypeRef);
351    );
352    let CFGetTypeID = dlsym!(
353        // Available: in all CF versions.
354        unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
355    );
356    // CoreFoundation/CFError.h
357    type CFErrorRef = CFTypeRef;
358    // CoreFoundation/CFData.h
359    type CFDataRef = CFTypeRef;
360    let CFDataCreateWithBytesNoCopy = dlsym!(
361        // Available: in all CF versions.
362        unsafe fn CFDataCreateWithBytesNoCopy(
363            allocator: CFAllocatorRef,
364            bytes: *const u8,
365            length: CFIndex,
366            bytes_deallocator: CFAllocatorRef,
367        ) -> CFDataRef;
368    );
369    // CoreFoundation/CFPropertyList.h
370    const kCFPropertyListImmutable: CFOptionFlags = 0;
371    type CFPropertyListFormat = CFIndex;
372    type CFPropertyListRef = CFTypeRef;
373    let CFPropertyListCreateWithData = dlsym!(
374        // Available: since macOS 10.6.
375        unsafe fn CFPropertyListCreateWithData(
376            allocator: CFAllocatorRef,
377            data: CFDataRef,
378            options: CFOptionFlags,
379            format: *mut CFPropertyListFormat,
380            error: *mut CFErrorRef,
381        ) -> CFPropertyListRef;
382    );
383    // CoreFoundation/CFString.h
384    type CFStringRef = CFTypeRef;
385    type CFStringEncoding = u32;
386    const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
387    let CFStringGetTypeID = dlsym!(
388        // Available: in all CF versions.
389        unsafe fn CFStringGetTypeID() -> CFTypeID;
390    );
391    let CFStringCreateWithCStringNoCopy = dlsym!(
392        // Available: in all CF versions.
393        unsafe fn CFStringCreateWithCStringNoCopy(
394            alloc: CFAllocatorRef,
395            c_str: *const c_char,
396            encoding: CFStringEncoding,
397            contents_deallocator: CFAllocatorRef,
398        ) -> CFStringRef;
399    );
400    let CFStringGetCString = dlsym!(
401        // Available: in all CF versions.
402        unsafe fn CFStringGetCString(
403            the_string: CFStringRef,
404            buffer: *mut c_char,
405            buffer_size: CFIndex,
406            encoding: CFStringEncoding,
407        ) -> Boolean;
408    );
409    // CoreFoundation/CFDictionary.h
410    type CFDictionaryRef = CFTypeRef;
411    let CFDictionaryGetTypeID = dlsym!(
412        // Available: in all CF versions.
413        unsafe fn CFDictionaryGetTypeID() -> CFTypeID;
414    );
415    let CFDictionaryGetValue = dlsym!(
416        // Available: in all CF versions.
417        unsafe fn CFDictionaryGetValue(
418            the_dict: CFDictionaryRef,
419            key: *const c_void,
420        ) -> *const c_void;
421    );
422
423    // MARK: Done declaring symbols.
424
425    let plist_data = unsafe {
426        CFDataCreateWithBytesNoCopy(
427            kCFAllocatorDefault,
428            plist_buffer.as_ptr(),
429            plist_buffer.len() as CFIndex,
430            kCFAllocatorNull,
431        )
432    };
433    assert!(!plist_data.is_null(), "failed creating data");
434    let _plist_data_release = Deferred(|| unsafe { CFRelease(plist_data) });
435
436    let plist = unsafe {
437        CFPropertyListCreateWithData(
438            kCFAllocatorDefault,
439            plist_data,
440            kCFPropertyListImmutable,
441            null_mut(), // Don't care about the format of the PList.
442            null_mut(), // Don't care about the error data.
443        )
444    };
445    assert!(
446        !plist.is_null(),
447        "failed reading PList in SystemVersion.plist"
448    );
449    let _plist_release = Deferred(|| unsafe { CFRelease(plist) });
450
451    assert_eq!(
452        unsafe { CFGetTypeID(plist) },
453        unsafe { CFDictionaryGetTypeID() },
454        "SystemVersion.plist did not contain a dictionary at the top level"
455    );
456    let plist = plist as CFDictionaryRef;
457
458    let get_string_key = |plist, lookup_key: &[u8]| {
459        let cf_lookup_key = unsafe {
460            CFStringCreateWithCStringNoCopy(
461                kCFAllocatorDefault,
462                lookup_key.as_ptr().cast(),
463                kCFStringEncodingUTF8,
464                kCFAllocatorNull,
465            )
466        };
467        assert!(!cf_lookup_key.is_null(), "failed creating CFString");
468        let _lookup_key_release = Deferred(|| unsafe { CFRelease(cf_lookup_key) });
469
470        let value = unsafe { CFDictionaryGetValue(plist, cf_lookup_key) as CFTypeRef };
471        // ^ getter, so don't release.
472        if value.is_null() {
473            return None;
474        }
475
476        assert_eq!(
477            unsafe { CFGetTypeID(value) },
478            unsafe { CFStringGetTypeID() },
479            "key in SystemVersion.plist must be a string"
480        );
481        let value = value as CFStringRef;
482
483        let mut version_str = [0u8; 32];
484        let ret = unsafe {
485            CFStringGetCString(
486                value,
487                version_str.as_mut_ptr().cast::<c_char>(),
488                version_str.len() as CFIndex,
489                kCFStringEncodingUTF8,
490            )
491        };
492        assert_ne!(ret, 0, "failed getting string from CFString");
493
494        let version_str =
495            CStr::from_bytes_until_nul(&version_str).expect("failed converting to CStr");
496
497        Some(OSVersion::from_bytes(version_str.to_bytes()))
498    };
499
500    // Same logic as in `version_from_sysctl`.
501    if cfg!(target_os = "ios") {
502        if let Some(ios_support_version) = get_string_key(plist, b"iOSSupportVersion\0") {
503            return ios_support_version;
504        }
505
506        // Force Mac Catalyst to use iOSSupportVersion (do not fall back to ProductVersion).
507        if cfg!(target_abi_macabi) {
508            panic!("expected iOSSupportVersion in SystemVersion.plist");
509        }
510    }
511
512    // On all other platforms, we can find the OS version by simply looking at `ProductVersion`.
513    get_string_key(plist, b"ProductVersion\0")
514        .expect("expected ProductVersion in SystemVersion.plist")
515}
516
517struct Deferred<F: FnMut()>(F);
518
519impl<F: FnMut()> Drop for Deferred<F> {
520    fn drop(&mut self) {
521        (self.0)();
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    use alloc::string::String;
530    use std::process::Command;
531
532    #[test]
533    fn sysctl_same_as_in_plist() {
534        if let Some(version) = version_from_sysctl() {
535            assert_eq!(version, version_from_plist());
536        }
537    }
538
539    #[test]
540    fn read_version() {
541        assert!(OSVersion::MIN < current_version(), "version cannot be min");
542        assert!(current_version() < OSVersion::MAX, "version cannot be max");
543    }
544
545    #[test]
546    #[cfg_attr(
547        not(target_os = "macos"),
548        ignore = "`sw_vers` is only available on macOS"
549    )]
550    fn compare_against_sw_vers() {
551        let expected = Command::new("sw_vers")
552            .arg("-productVersion")
553            .output()
554            .unwrap()
555            .stdout;
556        let expected = String::from_utf8(expected).unwrap();
557        let expected = OSVersion::from_str(expected.trim());
558
559        let actual = current_version();
560        assert_eq!(expected, actual);
561    }
562}