Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1aed11c

Browse files
committedMar 18, 2025·
Implement __isOSVersionAtLeast and __isPlatformVersionAtLeast
1 parent 6e91b03 commit 1aed11c

File tree

5 files changed

+840
-1
lines changed

5 files changed

+840
-1
lines changed
 

‎README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ These builtins are needed to support 128-bit integers.
178178
- [x] udivmodti4.c
179179
- [x] udivti3.c
180180
- [x] umodti3.c
181+
- [ ] os_version_check.c
182+
- [x] `__isOSVersionAtLeast` (Darwin)
183+
- [x] `__isPlatformVersionAtLeast` (Darwin)
184+
- [ ] `__isPlatformOrVariantPlatformVersionAtLeast` (macOS)
185+
- [ ] `__isOSVersionAtLeast` (Android)
181186

182187
These builtins are needed to support `f16` and `f128`, which are in the process
183188
of being added to Rust.
@@ -410,7 +415,6 @@ Miscellaneous functionality that is not used by Rust.
410415
- ~~i386/fp_mode.c~~
411416
- ~~int_util.c~~
412417
- ~~loongarch/fp_mode.c~~
413-
- ~~os_version_check.c~~
414418
- ~~riscv/fp_mode.c~~
415419
- ~~riscv/restore.S~~ (callee-saved registers)
416420
- ~~riscv/save.S~~ (callee-saved registers)

‎src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub mod float;
4343
pub mod int;
4444
pub mod math;
4545
pub mod mem;
46+
pub mod os_version_check;
4647

