Skip to main content

target_spec/
summaries.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Serialized versions of platform and target features.
5//!
6//! Some users of `target-spec` may want to serialize and deserialize its data structures into, say,
7//! TOML files. This module provides facilities for that.
8//!
9//! Summaries require the `summaries` feature to be enabled.
10
11use crate::{Error, Platform, TargetFeatures};
12use serde::{Deserialize, Serialize};
13use std::{borrow::Cow, collections::BTreeSet};
14
15impl Platform {
16    /// Converts this `Platform` to a serializable form.
17    ///
18    /// Requires the `summaries` feature to be enabled.
19    #[inline]
20    pub fn to_summary(&self) -> PlatformSummary {
21        PlatformSummary::from_platform(self)
22    }
23}
24
25/// An owned, serializable version of [`Platform`].
26///
27/// This structure can be serialized and deserialized using `serde`.
28///
29/// Requires the `summaries` feature to be enabled.
30#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "kebab-case")]
32#[non_exhaustive]
33pub struct PlatformSummary {
34    /// The platform triple.
35    pub triple: String,
36
37    /// JSON for custom platforms.
38    #[serde(skip_serializing_if = "Option::is_none", default)]
39    pub custom_json: Option<String>,
40
41    /// `rustc --print=cfg` output for custom platforms.
42    #[serde(skip_serializing_if = "Option::is_none", default)]
43    pub custom_cfg: Option<String>,
44
45    /// The target features used.
46    pub target_features: TargetFeaturesSummary,
47
48    /// The flags enabled.
49    #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
50    pub flags: BTreeSet<String>,
51}
52
53impl PlatformSummary {
54    /// Creates a new `PlatformSummary` with the provided triple and default options.
55    ///
56    /// The default options are:
57    ///
58    /// * `custom_json` is set to `None`.
59    /// * `custom_cfg` is set to `None`.
60    /// * `target_features` is set to [`TargetFeaturesSummary::Unknown`].
61    /// * `flags` is empty.
62    pub fn new(triple_str: impl Into<String>) -> Self {
63        Self {
64            triple: triple_str.into(),
65            custom_json: None,
66            custom_cfg: None,
67            target_features: TargetFeaturesSummary::Unknown,
68            flags: BTreeSet::new(),
69        }
70    }
71
72    /// If this represents a custom platform, sets the target
73    /// definition JSON for it.
74    ///
75    /// This clears any previously set `custom_cfg`, since only
76    /// one custom platform source is allowed.
77    ///
78    /// For more about target definition JSON, see [Creating a
79    /// custom
80    /// target](https://docs.rust-embedded.org/embedonomicon/custom-target.html)
81    /// in the Rust Embedonomicon.
82    pub fn with_custom_json(mut self, custom_json: impl Into<String>) -> Self {
83        self.custom_json = Some(custom_json.into());
84        self.custom_cfg = None;
85        self
86    }
87
88    /// If this represents a custom platform created from
89    /// `rustc --print=cfg` output, sets that output.
90    ///
91    /// This clears any previously set `custom_json`, since only
92    /// one custom platform source is allowed.
93    pub fn with_custom_cfg(mut self, custom_cfg: impl Into<String>) -> Self {
94        self.custom_cfg = Some(custom_cfg.into());
95        self.custom_json = None;
96        self
97    }
98
99    /// Sets the target features for this platform.
100    pub fn with_target_features(mut self, target_features: TargetFeaturesSummary) -> Self {
101        self.target_features = target_features;
102        self
103    }
104
105    /// Adds flags for this platform.
106    pub fn with_added_flags(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
107        self.flags.extend(flags.into_iter().map(|flag| flag.into()));
108        self
109    }
110
111    /// Creates a new `PlatformSummary` instance from a platform.
112    pub fn from_platform(platform: &Platform) -> Self {
113        Self {
114            triple: platform.triple_str().to_string(),
115            custom_json: platform.custom_json().map(|s| s.to_owned()),
116            custom_cfg: platform.custom_cfg_text().map(|s| s.to_owned()),
117            target_features: TargetFeaturesSummary::new(platform.target_features()),
118            flags: platform.flags().map(|flag| flag.to_string()).collect(),
119        }
120    }
121
122    /// Converts `self` to a `Platform`.
123    ///
124    /// Returns an `Error` if the platform was unknown.
125    pub fn to_platform(&self) -> Result<Platform, Error> {
126        if self.custom_json.is_some() && self.custom_cfg.is_some() {
127            return Err(Error::CustomPlatformCreate(
128                crate::errors::CustomTripleCreateError::ConflictingCustomPlatformSources {
129                    triple: self.triple.clone(),
130                },
131            ));
132        }
133
134        #[allow(unused_variables)] // in some feature branches, json/cfg aren't used
135        let mut platform = if let Some(json) = &self.custom_json {
136            #[cfg(not(feature = "custom"))]
137            return Err(Error::CustomPlatformCreate(
138                crate::errors::CustomTripleCreateError::CustomJsonUnavailable,
139            ));
140
141            #[cfg(feature = "custom")]
142            Platform::new_custom(
143                self.triple.to_owned(),
144                json,
145                self.target_features.to_target_features(),
146            )?
147        } else if let Some(cfg_text) = &self.custom_cfg {
148            #[cfg(not(feature = "custom-cfg"))]
149            return Err(Error::CustomPlatformCreate(
150                crate::errors::CustomTripleCreateError::CustomCfgUnavailable,
151            ));
152
153            #[cfg(feature = "custom-cfg")]
154            Platform::new_custom_cfg(
155                self.triple.to_owned(),
156                cfg_text,
157                self.target_features.to_target_features(),
158            )?
159        } else {
160            Platform::new(
161                self.triple.to_owned(),
162                self.target_features.to_target_features(),
163            )?
164        };
165
166        platform.add_flags(self.flags.iter().cloned());
167        Ok(platform)
168    }
169}
170
171/// An owned, serializable version of [`TargetFeatures`].
172///
173/// This type can be serialized and deserialized using `serde`.
174///
175/// Requires the `summaries` feature to be enabled.
176#[derive(Clone, Debug, Eq, PartialEq)]
177#[non_exhaustive]
178#[derive(Default)]
179pub enum TargetFeaturesSummary {
180    /// The target features are unknown.
181    ///
182    /// This is the default.
183    #[default]
184    Unknown,
185    /// Only match the specified features.
186    Features(BTreeSet<String>),
187    /// Match all features.
188    All,
189}
190
191impl TargetFeaturesSummary {
192    /// Creates a new `TargetFeaturesSummary` from a `TargetFeatures`.
193    pub fn new(target_features: &TargetFeatures) -> Self {
194        match target_features {
195            TargetFeatures::Unknown => TargetFeaturesSummary::Unknown,
196            TargetFeatures::Features(features) => TargetFeaturesSummary::Features(
197                features.iter().map(|feature| feature.to_string()).collect(),
198            ),
199            TargetFeatures::All => TargetFeaturesSummary::All,
200        }
201    }
202
203    /// Converts `self` to a `TargetFeatures` instance.
204    pub fn to_target_features(&self) -> TargetFeatures {
205        match self {
206            TargetFeaturesSummary::Unknown => TargetFeatures::Unknown,
207            TargetFeaturesSummary::All => TargetFeatures::All,
208            TargetFeaturesSummary::Features(features) => {
209                let features = features
210                    .iter()
211                    .map(|feature| Cow::Owned(feature.clone()))
212                    .collect();
213                TargetFeatures::Features(features)
214            }
215        }
216    }
217}
218
219mod platform_impl {
220    use super::*;
221    use serde::Deserializer;
222
223    impl<'de> Deserialize<'de> for PlatformSummary {
224        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225        where
226            D: Deserializer<'de>,
227        {
228            let d = PlatformSummaryDeserialize::deserialize(deserializer)?;
229            match d {
230                PlatformSummaryDeserialize::String(triple) => Ok(PlatformSummary {
231                    triple,
232                    custom_json: None,
233                    custom_cfg: None,
234                    target_features: TargetFeaturesSummary::default(),
235                    flags: BTreeSet::default(),
236                }),
237                PlatformSummaryDeserialize::Full {
238                    triple,
239                    custom_json,
240                    custom_cfg,
241                    target_features,
242                    flags,
243                } => Ok(PlatformSummary {
244                    triple,
245                    custom_json,
246                    custom_cfg,
247                    target_features,
248                    flags,
249                }),
250            }
251        }
252    }
253
254    #[derive(Deserialize)]
255    #[serde(untagged)]
256    enum PlatformSummaryDeserialize {
257        String(String),
258        #[serde(rename_all = "kebab-case")]
259        Full {
260            triple: String,
261            #[serde(default)]
262            custom_json: Option<String>,
263            #[serde(default)]
264            custom_cfg: Option<String>,
265            /// The target features used.
266            #[serde(default)]
267            target_features: TargetFeaturesSummary,
268            /// The flags enabled.
269            #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
270            flags: BTreeSet<String>,
271        },
272    }
273}
274
275mod target_features_impl {
276    use super::*;
277    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
278
279    impl Serialize for TargetFeaturesSummary {
280        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281        where
282            S: Serializer,
283        {
284            match self {
285                TargetFeaturesSummary::Unknown => "unknown".serialize(serializer),
286                TargetFeaturesSummary::All => "all".serialize(serializer),
287                TargetFeaturesSummary::Features(features) => features.serialize(serializer),
288            }
289        }
290    }
291
292    impl<'de> Deserialize<'de> for TargetFeaturesSummary {
293        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
294        where
295            D: Deserializer<'de>,
296        {
297            let d = TargetFeaturesDeserialize::deserialize(deserializer)?;
298            match d {
299                TargetFeaturesDeserialize::String(target_features) => {
300                    match target_features.as_str() {
301                        "unknown" => Ok(TargetFeaturesSummary::Unknown),
302                        "all" => Ok(TargetFeaturesSummary::All),
303                        other => Err(D::Error::custom(format!(
304                            "unknown string for target features: {other}",
305                        ))),
306                    }
307                }
308                TargetFeaturesDeserialize::List(target_features) => {
309                    Ok(TargetFeaturesSummary::Features(target_features))
310                }
311            }
312        }
313    }
314
315    #[derive(Deserialize)]
316    #[serde(untagged)]
317    enum TargetFeaturesDeserialize {
318        String(String),
319        List(BTreeSet<String>),
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    #![allow(clippy::vec_init_then_push)]
326
327    use super::*;
328
329    #[test]
330    fn platform_deserialize_valid() {
331        // Need a wrapper because of TOML restrictions
332        #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
333        struct Wrapper {
334            platform: PlatformSummary,
335        }
336
337        let mut valid = vec![];
338        valid.push((
339            r#"platform = "x86_64-unknown-linux-gnu""#,
340            PlatformSummary {
341                triple: "x86_64-unknown-linux-gnu".into(),
342                custom_json: None,
343                custom_cfg: None,
344                target_features: TargetFeaturesSummary::Unknown,
345                flags: BTreeSet::new(),
346            },
347        ));
348        valid.push((
349            r#"platform = { triple = "x86_64-unknown-linux-gnu" }"#,
350            PlatformSummary {
351                triple: "x86_64-unknown-linux-gnu".into(),
352                custom_json: None,
353                custom_cfg: None,
354                target_features: TargetFeaturesSummary::Unknown,
355                flags: BTreeSet::new(),
356            },
357        ));
358        valid.push((
359            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "unknown" }"#,
360            PlatformSummary {
361                triple: "x86_64-unknown-linux-gnu".into(),
362                custom_json: None,
363                custom_cfg: None,
364                target_features: TargetFeaturesSummary::Unknown,
365                flags: BTreeSet::new(),
366            },
367        ));
368        valid.push((
369            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "all" }"#,
370            PlatformSummary {
371                triple: "x86_64-unknown-linux-gnu".into(),
372                custom_json: None,
373                custom_cfg: None,
374                target_features: TargetFeaturesSummary::All,
375                flags: BTreeSet::new(),
376            },
377        ));
378        valid.push((
379            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = [] }"#,
380            PlatformSummary {
381                triple: "x86_64-unknown-linux-gnu".into(),
382                custom_json: None,
383                custom_cfg: None,
384                target_features: TargetFeaturesSummary::Features(BTreeSet::new()),
385                flags: BTreeSet::new(),
386            },
387        ));
388
389        let custom_json = r#"{"arch":"x86_64","target-pointer-width":"64","llvm-target":"x86_64-unknown-haiku","data-layout":"e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128","os":"haiku","abi":null,"env":null,"vendor":null,"families":[],"endian":"little","min-atomic-width":null,"max-atomic-width":64,"panic-strategy":"unwind"}"#;
390        let toml = format!(
391            r#"platform = {{ triple = "x86_64-unknown-haiku", custom-json = '{custom_json}' }}"#
392        );
393
394        valid.push((
395            &toml,
396            PlatformSummary {
397                triple: "x86_64-unknown-haiku".into(),
398                custom_json: Some(custom_json.to_owned()),
399                custom_cfg: None,
400                target_features: TargetFeaturesSummary::Unknown,
401                flags: BTreeSet::new(),
402            },
403        ));
404
405        let mut flags = BTreeSet::new();
406        flags.insert("cargo_web".to_owned());
407        valid.push((
408            r#"platform = { triple = "x86_64-unknown-linux-gnu", flags = ["cargo_web"] }"#,
409            PlatformSummary {
410                triple: "x86_64-unknown-linux-gnu".into(),
411                custom_json: None,
412                custom_cfg: None,
413                target_features: TargetFeaturesSummary::Unknown,
414                flags,
415            },
416        ));
417
418        let custom_cfg = indoc::indoc! {r#"
419            panic="unwind"
420            target_arch="x86_64"
421            target_endian="little"
422            target_env="gnu"
423            target_family="unix"
424            target_os="linux"
425            target_pointer_width="64"
426            target_vendor="unknown"
427        "#};
428        let toml_cfg = format!(
429            "[platform]\n\
430             triple = \"my-custom-linux\"\n\
431             custom-cfg = '''\n\
432             {custom_cfg}'''"
433        );
434        valid.push((
435            &toml_cfg,
436            PlatformSummary {
437                triple: "my-custom-linux".into(),
438                custom_json: None,
439                custom_cfg: Some(custom_cfg.to_owned()),
440                target_features: TargetFeaturesSummary::Unknown,
441                flags: BTreeSet::new(),
442            },
443        ));
444
445        for (input, expected) in valid {
446            let actual: Wrapper =
447                toml::from_str(input).unwrap_or_else(|err| panic!("input {input} is valid: {err}"));
448            assert_eq!(actual.platform, expected, "for input: {input}");
449
450            // Serialize and deserialize again.
451            let serialized = toml::to_string(&actual).expect("serialized correctly");
452            let actual_2: Wrapper = toml::from_str(&serialized)
453                .unwrap_or_else(|err| panic!("serialized input: {input} is valid: {err}"));
454            assert_eq!(actual, actual_2, "for input: {input}");
455
456            // Check that custom JSON functionality works.
457            if actual.platform.custom_json.is_some() {
458                #[cfg(feature = "custom")]
459                {
460                    let platform = actual
461                        .platform
462                        .to_platform()
463                        .expect("custom platform parsed successfully");
464                    assert!(platform.is_custom(), "this is a custom platform");
465                }
466
467                #[cfg(not(feature = "custom"))]
468                {
469                    use crate::errors::CustomTripleCreateError;
470
471                    let error = actual
472                        .platform
473                        .to_platform()
474                        .expect_err("custom platforms are disabled");
475                    assert!(matches!(
476                        error,
477                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomJsonUnavailable)
478                    ));
479                }
480            }
481
482            // Check that custom cfg functionality works.
483            if actual.platform.custom_cfg.is_some() {
484                #[cfg(feature = "custom-cfg")]
485                {
486                    let platform = actual
487                        .platform
488                        .to_platform()
489                        .expect("custom cfg platform parsed successfully");
490                    assert!(platform.is_custom(), "this is a custom platform");
491                }
492
493                #[cfg(not(feature = "custom-cfg"))]
494                {
495                    use crate::errors::CustomTripleCreateError;
496
497                    let error = actual
498                        .platform
499                        .to_platform()
500                        .expect_err("custom cfg platforms are disabled");
501                    assert!(matches!(
502                        error,
503                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomCfgUnavailable)
504                    ));
505                }
506            }
507        }
508    }
509}
510
511#[cfg(all(test, feature = "proptest1"))]
512mod proptests {
513    use super::*;
514    use proptest::prelude::*;
515    use std::collections::HashSet;
516
517    proptest! {
518        #[test]
519        fn summary_roundtrip(platform in Platform::strategy(any::<TargetFeatures>())) {
520            let summary = PlatformSummary::from_platform(&platform);
521            let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
522
523            let deserialized: PlatformSummary = toml::from_str(&serialized).expect("deserialization succeeded");
524            assert_eq!(summary, deserialized, "summary and deserialized should match");
525            let platform2 = deserialized.to_platform().expect("conversion to Platform succeeded");
526
527            assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
528            assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
529            assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
530        }
531    }
532}