apple_sdk/
parsed_sdk.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//! Data structures in Apple SDKs.
10
11use {
12    crate::{AppleSdk, Error, Platform, SdkPath, SdkVersion, SimpleSdk},
13    serde::Deserialize,
14    std::{
15        collections::HashMap,
16        path::{Path, PathBuf},
17    },
18};
19
20/// Represents the DefaultProperties key in a SDKSettings.json file.
21#[derive(Debug, Deserialize)]
22#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
23pub struct SdkSettingsJsonDefaultProperties {
24    pub platform_name: String,
25}
26
27/// Represents a SupportedTargets value in a SDKSettings.json file.
28#[derive(Clone, Debug, Deserialize)]
29#[serde(rename_all = "PascalCase")]
30pub struct SupportedTarget {
31    /// Names of machine architectures that can be targeted.
32    ///
33    /// e.g. `x86_64`, `arm64`, `arm64e`.
34    pub archs: Vec<String>,
35
36    /// Default deployment target version.
37    ///
38    /// Likely corresponds to the OS version this SDK is associated with.
39    /// e.g. the macOS 12.3 SDK would target `12.3` by default.
40    pub default_deployment_target: String,
41
42    /// The name of the settings variant to use by default.
43    pub default_variant: Option<String>,
44
45    /// The name of the toolchain setting that influences which deployment target version is used.
46    ///
47    /// e.g. on macOS this will be `MACOSX_DEPLOYMENT_TARGET`. This represents an
48    /// environment variable that can be set to influence which deployment target
49    /// version to use.
50    pub deployment_target_setting_name: Option<String>,
51
52    /// The lowest version of a platform that this SDK can target.
53    ///
54    /// Using this SDK, it is possible to emit code that will support running
55    /// down to the OS version specified by this value. e.g. `10.9` is a
56    /// common value for macOS SDKs.
57    pub minimum_deployment_target: String,
58
59    /// A name given to the platform.
60    ///
61    /// e.g. `macOS`.
62    pub platform_family_name: Option<String>,
63
64    /// List of platform versions that this SDK can target.
65    ///
66    /// This is likely a range of all major versions between `minimum_deployment_target`
67    /// and `default_deployment_target`.
68    pub valid_deployment_targets: Vec<String>,
69}
70
71impl SupportedTarget {
72    /// Obtain [SdkVersion] for each deployment target this target supports.
73    pub fn deployment_targets_versions(&self) -> Vec<SdkVersion> {
74        self.valid_deployment_targets
75            .iter()
76            .map(SdkVersion::from)
77            .collect::<Vec<_>>()
78    }
79}
80
81/// Used for deserializing a SDKSettings.json file in an SDK directory.
82#[derive(Debug, Deserialize)]
83#[serde(rename_all = "PascalCase")]
84pub struct SdkSettingsJson {
85    pub canonical_name: String,
86    pub default_deployment_target: String,
87    pub default_properties: SdkSettingsJsonDefaultProperties,
88    pub default_variant: Option<String>,
89    pub display_name: String,
90    pub maximum_deployment_target: String,
91    pub minimal_display_name: String,
92    pub supported_targets: HashMap<String, SupportedTarget>,
93    pub version: String,
94}
95
96/// An Apple SDK with parsed settings.
97///
98/// Unlike [SimpleSdk], this type gives you access to rich metadata about the
99/// Apple SDK. This includes things like targeting capabilities.
100#[derive(Clone, Debug)]
101pub struct ParsedSdk {
102    /// Root directory of the SDK.
103    path: PathBuf,
104
105    /// Whether the root directory is a symlink to another path.
106    is_symlink: bool,
107
108    /// The platform this SDK belongs to.
109    platform: Platform,
110
111    version: SdkVersion,
112
113    /// The name of the platform.
114    ///
115    /// This is likely the part before the `*.platform` in the platform directory in which
116    /// this SDK is located. e.g. `macosx`.
117    pub platform_name: String,
118
119    /// The canonical name of the SDK. e.g. `macosx12.3`.
120    pub name: String,
121
122    /// Version of the default deployment target for this SDK.
123    ///
124    /// This is likely the OS version the SDK came from. e.g. `12.3`.
125    pub default_deployment_target: String,
126
127    /// Name of default settings variant for this SDK.
128    ///
129    /// Some SDKs have named variants defining targeting settings. This field holds
130    /// the name of the default variant.
131    ///
132    /// For example, macOS SDKs have a `macos` variant for targeting macOS and an
133    /// `iosmac` variant for targeting iOS running on macOS.
134    pub default_variant: Option<String>,
135
136    /// Human friendly name of this SDK.
137    ///
138    /// e.g. `macOS 12.3`.
139    pub display_name: String,
140
141    /// Maximum deployment target version this SDK supports.
142    ///
143    /// This is a very string denoting the maximum version this SDK can target.
144    /// e.g. a `12.3` would list `12.3.99`.
145    pub maximum_deployment_target: String,
146
147    /// Human friendly value for name (probably just version string).
148    ///
149    /// A shortened display name. e.g. `12.3`.
150    pub minimal_display_name: String,
151
152    /// Describes named target configurations this SDK supports.
153    ///
154    /// SDKs can have multiple named targets defining pre-canned default targeting
155    /// settings. This field holds these data structures.
156    ///
157    /// Example keys are `macosx` and `iosmac`. Use the [Self::default_variant]
158    /// field to access the default target.
159    pub supported_targets: HashMap<String, SupportedTarget>,
160}
161
162impl AsRef<Path> for ParsedSdk {
163    fn as_ref(&self) -> &Path {
164        &self.path
165    }
166}
167
168impl AppleSdk for ParsedSdk {
169    fn from_directory(path: &Path) -> Result<Self, Error> {
170        let sdk = SdkPath::from_path(path)?;
171
172        // Need to call symlink_metadata so symlinks aren't followed.
173        let metadata = std::fs::symlink_metadata(path)?;
174
175        let is_symlink = metadata.file_type().is_symlink();
176
177        let json_path = path.join("SDKSettings.json");
178        let plist_path = path.join("SDKSettings.plist");
179
180        if json_path.exists() {
181            let fh = std::fs::File::open(&json_path)?;
182            let value: SdkSettingsJson = serde_json::from_reader(fh)?;
183
184            Self::from_json(path.to_path_buf(), is_symlink, sdk.platform, value)
185        } else if plist_path.exists() {
186            let value = plist::Value::from_file(&plist_path)?;
187
188            Self::from_plist(path.to_path_buf(), is_symlink, sdk.platform, value)
189        } else {
190            Err(Error::PathNotSdk(path.to_path_buf()))
191        }
192    }
193
194    fn is_symlink(&self) -> bool {
195        self.is_symlink
196    }
197
198    fn platform(&self) -> &Platform {
199        &self.platform
200    }
201
202    fn version(&self) -> Option<&SdkVersion> {
203        Some(&self.version)
204    }
205
206    /// Whether this SDK supports the given deployment target.
207    ///
208    /// This API does not work reliably on SDKs loaded from plists because the plist metadata
209    /// lacks the required version constraint annotations.
210    fn supports_deployment_target(
211        &self,
212        target_name: &str,
213        target_version: &SdkVersion,
214    ) -> Result<bool, Error> {
215        Ok(
216            if let Some(target) = self.supported_targets.get(target_name) {
217                target
218                    .deployment_targets_versions()
219                    .contains(target_version)
220            } else {
221                false
222            },
223        )
224    }
225}
226
227impl ParsedSdk {
228    /// Construct an instance by parsing an `SDKSettings.json` file in a directory.
229    ///
230    /// These files are only available in more modern SDKs. For macOS, that's 10.14+.
231    /// For more reliably SDK construction, use [Self::from_plist()].
232    pub fn from_json(
233        path: PathBuf,
234        is_symlink: bool,
235        platform: Platform,
236        value: SdkSettingsJson,
237    ) -> Result<Self, Error> {
238        Ok(Self {
239            path,
240            is_symlink,
241            platform,
242            version: value.version.into(),
243            platform_name: value.default_properties.platform_name,
244            name: value.canonical_name,
245            default_deployment_target: value.default_deployment_target,
246            default_variant: value.default_variant,
247            display_name: value.display_name,
248            maximum_deployment_target: value.maximum_deployment_target,
249            minimal_display_name: value.minimal_display_name,
250            supported_targets: value.supported_targets,
251        })
252    }
253
254    /// Construct an instance by parsing an `SDKSettings.plist` file in a directory.
255    ///
256    /// Plist files are the legacy mechanism for defining SDK settings. JSON files
257    /// are preferred, as they are newer. However, older SDKs lack `SDKSettings.json`
258    /// files.
259    pub fn from_plist(
260        path: PathBuf,
261        is_symlink: bool,
262        platform: Platform,
263        value: plist::Value,
264    ) -> Result<Self, Error> {
265        let value = value.into_dictionary().ok_or(Error::PlistNotDictionary)?;
266
267        let get_string = |dict: &plist::Dictionary, key: &str| -> Result<String, Error> {
268            Ok(dict
269                .get(key)
270                .ok_or_else(|| Error::PlistKeyMissing(key.to_string()))?
271                .as_string()
272                .ok_or_else(|| Error::PlistKeyNotString(key.to_string()))?
273                .to_string())
274        };
275
276        let name = get_string(&value, "CanonicalName")?;
277        let display_name = get_string(&value, "DisplayName")?;
278        let maximum_deployment_target = get_string(&value, "MaximumDeploymentTarget")?;
279        let minimal_display_name = get_string(&value, "MinimalDisplayName")?;
280        let version = get_string(&value, "Version")?;
281
282        let props = value
283            .get("DefaultProperties")
284            .ok_or_else(|| Error::PlistKeyMissing("DefaultProperties".to_string()))?
285            .as_dictionary()
286            .ok_or_else(|| Error::PlistKeyNotDictionary("DefaultProperties".to_string()))?;
287
288        let platform_name = get_string(props, "PLATFORM_NAME")?;
289
290        // The default deployment target can be specified a number of ways.
291        //
292        // Some SDKs have a property specifying the property defining it. That takes precedence, as
293        // explicit > implicit.
294        //
295        // Otherwise we have to fall back to a heuristic.
296        //
297        // First we try {platform_name}_DEPLOYMENT_TARGET. Then LLVM target triple + _DEPLOYMENT_TARGET.
298        // This heuristic appears to always work.
299        let default_deployment_target =
300            if let Ok(setting_name) = get_string(props, "DEPLOYMENT_TARGET_SETTING_NAME") {
301                get_string(props, &setting_name)?
302            } else if let Ok(value) = get_string(
303                props,
304                &format!("{}_DEPLOYMENT_TARGET", platform_name.to_ascii_uppercase()),
305            ) {
306                value
307            } else {
308                let supported_targets = value
309                    .get("SupportedTargets")
310                    .ok_or_else(|| Error::PlistKeyMissing("SupportedTargets".to_string()))?
311                    .as_dictionary()
312                    .ok_or_else(|| Error::PlistKeyNotDictionary("SupportedTargets".to_string()))?;
313
314                let default_target = supported_targets
315                    .get(&platform_name)
316                    .ok_or_else(|| Error::PlistKeyMissing(platform_name.clone()))?
317                    .as_dictionary()
318                    .ok_or_else(|| Error::PlistKeyNotDictionary(platform_name.clone()))?;
319
320                let llvm_target_triple = get_string(default_target, "LLVMTargetTripleSys")?;
321
322                get_string(
323                    props,
324                    &format!(
325                        "{}_DEPLOYMENT_TARGET",
326                        llvm_target_triple.to_ascii_uppercase()
327                    ),
328                )?
329            };
330
331        Ok(Self {
332            path,
333            is_symlink,
334            platform,
335            version: version.into(),
336            platform_name,
337            name,
338            default_deployment_target,
339            default_variant: None,
340            display_name,
341            maximum_deployment_target,
342            minimal_display_name,
343            supported_targets: HashMap::new(),
344        })
345    }
346}
347
348impl TryFrom<SimpleSdk> for ParsedSdk {
349    type Error = Error;
350
351    fn try_from(v: SimpleSdk) -> Result<Self, Self::Error> {
352        Self::from_directory(v.path())
353    }
354}
355
356#[cfg(test)]
357mod test {
358    use {
359        super::*,
360        crate::{
361            DeveloperDirectory, SdkSearch, SdkSearchLocation, COMMAND_LINE_TOOLS_DEFAULT_PATH,
362        },
363    };
364
365    const MACOSX_10_9_SETTINGS_PLIST: &[u8] = include_bytes!("testfiles/macosx10.9-settings.plist");
366    const MACOSX_10_10_SETTINGS_PLIST: &[u8] =
367        include_bytes!("testfiles/macosx10.10-settings.plist");
368    const MACOSX_10_15_SETTINGS_JSON: &[u8] = include_bytes!("testfiles/macosx10.15-settings.json");
369    const MACOSX_11_3_SETTINGS_JSON: &[u8] = include_bytes!("testfiles/macosx11.3-settings.json");
370
371    fn macosx_10_9() -> Result<ParsedSdk, Error> {
372        let value = plist::Value::from_reader(std::io::Cursor::new(MACOSX_10_9_SETTINGS_PLIST))?;
373
374        ParsedSdk::from_plist(
375            PathBuf::from("MacOSX10.9.sdk"),
376            false,
377            Platform::MacOsX,
378            value,
379        )
380    }
381
382    fn macosx_10_10() -> Result<ParsedSdk, Error> {
383        let value = plist::Value::from_reader(std::io::Cursor::new(MACOSX_10_10_SETTINGS_PLIST))?;
384
385        ParsedSdk::from_plist(
386            PathBuf::from("MacOSX10.10.sdk"),
387            false,
388            Platform::MacOsX,
389            value,
390        )
391    }
392
393    fn macosx_10_15() -> Result<ParsedSdk, Error> {
394        let value = serde_json::from_slice::<SdkSettingsJson>(MACOSX_10_15_SETTINGS_JSON)?;
395
396        ParsedSdk::from_json(
397            PathBuf::from("MacOSX10.15.sdk"),
398            false,
399            Platform::MacOsX,
400            value,
401        )
402    }
403
404    fn macosx_11_3() -> Result<ParsedSdk, Error> {
405        let value = serde_json::from_slice::<SdkSettingsJson>(MACOSX_11_3_SETTINGS_JSON)?;
406
407        ParsedSdk::from_json(
408            PathBuf::from("MacOSX11.3.sdk"),
409            false,
410            Platform::MacOsX,
411            value,
412        )
413    }
414
415    fn all_test_sdks() -> Result<Vec<ParsedSdk>, Error> {
416        Ok(vec![
417            macosx_10_9()?,
418            macosx_10_10()?,
419            macosx_10_15()?,
420            macosx_11_3()?,
421        ])
422    }
423
424    #[test]
425    fn find_default_sdks() -> Result<(), Error> {
426        if let Ok(developer_dir) = DeveloperDirectory::find_default_required() {
427            assert!(!developer_dir.sdks::<ParsedSdk>()?.is_empty());
428        }
429
430        Ok(())
431    }
432
433    #[test]
434    fn find_command_line_tools_sdks() -> Result<(), Error> {
435        let sdk_path = PathBuf::from(COMMAND_LINE_TOOLS_DEFAULT_PATH).join("SDKs");
436
437        let res = ParsedSdk::find_command_line_tools_sdks()?;
438
439        if sdk_path.exists() {
440            assert!(res.is_some());
441            assert!(!res.unwrap().is_empty());
442        } else {
443            assert!(res.is_none());
444        }
445
446        Ok(())
447    }
448
449    #[test]
450    fn find_all_sdks() -> Result<(), Error> {
451        for dir in DeveloperDirectory::find_system_xcodes()? {
452            for sdk in dir.sdks::<ParsedSdk>()? {
453                assert!(!matches!(sdk.platform(), Platform::Unknown(_)));
454                assert!(sdk.version().is_some());
455            }
456        }
457
458        SdkSearch::default()
459            .location(SdkSearchLocation::SystemXcodes)
460            .search::<ParsedSdk>()?;
461
462        Ok(())
463    }
464
465    #[test]
466    fn parse_test_sdks() -> Result<(), Error> {
467        all_test_sdks()?;
468
469        Ok(())
470    }
471
472    #[test]
473    fn supports_deployment_target() -> Result<(), Error> {
474        let sdk = macosx_10_15()?;
475
476        assert!(!sdk.supports_deployment_target("ios", &SdkVersion::from("55.0"))?);
477        assert!(!sdk.supports_deployment_target("macosx", &SdkVersion::from("10.5"))?);
478        assert!(!sdk.supports_deployment_target("macosx", &SdkVersion::from("10.16"))?);
479        assert!(!sdk.supports_deployment_target("macosx", &SdkVersion::from("11.0"))?);
480
481        let mut versions = vec!["10.9", "10.10", "10.11", "10.12", "10.13", "10.14", "10.15"];
482
483        for version in &versions {
484            assert!(sdk.supports_deployment_target("macosx", &SdkVersion::from(*version))?);
485        }
486
487        let sdk = macosx_11_3()?;
488        versions.extend(["11.0", "11.1", "11.2", "11.3"]);
489
490        for version in &versions {
491            assert!(sdk.supports_deployment_target("macosx", &SdkVersion::from(*version))?);
492        }
493
494        // API doesn't work for plists.
495        assert!(!macosx_10_9()?.supports_deployment_target("macosx", &SdkVersion::from("10.9"))?);
496        assert!(!macosx_10_10()?.supports_deployment_target("macosx", &SdkVersion::from("10.9"))?);
497
498        Ok(())
499    }
500}