-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
447 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,384 @@ | ||
//! Utilities for checking the runtime availability of APIs. | ||
//! | ||
//! TODO: Upstream some of this to `std`? | ||
use core::cmp::Ordering; | ||
|
||
/// The size of the fields here are limited by Mach-O's `LC_BUILD_VERSION`. | ||
#[repr(C)] | ||
#[derive(Clone, Copy, Debug, Default)] | ||
pub struct OSVersion { | ||
// Shuffle the versions around a little so that OSVersion has the same bit | ||
// representation as the `u32` returned from `to_u32`, allowing | ||
// comparisons to compile down to just between two `u32`s. | ||
#[cfg(target_endian = "little")] | ||
pub patch: u8, | ||
#[cfg(target_endian = "little")] | ||
pub minor: u8, | ||
#[cfg(target_endian = "little")] | ||
pub major: u16, | ||
|
||
#[cfg(target_endian = "big")] | ||
pub major: u16, | ||
#[cfg(target_endian = "big")] | ||
pub minor: u8, | ||
#[cfg(target_endian = "big")] | ||
pub patch: u8, | ||
} | ||
|
||
impl OSVersion { | ||
/// Parse the version from a string at `const` time. | ||
#[track_caller] | ||
pub const fn from_str(version: &str) -> Self { | ||
#[track_caller] | ||
const fn parse_usize(mut bytes: &[u8]) -> (usize, &[u8]) { | ||
// Ensure we have at least one digit (that is not just a period). | ||
let mut ret: usize = if let Some((&ascii, rest)) = bytes.split_first() { | ||
bytes = rest; | ||
|
||
match ascii { | ||
b'0'..=b'9' => (ascii - b'0') as usize, | ||
_ => panic!("found invalid digit when parsing version"), | ||
} | ||
} else { | ||
panic!("found empty version number part") | ||
}; | ||
|
||
// Parse the remaining digits. | ||
while let Some((&ascii, rest)) = bytes.split_first() { | ||
let digit = match ascii { | ||
b'0'..=b'9' => ascii - b'0', | ||
_ => break, | ||
}; | ||
|
||
bytes = rest; | ||
|
||
// This handles leading zeroes as well. | ||
match ret.checked_mul(10) { | ||
Some(val) => match val.checked_add(digit as _) { | ||
Some(val) => ret = val, | ||
None => panic!("version is too large"), | ||
}, | ||
None => panic!("version is too large"), | ||
}; | ||
} | ||
|
||
(ret, bytes) | ||
} | ||
|
||
let bytes = version.as_bytes(); | ||
|
||
let (major, bytes) = parse_usize(bytes); | ||
if major > u16::MAX as usize { | ||
panic!("major version is too large"); | ||
} | ||
let major = major as u16; | ||
|
||
let bytes = if let Some((period, bytes)) = bytes.split_first() { | ||
if *period != b'.' { | ||
panic!("expected period between major and minor version") | ||
} | ||
bytes | ||
} else { | ||
return Self { | ||
major, | ||
minor: 0, | ||
patch: 0, | ||
}; | ||
}; | ||
|
||
let (minor, bytes) = parse_usize(bytes); | ||
if minor > u8::MAX as usize { | ||
panic!("minor version is too large"); | ||
} | ||
let minor = minor as u8; | ||
|
||
let bytes = if let Some((period, bytes)) = bytes.split_first() { | ||
if *period != b'.' { | ||
panic!("expected period after minor version") | ||
} | ||
bytes | ||
} else { | ||
return Self { | ||
major, | ||
minor, | ||
patch: 0, | ||
}; | ||
}; | ||
|
||
let (patch, bytes) = parse_usize(bytes); | ||
if patch > u8::MAX as usize { | ||
panic!("patch version is too large"); | ||
} | ||
let patch = patch as u8; | ||
|
||
if !bytes.is_empty() { | ||
panic!("too many parts to version"); | ||
} | ||
|
||
Self { | ||
major, | ||
minor, | ||
patch, | ||
} | ||
} | ||
|
||
/// Look up the current version at runtime. | ||
/// | ||
/// The version is cached for performance. | ||
#[inline] | ||
#[cfg(target_vendor = "apple")] | ||
fn current() -> Self { | ||
// This is mostly a re-implementation of: | ||
// <https://github.com/llvm/llvm-project/blob/llvmorg-19.1.1/compiler-rt/lib/builtins/os_version_check.c> | ||
// | ||
// Note that this doesn't work with [zippered] `dylib`s yet, though | ||
// that's probably fine, `rustc` doesn't support those either. | ||
// [zippered]: https://github.com/nico/hack/blob/be6e1a6885a9d5179558b37a0b4c36bec9c4d377/notes/catalyst.md#building-a-zippered-dylib | ||
|
||
// Rust does not yet support weak externs, and besides, I suspect it'd | ||
// be dangerous to use, see: | ||
// - https://reviews.llvm.org/D150397 | ||
// - https://github.com/llvm/llvm-project/issues/64227 | ||
|
||
// TODO | ||
DEPLOYMENT_TARGET | ||
} | ||
|
||
#[inline] | ||
#[cfg(not(target_vendor = "apple"))] | ||
fn current() -> Self { | ||
Self::default() | ||
} | ||
|
||
/// Pack the version into a `u32`. | ||
/// | ||
/// This is used for faster comparisons. | ||
#[inline] | ||
pub const fn to_u32(self) -> u32 { | ||
// See comments in `OSVersion`, this should compile down to nothing. | ||
let (major, minor, patch) = (self.major as u32, self.minor as u32, self.patch as u32); | ||
(major << 16) | (minor << 8) | patch | ||
} | ||
} | ||
|
||
impl PartialEq for OSVersion { | ||
#[inline] | ||
fn eq(&self, other: &Self) -> bool { | ||
self.to_u32() == other.to_u32() | ||
} | ||
} | ||
|
||
impl PartialOrd for OSVersion { | ||
#[inline] | ||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
self.to_u32().partial_cmp(&other.to_u32()) | ||
} | ||
} | ||
|
||
/// The deployment target for the current OS. | ||
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"); | ||
|
||
// GNUStep etc. don't have a concept of deployment target. | ||
#[cfg(not(target_vendor = "apple"))] | ||
let var = None; | ||
|
||
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. | ||
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 { | ||
// GNUStep etc. don't have a concept of deployment target. | ||
(0, 0, 0) | ||
}; | ||
|
||
// 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 | ||
// `target_abi` is not in our MSRV (introduced in 1.78), and because | ||
// we have no way of knowing if the architecture is `arm64e`. | ||
let min = if cfg!(all(target_os = "macos", target_arch = "aarch64")) { | ||
(11, 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, | ||
} | ||
} | ||
}; | ||
|
||
/// The combined availability. | ||
/// | ||
/// This generally works closely together with the `available!` macro to make | ||
/// syntax checking inside that easier. | ||
/// | ||
/// We use `#[cfg]`s explicitly to allow the user to omit an annotation for | ||
/// a specific platform if they are never gonna need it, while still failing | ||
/// with a compile error if the code ends up being compiled for that platform. | ||
#[derive(Clone, Copy, Debug, Default)] | ||
pub struct AvailableVersion { | ||
#[cfg(target_os = "macos")] | ||
pub macos: OSVersion, | ||
#[cfg(target_os = "ios")] | ||
pub ios: OSVersion, | ||
#[cfg(target_os = "tvos")] | ||
pub tvos: OSVersion, | ||
#[cfg(target_os = "watchos")] | ||
pub watchos: OSVersion, | ||
#[cfg(target_os = "visionos")] | ||
pub visionos: OSVersion, | ||
} | ||
|
||
#[inline] | ||
pub fn is_available(version: AvailableVersion) -> bool { | ||
#[cfg(target_os = "macos")] | ||
let version = version.macos; | ||
#[cfg(target_os = "ios")] | ||
let version = version.ios; | ||
#[cfg(target_os = "tvos")] | ||
let version = version.tvos; | ||
#[cfg(target_os = "watchos")] | ||
let version = version.watchos; | ||
#[cfg(target_os = "visionos")] | ||
let version = version.visionos; | ||
|
||
// Assume that things are always available on GNUStep etc. | ||
#[cfg(not(target_vendor = "apple"))] | ||
let version = OSVersion::default(); | ||
|
||
// If the deployment target is high enough, the API is always available. | ||
// | ||
// This check should be optimized away at compile time. | ||
if version <= DEPLOYMENT_TARGET { | ||
return true; | ||
} | ||
|
||
// Otherwise, compare against the version at runtime. | ||
version <= OSVersion::current() | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
use crate::__available_version; | ||
|
||
#[track_caller] | ||
fn check(expected: (u16, u8, u8), actual: OSVersion) { | ||
assert_eq!( | ||
actual, | ||
OSVersion { | ||
major: expected.0, | ||
minor: expected.1, | ||
patch: expected.2, | ||
} | ||
) | ||
} | ||
|
||
#[test] | ||
fn test_parse() { | ||
check((1, 0, 0), __available_version!(1)); | ||
check((1, 2, 0), __available_version!(1.2)); | ||
check((1, 2, 3), __available_version!(1.2.3)); | ||
check((9999, 99, 99), __available_version!(9999.99.99)); | ||
|
||
// Ensure that the macro handles leading zeroes correctly | ||
check((10, 0, 0), __available_version!(010)); | ||
check((10, 20, 0), __available_version!(010.020)); | ||
check((10, 20, 30), __available_version!(010.020.030)); | ||
check( | ||
(10000, 100, 100), | ||
__available_version!(000010000.00100.00100), | ||
); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "too many parts to version"] | ||
fn test_too_many_version_parts() { | ||
let _ = __available_version!(1.2.3 .4); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "found invalid digit when parsing version"] | ||
fn test_macro_with_identifiers() { | ||
let _ = __available_version!(A.B); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "found empty version number part"] | ||
fn test_empty_version() { | ||
let _ = __available_version!(); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "found invalid digit when parsing version"] | ||
fn test_only_period() { | ||
let _ = __available_version!(.); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "found invalid digit when parsing version"] | ||
fn test_has_leading_period() { | ||
let _ = __available_version!(.1); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "found empty version number part"] | ||
fn test_has_trailing_period() { | ||
let _ = __available_version!(1.); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "major version is too large"] | ||
fn test_major_too_large() { | ||
let _ = __available_version!(100000); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "minor version is too large"] | ||
fn test_minor_too_large() { | ||
let _ = __available_version!(1.1000); | ||
} | ||
|
||
#[test] | ||
#[should_panic = "patch version is too large"] | ||
fn test_patch_too_large() { | ||
let _ = __available_version!(1.1.1000); | ||
} | ||
} |
Oops, something went wrong.