apple_sdk/
lib.rs

1// Copyright 2022 Gregory Szorc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Interact with Apple SDKs.
10//!
11//! # Important Concepts
12//!
13//! A *developer directory* is a filesystem tree holding SDKs and tools.
14//! If you have Xcode installed, this is likely `/Applications/Xcode.app/Contents/Developer`.
15//!
16//! A *platform* is a target OS/environment that you build applications for.
17//! These typically correspond to `*.platform` directories under `Platforms`
18//! subdirectory in the *developer directory*. e.g.
19//! `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform`.
20//!
21//! An *SDK* holds header files, library stubs, and other files enabling you
22//! to compile applications targeting a *platform* for a supported version range.
23//! SDKs usually exist in an `SDKs` directory under a *platform* directory. e.g.
24//! `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/SDKs/MacOSX12.3.sdk`
25//! or `/Library/Developer/CommandLineTools/SDKs/MacOSX12.3.sdk`.
26//!
27//! # Developer Directories
28//!
29//! Developer Directories are modeled via the [DeveloperDirectory] struct. This
30//! type contains functions for locating developer directories and resolving the
31//! default developer directory to use.
32//!
33//! # Apple Platforms
34//!
35//! We model an abstract Apple platform via the [Platform] enum.
36//!
37//! A directory containing an Apple platform is represented by the
38//! [PlatformDirectory] struct.
39//!
40//! # Apple SDKs
41//!
42//! We model Apple SDKs using the [SimpleSdk] and [ParsedSdk] types. The
43//! latter requires the `parse` crate feature in order to activate support for
44//! parsing JSON and plist files.
45//!
46//! Both these types are essentially a reference to a directory. [SimpleSdk]
47//! is little more than a reference to a filesystem path. However, [ParsedSdk]
48//! parses the `SDKSettings.json` or `SDKSettings.plist` file within the SDK
49//! and is able to obtain rich metadata about the SDK, such as the names of
50//! machine architectures it can target, which OS versions it supports targeting,
51//! and more.
52//!
53//! Both these types implement the [AppleSdk] trait, which you'll likely want
54//! to import in order to use its APIs for searching for and constructing SDKs.
55//!
56//! # SDK Searching
57//!
58//! This crate supports searching for an appropriate SDK to use given search
59//! parameters and requirements. This functionality can be used to locate the
60//! most appropriate SDK from many available on the current system.
61//!
62//! This functionality is exposed through the [SdkSearch] struct. See its
63//! documentation for more.
64//!
65//! # Common Functionality
66//!
67//! To locate the default SDK to use, do something like this:
68//!
69//! ```
70//! use apple_sdk::{SdkSearch, Platform, SimpleSdk, SdkSorting, AppleSdk};
71//!
72//! // This search will honor the `SDKROOT` and `DEVELOPER_DIR` environment variables.
73//! let sdks = SdkSearch::default()
74//!     .platform(Platform::MacOsX)
75//!     // Ideally we'd call e.g. `.deployment_target("macosx", "11.0")` to require
76//!     // the SDK to support a specific deployment target. This requires the
77//!     // `ParsedSdk` type, which requires the `parse` crate feature.
78//!     .sorting(SdkSorting::VersionDescending)
79//!     .search::<SimpleSdk>()
80//!     .expect("failed to search for SDKs");
81//!
82//! if let Some(sdk) = sdks.first() {
83//!     println!("{}", sdk.sdk_path());
84//! }
85//! ```
86
87#[cfg(feature = "parse")]
88mod parsed_sdk;
89mod search;
90mod simple_sdk;
91
92use std::{
93    cmp::Ordering,
94    fmt::{Display, Formatter},
95    ops::Deref,
96    path::{Path, PathBuf},
97    process::{Command, ExitStatus, Stdio},
98    str::FromStr,
99};
100
101pub use crate::{search::*, simple_sdk::SimpleSdk};
102
103#[cfg(feature = "parse")]
104pub use crate::parsed_sdk::{
105    ParsedSdk, SdkSettingsJson, SdkSettingsJsonDefaultProperties, SupportedTarget,
106};
107
108/// Default install path for the Xcode command line tools.
109pub const COMMAND_LINE_TOOLS_DEFAULT_PATH: &str = "/Library/Developer/CommandLineTools";
110
111/// Default path to Xcode application.
112pub const XCODE_APP_DEFAULT_PATH: &str = "/Applications/Xcode.app";
113
114/// Relative path under Xcode.app directories defining a `Developer` directory.
115///
116/// This directory contains platforms, toolchains, etc.
117pub const XCODE_APP_RELATIVE_PATH_DEVELOPER: &str = "Contents/Developer";
118
119/// Error type for this crate.
120#[derive(Debug)]
121pub enum Error {
122    /// Error occurred when trying to read `xcode-select` paths.
123    XcodeSelectPathFailedReading(std::io::Error),
124    /// Error occurred when running `xcode-select`.
125    XcodeSelectRun(std::io::Error),
126    /// `xcode-select` did not run successfully.
127    XcodeSelectBadStatus(ExitStatus),
128    /// Generic I/O error.
129    Io(std::io::Error),
130    /// A developer directory could not be found.
131    DeveloperDirectoryNotFound,
132    /// A path is not a Developer Directory.
133    PathNotDeveloper(PathBuf),
134    /// A path is not an Apple Platform directory.
135    PathNotPlatform(PathBuf),
136    /// A path is not an Apple SDK.
137    PathNotSdk(PathBuf),
138    /// A version string could not be parsed.
139    VersionParse(String),
140    /// Certain functionality is not supported.
141    FunctionalityNotSupported(&'static str),
142    /// A plist value is not a dictionary.
143    PlistNotDictionary,
144    /// An expected plist key is missing.
145    ///
146    /// If you see this, it might represent a logic error in this crate.
147    PlistKeyMissing(String),
148    /// A plist key's value is not a dictionary.
149    ///
150    /// If you see this, it might represent a logic error in this crate.
151    PlistKeyNotDictionary(String),
152    /// A plist key's value is not a string.
153    ///
154    /// If you see this, it might represent a logic error in this crate.
155    PlistKeyNotString(String),
156    #[cfg(feature = "parse")]
157    SerdeJson(serde_json::Error),
158    #[cfg(feature = "plist")]
159    Plist(plist::Error),
160    /// Maybe a new target is added to rust toolchain.
161    UnknownTarget(String),
162}
163
164impl Display for Error {
165    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166        match self {
167            Self::XcodeSelectPathFailedReading(err) => {
168                f.write_fmt(format_args!("Error reading xcode-select paths: {err}"))
169            }
170            Self::XcodeSelectRun(err) => {
171                f.write_fmt(format_args!("Error running xcode-select: {err}"))
172            }
173            Self::XcodeSelectBadStatus(v) => {
174                f.write_fmt(format_args!("Error running xcode-select: {v}"))
175            }
176            Self::Io(err) => f.write_fmt(format_args!("I/O error: {err}")),
177            Self::DeveloperDirectoryNotFound => f.write_str("could not find a Developer Directory"),
178            Self::PathNotDeveloper(p) => f.write_fmt(format_args!(
179                "path is not a Developer directory: {}",
180                p.display()
181            )),
182            Self::PathNotPlatform(p) => f.write_fmt(format_args!(
183                "path is not an Apple Platform: {}",
184                p.display()
185            )),
186            Self::PathNotSdk(p) => {
187                f.write_fmt(format_args!("path is not an Apple SDK: {}", p.display()))
188            }
189            Self::VersionParse(s) => f.write_fmt(format_args!("malformed version string: {s}")),
190            Self::FunctionalityNotSupported(s) => f.write_fmt(format_args!("not supported: {s}")),
191            Self::PlistNotDictionary => f.write_str("plist value not a dictionary"),
192            Self::PlistKeyMissing(key) => f.write_fmt(format_args!("plist key missing: {key}")),
193            Self::PlistKeyNotDictionary(key) => {
194                f.write_fmt(format_args!("plist key not a dictionary: {key}"))
195            }
196            Self::PlistKeyNotString(key) => {
197                f.write_fmt(format_args!("plist key not a string: {key}"))
198            }
199            #[cfg(feature = "parse")]
200            Self::SerdeJson(err) => f.write_fmt(format_args!("JSON parsing error: {err}")),
201            #[cfg(feature = "plist")]
202            Self::Plist(err) => f.write_fmt(format_args!("plist error: {err}")),
203            Self::UnknownTarget(target) => f.write_fmt(format_args!("unknown target: {target}")),
204        }
205    }
206}
207
208impl std::error::Error for Error {}
209
210impl From<std::io::Error> for Error {
211    fn from(e: std::io::Error) -> Self {
212        Self::Io(e)
213    }
214}
215
216#[cfg(feature = "parse")]
217impl From<serde_json::Error> for Error {
218    fn from(e: serde_json::Error) -> Self {
219        Self::SerdeJson(e)
220    }
221}
222
223#[cfg(feature = "parse")]
224impl From<plist::Error> for Error {
225    fn from(e: plist::Error) -> Self {
226        Self::Plist(e)
227    }
228}
229
230/// A known Apple platform type.
231///
232/// Instances are equivalent to each other if their filesystem representation
233/// is equivalent. This ensures that [Self::Unknown] will equate to a variant of
234/// its string value matches a known type.
235#[derive(Clone, Debug)]
236pub enum Platform {
237    AppleTvOs,
238    AppleTvSimulator,
239    DriverKit,
240    IPhoneOs,
241    IPhoneSimulator,
242    MacOsX,
243    WatchOs,
244    WatchSimulator,
245    XrOs,
246    XrOsSimulator,
247    Unknown(String),
248}
249
250impl FromStr for Platform {
251    type Err = Error;
252
253    fn from_str(s: &str) -> Result<Self, Self::Err> {
254        // We do a case insensitive comparison so we're lenient in parsing input.
255        match s.to_ascii_lowercase().as_str() {
256            "appletvos" => Ok(Self::AppleTvOs),
257            "appletvsimulator" => Ok(Self::AppleTvSimulator),
258            "driverkit" => Ok(Self::DriverKit),
259            "iphoneos" => Ok(Self::IPhoneOs),
260            "iphonesimulator" => Ok(Self::IPhoneSimulator),
261            "macosx" => Ok(Self::MacOsX),
262            "watchos" => Ok(Self::WatchOs),
263            "watchsimulator" => Ok(Self::WatchSimulator),
264            "xros" => Ok(Self::XrOs),
265            "xrsimulator" => Ok(Self::XrOsSimulator),
266            v => Ok(Self::Unknown(v.to_string())),
267        }
268    }
269}
270
271impl PartialEq for Platform {
272    fn eq(&self, other: &Self) -> bool {
273        self.filesystem_name().eq(other.filesystem_name())
274    }
275}
276
277impl Eq for Platform {}
278
279impl TryFrom<&str> for Platform {
280    type Error = Error;
281
282    fn try_from(s: &str) -> Result<Self, Self::Error> {
283        Self::from_str(s)
284    }
285}
286
287impl Platform {
288    /// Attempt to construct an instance from a filesystem path to a platform directory.
289    ///
290    /// The argument should be the path of a `*.platform` directory. e.g.
291    /// `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform`.
292    ///
293    /// Will return [Error::PathNotPlatform] if this does not appear to be a known
294    /// platform path.
295    pub fn from_platform_path(p: &Path) -> Result<Self, Error> {
296        let (name, platform) = p
297            .file_name()
298            .ok_or_else(|| Error::PathNotPlatform(p.to_path_buf()))?
299            .to_str()
300            .ok_or_else(|| Error::PathNotPlatform(p.to_path_buf()))?
301            .split_once('.')
302            .ok_or_else(|| Error::PathNotPlatform(p.to_path_buf()))?;
303
304        if platform == "platform" {
305            Self::from_str(name)
306        } else {
307            Err(Error::PathNotPlatform(p.to_path_buf()))
308        }
309    }
310
311    /// Attempt to construct an instance from a target triple.
312    ///
313    /// The argument should be a target triple of a Rust toolchain. e.g.
314    /// `x86_64-apple-darwin`.
315    ///
316    /// Will return [Error::UnknownTarget] if this does not appear to be a known
317    /// target triple.
318    pub fn from_target_triple(target: &str) -> Result<Self, Error> {
319        let platform = match target {
320            target if target.ends_with("-apple-darwin") => Self::MacOsX,
321            "i386-apple-ios" | "x86_64-apple-ios" => Self::IPhoneSimulator,
322            target if target.ends_with("-apple-ios-sim") => Platform::IPhoneSimulator,
323            target if target.ends_with("-apple-ios") => Platform::IPhoneOs,
324            target if target.ends_with("-apple-ios-macabi") => Platform::IPhoneOs,
325            "i386-apple-watchos" => Self::WatchSimulator,
326            target if target.ends_with("-apple-watchos-sim") => Self::WatchSimulator,
327            target if target.ends_with("-apple-watchos") => Platform::WatchOs,
328            "x86_64-apple-tvos" => Self::AppleTvSimulator,
329            target if target.ends_with("-apple-tvos") => Platform::AppleTvOs,
330            "aarch64-apple-xros-sim" => Platform::XrOsSimulator,
331            target if target.ends_with("-apple-xros") => Platform::XrOs,
332            _ => return Err(Error::UnknownTarget(target.to_string())),
333        };
334        Ok(platform)
335    }
336
337    /// Obtain the name of this platform as used in filesystem paths.
338    ///
339    /// This is just the platform part of the name without the trailing
340    /// `.platform`. This string appears in the `*.platform` directory names
341    /// as well as in SDK directory names preceding the trailing `.sdk` and
342    /// optional SDK version.
343    pub fn filesystem_name(&self) -> &str {
344        match self {
345            Self::AppleTvOs => "AppleTVOS",
346            Self::AppleTvSimulator => "AppleTVSimulator",
347            Self::DriverKit => "DriverKit",
348            Self::IPhoneOs => "iPhoneOS",
349            Self::IPhoneSimulator => "iPhoneSimulator",
350            Self::MacOsX => "MacOSX",
351            Self::WatchOs => "WatchOS",
352            Self::WatchSimulator => "WatchSimulator",
353            Self::XrOs => "XROS",
354            Self::XrOsSimulator => "XRSimulator",
355            Self::Unknown(v) => v,
356        }
357    }
358
359    /// Obtain the directory name of this platform.
360    ///
361    /// This simply appends `.platform` to [Self::filesystem_name()].
362    pub fn directory_name(&self) -> String {
363        format!("{}.platform", self.filesystem_name())
364    }
365
366    /// Obtain the path of this platform relative to a developer directory root.
367    pub fn path_in_developer_directory(&self, developer_directory: impl AsRef<Path>) -> PathBuf {
368        developer_directory
369            .as_ref()
370            .join("Platforms")
371            .join(self.directory_name())
372    }
373}
374
375/// Represents an Apple Platform directory.
376///
377/// This is just a thin abstraction over a filesystem path and a [Platform] instance.
378///
379/// Equivalence and sorting are implemented in terms of the path component
380/// only. The assumption here is the [Platform] is fully derived from the filesystem
381/// path and this derivation is deterministic.
382pub struct PlatformDirectory {
383    /// The filesystem path to this directory.
384    path: PathBuf,
385
386    /// The platform within this directory.
387    platform: Platform,
388}
389
390impl PlatformDirectory {
391    /// Attempt to construct an instance from a filesystem path.
392    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
393        let path = path.as_ref().to_path_buf();
394        let platform = Platform::from_platform_path(&path)?;
395
396        Ok(Self { path, platform })
397    }
398
399    /// The filesystem path of this instance.
400    pub fn path(&self) -> &Path {
401        &self.path
402    }
403
404    /// The filesystem path to the directory holding SDKs.
405    ///
406    /// The returned path is not validated to exist.
407    pub fn sdks_path(&self) -> PathBuf {
408        self.path.join("Developer").join("SDKs")
409    }
410
411    /// Finds SDKs in this platform directory.
412    ///
413    /// The type of SDK to resolve must be specified by the caller.
414    ///
415    /// This function is a simple wrapper around [AppleSdk::find_in_directory()] looking
416    /// under the `Developer/SDKs` directory, which is where SDKs are located in platform
417    /// directories.
418    pub fn find_sdks<T: AppleSdk>(&self) -> Result<Vec<T>, Error> {
419        T::find_in_directory(&self.sdks_path())
420    }
421}
422
423impl AsRef<Path> for PlatformDirectory {
424    fn as_ref(&self) -> &Path {
425        &self.path
426    }
427}
428
429impl AsRef<Platform> for PlatformDirectory {
430    fn as_ref(&self) -> &Platform {
431        &self.platform
432    }
433}
434
435impl Deref for PlatformDirectory {
436    type Target = Platform;
437
438    fn deref(&self) -> &Self::Target {
439        &self.platform
440    }
441}
442
443impl PartialEq for PlatformDirectory {
444    fn eq(&self, other: &Self) -> bool {
445        self.path.eq(&other.path)
446    }
447}
448
449impl Eq for PlatformDirectory {}
450
451impl PartialOrd for PlatformDirectory {
452    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
453        Some(self.cmp(other))
454    }
455}
456
457impl Ord for PlatformDirectory {
458    fn cmp(&self, other: &Self) -> Ordering {
459        self.path.cmp(&other.path)
460    }
461}
462
463/// A directory containing Apple platforms, SDKs, and other tools.
464#[derive(Clone, Debug, Eq, PartialEq)]
465pub struct DeveloperDirectory {
466    path: PathBuf,
467}
468
469impl AsRef<Path> for DeveloperDirectory {
470    fn as_ref(&self) -> &Path {
471        &self.path
472    }
473}
474
475impl From<&Path> for DeveloperDirectory {
476    fn from(p: &Path) -> Self {
477        Self {
478            path: p.to_path_buf(),
479        }
480    }
481}
482
483impl From<PathBuf> for DeveloperDirectory {
484    fn from(path: PathBuf) -> Self {
485        Self { path }
486    }
487}
488
489impl From<&PathBuf> for DeveloperDirectory {
490    fn from(path: &PathBuf) -> Self {
491        Self { path: path.clone() }
492    }
493}
494
495impl DeveloperDirectory {
496    /// Resolve an instance from the `DEVELOPER_DIR` environment variable.
497    ///
498    /// This environment variable is used by convention to override default search
499    /// locations for the developer directory.
500    ///
501    /// If `DEVELOPER_DIR` is defined, the value/path is validated for existence
502    /// and an error is returned if it doesn't exist.
503    ///
504    /// If `DEVELOPER_DIR` isn't defined, returns `Ok(None)`.
505    pub fn from_env() -> Result<Option<Self>, Error> {
506        if let Some(value) = std::env::var_os("DEVELOPER_DIR") {
507            let path = PathBuf::from(value);
508
509            if path.exists() {
510                Ok(Some(Self { path }))
511            } else {
512                Err(Error::PathNotDeveloper(path))
513            }
514        } else {
515            Ok(None)
516        }
517    }
518
519    /// Attempt to resolve an instance by checking the paths that
520    /// `xcode-select --switch` configures. If there is no path configured,
521    /// this returns `None`.
522    ///
523    /// This checks, in order:
524    /// - The path pointed to by `/var/db/xcode_select_link`.
525    /// - The path pointed to by `/usr/share/xcode-select/xcode_dir_link`
526    ///   (legacy, previously created by `xcode-select`).
527    /// - The path stored in `/usr/share/xcode-select/xcode_dir_path`
528    ///   (legacy, previously created by `xcode-select`).
529    ///
530    /// There are no sources available for `xcode-select`, so we do not know
531    /// if these are the only paths that `xcode-select` uses. We can be fairly
532    /// sure, though, since the logic has been reverse-engineered
533    /// [several][darling-xcselect] [times][bouldev-xcselect].
534    ///
535    /// The exact list of paths that `apple-sdk` searches here is an
536    /// implementation detail, and may change in the future (e.g. if
537    /// `xcode-select` is changed to use a different set of paths).
538    ///
539    /// [darling-xcselect]: https://github.com/darlinghq/darling/blob/773e9874cf38fdeb9518f803e041924e255d0ebe/src/xcselect/xcselect.c#L138-L197
540    /// [bouldev-xcselect]: https://github.com/bouldev/libxcselect-shim/blob/c5387de92c30ab16cbfc8012e98c74c718ce8eff/src/libxcselect/xcselect_get_developer_dir_path.c#L39-L86
541    pub fn from_xcode_select_paths() -> Result<Option<Self>, Error> {
542        match std::fs::read_link("/var/db/xcode_select_link") {
543            Ok(path) => return Ok(Some(Self { path })),
544            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
545                // Ignore if the path does not exist
546            }
547            Err(err) => return Err(Error::XcodeSelectPathFailedReading(err)),
548        }
549
550        match std::fs::read_link("/usr/share/xcode-select/xcode_dir_link") {
551            Ok(path) => return Ok(Some(Self { path })),
552            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
553                // Ignore if the path does not exist
554            }
555            Err(err) => return Err(Error::XcodeSelectPathFailedReading(err)),
556        }
557
558        match std::fs::read_to_string("/usr/share/xcode-select/xcode_dir_path") {
559            Ok(s) => {
560                let path = PathBuf::from(s.trim_end_matches('\n'));
561                return Ok(Some(Self { path }));
562            }
563            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
564                // Ignore if the path does not exist
565            }
566            Err(err) => return Err(Error::XcodeSelectPathFailedReading(err)),
567        }
568
569        Ok(None)
570    }
571
572    /// Attempt to resolve an instance by running `xcode-select`.
573    ///
574    /// The output from `xcode-select` is implicitly trusted and no validation
575    /// of the path is performed.
576    pub fn from_xcode_select() -> Result<Self, Error> {
577        let output = Command::new("xcode-select")
578            .args(["--print-path"])
579            .stderr(Stdio::null())
580            .output()
581            .map_err(Error::XcodeSelectRun)?;
582
583        if output.status.success() {
584            // We should arguably use OsString here. Keep it simple until someone
585            // complains.
586            let path = String::from_utf8_lossy(&output.stdout);
587            let path = PathBuf::from(path.trim());
588
589            Ok(Self { path })
590        } else {
591            Err(Error::XcodeSelectBadStatus(output.status))
592        }
593    }
594
595    /// Attempt to resolve an instance from the default Xcode.app location.
596    ///
597    /// This looks for a system installed `Xcode.app` and for the developer
598    /// directory within. If found, returns `Some`. If not, returns `None`.
599    pub fn default_xcode() -> Option<Self> {
600        let path = PathBuf::from(XCODE_APP_DEFAULT_PATH).join(XCODE_APP_RELATIVE_PATH_DEVELOPER);
601
602        if path.exists() {
603            Some(Self { path })
604        } else {
605            None
606        }
607    }
608
609    /// Finds all `Developer` directories for system installed Xcode applications.
610    ///
611    /// This is a convenience method for [find_system_xcode_applications()] plus
612    /// resolving the `Developer` directory and filtering on missing items.
613    ///
614    /// It will return all available `Developer` directories for all Xcode installs
615    /// under `/Applications`.
616    pub fn find_system_xcodes() -> Result<Vec<Self>, Error> {
617        Ok(find_system_xcode_applications()?
618            .into_iter()
619            .filter_map(|p| {
620                let path = p.join(XCODE_APP_RELATIVE_PATH_DEVELOPER);
621
622                if path.exists() {
623                    Some(Self { path })
624                } else {
625                    None
626                }
627            })
628            .collect::<Vec<_>>())
629    }
630
631    /// Attempt to find a Developer Directory using reasonable semantics.
632    ///
633    /// This is probably what most end-users want to use for resolving the path to a
634    /// Developer Directory.
635    ///
636    /// This is a convenience function for calling other APIs on this type to resolve
637    /// the default instance.
638    ///
639    /// In priority order:
640    ///
641    /// 1. `DEVELOPER_DIR`
642    /// 2. System Xcode.app application.
643    /// 3. `xcode-select` output.
644    ///
645    /// Errors only if `DEVELOPER_DIR` is defined and it points to an invalid path.
646    /// Errors from running `xcode-select` are ignored.
647    pub fn find_default() -> Result<Option<Self>, Error> {
648        if let Some(v) = Self::from_env()? {
649            Ok(Some(v))
650        } else if let Some(v) = Self::default_xcode() {
651            Ok(Some(v))
652        } else if let Ok(v) = Self::from_xcode_select() {
653            Ok(Some(v))
654        } else {
655            Ok(None)
656        }
657    }
658
659    /// Find the Developer Directory and error if not found.
660    ///
661    /// This is a wrapper around [Self::find_default()] that will error if no Developer Directory
662    /// could be found.
663    pub fn find_default_required() -> Result<Self, Error> {
664        if let Some(v) = Self::find_default()? {
665            Ok(v)
666        } else {
667            Err(Error::DeveloperDirectoryNotFound)
668        }
669    }
670
671    /// The filesystem path to this developer directory.
672    pub fn path(&self) -> &Path {
673        &self.path
674    }
675
676    /// The path to the directory containing platforms.
677    pub fn platforms_path(&self) -> PathBuf {
678        self.path.join("Platforms")
679    }
680
681    /// Find platform directories within this developer directory.
682    ///
683    /// Platforms are defined by the presence of a `Platforms` directory under
684    /// the developer directory. This directory layout is only recognized
685    /// for modern Xcode layouts.
686    ///
687    /// Returns all discovered instances inside this developer directory.
688    ///
689    /// The return order is sorted and deterministic.
690    pub fn platforms(&self) -> Result<Vec<PlatformDirectory>, Error> {
691        let platforms_path = self.platforms_path();
692
693        let dir = match std::fs::read_dir(platforms_path) {
694            Ok(v) => Ok(v),
695            Err(e) => {
696                if e.kind() == std::io::ErrorKind::NotFound {
697                    return Ok(vec![]);
698                } else {
699                    Err(Error::from(e))
700                }
701            }
702        }?;
703
704        let mut res = vec![];
705
706        for entry in dir {
707            let entry = entry?;
708
709            if let Ok(platform) = PlatformDirectory::from_path(entry.path()) {
710                res.push(platform);
711            }
712        }
713
714        // Make deterministic.
715        res.sort();
716
717        Ok(res)
718    }
719
720    /// Find SDKs within this developer directory.
721    ///
722    /// This is a convenience method for calling [Self::platforms()] +
723    /// [PlatformDirectory::find_sdks()] and chaining the results.
724    pub fn sdks<SDK: AppleSdk>(&self) -> Result<Vec<SDK>, Error> {
725        Ok(self
726            .platforms()?
727            .into_iter()
728            .map(|platform| Ok(platform.find_sdks()?.into_iter()))
729            .collect::<Result<Vec<_>, Error>>()?
730            .into_iter()
731            .flatten()
732            .collect::<Vec<_>>())
733    }
734}
735
736/// Obtain the path to SDKs within an Xcode Command Line Tools installation.
737///
738/// Returns [Some] if we found a path in the expected location or [None] otherwise.
739pub fn command_line_tools_sdks_directory() -> Option<PathBuf> {
740    let sdk_path = PathBuf::from(COMMAND_LINE_TOOLS_DEFAULT_PATH).join("SDKs");
741
742    if sdk_path.exists() {
743        Some(sdk_path)
744    } else {
745        None
746    }
747}
748
749/// Attempt to resolve all available Xcode applications in an `Applications` directory.
750///
751/// This function is a convenience method for iterating a directory
752/// and filtering for `Xcode*.app` entries.
753///
754/// No guarantee is made about whether the directory constitutes a working
755/// Xcode application.
756///
757/// The results are sorted according to the directory name. However, `Xcode.app` always
758/// sorts first so the default application name is always preferred.
759pub fn find_xcode_apps(applications_dir: &Path) -> Result<Vec<PathBuf>, Error> {
760    let dir = match std::fs::read_dir(applications_dir) {
761        Ok(v) => Ok(v),
762        Err(e) => {
763            if e.kind() == std::io::ErrorKind::NotFound {
764                return Ok(vec![]);
765            } else {
766                Err(Error::from(e))
767            }
768        }
769    }?;
770
771    let mut res = dir
772        .into_iter()
773        .map(|entry| {
774            let entry = entry?;
775
776            let name = entry.file_name();
777            let file_name = name.to_string_lossy();
778
779            if file_name.starts_with("Xcode") && file_name.ends_with(".app") {
780                Ok(Some(entry.path()))
781            } else {
782                Ok(None)
783            }
784        })
785        .collect::<Result<Vec<_>, Error>>()?
786        .into_iter()
787        .flatten()
788        .collect::<Vec<_>>();
789
790    // Make deterministic.
791    res.sort_by(|a, b| match (a.file_name(), b.file_name()) {
792        (Some(x), _) if x == "Xcode.app" => Ordering::Less,
793        (_, Some(x)) if x == "Xcode.app" => Ordering::Greater,
794        (_, _) => a.cmp(b),
795    });
796
797    Ok(res)
798}
799
800/// Find all system installed Xcode applications.
801///
802/// This is a convenience method for [find_xcode_apps()] looking under `/Applications`.
803/// This location is typically where Xcode is installed.
804pub fn find_system_xcode_applications() -> Result<Vec<PathBuf>, Error> {
805    find_xcode_apps(&PathBuf::from("/Applications"))
806}
807
808/// Represents an SDK version string.
809///
810/// This type attempts to apply semantic versioning onto SDK version strings
811/// without pulling in additional crates.
812///
813/// The version string is not validated for correctness at construction time:
814/// any string can be stored.
815///
816/// The string is interpreted as a `X.Y` or `X.Y.Z` semantic version string
817/// where each component is an integer.
818///
819/// For ordering, an invalid string is interpreted as the version `0.0.0` and
820/// therefore should always sort less than a well-formed version.
821#[derive(Clone, Debug, Eq, PartialEq)]
822pub struct SdkVersion {
823    value: String,
824}
825
826impl Display for SdkVersion {
827    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
828        self.value.fmt(f)
829    }
830}
831
832impl AsRef<str> for SdkVersion {
833    fn as_ref(&self) -> &str {
834        &self.value
835    }
836}
837
838impl From<String> for SdkVersion {
839    fn from(value: String) -> Self {
840        Self { value }
841    }
842}
843
844impl From<&str> for SdkVersion {
845    fn from(s: &str) -> Self {
846        Self::from(s.to_string())
847    }
848}
849
850impl From<&String> for SdkVersion {
851    fn from(s: &String) -> Self {
852        Self::from(s.to_string())
853    }
854}
855
856impl SdkVersion {
857    fn normalized_version(&self) -> Result<(u8, u8, u8), Error> {
858        let ints = self
859            .value
860            .split('.')
861            .map(|x| u8::from_str(x).map_err(|_| Error::VersionParse(self.value.to_string())))
862            .collect::<Result<Vec<_>, Error>>()?;
863
864        match ints.len() {
865            1 => Ok((ints[0], 0, 0)),
866            2 => Ok((ints[0], ints[1], 0)),
867            3 => Ok((ints[0], ints[1], ints[2])),
868            _ => Err(Error::VersionParse(self.value.to_string())),
869        }
870    }
871
872    /// Resolve a version string that adheres to Rust's semantic version string format.
873    ///
874    /// The returned string will have the form `X.Y.Z` where all components are
875    /// integers.
876    pub fn semantic_version(&self) -> Result<String, Error> {
877        let (x, y, z) = self.normalized_version()?;
878
879        Ok(format!("{x}.{y}.{z}"))
880    }
881}
882
883impl PartialOrd for SdkVersion {
884    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
885        Some(self.cmp(other))
886    }
887}
888
889impl Ord for SdkVersion {
890    fn cmp(&self, other: &Self) -> Ordering {
891        let a = self.normalized_version().unwrap_or((0, 0, 0));
892        let b = other.normalized_version().unwrap_or((0, 0, 0));
893
894        a.cmp(&b)
895    }
896}
897
898/// Represents an SDK path with metadata parsed from the path.
899#[derive(Clone, Debug)]
900pub struct SdkPath {
901    /// The filesystem path.
902    pub path: PathBuf,
903
904    /// The platform this SDK belongs to.
905    pub platform: Platform,
906
907    /// The version of the SDK.
908    ///
909    /// Only present if the version occurred in the directory name. Use
910    /// [AppleSdk] to parse SDK directories to reliably obtain the SDK version.
911    pub version: Option<SdkVersion>,
912}
913
914impl Display for SdkPath {
915    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
916        f.write_fmt(format_args!(
917            "{} (version: {}) SDK at {}",
918            self.platform.filesystem_name(),
919            if let Some(version) = &self.version {
920                version.value.as_str()
921            } else {
922                "unknown"
923            },
924            self.path.display()
925        ))
926    }
927}
928
929impl SdkPath {
930    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
931        let path = path.as_ref().to_path_buf();
932
933        let s = path
934            .file_name()
935            .ok_or_else(|| Error::PathNotSdk(path.clone()))?
936            .to_str()
937            .ok_or_else(|| Error::PathNotSdk(path.clone()))?;
938
939        let (prefix, sdk) = s
940            .rsplit_once('.')
941            .ok_or_else(|| Error::PathNotSdk(path.clone()))?;
942
943        if sdk != "sdk" {
944            return Err(Error::PathNotSdk(path));
945        }
946
947        // prefix can be a platform name (e.g. `MacOSX`) or a platform name + version
948        // (e.g. `MacOSX12.4`).
949        let (platform_name, version) = if let Some(first_digit) = prefix
950            .chars()
951            .enumerate()
952            .find_map(|(i, c)| if c.is_numeric() { Some(i) } else { None })
953        {
954            let (name, version) = prefix.split_at(first_digit);
955
956            (name, Some(version.to_string().into()))
957        } else {
958            (prefix, None)
959        };
960
961        let platform = Platform::from_str(platform_name)?;
962
963        Ok(Self {
964            path,
965            platform,
966            version,
967        })
968    }
969}
970
971/// Defines common behavior for types representing Apple SDKs.
972pub trait AppleSdk: Sized + AsRef<Path> {
973    /// Attempt to construct an instance from a filesystem directory.
974    ///
975    /// Implementations will likely error with [Error::PathNotSdk] or
976    /// [Error::Io] if the input path is not an Apple SDK.
977    fn from_directory(path: &Path) -> Result<Self, Error>;
978
979    /// Find Apple SDKs in a specified directory.
980    ///
981    /// Directory entries are often symlinks pointing to other directories.
982    /// SDKs are annotated with an `is_symlink` field to denote when this is
983    /// the case. Callers may want to filter out symlinked SDKs to avoid
984    /// duplicates.
985    fn find_in_directory(root: &Path) -> Result<Vec<Self>, Error> {
986        let dir = match std::fs::read_dir(root) {
987            Ok(v) => Ok(v),
988            Err(e) => {
989                if e.kind() == std::io::ErrorKind::NotFound {
990                    return Ok(vec![]);
991                } else {
992                    Err(Error::from(e))
993                }
994            }
995        }?;
996
997        let mut res = vec![];
998
999        for entry in dir {
1000            let entry = entry?;
1001
1002            match Self::from_directory(&entry.path()) {
1003                Ok(sdk) => {
1004                    res.push(sdk);
1005                }
1006                Err(Error::PathNotSdk(_)) => {}
1007                Err(err) => return Err(err),
1008            }
1009        }
1010
1011        Ok(res)
1012    }
1013
1014    /// Locate SDKs installed as part of the Xcode Command Line Tools.
1015    ///
1016    /// This is a convenience method for looking for SDKs in the `SDKs` directory
1017    /// under the default install path for the Xcode Command Line Tools.
1018    ///
1019    /// Returns `Ok(None)` if the Xcode Command Line Tools are not present in
1020    /// this directory or doesn't have an `SDKs` directory.
1021    fn find_command_line_tools_sdks() -> Result<Option<Vec<Self>>, Error> {
1022        if let Some(path) = command_line_tools_sdks_directory() {
1023            Ok(Some(Self::find_in_directory(&path)?))
1024        } else {
1025            Ok(None)
1026        }
1027    }
1028
1029    /// Obtain an [SdkPath] represent this SDK.
1030    fn sdk_path(&self) -> SdkPath {
1031        SdkPath {
1032            path: self.path().to_path_buf(),
1033            platform: self.platform().clone(),
1034            version: self.version().cloned(),
1035        }
1036    }
1037
1038    #[deprecated(since = "0.1.1", note = "plase use `sdk_path` instead")]
1039    fn as_sdk_path(&self) -> SdkPath {
1040        self.sdk_path()
1041    }
1042
1043    /// Obtain the filesystem path to this SDK.
1044    fn path(&self) -> &Path {
1045        self.as_ref()
1046    }
1047
1048    /// Whether this SDK path is a symlink.
1049    fn is_symlink(&self) -> bool;
1050
1051    /// The platform this SDK is for.
1052    fn platform(&self) -> &Platform;
1053
1054    /// Obtain the version string for this SDK.
1055    ///
1056    /// This should always be [Some] for [ParsedSdk]. It can be [None] if SDK
1057    /// metadata is not loaded and the version string isn't available from side-channels
1058    /// such as the directory name.
1059    fn version(&self) -> Option<&SdkVersion>;
1060
1061    /// Whether this SDK supports targeting the given target name at specified OS version.
1062    fn supports_deployment_target(
1063        &self,
1064        target_name: &str,
1065        target_version: &SdkVersion,
1066    ) -> Result<bool, Error>;
1067}
1068
1069#[cfg(test)]
1070mod test {
1071    use super::*;
1072
1073    #[test]
1074    fn find_system_xcode_applications() -> Result<(), Error> {
1075        let res = crate::find_system_xcode_applications()?;
1076
1077        if PathBuf::from(XCODE_APP_DEFAULT_PATH).exists() {
1078            assert!(!res.is_empty());
1079        }
1080
1081        Ok(())
1082    }
1083
1084    #[test]
1085    fn find_system_xcode_developer_directories() -> Result<(), Error> {
1086        let res = DeveloperDirectory::find_system_xcodes()?;
1087
1088        if PathBuf::from(XCODE_APP_DEFAULT_PATH).exists() {
1089            assert!(!res.is_empty());
1090        }
1091
1092        Ok(())
1093    }
1094
1095    #[test]
1096    fn find_all_platform_directories() -> Result<(), Error> {
1097        for dir in DeveloperDirectory::find_system_xcodes()? {
1098            for platform in dir.platforms()? {
1099                // Paths should agree.
1100                assert_eq!(
1101                    platform.path,
1102                    dir.platforms_path().join(platform.directory_name())
1103                );
1104                assert_eq!(
1105                    platform.path,
1106                    platform.path_in_developer_directory(dir.path())
1107                );
1108
1109                // Ensure we're able to parse all platform types in existence. We want
1110                // this to fail when Apple introduces new platforms so we can implement
1111                // support for the new platform!
1112                assert!(!matches!(platform.platform, Platform::Unknown(_)));
1113            }
1114        }
1115
1116        Ok(())
1117    }
1118
1119    #[test]
1120    fn apple_platform() -> Result<(), Error> {
1121        assert_eq!(Platform::from_str("macosx")?, Platform::MacOsX);
1122        assert_eq!(Platform::from_str("MacOSX")?, Platform::MacOsX);
1123
1124        Ok(())
1125    }
1126
1127    #[test]
1128    fn target_platform() -> Result<(), Error> {
1129        use Platform::*;
1130        fn test(target: &str, platform: Platform) {
1131            assert_eq!(Platform::from_target_triple(target).unwrap(), platform);
1132        }
1133        test("aarch64-apple-darwin", MacOsX);
1134        test("aarch64-apple-ios", IPhoneOs);
1135        test("aarch64-apple-ios-macabi", IPhoneOs);
1136        test("aarch64-apple-ios-sim", IPhoneSimulator);
1137        test("aarch64-apple-tvos", AppleTvOs); // this can also can be simulator
1138        test("aarch64-apple-watchos-sim", WatchSimulator);
1139        test("arm64_32-apple-watchos", WatchOs);
1140        test("armv7-apple-ios", IPhoneOs);
1141        test("armv7k-apple-watchos", WatchOs);
1142        test("armv7s-apple-ios", IPhoneOs);
1143        test("i386-apple-ios", IPhoneSimulator);
1144        test("i686-apple-darwin", MacOsX);
1145        test("x86_64-apple-darwin", MacOsX);
1146        test("x86_64-apple-ios", IPhoneSimulator);
1147        test("x86_64-apple-ios-macabi", IPhoneOs);
1148        test("x86_64-apple-tvos", AppleTvSimulator);
1149        test("x86_64-apple-watchos-sim", WatchSimulator);
1150        test("aarch64-apple-xros", XrOs);
1151        test("aarch64-apple-xros-sim", XrOsSimulator);
1152
1153        assert!(Platform::from_target_triple("x86_64-unknown-linux-gnu").is_err());
1154
1155        Ok(())
1156    }
1157
1158    #[test]
1159    fn sdk_version() -> Result<(), Error> {
1160        let v = SdkVersion::from("foo");
1161        assert!(v.normalized_version().is_err());
1162        assert!(v.semantic_version().is_err());
1163
1164        let v = SdkVersion::from("12");
1165        assert_eq!(v.normalized_version()?, (12, 0, 0));
1166        assert_eq!(v.semantic_version()?, "12.0.0");
1167
1168        let v = SdkVersion::from("12.3");
1169        assert_eq!(v.normalized_version()?, (12, 3, 0));
1170        assert_eq!(v.semantic_version()?, "12.3.0");
1171
1172        let v = SdkVersion::from("12.3.1");
1173        assert_eq!(v.normalized_version()?, (12, 3, 1));
1174        assert_eq!(v.semantic_version()?, "12.3.1");
1175
1176        let v = SdkVersion::from("12.3.1.2");
1177        assert!(v.normalized_version().is_err());
1178
1179        assert_eq!(
1180            SdkVersion::from("12").cmp(&SdkVersion::from("11")),
1181            Ordering::Greater
1182        );
1183        assert_eq!(
1184            SdkVersion::from("12").cmp(&SdkVersion::from("12")),
1185            Ordering::Equal
1186        );
1187        assert_eq!(
1188            SdkVersion::from("12").cmp(&SdkVersion::from("13")),
1189            Ordering::Less
1190        );
1191
1192        Ok(())
1193    }
1194
1195    #[test]
1196    fn sdk_sorting() {
1197        let sorting = SdkSorting::VersionAscending;
1198
1199        assert_eq!(
1200            sorting.compare_version(Some(&SdkVersion::from("12")), Some(&SdkVersion::from("11"))),
1201            Ordering::Greater
1202        );
1203        assert_eq!(
1204            sorting.compare_version(Some(&SdkVersion::from("11")), Some(&SdkVersion::from("12"))),
1205            Ordering::Less
1206        );
1207
1208        let sorting = SdkSorting::VersionDescending;
1209
1210        assert_eq!(
1211            sorting.compare_version(Some(&SdkVersion::from("12")), Some(&SdkVersion::from("11"))),
1212            Ordering::Less
1213        );
1214        assert_eq!(
1215            sorting.compare_version(Some(&SdkVersion::from("11")), Some(&SdkVersion::from("12"))),
1216            Ordering::Greater
1217        );
1218    }
1219
1220    #[test]
1221    fn parse_sdk_path() -> Result<(), Error> {
1222        assert!(SdkPath::from_path("foo").is_err());
1223        assert!(SdkPath::from_path("foo.bar").is_err());
1224
1225        let sdk = SdkPath::from_path("MacOSX.sdk")?;
1226        assert_eq!(sdk.platform, Platform::MacOsX);
1227        assert_eq!(sdk.version, None);
1228
1229        let sdk = SdkPath::from_path("MacOSX12.3.sdk")?;
1230        assert_eq!(sdk.platform, Platform::MacOsX);
1231        assert_eq!(sdk.version, Some("12.3".to_string().into()));
1232
1233        Ok(())
1234    }
1235
1236    #[test]
1237    fn search_all() -> Result<(), Error> {
1238        let search = SdkSearch::default().location(SdkSearchLocation::SystemXcodes);
1239
1240        search.search::<SimpleSdk>()?;
1241
1242        Ok(())
1243    }
1244
1245    /// Verifies various discovery operations on a macOS GitHub Actions runner.
1246    ///
1247    /// This assumes we're using GitHub's official macOS runners.
1248    #[cfg(target_os = "macos")]
1249    #[test]
1250    fn github_actions() -> Result<(), Error> {
1251        if std::env::var("GITHUB_ACTIONS").is_err() {
1252            return Ok(());
1253        }
1254
1255        assert_eq!(
1256            DeveloperDirectory::default_xcode(),
1257            Some(DeveloperDirectory {
1258                path: PathBuf::from("/Applications/Xcode.app/Contents/Developer")
1259            })
1260        );
1261        assert!(PathBuf::from(COMMAND_LINE_TOOLS_DEFAULT_PATH).exists());
1262
1263        // GitHub Actions runners have multiple Xcode applications installed.
1264        assert!(crate::find_system_xcode_applications()?.len() > 5);
1265
1266        // We should be able to resolve developer directories for all system Xcode
1267        // applications.
1268        assert_eq!(
1269            crate::find_system_xcode_applications()?.len(),
1270            DeveloperDirectory::find_system_xcodes()?.len()
1271        );
1272
1273        // We should be able to find SDKs for common platforms by default.
1274        for platform in [Platform::MacOsX, Platform::IPhoneOs, Platform::WatchOs] {
1275            let sdks = SdkSearch::default()
1276                .platform(platform)
1277                .search::<SimpleSdk>()?;
1278            assert!(!sdks.is_empty());
1279        }
1280
1281        // We should be able to find a macOS 11.0+ SDK by default.
1282        let sdks = SdkSearch::default()
1283            .platform(Platform::MacOsX)
1284            .minimum_version(SdkVersion::from("11.0"))
1285            .search::<SimpleSdk>()?;
1286        assert!(!sdks.is_empty());
1287
1288        Ok(())
1289    }
1290}