use {
crate::{
command_line_tools_sdks_directory, AppleSdk, DeveloperDirectory, Error, Platform,
PlatformDirectory, SdkPath, SdkVersion,
},
std::{
cmp::Ordering,
collections::HashSet,
fmt::{Display, Formatter},
path::PathBuf,
},
};
enum SdkSearchResolvedLocation {
None,
PlatformDirectories(Vec<PlatformDirectory>),
SdksDirectory(PathBuf),
SdkDirectory(PathBuf),
SdkDirectoryUnfiltered(PathBuf),
}
impl SdkSearchResolvedLocation {
fn apply_sdk_filter(&self) -> bool {
!matches!(self, Self::SdkDirectoryUnfiltered(_))
}
}
#[derive(Clone, Debug)]
pub enum SdkSearchLocation {
SdkRootEnv,
DeveloperDirEnv,
SystemXcode,
CommandLineTools,
XcodeSelectPaths,
XcodeSelect,
SystemXcodes,
Developer(DeveloperDirectory),
Sdks(PathBuf),
Sdk(PathBuf),
}
impl Display for SdkSearchLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::SdkRootEnv => f.write_str("SDKROOT environment variable"),
Self::DeveloperDirEnv => f.write_str("DEVELOPER_DIR environment variable"),
Self::SystemXcode => f.write_str("System-installed Xcode application"),
Self::CommandLineTools => f.write_str("Xcode Command Line Tools installation"),
Self::XcodeSelectPaths => {
f.write_str("Internal xcode-select paths (`/var/db/xcode_select_link`)")
}
Self::XcodeSelect => f.write_str("xcode-select"),
Self::SystemXcodes => f.write_str("All system-installed Xcode applications"),
Self::Developer(dir) => {
f.write_fmt(format_args!("Developer Directory {}", dir.path().display()))
}
Self::Sdks(path) => f.write_fmt(format_args!("SDKs directory {}", path.display())),
Self::Sdk(path) => f.write_fmt(format_args!("SDK directory {}", path.display())),
}
}
}
impl SdkSearchLocation {
fn is_terminal(&self) -> bool {
matches!(self, Self::SdkRootEnv | Self::DeveloperDirEnv)
}
fn resolve_location(&self) -> Result<SdkSearchResolvedLocation, Error> {
match self {
Self::SdkRootEnv => {
if let Some(path) = std::env::var_os("SDKROOT") {
let path = PathBuf::from(path);
if path.exists() {
Ok(SdkSearchResolvedLocation::SdkDirectoryUnfiltered(path))
} else {
Err(Error::PathNotSdk(path))
}
} else {
Ok(SdkSearchResolvedLocation::None)
}
}
Self::DeveloperDirEnv => {
if let Some(dir) = DeveloperDirectory::from_env()? {
Ok(SdkSearchResolvedLocation::PlatformDirectories(
dir.platforms()?,
))
} else {
Ok(SdkSearchResolvedLocation::None)
}
}
Self::SystemXcode => {
if let Some(dir) = DeveloperDirectory::default_xcode() {
Ok(SdkSearchResolvedLocation::PlatformDirectories(
dir.platforms()?,
))
} else {
Ok(SdkSearchResolvedLocation::None)
}
}
Self::CommandLineTools => {
if let Some(path) = command_line_tools_sdks_directory() {
Ok(SdkSearchResolvedLocation::SdksDirectory(path))
} else {
Ok(SdkSearchResolvedLocation::None)
}
}
Self::XcodeSelectPaths => {
if let Some(dir) = DeveloperDirectory::from_xcode_select_paths()? {
Ok(SdkSearchResolvedLocation::PlatformDirectories(
dir.platforms()?,
))
} else {
Ok(SdkSearchResolvedLocation::None)
}
}
Self::XcodeSelect => Ok(SdkSearchResolvedLocation::PlatformDirectories(
DeveloperDirectory::from_xcode_select()?.platforms()?,
)),
Self::SystemXcodes => Ok(SdkSearchResolvedLocation::PlatformDirectories(
DeveloperDirectory::find_system_xcodes()?
.into_iter()
.map(|dir| dir.platforms())
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>(),
)),
Self::Developer(dir) => Ok(SdkSearchResolvedLocation::PlatformDirectories(
dir.platforms()?,
)),
Self::Sdks(path) => Ok(SdkSearchResolvedLocation::SdksDirectory(path.clone())),
Self::Sdk(path) => Ok(SdkSearchResolvedLocation::SdkDirectory(path.clone())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SdkSorting {
None,
VersionDescending,
VersionAscending,
}
impl Display for SdkSorting {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::None => "nothing",
Self::VersionDescending => "descending version",
Self::VersionAscending => "ascending version",
})
}
}
impl SdkSorting {
pub fn compare_version(&self, a: Option<&SdkVersion>, b: Option<&SdkVersion>) -> Ordering {
match self {
Self::None => Ordering::Equal,
Self::VersionAscending => match (a, b) {
(Some(a), Some(b)) => a.cmp(b),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
},
Self::VersionDescending => match (a, b) {
(Some(a), Some(b)) => b.cmp(a),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
},
}
}
}
pub enum SdkSearchEvent {
SearchingLocation(SdkSearchLocation),
PlatformDirectoryInclude(PathBuf),
PlatformDirectoryExclude(PathBuf),
SdkFilterSkip(SdkPath),
SdkFilterMatch(SdkPath),
SdkFilterExclude(SdkPath, String),
Sorting(usize, SdkSorting),
}
impl Display for SdkSearchEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::SearchingLocation(location) => f.write_fmt(format_args!("searching {location}")),
Self::PlatformDirectoryInclude(path) => f.write_fmt(format_args!(
"searching Platform directory {}",
path.display()
)),
Self::PlatformDirectoryExclude(path) => f.write_fmt(format_args!(
"excluding Platform directory {}",
path.display()
)),
Self::SdkFilterSkip(sdk) => f.write_fmt(format_args!("SDK {sdk} bypasses filter")),
Self::SdkFilterMatch(sdk) => {
f.write_fmt(format_args!("SDK {sdk} matches search filter"))
}
Self::SdkFilterExclude(sdk, reason) => {
f.write_fmt(format_args!("SDK {sdk} discarded because {reason}"))
}
Self::Sorting(count, sorting) => {
f.write_fmt(format_args!("sorting {count} SDKs by {sorting}"))
}
}
}
}
pub type SdkProgressCallback = fn(SdkSearchEvent);
#[derive(Clone)]
pub struct SdkSearch {
progress_callback: Option<SdkProgressCallback>,
locations: Vec<SdkSearchLocation>,
platform: Option<Platform>,
minimum_version: Option<SdkVersion>,
maximum_version: Option<SdkVersion>,
deployment_target: Option<(String, SdkVersion)>,
sorting: SdkSorting,
}
impl Default for SdkSearch {
fn default() -> Self {
Self {
progress_callback: None,
locations: vec![
SdkSearchLocation::SdkRootEnv,
SdkSearchLocation::DeveloperDirEnv,
SdkSearchLocation::XcodeSelectPaths,
SdkSearchLocation::SystemXcode,
SdkSearchLocation::CommandLineTools,
],
platform: None,
minimum_version: None,
maximum_version: None,
deployment_target: None,
sorting: SdkSorting::None,
}
}
}
impl SdkSearch {
pub fn empty() -> Self {
let mut s = Self::default();
s.locations.clear();
s
}
pub fn progress_callback(mut self, callback: SdkProgressCallback) -> Self {
self.progress_callback = Some(callback);
self
}
pub fn location(mut self, location: SdkSearchLocation) -> Self {
self.locations.push(location);
self
}
pub fn platform(mut self, platform: Platform) -> Self {
self.platform = Some(platform);
self
}
pub fn minimum_version(mut self, version: impl Into<SdkVersion>) -> Self {
self.minimum_version = Some(version.into());
self
}
pub fn maximum_version(mut self, version: impl Into<SdkVersion>) -> Self {
self.maximum_version = Some(version.into());
self
}
pub fn deployment_target(
mut self,
target: impl ToString,
version: impl Into<SdkVersion>,
) -> Self {
self.deployment_target = Some((target.to_string(), version.into()));
self
}
pub fn sorting(mut self, sorting: SdkSorting) -> Self {
self.sorting = sorting;
self
}
pub fn search<SDK: AppleSdk>(&self) -> Result<Vec<SDK>, Error> {
let mut sdks = vec![];
let mut searched_platform_dirs = HashSet::new();
let mut searched_sdks_dirs = HashSet::new();
for location in &self.locations {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SearchingLocation(location.clone()));
}
let resolved = location.resolve_location()?;
let candidate_sdks = match &resolved {
SdkSearchResolvedLocation::None => {
vec![]
}
SdkSearchResolvedLocation::PlatformDirectories(dirs) => dirs
.iter()
.filter(|dir| {
if let Some(wanted_platform) = &self.platform {
if &dir.platform == wanted_platform {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::PlatformDirectoryInclude(dir.path.clone()));
}
true
} else {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::PlatformDirectoryExclude(dir.path.clone()));
}
false
}
} else {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::PlatformDirectoryInclude(dir.path.clone()));
}
true
}
})
.filter(|dir| {
if searched_platform_dirs.contains(dir.path()) {
false
} else {
searched_platform_dirs.insert(dir.path().to_path_buf());
true
}
})
.map(|dir| dir.find_sdks::<SDK>())
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>(),
SdkSearchResolvedLocation::SdksDirectory(path) => {
if searched_sdks_dirs.contains(path) {
vec![]
} else {
searched_sdks_dirs.insert(path.clone());
SDK::find_in_directory(path)?
}
}
SdkSearchResolvedLocation::SdkDirectory(path)
| SdkSearchResolvedLocation::SdkDirectoryUnfiltered(path) => {
vec![SDK::from_directory(path)?]
}
};
let mut added_count = 0;
for sdk in candidate_sdks {
let include = if resolved.apply_sdk_filter() {
self.filter_sdk(&sdk)?
} else {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterSkip(sdk.sdk_path()));
}
true
};
if include {
sdks.push(sdk);
added_count += 1;
}
}
if location.is_terminal() && added_count > 0 {
break;
}
}
if self.sorting != SdkSorting::None {
sdks.sort_by(|a, b| self.sorting.compare_version(a.version(), b.version()))
}
Ok(sdks)
}
pub fn filter_sdk<SDK: AppleSdk>(&self, sdk: &SDK) -> Result<bool, Error> {
let sdk_path = sdk.sdk_path();
if let Some(wanted_platform) = &self.platform {
if sdk.platform() != wanted_platform {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!(
"platform {} != {}",
sdk.platform().filesystem_name(),
wanted_platform.filesystem_name()
),
));
}
return Ok(false);
}
}
if let Some(min_version) = &self.minimum_version {
if let Some(sdk_version) = sdk.version() {
if sdk_version < min_version {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!("SDK version {sdk_version} < minimum version {min_version}"),
));
}
return Ok(false);
}
} else {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!("Unknown SDK version fails to meet minimum version {min_version}"),
));
}
return Ok(false);
}
}
if let Some(max_version) = &self.maximum_version {
if let Some(sdk_version) = sdk.version() {
if sdk_version > max_version {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!("SDK version {sdk_version} > maximum version {max_version}"),
));
}
return Ok(false);
}
} else {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!("Unknown SDK version fails to meet maximum version {max_version}"),
));
}
return Ok(false);
}
}
if let Some((target, version)) = &self.deployment_target {
if !sdk.supports_deployment_target(target, version)? {
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterExclude(
sdk_path,
format!("does not support deployment target {target}:{version}"),
));
}
return Ok(false);
}
}
if let Some(cb) = &self.progress_callback {
cb(SdkSearchEvent::SdkFilterMatch(sdk_path));
}
Ok(true)
}
}