objc2/__macro_helpers/os_version/
apple.rs1use 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
17pub(crate) const DEPLOYMENT_TARGET: OSVersion = {
19 #[cfg(target_os = "macos")]
22 let var = option_env!("MACOSX_DEPLOYMENT_TARGET");
23 #[cfg(target_os = "ios")] 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 #[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 #[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#[inline]
115pub(crate) fn current_version() -> OSVersion {
116 static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
121
122 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#[cold]
147extern "C" fn lookup_version() -> u32 {
150 let version = version_from_sysctl().unwrap_or_else(version_from_plist);
153
154 assert_ne!(version, OSVersion::MIN, "version cannot be 0.0.0");
156 version.to_u32()
157}
158
159fn version_from_sysctl() -> Option<OSVersion> {
163 if cfg!(target_simulator) {
167 return None;
169 }
170
171 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 return None;
193 }
194 let buf = &buf[..(size - 1)];
195
196 if buf.is_empty() {
197 return None;
202 }
203
204 Some(OSVersion::from_bytes(buf))
205 };
206
207 if cfg!(target_os = "ios") {
228 if let Some(ios_support_version) = sysctl_version(b"kern.iossupportversion\0") {
230 return Some(ios_support_version);
231 }
232
233 if cfg!(target_abi_macabi) {
236 return None;
237 }
238 }
239
240 sysctl_version(b"kern.osproductversion\0")
243}
244
245fn version_from_plist() -> OSVersion {
262 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 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#[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 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 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 unsafe {
324 core::mem::transmute::<
325 *mut c_void,
326 unsafe extern "C" fn($($param_ty),*) $(-> $ret)?,
327 >(ptr)
328 }
329 }};
330 }
331
332 type Boolean = u8;
334 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 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 unsafe fn CFRelease(cf: CFTypeRef);
351 );
352 let CFGetTypeID = dlsym!(
353 unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
355 );
356 type CFErrorRef = CFTypeRef;
358 type CFDataRef = CFTypeRef;
360 let CFDataCreateWithBytesNoCopy = dlsym!(
361 unsafe fn CFDataCreateWithBytesNoCopy(
363 allocator: CFAllocatorRef,
364 bytes: *const u8,
365 length: CFIndex,
366 bytes_deallocator: CFAllocatorRef,
367 ) -> CFDataRef;
368 );
369 const kCFPropertyListImmutable: CFOptionFlags = 0;
371 type CFPropertyListFormat = CFIndex;
372 type CFPropertyListRef = CFTypeRef;
373 let CFPropertyListCreateWithData = dlsym!(
374 unsafe fn CFPropertyListCreateWithData(
376 allocator: CFAllocatorRef,
377 data: CFDataRef,
378 options: CFOptionFlags,
379 format: *mut CFPropertyListFormat,
380 error: *mut CFErrorRef,
381 ) -> CFPropertyListRef;
382 );
383 type CFStringRef = CFTypeRef;
385 type CFStringEncoding = u32;
386 const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
387 let CFStringGetTypeID = dlsym!(
388 unsafe fn CFStringGetTypeID() -> CFTypeID;
390 );
391 let CFStringCreateWithCStringNoCopy = dlsym!(
392 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 unsafe fn CFStringGetCString(
403 the_string: CFStringRef,
404 buffer: *mut c_char,
405 buffer_size: CFIndex,
406 encoding: CFStringEncoding,
407 ) -> Boolean;
408 );
409 type CFDictionaryRef = CFTypeRef;
411 let CFDictionaryGetTypeID = dlsym!(
412 unsafe fn CFDictionaryGetTypeID() -> CFTypeID;
414 );
415 let CFDictionaryGetValue = dlsym!(
416 unsafe fn CFDictionaryGetValue(
418 the_dict: CFDictionaryRef,
419 key: *const c_void,
420 ) -> *const c_void;
421 );
422
423 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(), null_mut(), )
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 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 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 if cfg!(target_abi_macabi) {
508 panic!("expected iOSSupportVersion in SystemVersion.plist");
509 }
510 }
511
512 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}