-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathapple.rs
More file actions
302 lines (274 loc) · 11.1 KB
/
apple.rs
File metadata and controls
302 lines (274 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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};
/// The deployment target for the current OS.
pub(crate) const DEPLOYMENT_TARGET: OSVersion = {
// Intentionally use `#[cfg]` guards instead of `cfg!` here, to avoid
// recompiling when unrelated environment variables change.
#[cfg(target_os = "macos")]
let var = option_env!("MACOSX_DEPLOYMENT_TARGET");
#[cfg(target_os = "ios")] // Also used on Mac Catalyst.
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 {
// Default operating system version.
// See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L207-L215>
//
// Note that we cannot do as they suggest, and use
// `rustc --print=deployment-target`, as this has to work at `const`
// time.
#[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")
};
// On certain targets it makes sense to raise the minimum OS version.
//
// See <https://github.com/rust-lang/rust/blob/1e5719bdc40bb553089ce83525f07dfe0b2e71e9/compiler/rustc_target/src/spec/base/apple/mod.rs#L217-L231>
//
// Note that we cannot do all the same checks as `rustc` does, because
// we have no way of knowing if the architecture is `arm64e` without
// reading the target triple itself (and we want to get rid of build
// scripts).
#[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,
}
}
};
/// Look up the current version at runtime.
///
/// Note that this doesn't work with "zippered" `dylib`s yet, though
/// that's probably fine, `rustc` doesn't support those either:
/// <https://github.com/rust-lang/rust/issues/131216>
#[inline]
pub(crate) fn current_version() -> OSVersion {
// Cache the lookup for performance.
//
// We assume that 0.0.0 is never gonna be a valid version,
// and use that as our sentinel value.
static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
// We use relaxed atomics, it doesn't matter if two threads end up racing
// to read or write the version.
let version = CURRENT_VERSION.load(Ordering::Relaxed);
OSVersion::from_u32(if version == 0 {
// TODO: Consider using `std::panic::abort_unwind` here for code-size?
let version = lookup_version().get();
CURRENT_VERSION.store(version, Ordering::Relaxed);
version
} else {
version
})
}
#[cold]
fn lookup_version() -> NonZeroU32 {
// Since macOS 10.15, libSystem has provided the undocumented
// `_availability_version_check` via `libxpc` for doing this version
// lookup, though it's usage may be a bit dangerous, see:
// - https://reviews.llvm.org/D150397
// - https://github.com/llvm/llvm-project/issues/64227
//
// So instead, we use the safer approach of reading from `sysctl`, and
// if that fails, we fall back to the property list (this is what
// `_availability_version_check` does internally).
let version = version_from_sysctl().unwrap_or_else(version_from_plist);
// Use `NonZeroU32` to try to make it clearer to the optimizer that this
// will never return 0.
NonZeroU32::new(version.to_u32()).expect("version cannot be 0.0.0")
}
/// Read the version from `kern.osproductversion` or `kern.iossupportversion`.
fn version_from_sysctl() -> Option<OSVersion> {
// This won't work in the simulator, `kern.osproductversion` will return
// the host macOS version.
if cfg!(target_simulator) {
return None;
}
// SAFETY: Same signature as in `libc`
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 {
// Introduced in macOS 10.13.4.
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 {
// `sysctlbyname` is not available.
return None;
}
Some(OSVersion::from_bytes(&buf[..(size - 1)]))
}
/// Look up the current OS version from the `ProductVersion` or
/// `iOSSupportVersion` in `/System/Library/CoreServices/SystemVersion.plist`.
/// This file was introduced in macOS 10.3.0.
///
/// This is also what is done in `compiler-rt`:
/// <https://github.com/llvm/llvm-project/blob/llvmorg-19.1.1/compiler-rt/lib/builtins/os_version_check.c>
///
/// NOTE: I don't _think_ we need to do a similar thing as what Zig does to
/// handle the fake 10.16 versions returned when the SDK version of the binary
/// is less than 11.0:
/// <https://github.com/ziglang/zig/blob/0.13.0/lib/std/zig/system/darwin/macos.zig>
///
/// My reasoning is that we _want_ to follow Apple's behaviour here, and
/// return 10.16 when compiled with an older SDK; the user should upgrade
/// their tooling.
///
/// NOTE: `rustc` currently doesn't set the right SDK version when linking
/// with ld64, so this will usually have the wrong behaviour on x86_64. But
/// that's a `rustc` bug, and is tracked in:
/// <https://github.com/rust-lang/rust/issues/129432>
///
///
/// # Panics
///
/// Panics if reading or parsing the PList fails (or if the system was out of
/// memory).
///
/// We deliberately choose to panic, as having this lookup silently return
/// an empty OS version would be impossible for a user to debug.
fn version_from_plist() -> OSVersion {
// Use Foundation's mechanisms for reading the PList.
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();
// SAFETY: Allocating a string is valid on all threads.
let alloc: Allocated<NSObject> = unsafe { Allocated::alloc(class!(NSString)) };
// SAFETY: The bytes are valid, and the length is correct.
unsafe {
let bytes_ptr: *const c_void = path.as_ptr().cast();
msg_send![
alloc,
initWithBytes: bytes_ptr,
length: path.len(),
// OsStr is a superset of UTF-8 on unix platforms
encoding: UTF8_ENCODING,
]
}
} else {
let path: *const c_char = b"/System/Library/CoreServices/SystemVersion.plist\0"
.as_ptr()
.cast();
// SAFETY: The path is NULL terminated.
unsafe { msg_send![class!(NSString), stringWithUTF8String: path] }
};
// SAFETY: dictionaryWithContentsOfFile: is safe to call.
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",
);
// Read `ProductVersion`, except when running on Mac Catalyst, then we
// read `iOSSupportVersion` instead.
let lookup_key: *const c_char = if cfg!(target_abi_macabi) {
b"iOSSupportVersion\0".as_ptr().cast()
} else {
b"ProductVersion\0".as_ptr().cast()
};
// SAFETY: The lookup key is NULL terminated.
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"
);
// SAFETY: The given object is an NSString, and the returned string
// slice is not used outside of the current pool.
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);
}
}