4748
// `libm` expects its `support` module to be available in the crate root. This config can be
4849
// cleaned up once `libm` is made always available.
Lines changed: 589 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,589 @@
1+
use core::{
2+
ffi::{c_char, c_int, c_long, c_uint, c_void, CStr},
3+
num::NonZero,
4+
ptr::null_mut,
5+
slice,
6+
sync::atomic::{AtomicU32, Ordering},
7+
};
8+
9+
/// Get the current OS version.
10+
#[inline]
11+
pub(super) fn current_version() -> u32 {
12+
// Cache the lookup for performance.
13+
//
14+
// 0.0.0 is never gonna be a valid version, so we use that as our sentinel value.
15+
static CURRENT_VERSION: AtomicU32 = AtomicU32::new(0);
16+
17+
// We use relaxed atomics, it doesn't matter if multiple threads end up racing to read or write
18+
// the version, `lookup_version` should be idempotent and always return the same value.
19+
//
20+
// `compiler-rt` uses `dispatch_once`, but that's overkill for the reasons above.
21+
let version = CURRENT_VERSION.load(Ordering::Relaxed);
22+
if version == 0 {
23+
let version = lookup_version().get();
24+
CURRENT_VERSION.store(version, Ordering::Relaxed);
25+
version
26+
} else {
27+
version
28+
}
29+
}
30+
31+
#[cold]
32+
// Use `extern "C"` to abort on panic, allowing `current_version` to be free of panic handling.
33+
pub(super) extern "C" fn lookup_version() -> NonZero<OSVersion> {
34+
// Since macOS 10.15, libSystem has provided the undocumented `_availability_version_check` via
35+
// `libxpc` (zippered, so requires platform parameter to differentiate between on macOS and Mac
36+
// Catalyst) for doing the version lookup, though it's usage may be a bit dangerous, see:
37+
// - https://reviews.llvm.org/D150397
38+
// - https://github.com/llvm/llvm-project/issues/64227
39+
//
40+
// So instead, we use the safer approach of reading from `sysctl` (which is faster), and if that
41+
// fails, we fall back to the property list (this is what `_availability_version_check` does
42+
// internally).
43+
let version = version_from_sysctl().unwrap_or_else(version_from_plist);
44+
45+
// Use `NonZero` to try to make it clearer to the optimizer that this will never return 0.
46+
NonZero::new(version).expect("version cannot be 0.0.0")
47+
}
48+
49+
/// Look up the current OS version(s) from `/System/Library/CoreServices/SystemVersion.plist`.
50+
///
51+
/// More specifically, from the `ProductVersion` and `iOSSupportVersion` keys, and from
52+
/// `$IPHONE_SIMULATOR_ROOT/System/Library/CoreServices/SystemVersion.plist` on the simulator.
53+
///
54+
/// This file was introduced in macOS 10.3, which is well below the minimum supported version by
55+
/// `rustc`, which is currently macOS 10.12.
56+
///
57+
/// # Panics
58+
///
59+
/// Panics if reading or parsing the version fails (or if the system was out of memory).
60+
///
61+
/// We deliberately choose to panic, as having this silently return an invalid OS version would be
62+
/// impossible for a user to debug.
63+
#[allow(non_upper_case_globals, non_snake_case)]
64+
pub(super) fn version_from_plist() -> OSVersion {
65+
#[allow(clippy::upper_case_acronyms)]
66+
enum FILE {}
67+
68+
const SEEK_END: c_int = 2;
69+
70+
const RTLD_LAZY: c_int = 0x1;
71+
const RTLD_LOCAL: c_int = 0x4;
72+
73+
// SAFETY: Same signatures as in `libc`.
74+
//
75+
// NOTE: We do not need to link these; that will be done by `std` by linking `libSystem`
76+
// (which is required on macOS/Darwin).
77+
unsafe extern "C" {
78+
unsafe fn getenv(s: *const c_char) -> *mut c_char;
79+
safe fn malloc(size: usize) -> *mut c_void;
80+
unsafe fn free(p: *mut c_void);
81+
unsafe fn strcpy(dst: *mut c_char, src: *const c_char) -> *mut c_char;
82+
unsafe fn strcat(s: *mut c_char, ct: *const c_char) -> *mut c_char;
83+
84+
unsafe fn fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE;
85+
unsafe fn fseek(stream: *mut FILE, offset: c_long, whence: c_int) -> c_int;
86+
unsafe fn ftell(stream: *mut FILE) -> c_long;
87+
unsafe fn rewind(stream: *mut FILE);
88+
unsafe fn fread(ptr: *mut c_void, size: usize, nobj: usize, stream: *mut FILE) -> usize;
89+
unsafe fn fclose(file: *mut FILE) -> c_int;
90+
91+
unsafe fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
92+
unsafe fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
93+
// NOTE: Cannot use this because we cannot Debug print `CStr` in `compiler-builtins`.
94+
// safe fn dlerror() -> *mut c_char;
95+
unsafe fn dlclose(handle: *mut c_void) -> c_int;
96+
}
97+
98+
// We do not need to do a similar thing as what Zig does to handle the fake 10.16 versions
99+
// returned when the SDK version of the binary is less than 11.0:
100+
// <https://github.com/ziglang/zig/blob/0.13.0/lib/std/zig/system/darwin/macos.zig>
101+
//
102+
// <https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/libsyscall/wrappers/system-version-compat.c>
103+
//
104+
// The reasoning is that we _want_ to follow Apple's behaviour here, and return 10.16 when
105+
// compiled with an older SDK; the user should upgrade their tooling.
106+
//
107+
// NOTE: `rustc` currently doesn't set the right SDK version when linking with ld64, so this
108+
// will have the wrong behaviour with `-Clinker=ld` on x86_64. But that's a `rustc` bug:
109+
// <https://github.com/rust-lang/rust/issues/129432>
110+
111+
struct Deferred<F: FnMut()>(F);
112+
impl<F: FnMut()> Drop for Deferred<F> {
113+
fn drop(&mut self) {
114+
(self.0)();
115+
}
116+
}
117+
118+
let path = c"/System/Library/CoreServices/SystemVersion.plist";
119+
let _path_free;
120+
let path = if cfg!(target_abi = "sim") {
121+
let root = unsafe { getenv(c"IPHONE_SIMULATOR_ROOT".as_ptr()) };
122+
if root.is_null() {
123+
panic!(
124+
"environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator"
125+
);
126+
}
127+
let root = unsafe { CStr::from_ptr(root) };
128+
129+
let ptr = malloc(root.count_bytes() + path.count_bytes() + 1);
130+
assert!(!ptr.is_null(), "failed allocating path");
131+
_path_free = Deferred(move || unsafe { free(ptr) });
132+
133+
let ptr = ptr.cast::<c_char>();
134+
unsafe { strcpy(ptr, root.as_ptr()) };
135+
unsafe { strcat(ptr, path.as_ptr()) };
136+
unsafe { CStr::from_ptr(ptr) }
137+
} else {
138+
path
139+
};
140+
141+
let plist_file = unsafe { fopen(path.as_ptr(), c"r".as_ptr()) };
142+
assert!(!plist_file.is_null(), "failed opening SystemVersion.plist");
143+
let _plist_file_close = Deferred(|| {
144+
if unsafe { fclose(plist_file) } != 0 {
145+
panic!("failed closing SystemVersion.plist");
146+
}
147+
});
148+
149+
let ret = unsafe { fseek(plist_file, 0, SEEK_END) };
150+
assert!(ret == 0, "failed seeking SystemVersion.plist");
151+
let file_size = unsafe { ftell(plist_file) };
152+
assert!(
153+
0 <= file_size,
154+
"failed reading file length of SystemVersion.plist"
155+
);
156+
unsafe { rewind(plist_file) };
157+
158+
let plist_buffer = malloc(file_size as usize);
159+
assert!(
160+
!plist_buffer.is_null(),
161+
"failed allocating buffer to hold PList"
162+
);
163+
let _plist_buffer_free = Deferred(|| unsafe { free(plist_buffer) });
164+
165+
let num_read = unsafe { fread(plist_buffer, 1, file_size as usize, plist_file) };
166+
assert!(
167+
num_read == file_size as usize,
168+
"failed reading all bytes from SystemVersion.plist"
169+
);
170+
171+
let plist_buffer = unsafe { slice::from_raw_parts(plist_buffer.cast::<u8>(), num_read) };
172+
173+
// We do roughly the same thing here as `compiler-rt`, and dynamically look up CoreFoundation
174+
// utilities for reading PLists (to avoid having to re-implement that in here).
175+
176+
// Link to the CoreFoundation dylib. Explicitly use non-versioned path here, to allow this to
177+
// work on older iOS devices.
178+
let cf = c"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
179+
let _cf_free;
180+
let cf = if cfg!(target_abi = "sim") {
181+
let root = unsafe { getenv(c"IPHONE_SIMULATOR_ROOT".as_ptr()) };
182+
if root.is_null() {
183+
panic!(
184+
"environment variable `IPHONE_SIMULATOR_ROOT` must be set when executing under simulator"
185+
);
186+
}
187+
let root = unsafe { CStr::from_ptr(root) };
188+
189+
let ptr = malloc(root.count_bytes() + cf.count_bytes() + 1);
190+
assert!(
191+
!ptr.is_null(),
192+
"failed allocating CoreFoundation framework path"
193+
);
194+
_cf_free = Deferred(move || unsafe { free(ptr) });
195+
196+
let ptr = ptr.cast::<c_char>();
197+
unsafe { strcpy(ptr, root.as_ptr()) };
198+
unsafe { strcat(ptr, cf.as_ptr()) };
199+
unsafe { CStr::from_ptr(ptr) }
200+
} else {
201+
cf
202+
};
203+
204+
let cf_handle = unsafe { dlopen(cf.as_ptr(), RTLD_LAZY | RTLD_LOCAL) };
205+
if cf_handle.is_null() {
206+
// let err = unsafe { CStr::from_ptr(dlerror()) };
207+
panic!("could not open CoreFoundation.framework");
208+
}
209+
let _handle_free = Deferred(|| {
210+
// Ignore errors when closing. This is also what `libloading` does:
211+
// https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374
212+
let _ = unsafe { dlclose(cf_handle) };
213+
});
214+
215+
macro_rules! dlsym {
216+
(
217+
unsafe fn $name:ident($($param:ident: $param_ty:ty),* $(,)?) $(-> $ret:ty)?;
218+
) => {{
219+
let ptr = unsafe { dlsym(cf_handle, concat!(stringify!($name), '\0').as_bytes().as_ptr().cast()) };
220+
if ptr.is_null() {
221+
// let err = unsafe { CStr::from_ptr(dlerror()) };
222+
panic!("could not find function {}", stringify!($name));
223+
}
224+
unsafe { core::mem::transmute::<*mut c_void, unsafe extern "C-unwind" fn($($param_ty),*) $(-> $ret)?>(ptr) }
225+
}};
226+
}
227+
228+
// MacTypes.h
229+
type Boolean = u8;
230+
// CoreFoundation/CFBase.h
231+
type CFTypeID = usize;
232+
type CFOptionFlags = usize;
233+
type CFIndex = isize;
234+
type CFTypeRef = *mut c_void;
235+
type CFAllocatorRef = CFTypeRef;
236+
const kCFAllocatorDefault: CFAllocatorRef = null_mut();
237+
let allocator_null = unsafe { dlsym(cf_handle, c"kCFAllocatorNull".as_ptr()) };
238+
if allocator_null.is_null() {
239+
// let err = unsafe { CStr::from_ptr(dlerror()) };
240+
panic!("could not find kCFAllocatorNull");
241+
}
242+
let kCFAllocatorNull = unsafe { *allocator_null.cast::<CFAllocatorRef>() };
243+
let CFRelease = dlsym!(
244+
unsafe fn CFRelease(cf: CFTypeRef);
245+
);
246+
let CFGetTypeID = dlsym!(
247+
unsafe fn CFGetTypeID(cf: CFTypeRef) -> CFTypeID;
248+
);
249+
// CoreFoundation/CFError.h
250+
type CFErrorRef = CFTypeRef;
251+
// CoreFoundation/CFData.h
252+
type CFDataRef = CFTypeRef;
253+
let CFDataCreateWithBytesNoCopy = dlsym!(
254+
unsafe fn CFDataCreateWithBytesNoCopy(
255+
allocator: CFAllocatorRef,
256+
bytes: *const u8,
257+
length: CFIndex,
258+
bytes_deallocator: CFAllocatorRef,
259+
) -> CFDataRef;
260+
);
261+
// CoreFoundation/CFPropertyList.h
262+
const kCFPropertyListImmutable: CFOptionFlags = 0;
263+
type CFPropertyListFormat = CFIndex;
264+
type CFPropertyListRef = CFTypeRef;
265+
let CFPropertyListCreateWithData = dlsym!(
266+
unsafe fn CFPropertyListCreateWithData(
267+
allocator: CFAllocatorRef,
268+
data: CFDataRef,
269+
options: CFOptionFlags,
270+
format: *mut CFPropertyListFormat,
271+
error: *mut CFErrorRef,
272+
) -> CFPropertyListRef;
273+
);
274+
// CoreFoundation/CFString.h
275+
type CFStringRef = CFTypeRef;
276+
type CFStringEncoding = u32;
277+
const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100;
278+
let CFStringGetTypeID = dlsym!(
279+
unsafe fn CFStringGetTypeID() -> CFTypeID;
280+
);
281+
let CFStringCreateWithCStringNoCopy = dlsym!(
282+
unsafe fn CFStringCreateWithCStringNoCopy(
283+
alloc: CFAllocatorRef,
284+
c_str: *const c_char,
285+
encoding: CFStringEncoding,
286+
contents_deallocator: CFAllocatorRef,
287+
) -> CFStringRef;
288+
);
289+
let CFStringGetCString = dlsym!(
290+
unsafe fn CFStringGetCString(
291+
the_string: CFStringRef,
292+
buffer: *mut c_char,
293+
buffer_size: CFIndex,
294+
encoding: CFStringEncoding,
295+
) -> Boolean;
296+
);
297+
// CoreFoundation/CFDictionary.h
298+
type CFDictionaryRef = CFTypeRef;
299+
let CFDictionaryGetTypeID = dlsym!(
300+
unsafe fn CFDictionaryGetTypeID() -> CFTypeID;
301+
);
302+
let CFDictionaryGetValue = dlsym!(
303+
unsafe fn CFDictionaryGetValue(
304+
the_dict: CFDictionaryRef,
305+
key: *const c_void,
306+
) -> *const c_void;
307+
);
308+
309+
let plist_data = unsafe {
310+
CFDataCreateWithBytesNoCopy(
311+
kCFAllocatorDefault,
312+
plist_buffer.as_ptr(),
313+
plist_buffer.len() as CFIndex,
314+
kCFAllocatorNull,
315+
)
316+
};
317+
assert!(!plist_data.is_null(), "failed creating data");
318+
let _plist_data_release = Deferred(|| unsafe { CFRelease(plist_data) });
319+
320+
let plist = unsafe {
321+
CFPropertyListCreateWithData(
322+
kCFAllocatorDefault,
323+
plist_data,
324+
kCFPropertyListImmutable,
325+
null_mut(), // Don't care about the format of the PList.
326+
null_mut(), // Don't care about the error data.
327+
)
328+
};
329+
assert!(
330+
!plist.is_null(),
331+
"failed reading PList in SystemVersion.plist"
332+
);
333+
let _plist_release = Deferred(|| unsafe { CFRelease(plist) });
334+
335+
assert!(
336+
unsafe { CFGetTypeID(plist) } == unsafe { CFDictionaryGetTypeID() },
337+
"SystemVersion.plist did not contain a dictionary at the top level"
338+
);
339+
let plist = plist as CFDictionaryRef;
340+
341+
// NOTE: Have to use a macro here instead of a closure, because a closure errors with:
342+
// "`compiler_builtins` cannot call functions through upstream monomorphizations".
343+
let get_string_key = |plist, lookup_key: &CStr| {
344+
let cf_lookup_key = unsafe {
345+
CFStringCreateWithCStringNoCopy(
346+
kCFAllocatorDefault,
347+
lookup_key.as_ptr(),
348+
kCFStringEncodingUTF8,
349+
kCFAllocatorNull,
350+
)
351+
};
352+
assert!(!cf_lookup_key.is_null(), "failed creating CFString");
353+
let _lookup_key_release = Deferred(|| unsafe { CFRelease(cf_lookup_key) });
354+
355+
let value = unsafe { CFDictionaryGetValue(plist, cf_lookup_key) as CFTypeRef };
356+
// ^ getter, so don't release.
357+
358+
if !value.is_null() {
359+
assert!(
360+
unsafe { CFGetTypeID(value) } == unsafe { CFStringGetTypeID() },
361+
"key in SystemVersion.plist must be a string"
362+
);
363+
let value = value as CFStringRef;
364+
365+
let mut version_str = [0u8; 32];
366+
let ret = unsafe {
367+
CFStringGetCString(
368+
value,
369+
version_str.as_mut_ptr().cast::<c_char>(),
370+
version_str.len() as CFIndex,
371+
kCFStringEncodingUTF8,
372+
)
373+
};
374+
assert!(ret != 0, "failed getting string from CFString");
375+
376+
let version_str = trim_trailing_nul(&version_str);
377+
378+
Some(parse_os_version(version_str))
379+
} else {
380+
None
381+
}
382+
};
383+
384+
// When `target_os = "ios"`, we may be in many different states:
385+
// - Native iOS device.
386+
// - iOS Simulator.
387+
// - Mac Catalyst.
388+
// - Mac + "Designed for iPad".
389+
// - Native visionOS device + "Designed for iPad".
390+
// - visionOS simulator + "Designed for iPad".
391+
//
392+
// Of these, only native, Mac Catalyst and simulators can be differentiated at compile-time
393+
// (with `target_abi = ""`, `target_abi = "macabi"` and `target_abi = "sim"` respectively).
394+
//
395+
// That is, "Designed for iPad" will act as iOS at compile-time, but the `ProductVersion` will
396+
// still be the host macOS or visionOS version.
397+
//
398+
// Furthermore, we can't even reliably differentiate between these at runtime, since
399+
// `dyld_get_active_platform` isn't publically available.
400+
//
401+
// Fortunately, we won't need to know any of that; we can simply attempt to get the
402+
// `iOSSupportVersion` (which may be set on native iOS too, but then it will be set to the host
403+
// iOS version), and if that fails, fall back to the `ProductVersion`.
404+
if cfg!(target_os = "ios") {
405+
if let Some(ios_support_version) = get_string_key(plist, c"iOSSupportVersion") {
406+
return ios_support_version;
407+
}
408+
409+
// On Mac Catalyst, if we failed looking up `iOSSupportVersion`, we don't want to
410+
// accidentally fall back to `ProductVersion`.
411+
if cfg!(target_abi = "macabi") {
412+
panic!("expected iOSSupportVersion in SystemVersion.plist");
413+
}
414+
}
415+
416+
// On all other platforms, we can find the OS version by simply looking at `ProductVersion`.
417+
get_string_key(plist, c"ProductVersion")
418+
.unwrap_or_else(|| panic!("expected ProductVersion in SystemVersion.plist"))
419+
}
420+
421+
/// Read the version from `kern.osproductversion` or `kern.iossupportversion`.
422+
///
423+
/// This is faster than `version_from_plist`, since it doesn't need to invoke `dlsym`.
424+
pub(super) fn version_from_sysctl() -> Option<OSVersion> {
425+
// This won't work in the simulator, as `kern.osproductversion` returns the host macOS version,
426+
// and `kern.iossupportversion` returns the host macOS' iOSSupportVersion (while you can run
427+
// simulators with many different iOS versions).
428+
if cfg!(target_abi = "sim") {
429+
return None;
430+
}
431+
432+
// SAFETY: Same signatures as in `libc`.
433+
//
434+
// NOTE: We do not need to link this, that will be done by `std` by linking `libSystem`
435+
// (which is required on macOS/Darwin).
436+
unsafe extern "C" {
437+
unsafe fn sysctlbyname(
438+
name: *const c_char,
439+
oldp: *mut c_void,
440+
oldlenp: *mut usize,
441+
newp: *mut c_void,
442+
newlen: usize,
443+
) -> c_uint;
444+
}
445+
446+
// Same logic as in `version_from_plist`.
447+
if cfg!(target_os = "ios") {
448+
// https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2077-L2100
449+
let name = c"kern.iossupportversion".as_ptr();
450+
let mut buf: [u8; 32] = [0; 32];
451+
let mut size = buf.len();
452+
let ret = unsafe { sysctlbyname(name, buf.as_mut_ptr().cast(), &mut size, null_mut(), 0) };
453+
if ret != 0 {
454+
// This sysctl is not available.
455+
return None;
456+
}
457+
let buf = &buf[..(size - 1)];
458+
459+
// The buffer may be empty when using `kern.iossupportversion` on iOS, or on visionOS when
460+
// running under "Designed for iPad". In that case, fall back to `kern.osproductversion`.
461+
if !buf.is_empty() {
462+
return Some(parse_os_version(buf));
463+
}
464+
465+
// Force Mac Catalyst to use the iOSSupportVersion.
466+
if cfg!(target_abi = "macabi") {
467+
return None;
468+
}
469+
}
470+
471+
// Introduced in macOS 10.13.4.
472+
// https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.81.4/bsd/kern/kern_sysctl.c#L2015-L2051
473+
let name = c"kern.osproductversion".as_ptr();
474+
let mut buf: [u8; 32] = [0; 32];
475+
let mut size = buf.len();
476+
let ret = unsafe { sysctlbyname(name, buf.as_mut_ptr().cast(), &mut size, null_mut(), 0) };
477+
if ret != 0 {
478+
// This sysctl is not available.
479+
return None;
480+
}
481+
let buf = &buf[..(size - 1)];
482+
483+
Some(parse_os_version(buf))
484+
}
485+
486+
/// The version of the operating system.
487+
///
488+
/// We use a packed u32 here to allow for fast comparisons and to match Mach-O's `LC_BUILD_VERSION`.
489+
pub(super) type OSVersion = u32;
490+
491+
/// Combine parts of a version into an [`OSVersion`].
492+
///
493+
/// The size of the parts are inherently limited by Mach-O's `LC_BUILD_VERSION`.
494+
#[inline]
495+
pub(super) const fn pack_os_version(major: u16, minor: u8, patch: u8) -> OSVersion {
496+
let (major, minor, patch) = (major as u32, minor as u32, patch as u32);
497+
(major << 16) | (minor << 8) | patch
498+
}
499+
500+
/// We'd usually use `CStr::from_bytes_until_nul`, but that can't be used in `compiler-builtins`.
501+
#[inline]
502+
fn trim_trailing_nul(mut bytes: &[u8]) -> &[u8] {
503+
while let Some((b'\0', rest)) = bytes.split_last() {
504+
bytes = rest;
505+
}
506+
bytes
507+
}
508+
509+
/// Parse an OS version from a bytestring like b"10.1" or b"14.3.7".
510+
#[track_caller]
511+
pub(super) const fn parse_os_version(bytes: &[u8]) -> OSVersion {
512+
let (major, bytes) = parse_usize(bytes);
513+
if major > u16::MAX as usize {
514+
panic!("major version is too large");
515+
}
516+
let major = major as u16;
517+
518+
let bytes = if let Some((period, bytes)) = bytes.split_first() {
519+
if *period != b'.' {
520+
panic!("expected period between major and minor version")
521+
}
522+
bytes
523+
} else {
524+
return pack_os_version(major, 0, 0);
525+
};
526+
527+
let (minor, bytes) = parse_usize(bytes);
528+
if minor > u8::MAX as usize {
529+
panic!("minor version is too large");
530+
}
531+
let minor = minor as u8;
532+
533+
let bytes = if let Some((period, bytes)) = bytes.split_first() {
534+
if *period != b'.' {
535+
panic!("expected period after minor version")
536+
}
537+
bytes
538+
} else {
539+
return pack_os_version(major, minor, 0);
540+
};
541+
542+
let (patch, bytes) = parse_usize(bytes);
543+
if patch > u8::MAX as usize {
544+
panic!("patch version is too large");
545+
}
546+
let patch = patch as u8;
547+
548+
if !bytes.is_empty() {
549+
panic!("too many parts to version");
550+
}
551+
552+
pack_os_version(major, minor, patch)
553+
}
554+
555+
#[track_caller]
556+
const fn parse_usize(mut bytes: &[u8]) -> (usize, &[u8]) {
557+
// Ensure we have at least one digit (that is not just a period).
558+
let mut ret: usize = if let Some((&ascii, rest)) = bytes.split_first() {
559+
bytes = rest;
560+
561+
match ascii {
562+
b'0'..=b'9' => (ascii - b'0') as usize,
563+
_ => panic!("found invalid digit when parsing version"),
564+
}
565+
} else {
566+
panic!("found empty version number part")
567+
};
568+
569+
// Parse the remaining digits.
570+
while let Some((&ascii, rest)) = bytes.split_first() {
571+
let digit = match ascii {
572+
b'0'..=b'9' => ascii - b'0',
573+
_ => break,
574+
};
575+
576+
bytes = rest;
577+
578+
// This handles leading zeroes as well.
579+
match ret.checked_mul(10) {
580+
Some(val) => match val.checked_add(digit as _) {
581+
Some(val) => ret = val,
582+
None => panic!("version is too large"),
583+
},
584+
None => panic!("version is too large"),
585+
};
586+
}
587+
588+
(ret, bytes)
589+
}

