use core::ffi::{c_char, c_uint, c_void};
use core::num::NonZeroU32;
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use super::OSVersion;
use crate::rc::{autoreleasepool, Allocated, Retained};
use crate::runtime::__nsstring::{nsstring_to_str, UTF8_ENCODING};
use crate::runtime::{NSObject, NSObjectProtocol};
use crate::{class, msg_send};
pub(crate) const DEPLOYMENT_TARGET: OSVersion = {
#[cfg(target_os = "macos")]
let var = option_env!("MACOSX_DEPLOYMENT_TARGET");
#[cfg(target_os = "ios")] let var = option_env!("IPHONEOS_DEPLOYMENT_TARGET");
#[cfg(target_os = "tvos")]
let var = option_env!("TVOS_DEPLOYMENT_TARGET");
#[cfg(target_os = "watchos")]
let var = option_env!("WATCHOS_DEPLOYMENT_TARGET");
#[cfg(target_os = "visionos")]
let var = option_env!("XROS_DEPLOYMENT_TARGET");
if let Some(var) = var {
OSVersion::from_str(var)
} else {
#[allow(clippy::if_same_then_else)]
let os_min = if cfg!(target_os = "macos") {
(10, 12, 0)
} else if cfg!(target_os = "ios") {
(10, 0, 0)
} else if cfg!(target_os = "tvos") {
(10, 0, 0)
} else if cfg!(target_os = "watchos") {
(5, 0, 0)
} else if cfg!(target_os = "visionos") {
(1, 0, 0)
} else {
panic!("unknown Apple OS")
};
#[allow(clippy::if_same_then_else)]
let min = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
(11, 0, 0)
} else if cfg!(all(
target_os = "ios",
target_arch = "aarch64",
target_abi_macabi
)) {
(14, 0, 0)
} else if cfg!(all(
target_os = "ios",
target_arch = "aarch64",
target_simulator
)) {
(14, 0, 0)
} else if cfg!(all(target_os = "tvos", target_arch = "aarch64")) {
(14, 0, 0)
} else if cfg!(all(target_os = "watchos", target_arch = "aarch64")) {
(7, 0, 0)
} else {
os_min
};
OSVersion {
major: min.0,
minor: min.1,
patch: min.2,
}
}
};
#[inline]
pub(crate) fn current_version() -> OSVersion {
static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
let version = CURRENT_VERSION.load(Ordering::Relaxed);
OSVersion::from_u32(if version == 0 {
let version = lookup_version().get();
CURRENT_VERSION.store(version, Ordering::Relaxed);
version
} else {
version
})
}
#[cold]
fn lookup_version() -> NonZeroU32 {
let version = version_from_sysctl().unwrap_or_else(version_from_plist);
NonZeroU32::new(version.to_u32()).expect("version cannot be 0.0.0")
}
fn version_from_sysctl() -> Option<OSVersion> {
if cfg!(target_simulator) {
return None;
}
extern "C" {
fn sysctlbyname(
name: *const c_char,
oldp: *mut c_void,
oldlenp: *mut usize,
newp: *mut c_void,
newlen: usize,
) -> c_uint;
}
let name = if cfg!(target_abi_macabi) {
b"kern.iossupportversion\0".as_ptr().cast()
} else {
b"kern.osproductversion\0".as_ptr().cast()
};
let mut buf: [u8; 32] = [0; 32];
let mut size = buf.len();
let ret = unsafe { sysctlbyname(name, buf.as_mut_ptr().cast(), &mut size, ptr::null_mut(), 0) };
if ret != 0 {
return None;
}
Some(OSVersion::from_bytes(&buf[..(size - 1)]))
}
fn version_from_plist() -> OSVersion {
autoreleasepool(|pool| {
let path: Retained<NSObject> = if cfg!(target_simulator) {
let root = std::env::var_os("IPHONE_SIMULATOR_ROOT")
.expect("environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator");
let path = PathBuf::from(root).join("System/Library/CoreServices/SystemVersion.plist");
let path = path.as_os_str().as_bytes();
let alloc: Allocated<NSObject> = unsafe { Allocated::alloc(class!(NSString)) };
unsafe {
let bytes_ptr: *const c_void = path.as_ptr().cast();
msg_send![
alloc,
initWithBytes: bytes_ptr,
length: path.len(),
encoding: UTF8_ENCODING,
]
}
} else {
let path: *const c_char = b"/System/Library/CoreServices/SystemVersion.plist\0"
.as_ptr()
.cast();
unsafe { msg_send![class!(NSString), stringWithUTF8String: path] }
};
let data: Option<Retained<NSObject>> =
unsafe { msg_send![class!(NSDictionary), dictionaryWithContentsOfFile: &*path] };
let data = data.expect(
"`/System/Library/CoreServices/SystemVersion.plist` must be readable, and contain a valid PList",
);
let lookup_key: *const c_char = if cfg!(target_abi_macabi) {
b"iOSSupportVersion\0".as_ptr().cast()
} else {
b"ProductVersion\0".as_ptr().cast()
};
let lookup_key: Retained<NSObject> =
unsafe { msg_send![class!(NSString), stringWithUTF8String: lookup_key] };
let version: Retained<NSObject> = unsafe { msg_send![&data, objectForKey: &*lookup_key] };
assert!(
version.isKindOfClass(class!(NSString)),
"`ProductVersion` key in `/System/Library/CoreServices/SystemVersion.plist` must be a string"
);
let version = unsafe { nsstring_to_str(&version, pool) };
OSVersion::from_str(version)
})
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::String;
use std::process::Command;
#[test]
fn sysctl_same_as_in_plist() {
if let Some(version) = version_from_sysctl() {
assert_eq!(version, version_from_plist());
}
}
#[test]
fn read_version() {
assert!(OSVersion::MIN < current_version(), "version cannot be min");
assert!(current_version() < OSVersion::MAX, "version cannot be max");
}
#[test]
#[cfg_attr(
not(target_os = "macos"),
ignore = "`sw_vers` is only available on macOS"
)]
fn compare_against_sw_vers() {
let expected = Command::new("sw_vers")
.arg("-productVersion")
.output()
.unwrap()
.stdout;
let expected = String::from_utf8(expected).unwrap();
let expected = OSVersion::from_str(expected.trim());
let actual = current_version();
assert_eq!(expected, actual);
}
}