apple_sdk/
search.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
9use {
10    crate::{
11        command_line_tools_sdks_directory, AppleSdk, DeveloperDirectory, Error, Platform,
12        PlatformDirectory, SdkPath, SdkVersion,
13    },
14    std::{
15        cmp::Ordering,
16        collections::HashSet,
17        fmt::{Display, Formatter},
18        path::PathBuf,
19    },
20};
21
22/// The search location that a [SdkSearchLocation] normalizes to.
23enum SdkSearchResolvedLocation {
24    /// Nothing.
25    None,
26    /// A collection of platform directories.
27    PlatformDirectories(Vec<PlatformDirectory>),
28    /// A directory holding SDKs.
29    SdksDirectory(PathBuf),
30    /// A specific directory with an SDK.
31    SdkDirectory(PathBuf),
32    /// A specified directory with an SDK excluded from SDK filtering.
33    SdkDirectoryUnfiltered(PathBuf),
34}
35
36impl SdkSearchResolvedLocation {
37    fn apply_sdk_filter(&self) -> bool {
38        !matches!(self, Self::SdkDirectoryUnfiltered(_))
39    }
40}
41
42/// Represents a location to search for SDKs.
43#[derive(Clone, Debug)]
44pub enum SdkSearchLocation {
45    /// Use the path specified by the `SDKROOT` environment variable.
46    ///
47    /// If this environment variable is defined and the path is not valid, an error
48    /// occurs.
49    ///
50    /// An SDK yielded from this location skips search filtering. This is because the
51    /// semantic intent of the `SDKROOT` environment variable is to force usage of a
52    /// specific SDK.
53    ///
54    /// If this location yields an SDK, the SDK search will be aborted and subsequent
55    /// locations will not be searched. This effectively honors the intent of `SDKROOT`
56    /// to force usage of a given SDK.
57    ///
58    /// If this behavior is not desirable, construct an [SdkSearch] with a
59    /// [SdkSearchLocation::Sdk] using the value of `SDKROOT`.
60    SdkRootEnv,
61
62    /// Use the Developer Directory specified by the `DEVELOPER_DIR` environment variable.
63    ///
64    /// If this environment variable is defined and the path is not valid, an error
65    /// occurs.
66    ///
67    /// If this location yields an SDK, the SDK search will be aborted and subsequent
68    /// locations will not be searched. This effectively honors the intent of `DEVELOPER_DIR`
69    /// to explicitly define a developer directory to use for SDK searching.
70    ///
71    /// If this behavior is not desirable, construct an [SdkSearch] with a
72    /// [SdkSearchLocation::Developer] using the value of `DEVELOPER_DIR`.
73    DeveloperDirEnv,
74
75    /// Look for SDKs within the system installed `Xcode` application.
76    ///
77    /// This effectively controls whether the Developer Directory resolved by
78    /// [DeveloperDirectory::default_xcode()] will be searched, if available.
79    SystemXcode,
80
81    /// Look for SDKs within the system install `Xcode Command Line Tools` installation.
82    ///
83    /// This effectively uses the directory returned by [command_line_tools_sdks_directory()],
84    /// if available.
85    CommandLineTools,
86
87    /// Check the paths configured by `xcode-select --switch`.
88    ///
89    /// This effectively controls whether the Developer Directory resolved by
90    /// [DeveloperDirectory::from_xcode_select_paths()] will be searched, if available.
91    XcodeSelectPaths,
92
93    /// Invoke `xcode-select` to find a *Developer Directory* to search.
94    ///
95    /// This mechanism is intended as a fallback in case other (pure Rust) mechanisms for locating
96    /// the default *Developer Directory* fail. If you find yourself needing this, it likely
97    /// points to a gap in our feature coverage to locate the default *Developer Directory* without
98    /// running external tools. Consider filing a bug against this crate to track closing the
99    /// feature gap.
100    XcodeSelect,
101
102    /// Look for SDKs within all system installed `Xcode` applications.
103    ///
104    /// This effectively controls whether the paths resolved by
105    /// [DeveloperDirectory::find_system_xcodes()] will be searched, if present.
106    ///
107    /// Many macOS systems only have a single Xcode application at `/Applications/Xcode.app`.
108    /// However, environments like CI workers and developers having beta versions of Xcode installed
109    /// may have multiple versions of Xcode available. This option can enable multiple copies
110    /// of Xcode to be used.
111    SystemXcodes,
112
113    /// Use an explicit *Developer Directory*.
114    ///
115    /// This can be used to point a search at a non-standard location holding a *Developer
116    /// Directory*. A common use case for this is when cross-compiling or using hermetic / chroot /
117    /// container build environments that don't resemble a common macOS system layout and therefore
118    /// prohibit use of mechanisms for locating a *Developer Directory* in default locations.
119    Developer(DeveloperDirectory),
120
121    /// Use an explicit directory holding SDKs.
122    ///
123    /// This is similar to [Self::Developer] with regards to its intended use cases. The difference
124    /// is the path is a directory holding `*.sdk` directories, not a *Developer Directory*.
125    Sdks(PathBuf),
126
127    /// Use an explicit directory holding an SDK.
128    Sdk(PathBuf),
129}
130
131impl Display for SdkSearchLocation {
132    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133        match self {
134            Self::SdkRootEnv => f.write_str("SDKROOT environment variable"),
135            Self::DeveloperDirEnv => f.write_str("DEVELOPER_DIR environment variable"),
136            Self::SystemXcode => f.write_str("System-installed Xcode application"),
137            Self::CommandLineTools => f.write_str("Xcode Command Line Tools installation"),
138            Self::XcodeSelectPaths => {
139                f.write_str("Internal xcode-select paths (`/var/db/xcode_select_link`)")
140            }
141            Self::XcodeSelect => f.write_str("xcode-select"),
142            Self::SystemXcodes => f.write_str("All system-installed Xcode applications"),
143            Self::Developer(dir) => {
144                f.write_fmt(format_args!("Developer Directory {}", dir.path().display()))
145            }
146            Self::Sdks(path) => f.write_fmt(format_args!("SDKs directory {}", path.display())),
147            Self::Sdk(path) => f.write_fmt(format_args!("SDK directory {}", path.display())),
148        }
149    }
150}
151
152impl SdkSearchLocation {
153    /// Whether this search location is terminal.
154    fn is_terminal(&self) -> bool {
155        matches!(self, Self::SdkRootEnv | Self::DeveloperDirEnv)
156    }
157
158    fn resolve_location(&self) -> Result<SdkSearchResolvedLocation, Error> {
159        match self {
160            Self::SdkRootEnv => {
161                if let Some(path) = std::env::var_os("SDKROOT") {
162                    let path = PathBuf::from(path);
163
164                    if path.exists() {
165                        Ok(SdkSearchResolvedLocation::SdkDirectoryUnfiltered(path))
166                    } else {
167                        Err(Error::PathNotSdk(path))
168                    }
169                } else {
170                    Ok(SdkSearchResolvedLocation::None)
171                }
172            }
173            Self::DeveloperDirEnv => {
174                if let Some(dir) = DeveloperDirectory::from_env()? {
175                    Ok(SdkSearchResolvedLocation::PlatformDirectories(
176                        dir.platforms()?,
177                    ))
178                } else {
179                    Ok(SdkSearchResolvedLocation::None)
180                }
181            }
182            Self::SystemXcode => {
183                if let Some(dir) = DeveloperDirectory::default_xcode() {
184                    Ok(SdkSearchResolvedLocation::PlatformDirectories(
185                        dir.platforms()?,
186                    ))
187                } else {
188                    Ok(SdkSearchResolvedLocation::None)
189                }
190            }
191            Self::CommandLineTools => {
192                if let Some(path) = command_line_tools_sdks_directory() {
193                    Ok(SdkSearchResolvedLocation::SdksDirectory(path))
194                } else {
195                    Ok(SdkSearchResolvedLocation::None)
196                }
197            }
198            Self::XcodeSelectPaths => {
199                if let Some(dir) = DeveloperDirectory::from_xcode_select_paths()? {
200                    Ok(SdkSearchResolvedLocation::PlatformDirectories(
201                        dir.platforms()?,
202                    ))
203                } else {
204                    Ok(SdkSearchResolvedLocation::None)
205                }
206            }
207            Self::XcodeSelect => Ok(SdkSearchResolvedLocation::PlatformDirectories(
208                DeveloperDirectory::from_xcode_select()?.platforms()?,
209            )),
210            Self::SystemXcodes => Ok(SdkSearchResolvedLocation::PlatformDirectories(
211                DeveloperDirectory::find_system_xcodes()?
212                    .into_iter()
213                    .map(|dir| dir.platforms())
214                    .collect::<Result<Vec<_>, Error>>()?
215                    .into_iter()
216                    .flatten()
217                    .collect::<Vec<_>>(),
218            )),
219            Self::Developer(dir) => Ok(SdkSearchResolvedLocation::PlatformDirectories(
220                dir.platforms()?,
221            )),
222            Self::Sdks(path) => Ok(SdkSearchResolvedLocation::SdksDirectory(path.clone())),
223            Self::Sdk(path) => Ok(SdkSearchResolvedLocation::SdkDirectory(path.clone())),
224        }
225    }
226}
227
228/// Sorting strategy to apply to SDK searches.
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub enum SdkSorting {
231    /// Do not apply any sorting.
232    ///
233    /// This will return SDKs in the order they are discovered from the input
234    /// paths.
235    None,
236
237    /// Order SDKs by their version in descending order.
238    ///
239    /// Newer SDKs will come before older SDKs.
240    VersionDescending,
241
242    /// Order SDKs by their version in ascending order.
243    ///
244    /// Older SDKs will come before newer SDKs.
245    VersionAscending,
246}
247
248impl Display for SdkSorting {
249    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250        f.write_str(match self {
251            Self::None => "nothing",
252            Self::VersionDescending => "descending version",
253            Self::VersionAscending => "ascending version",
254        })
255    }
256}
257
258impl SdkSorting {
259    pub fn compare_version(&self, a: Option<&SdkVersion>, b: Option<&SdkVersion>) -> Ordering {
260        match self {
261            Self::None => Ordering::Equal,
262            Self::VersionAscending => match (a, b) {
263                (Some(a), Some(b)) => a.cmp(b),
264                (Some(_), None) => Ordering::Greater,
265                (None, Some(_)) => Ordering::Less,
266                (None, None) => Ordering::Equal,
267            },
268            Self::VersionDescending => match (a, b) {
269                (Some(a), Some(b)) => b.cmp(a),
270                (Some(_), None) => Ordering::Less,
271                (None, Some(_)) => Ordering::Greater,
272                (None, None) => Ordering::Equal,
273            },
274        }
275    }
276}
277
278/// Describes an event during SDK discovery.
279///
280/// This events are sent to the progress callback to allow monitoring and debugging
281/// of SDK searching activity.
282pub enum SdkSearchEvent {
283    /// Beginning a search of a given location.
284    SearchingLocation(SdkSearchLocation),
285    PlatformDirectoryInclude(PathBuf),
286    PlatformDirectoryExclude(PathBuf),
287    SdkFilterSkip(SdkPath),
288    SdkFilterMatch(SdkPath),
289    SdkFilterExclude(SdkPath, String),
290    Sorting(usize, SdkSorting),
291}
292
293impl Display for SdkSearchEvent {
294    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::SearchingLocation(location) => f.write_fmt(format_args!("searching {location}")),
297            Self::PlatformDirectoryInclude(path) => f.write_fmt(format_args!(
298                "searching Platform directory {}",
299                path.display()
300            )),
301            Self::PlatformDirectoryExclude(path) => f.write_fmt(format_args!(
302                "excluding Platform directory {}",
303                path.display()
304            )),
305            Self::SdkFilterSkip(sdk) => f.write_fmt(format_args!("SDK {sdk} bypasses filter")),
306            Self::SdkFilterMatch(sdk) => {
307                f.write_fmt(format_args!("SDK {sdk} matches search filter"))
308            }
309            Self::SdkFilterExclude(sdk, reason) => {
310                f.write_fmt(format_args!("SDK {sdk} discarded because {reason}"))
311            }
312            Self::Sorting(count, sorting) => {
313                f.write_fmt(format_args!("sorting {count} SDKs by {sorting}"))
314            }
315        }
316    }
317}
318
319/// A callable that receives progress during an SDK search.
320pub type SdkProgressCallback = fn(SdkSearchEvent);
321
322/// Search parameters for locating an Apple SDK.
323///
324/// This type can be used to construct a search for an Apple SDK given user chosen
325/// search parameters.
326///
327/// The search algorithm is essentially:
328///
329/// 1. Iterate through each registered search location.
330/// 2. Discover candidate SDKs and filter.
331/// 3. Globally sort (if enabled).
332///
333/// # Search Locations
334///
335/// Search mechanisms / locations are represented via [SdkSearchLocation] and internally
336/// the searcher maintains a vector of locations. The default search locations are:
337///
338/// 1. Use path specified by `SDKROOT` environment variable, if defined.
339/// 2. Find SDKs within the Developer Directory defined by the `DEVELOPER_DIR` environment
340///    variable.
341/// 3. Find SDKs within the path configured with `xcode-select --switch`.
342/// 4. Find SDKs within the system installed Xcode application.
343/// 5. Find SDKs within the system installed Xcode Command Line Tools.
344///
345/// Simply call [Self::location()] to register a new location. If the default locations
346/// are not desirable, construct an empty instance via [Self::empty()] and register your
347/// explicit list of locations.
348///
349/// An attempt is made to only search a given location at most once. This is done in
350/// order to avoid redundant work. If a location is specified multiple times - even via
351/// different [SdkSearchLocation] variants - subsequent searches of that location will
352/// yield no distinct results. Duplicate SDKs can occur in the returned list.
353///
354/// # Filtering
355///
356/// Filters can be registered to control which SDKs are emitted from the search.
357///
358/// By default, no filtering is performed. This means all SDKs in all search locations
359/// are returned. This can return SDKs belonging to multiple platforms (e.g. macOS and iOS).
360///
361/// The following functions control filtering:
362///
363/// * [Self::platform()]
364/// * [Self::minimum_version()]
365/// * [Self::maximum_version()]
366/// * [Self::deployment_target()]
367///
368/// If you are looking for an SDK to use (e.g. for compilation), you should at least use a
369/// platform filter. Otherwise you may see SDKs for platforms you aren't targeting! It is
370/// also an encouraged practice to specify a minimum or maximum SDK version to use.
371///
372/// If you know you are targeting a specific OS version, applying a targeting filter
373/// via [Self::deployment_target()] is recommended. However, this filter is not always
374/// reliable. See the caveats in its documentation.
375///
376/// # Sorting
377///
378/// By default, the returned list of SDKs is the chained result of SDKs discovered
379/// in all registered search locations. The order of the SDK within each search
380/// location is likely the sorted order of directory names as they appear on the filesystem.
381///
382/// If using an SDK for compilation, sorting by the SDK version is likely desired.
383/// Using the latest/newest SDK that supports a given deployment target is generally
384/// a best practice.
385#[derive(Clone)]
386pub struct SdkSearch {
387    progress_callback: Option<SdkProgressCallback>,
388    locations: Vec<SdkSearchLocation>,
389    platform: Option<Platform>,
390    minimum_version: Option<SdkVersion>,
391    maximum_version: Option<SdkVersion>,
392    deployment_target: Option<(String, SdkVersion)>,
393    sorting: SdkSorting,
394}
395
396impl Default for SdkSearch {
397    fn default() -> Self {
398        Self {
399            progress_callback: None,
400            locations: vec![
401                SdkSearchLocation::SdkRootEnv,
402                SdkSearchLocation::DeveloperDirEnv,
403                SdkSearchLocation::XcodeSelectPaths,
404                SdkSearchLocation::SystemXcode,
405                SdkSearchLocation::CommandLineTools,
406            ],
407            platform: None,
408            minimum_version: None,
409            maximum_version: None,
410            deployment_target: None,
411            sorting: SdkSorting::None,
412        }
413    }
414}
415
416impl SdkSearch {
417    /// Obtain an instance with an empty set of search locations.
418    ///
419    /// The search will not resolve any SDKs unless a search location is registered
420    /// with the instance.
421    pub fn empty() -> Self {
422        let mut s = Self::default();
423        s.locations.clear();
424        s
425    }
426
427    /// Define a function that will be called to provide updates on SDK search status.
428    pub fn progress_callback(mut self, callback: SdkProgressCallback) -> Self {
429        self.progress_callback = Some(callback);
430        self
431    }
432
433    /// Add a location to search.
434    ///
435    /// The location will be appended to the current search location list.
436    pub fn location(mut self, location: SdkSearchLocation) -> Self {
437        self.locations.push(location);
438        self
439    }
440
441    /// Set the SDK platform to search for.
442    ///
443    /// If you do not call this, SDKs for all platforms are returned.
444    ///
445    /// If you are looking for a specific SDK to use, you probably want to call this.
446    /// If you are searching for all available SDKs, you probably don't want to call this.
447    pub fn platform(mut self, platform: Platform) -> Self {
448        self.platform = Some(platform);
449        self
450    }
451
452    /// Minimum SDK version to require.
453    ///
454    /// Effectively imposes a `>=` filter on found SDKs.
455    ///
456    /// If using `SimpleSdk` and the SDK version could not be determined from
457    /// the filesystem path, the version is assumed to be `0.0` and this filter
458    /// will likely exclude the SDK.
459    pub fn minimum_version(mut self, version: impl Into<SdkVersion>) -> Self {
460        self.minimum_version = Some(version.into());
461        self
462    }
463
464    /// Maximum SDK version to return.
465    ///
466    /// Effectively imposes a `<=` filter on found SDKs.
467    pub fn maximum_version(mut self, version: impl Into<SdkVersion>) -> Self {
468        self.maximum_version = Some(version.into());
469        self
470    }
471
472    /// Deployment target that the SDK must support.
473    ///
474    /// When set, only SDKs that support targeting the given target-version pair will
475    /// be returned. Example values are (`macosx`, `10.15`).
476    ///
477    /// Only modern SDKs with `SDKSettings.json` files advertise their targeting settings
478    /// in a way that allows this filter to work.
479    ///
480    /// Attempting to use this filter on `SimpleSdk` will result in a run-time
481    /// error at search time since these SDKs do not parse `SDKSettings` files.
482    pub fn deployment_target(
483        mut self,
484        target: impl ToString,
485        version: impl Into<SdkVersion>,
486    ) -> Self {
487        self.deployment_target = Some((target.to_string(), version.into()));
488        self
489    }
490
491    /// Define the sorting order for returned SDKs.
492    ///
493    /// Default is [SdkSorting::None].
494    pub fn sorting(mut self, sorting: SdkSorting) -> Self {
495        self.sorting = sorting;
496        self
497    }
498
499    /// Perform a search, yielding found SDKs sorted by the search's preferences.
500    ///
501    /// May return an empty vector.
502    pub fn search<SDK: AppleSdk>(&self) -> Result<Vec<SDK>, Error> {
503        let mut sdks = vec![];
504
505        // Track searched locations to avoid redundant work.
506        let mut searched_platform_dirs = HashSet::new();
507        let mut searched_sdks_dirs = HashSet::new();
508
509        for location in &self.locations {
510            if let Some(cb) = &self.progress_callback {
511                cb(SdkSearchEvent::SearchingLocation(location.clone()));
512            }
513
514            // Expand each location to SDKs.
515            let resolved = location.resolve_location()?;
516
517            let candidate_sdks = match &resolved {
518                SdkSearchResolvedLocation::None => {
519                    vec![]
520                }
521                SdkSearchResolvedLocation::PlatformDirectories(dirs) => dirs
522                    .iter()
523                    // Apply platform filter.
524                    .filter(|dir| {
525                        if let Some(wanted_platform) = &self.platform {
526                            if &dir.platform == wanted_platform {
527                                if let Some(cb) = &self.progress_callback {
528                                    cb(SdkSearchEvent::PlatformDirectoryInclude(dir.path.clone()));
529                                }
530
531                                true
532                            } else {
533                                if let Some(cb) = &self.progress_callback {
534                                    cb(SdkSearchEvent::PlatformDirectoryExclude(dir.path.clone()));
535                                }
536
537                                false
538                            }
539                        } else {
540                            if let Some(cb) = &self.progress_callback {
541                                cb(SdkSearchEvent::PlatformDirectoryInclude(dir.path.clone()));
542                            }
543
544                            true
545                        }
546                    })
547                    // Apply duplicate search filter.
548                    .filter(|dir| {
549                        if searched_platform_dirs.contains(dir.path()) {
550                            false
551                        } else {
552                            searched_platform_dirs.insert(dir.path().to_path_buf());
553                            true
554                        }
555                    })
556                    .map(|dir| dir.find_sdks::<SDK>())
557                    .collect::<Result<Vec<_>, Error>>()?
558                    .into_iter()
559                    .flatten()
560                    .collect::<Vec<_>>(),
561                SdkSearchResolvedLocation::SdksDirectory(path) => {
562                    if searched_sdks_dirs.contains(path) {
563                        vec![]
564                    } else {
565                        searched_sdks_dirs.insert(path.clone());
566                        SDK::find_in_directory(path)?
567                    }
568                }
569                SdkSearchResolvedLocation::SdkDirectory(path)
570                | SdkSearchResolvedLocation::SdkDirectoryUnfiltered(path) => {
571                    vec![SDK::from_directory(path)?]
572                }
573            };
574
575            let mut added_count = 0;
576
577            for sdk in candidate_sdks {
578                let include = if resolved.apply_sdk_filter() {
579                    self.filter_sdk(&sdk)?
580                } else {
581                    if let Some(cb) = &self.progress_callback {
582                        cb(SdkSearchEvent::SdkFilterSkip(sdk.sdk_path()));
583                    }
584
585                    true
586                };
587
588                if include {
589                    sdks.push(sdk);
590                    added_count += 1;
591                }
592            }
593
594            if location.is_terminal() && added_count > 0 {
595                break;
596            }
597        }
598
599        // Sorting should be stable with None variant. But we can avoid the
600        // overhead.
601        if self.sorting != SdkSorting::None {
602            sdks.sort_by(|a, b| self.sorting.compare_version(a.version(), b.version()))
603        }
604
605        Ok(sdks)
606    }
607
608    /// Whether an SDK matches our search filter.
609    ///
610    /// This is exposed as a convenience method to allow custom implementations of
611    /// SDK searching using the filtering logic on this type.
612    pub fn filter_sdk<SDK: AppleSdk>(&self, sdk: &SDK) -> Result<bool, Error> {
613        let sdk_path = sdk.sdk_path();
614
615        if let Some(wanted_platform) = &self.platform {
616            if sdk.platform() != wanted_platform {
617                if let Some(cb) = &self.progress_callback {
618                    cb(SdkSearchEvent::SdkFilterExclude(
619                        sdk_path,
620                        format!(
621                            "platform {} != {}",
622                            sdk.platform().filesystem_name(),
623                            wanted_platform.filesystem_name()
624                        ),
625                    ));
626                }
627
628                return Ok(false);
629            }
630        }
631
632        if let Some(min_version) = &self.minimum_version {
633            if let Some(sdk_version) = sdk.version() {
634                if sdk_version < min_version {
635                    if let Some(cb) = &self.progress_callback {
636                        cb(SdkSearchEvent::SdkFilterExclude(
637                            sdk_path,
638                            format!("SDK version {sdk_version} < minimum version {min_version}"),
639                        ));
640                    }
641
642                    return Ok(false);
643                }
644            } else {
645                // SDKs without a version always fail.
646                if let Some(cb) = &self.progress_callback {
647                    cb(SdkSearchEvent::SdkFilterExclude(
648                        sdk_path,
649                        format!("Unknown SDK version fails to meet minimum version {min_version}"),
650                    ));
651                }
652
653                return Ok(false);
654            }
655        }
656
657        if let Some(max_version) = &self.maximum_version {
658            if let Some(sdk_version) = sdk.version() {
659                if sdk_version > max_version {
660                    if let Some(cb) = &self.progress_callback {
661                        cb(SdkSearchEvent::SdkFilterExclude(
662                            sdk_path,
663                            format!("SDK version {sdk_version} > maximum version {max_version}"),
664                        ));
665                    }
666
667                    return Ok(false);
668                }
669            } else {
670                // SDKs without a version always fail.
671
672                if let Some(cb) = &self.progress_callback {
673                    cb(SdkSearchEvent::SdkFilterExclude(
674                        sdk_path,
675                        format!("Unknown SDK version fails to meet maximum version {max_version}"),
676                    ));
677                }
678
679                return Ok(false);
680            }
681        }
682
683        if let Some((target, version)) = &self.deployment_target {
684            if !sdk.supports_deployment_target(target, version)? {
685                if let Some(cb) = &self.progress_callback {
686                    cb(SdkSearchEvent::SdkFilterExclude(
687                        sdk_path,
688                        format!("does not support deployment target {target}:{version}"),
689                    ));
690                }
691
692                return Ok(false);
693            }
694        }
695
696        if let Some(cb) = &self.progress_callback {
697            cb(SdkSearchEvent::SdkFilterMatch(sdk_path));
698        }
699
700        Ok(true)
701    }
702}