‎src/os_version_check/mod.rs‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! os_version_check.c
2+
//! <https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/compiler-rt/lib/builtins/os_version_check.c>
3+
//!
4+
//! Used by Objective-C's `@available` / Clang's `__builtin_available` macro / Swift's `#available`,
5+
//! and is useful when linking together with code written in those languages.
6+
#![allow(non_snake_case)]
7+
8+
#[cfg(target_vendor = "apple")]
9+
mod darwin_impl;
10+
11+
intrinsics! {
12+
/// Old entry point for availability. Used when compiling with older Clang versions.
13+
#[inline]
14+
#[cfg(target_vendor = "apple")]
15+
pub extern "C" fn __isOSVersionAtLeast(major: u32, minor: u32, subminor: u32) -> i32 {
16+
let version = darwin_impl::pack_os_version(
17+
major as u16,
18+
minor as u8,
19+
subminor as u8,
20+
);
21+
(version <= darwin_impl::current_version()) as i32
22+
}
23+
24+
/// Whether the current platform's OS version is higher than or equal to the given version.
25+
///
26+
/// The first argument is the _base_ Mach-O platform (i.e. `PLATFORM_MACOS`, `PLATFORM_IOS`,
27+
/// etc., but not `PLATFORM_IOSSIMULATOR` or `PLATFORM_MACCATALYST`) of the invoking binary.
28+
//
29+
// Versions are specified statically by the compiler. Inlining with LTO should allow them to be
30+
// combined into a single `u32`, which should make comparisons faster, and make the
31+
// `BASE_TARGET_PLATFORM` check a no-op.
32+
#[inline]
33+
#[cfg(target_vendor = "apple")]
34+
// extern "C" is correct, LLVM assumes the function cannot unwind:
35+
// https://github.com/llvm/llvm-project/blob/llvmorg-20.1.0/clang/lib/CodeGen/CGObjC.cpp#L3980
36+
pub extern "C" fn __isPlatformVersionAtLeast(platform: i32, major: u32, minor: u32, subminor: u32) -> i32 {
37+
let version = darwin_impl::pack_os_version(
38+
major as u16,
39+
minor as u8,
40+
subminor as u8,
41+
);
42+
43+
// Mac Catalyst is a technology that allows macOS to run in a different "mode" that closely
44+
// resembles iOS (and has iOS libraries like UIKit available).
45+
//
46+
// (Apple has added a "Designed for iPad" mode later on that allows running iOS apps
47+
// natively, but we don't need to think too much about those, since they link to
48+
// iOS-specific system binaries as well).
49+
//
50+
// To support Mac Catalyst, Apple has the concept of a "zippered" binary, which is a single
51+
// binary that can be run on both macOS and Mac Catalyst (has two `LC_BUILD_VERSION` Mach-O
52+
// commands, one set to `PLATFORM_MACOS` and one to `PLATFORM_MACCATALYST`).
53+
//
54+
// Most system libraries are zippered, which allows re-use across macOS and Mac Catalyst.
55+
// This includes the `libclang_rt.osx.a` shipped with Xcode! This means that `compiler-rt`
56+
// can't statically know whether it's compiled for macOS or Mac Catalyst, and thus this new
57+
// API (which replaces `__isOSVersionAtLeast`) is needed.
58+
//
59+
// In short:
60+
// normal binary calls normal compiler-rt --> `__isOSVersionAtLeast` was enough
61+
// normal binary calls zippered compiler-rt --> `__isPlatformVersionAtLeast` required
62+
// zippered binary calls zippered compiler-rt --> `__isPlatformOrVariantPlatformVersionAtLeast` called
63+
64+
// FIXME(madsmtm): `rustc` doesn't support zippered binaries yet, see rust-lang/rust#131216.
65+
// But once it does, we need the pre-compiled `std`/`compiler-builtins` shipped with rustup
66+
// to be zippered, and thus we also need to handle the `platform` difference here:
67+
//
68+
// if cfg!(target_os = "macos") && platform == 2 /* PLATFORM_IOS */ && cfg!(zippered) {
69+
// return (version.to_u32() <= darwin_impl::current_ios_version()) as i32;
70+
// }
71+
//
72+
// `__isPlatformOrVariantPlatformVersionAtLeast` would also need to be implemented.
73+
74+
// The base Mach-O platform for the current target.
75+
const BASE_TARGET_PLATFORM: i32 = if cfg!(target_os = "macos") {
76+
1 // PLATFORM_MACOS
77+
} else if cfg!(target_os = "ios") {
78+
2 // PLATFORM_IOS
79+
} else if cfg!(target_os = "tvos") {
80+
3 // PLATFORM_TVOS
81+
} else if cfg!(target_os = "watchos") {
82+
4 // PLATFORM_WATCHOS
83+
} else if cfg!(target_os = "visionos") {
84+
11 // PLATFORM_VISIONOS
85+
} else {
86+
0 // PLATFORM_UNKNOWN
87+
};
88+
debug_assert!(platform == BASE_TARGET_PLATFORM, "invalid platform provided to __isPlatformVersionAtLeast");
89+
90+
(version <= darwin_impl::current_version()) as i32
91+
}
92+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#![cfg(target_vendor = "apple")]
2+
use std::process::Command;
3+
4+
use compiler_builtins::os_version_check::__isOSVersionAtLeast;
5+
6+
#[test]
7+
fn test_general_available() {
8+
// Lowest version always available.
9+
assert_eq!(__isOSVersionAtLeast(0, 0, 0), 1);
10+
// This high version never available.
11+
assert_eq!(__isOSVersionAtLeast(9999, 99, 99), 0);
12+
}
13+
14+
#[test]
15+
#[cfg_attr(
16+
not(target_os = "macos"),
17+
ignore = "`sw_vers` is only available on macOS"
18+
)]
19+
fn compare_against_sw_vers() {
20+
let sw_vers = Command::new("sw_vers")
21+
.arg("-productVersion")
22+
.output()
23+
.unwrap()
24+
.stdout;
25+
let sw_vers = String::from_utf8(sw_vers).unwrap();
26+
let mut sw_vers = sw_vers.trim().split('.');
27+
28+
let major: u32 = sw_vers.next().unwrap().parse().unwrap();
29+
let minor: u32 = sw_vers.next().unwrap_or("0").parse().unwrap();
30+
let subminor: u32 = sw_vers.next().unwrap_or("0").parse().unwrap();
31+
assert_eq!(sw_vers.count(), 0);
32+
33+
// Current version is available
34+
assert_eq!(__isOSVersionAtLeast(major, minor, subminor), 1);
35+
36+
// One lower is available
37+
assert_eq!(
38+
__isOSVersionAtLeast(major, minor, subminor.saturating_sub(1)),
39+
1
40+
);
41+
assert_eq!(
42+
__isOSVersionAtLeast(major, minor.saturating_sub(1), subminor),
43+
1
44+
);
45+
assert_eq!(
46+
__isOSVersionAtLeast(major.saturating_sub(1), minor, subminor),
47+
1
48+
);
49+
50+
// One higher isn't available
51+
assert_eq!(__isOSVersionAtLeast(major, minor, subminor + 1), 0);
52+
assert_eq!(__isOSVersionAtLeast(major, minor + 1, subminor), 0);
53+
assert_eq!(__isOSVersionAtLeast(major + 1, minor, subminor), 0);
54+
}
55+
56+
// Test internals
57+
58+
#[path = "../../src/os_version_check/darwin_impl.rs"]
59+
#[allow(dead_code)]
60+
mod darwin_impl;
61+
62+
#[test]
63+
fn sysctl_same_as_in_plist() {
64+
if let Some(version) = darwin_impl::version_from_sysctl() {
65+
assert_eq!(version, darwin_impl::version_from_plist());
66+
}
67+
}
68+
69+
#[test]
70+
fn lookup_idempotent() {
71+
let version = darwin_impl::lookup_version();
72+
for _ in 0..10 {
73+
assert_eq!(version, darwin_impl::lookup_version());
74+
}
75+
}
76+
77+
#[test]
78+
fn parse_version() {
79+
#[track_caller]
80+
fn check(major: u16, minor: u8, patch: u8, version: &str) {
81+
assert_eq!(
82+
darwin_impl::pack_os_version(major, minor, patch),
83+
darwin_impl::parse_os_version(version.as_bytes()),
84+
)
85+
}
86+
87+
check(0, 0, 0, "0");
88+
check(0, 0, 0, "0.0.0");
89+
check(1, 0, 0, "1");
90+
check(1, 2, 0, "1.2");
91+
check(1, 2, 3, "1.2.3");
92+
check(9999, 99, 99, "9999.99.99");
93+
94+
// Check leading zeroes
95+
check(10, 0, 0, "010");
96+
check(10, 20, 0, "010.020");
97+
check(10, 20, 30, "010.020.030");
98+
check(10000, 100, 100, "000010000.00100.00100");
99+
}
100+
101+
#[test]
102+
#[should_panic = "too many parts to version"]
103+
fn test_too_many_version_parts() {
104+
let _ = darwin_impl::parse_os_version(b"1.2.3.4");
105+
}
106+
107+
#[test]
108+
#[should_panic = "found invalid digit when parsing version"]
109+
fn test_macro_with_identifiers() {
110+
let _ = darwin_impl::parse_os_version(b"A.B");
111+
}
112+
113+
#[test]
114+
#[should_panic = "found empty version number part"]
115+
fn test_empty_version() {
116+
let _ = darwin_impl::parse_os_version(b"");
117+
}
118+
119+
#[test]
120+
#[should_panic = "found invalid digit when parsing version"]
121+
fn test_only_period() {
122+
let _ = darwin_impl::parse_os_version(b".");
123+
}
124+
125+
#[test]
126+
#[should_panic = "found invalid digit when parsing version"]
127+
fn test_has_leading_period() {
128+
let _ = darwin_impl::parse_os_version(b".1");
129+
}
130+
131+
#[test]
132+
#[should_panic = "found empty version number part"]
133+
fn test_has_trailing_period() {
134+
let _ = darwin_impl::parse_os_version(b"1.");
135+
}
136+
137+
#[test]
138+
#[should_panic = "major version is too large"]
139+
fn test_major_too_large() {
140+
let _ = darwin_impl::parse_os_version(b"100000");
141+
}
142+
143+
#[test]
144+
#[should_panic = "minor version is too large"]
145+
fn test_minor_too_large() {
146+
let _ = darwin_impl::parse_os_version(b"1.1000");
147+
}
148+
149+
#[test]
150+
#[should_panic = "patch version is too large"]
151+
fn test_patch_too_large() {
152+
let _ = darwin_impl::parse_os_version(b"1.1.1000");
153+
}

0 commit comments

Comments
 (0)
Please sign in to comment.