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::{CustomSource, 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        let (custom_json, custom_cfg) = match platform.custom_source() {
114            None => (None, None),
115            Some(CustomSource::Json(json)) => (Some(json.to_owned()), None),
116            Some(CustomSource::Cfg(cfg_text)) => (None, Some(cfg_text.to_owned())),
117        };
118        Self {
119            triple: platform.triple_str().to_string(),
120            custom_json,
121            custom_cfg,
122            target_features: TargetFeaturesSummary::new(platform.target_features()),
123            flags: platform.flags().map(|flag| flag.to_string()).collect(),
124        }
125    }
126
127    /// Converts `self` to a `Platform`.
128    ///
129    /// Returns an `Error` if the platform was unknown.
130    pub fn to_platform(&self) -> Result<Platform, Error> {
131        if self.custom_json.is_some() && self.custom_cfg.is_some() {
132            return Err(Error::CustomPlatformCreate(
133                crate::errors::CustomTripleCreateError::ConflictingCustomPlatformSources {
134                    triple: self.triple.clone(),
135                },
136            ));
137        }
138
139        #[allow(unused_variables)] // in some feature branches, json/cfg aren't used
140        let mut platform = if let Some(json) = &self.custom_json {
141            #[cfg(not(feature = "custom"))]
142            return Err(Error::CustomPlatformCreate(
143                crate::errors::CustomTripleCreateError::CustomJsonUnavailable,
144            ));
145
146            #[cfg(feature = "custom")]
147            Platform::new_custom(
148                self.triple.to_owned(),
149                json,
150                self.target_features.to_target_features(),
151            )?
152        } else if let Some(cfg_text) = &self.custom_cfg {
153            #[cfg(not(feature = "custom-cfg"))]
154            return Err(Error::CustomPlatformCreate(
155                crate::errors::CustomTripleCreateError::CustomCfgUnavailable,
156            ));
157
158            #[cfg(feature = "custom-cfg")]
159            Platform::new_custom_cfg(
160                self.triple.to_owned(),
161                cfg_text,
162                self.target_features.to_target_features(),
163            )?
164        } else {
165            Platform::new(
166                self.triple.to_owned(),
167                self.target_features.to_target_features(),
168            )?
169        };
170
171        platform.add_flags(self.flags.iter().cloned());
172        Ok(platform)
173    }
174}
175
176/// An owned, serializable version of [`TargetFeatures`].
177///
178/// This type can be serialized and deserialized using `serde`.
179///
180/// Requires the `summaries` feature to be enabled.
181#[derive(Clone, Debug, Eq, PartialEq)]
182#[non_exhaustive]
183#[derive(Default)]
184pub enum TargetFeaturesSummary {
185    /// The target features are unknown.
186    ///
187    /// This is the default.
188    #[default]
189    Unknown,
190    /// Only match the specified features.
191    Features(BTreeSet<String>),
192    /// Match all features.
193    All,
194}
195
196impl TargetFeaturesSummary {
197    /// Creates a new `TargetFeaturesSummary` from a `TargetFeatures`.
198    pub fn new(target_features: &TargetFeatures) -> Self {
199        match target_features {
200            TargetFeatures::Unknown => TargetFeaturesSummary::Unknown,
201            TargetFeatures::Features(features) => TargetFeaturesSummary::Features(
202                features.iter().map(|feature| feature.to_string()).collect(),
203            ),
204            TargetFeatures::All => TargetFeaturesSummary::All,
205        }
206    }
207
208    /// Converts `self` to a `TargetFeatures` instance.
209    pub fn to_target_features(&self) -> TargetFeatures {
210        match self {
211            TargetFeaturesSummary::Unknown => TargetFeatures::Unknown,
212            TargetFeaturesSummary::All => TargetFeatures::All,
213            TargetFeaturesSummary::Features(features) => {
214                let features = features
215                    .iter()
216                    .map(|feature| Cow::Owned(feature.clone()))
217                    .collect();
218                TargetFeatures::Features(features)
219            }
220        }
221    }
222}
223
224mod platform_impl {
225    use super::*;
226    use serde::{
227        Deserializer,
228        de::{self, MapAccess, Visitor, value::MapAccessDeserializer},
229    };
230    use std::fmt;
231
232    impl<'de> Deserialize<'de> for PlatformSummary {
233        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
234        where
235            D: Deserializer<'de>,
236        {
237            deserializer.deserialize_any(PlatformSummaryVisitor)
238        }
239    }
240
241    // This is a hand-written visitor and not serde(untagged) for better error
242    // messages.
243    struct PlatformSummaryVisitor;
244
245    impl<'de> Visitor<'de> for PlatformSummaryVisitor {
246        type Value = PlatformSummary;
247
248        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249            f.write_str("a platform (a triple string or a table with a `triple` key)")
250        }
251
252        fn visit_str<E>(self, triple: &str) -> Result<Self::Value, E>
253        where
254            E: de::Error,
255        {
256            validate_triple(triple)?;
257            Ok(PlatformSummary::new(triple))
258        }
259
260        fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
261        where
262            A: MapAccess<'de>,
263        {
264            let PlatformSummaryTable {
265                triple,
266                custom_json,
267                custom_cfg,
268                target_features,
269                flags,
270            } = PlatformSummaryTable::deserialize(MapAccessDeserializer::new(map))?;
271            validate_triple(&triple)?;
272            Ok(PlatformSummary {
273                triple,
274                custom_json,
275                custom_cfg,
276                target_features,
277                flags,
278            })
279        }
280    }
281
282    fn validate_triple<E: de::Error>(triple: &str) -> Result<(), E> {
283        if triple.is_empty() {
284            return Err(E::custom("a platform triple cannot be empty"));
285        }
286        Ok(())
287    }
288
289    #[derive(Deserialize)]
290    #[serde(rename_all = "kebab-case", deny_unknown_fields)]
291    struct PlatformSummaryTable {
292        triple: String,
293        #[serde(default)]
294        custom_json: Option<String>,
295        #[serde(default)]
296        custom_cfg: Option<String>,
297        #[serde(default)]
298        target_features: TargetFeaturesSummary,
299        #[serde(default)]
300        flags: BTreeSet<String>,
301    }
302}
303
304mod target_features_impl {
305    use super::*;
306    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
307
308    impl Serialize for TargetFeaturesSummary {
309        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
310        where
311            S: Serializer,
312        {
313            match self {
314                TargetFeaturesSummary::Unknown => "unknown".serialize(serializer),
315                TargetFeaturesSummary::All => "all".serialize(serializer),
316                TargetFeaturesSummary::Features(features) => features.serialize(serializer),
317            }
318        }
319    }
320
321    impl<'de> Deserialize<'de> for TargetFeaturesSummary {
322        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
323        where
324            D: Deserializer<'de>,
325        {
326            let d = TargetFeaturesDeserialize::deserialize(deserializer)?;
327            match d {
328                TargetFeaturesDeserialize::String(target_features) => {
329                    match target_features.as_str() {
330                        "unknown" => Ok(TargetFeaturesSummary::Unknown),
331                        "all" => Ok(TargetFeaturesSummary::All),
332                        other => Err(D::Error::custom(format!(
333                            "unknown string for target features: {other}",
334                        ))),
335                    }
336                }
337                TargetFeaturesDeserialize::List(target_features) => {
338                    Ok(TargetFeaturesSummary::Features(target_features))
339                }
340            }
341        }
342    }
343
344    #[derive(Deserialize)]
345    #[serde(untagged)]
346    enum TargetFeaturesDeserialize {
347        String(String),
348        List(BTreeSet<String>),
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    #![allow(clippy::vec_init_then_push)]
355
356    use super::*;
357
358    #[test]
359    fn platform_deserialize_valid() {
360        // Need a wrapper because of TOML restrictions
361        #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
362        struct Wrapper {
363            platform: PlatformSummary,
364        }
365
366        let mut valid = vec![];
367        valid.push((
368            r#"platform = "x86_64-unknown-linux-gnu""#,
369            PlatformSummary {
370                triple: "x86_64-unknown-linux-gnu".into(),
371                custom_json: None,
372                custom_cfg: None,
373                target_features: TargetFeaturesSummary::Unknown,
374                flags: BTreeSet::new(),
375            },
376        ));
377        valid.push((
378            r#"platform = { triple = "x86_64-unknown-linux-gnu" }"#,
379            PlatformSummary {
380                triple: "x86_64-unknown-linux-gnu".into(),
381                custom_json: None,
382                custom_cfg: None,
383                target_features: TargetFeaturesSummary::Unknown,
384                flags: BTreeSet::new(),
385            },
386        ));
387        valid.push((
388            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "unknown" }"#,
389            PlatformSummary {
390                triple: "x86_64-unknown-linux-gnu".into(),
391                custom_json: None,
392                custom_cfg: None,
393                target_features: TargetFeaturesSummary::Unknown,
394                flags: BTreeSet::new(),
395            },
396        ));
397        valid.push((
398            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "all" }"#,
399            PlatformSummary {
400                triple: "x86_64-unknown-linux-gnu".into(),
401                custom_json: None,
402                custom_cfg: None,
403                target_features: TargetFeaturesSummary::All,
404                flags: BTreeSet::new(),
405            },
406        ));
407        valid.push((
408            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = [] }"#,
409            PlatformSummary {
410                triple: "x86_64-unknown-linux-gnu".into(),
411                custom_json: None,
412                custom_cfg: None,
413                target_features: TargetFeaturesSummary::Features(BTreeSet::new()),
414                flags: BTreeSet::new(),
415            },
416        ));
417
418        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,"target-family":["unix"],"target-endian":"little","min-atomic-width":null,"max-atomic-width":64,"panic-strategy":"unwind"}"#;
419        let toml = format!(
420            r#"platform = {{ triple = "x86_64-unknown-haiku", custom-json = '{custom_json}' }}"#
421        );
422
423        valid.push((
424            &toml,
425            PlatformSummary {
426                triple: "x86_64-unknown-haiku".into(),
427                custom_json: Some(custom_json.to_owned()),
428                custom_cfg: None,
429                target_features: TargetFeaturesSummary::Unknown,
430                flags: BTreeSet::new(),
431            },
432        ));
433
434        let custom_json_pretty = indoc::indoc! {r#"
435            {
436              "arch": "x86_64",
437              "cpu": "x86-64",
438              "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
439              "dynamic-linking": true,
440              "env": "gnu",
441              "executables": true,
442              "features": "+sse2,+fxsr",
443              "linker-flavor": "gnu-cc",
444              "llvm-target": "x86_64-unknown-linux-gnu",
445              "max-atomic-width": 64,
446              "os": "linux",
447              "target-family": [
448                "unix"
449              ],
450              "target-pointer-width": 64
451            }
452        "#};
453        let toml_json_pretty = format!(
454            "[platform]\n\
455             triple = \"my-custom-x86_64\"\n\
456             custom-json = '''\n\
457             {custom_json_pretty}'''"
458        );
459        valid.push((
460            &toml_json_pretty,
461            PlatformSummary {
462                triple: "my-custom-x86_64".into(),
463                custom_json: Some(custom_json_pretty.to_owned()),
464                custom_cfg: None,
465                target_features: TargetFeaturesSummary::Unknown,
466                flags: BTreeSet::new(),
467            },
468        ));
469
470        let mut flags = BTreeSet::new();
471        flags.insert("cargo_web".to_owned());
472        valid.push((
473            r#"platform = { triple = "x86_64-unknown-linux-gnu", flags = ["cargo_web"] }"#,
474            PlatformSummary {
475                triple: "x86_64-unknown-linux-gnu".into(),
476                custom_json: None,
477                custom_cfg: None,
478                target_features: TargetFeaturesSummary::Unknown,
479                flags,
480            },
481        ));
482
483        let custom_cfg = indoc::indoc! {r#"
484            panic="unwind"
485            target_arch="x86_64"
486            target_endian="little"
487            target_env="gnu"
488            target_family="unix"
489            target_os="linux"
490            target_pointer_width="64"
491            target_vendor="unknown"
492        "#};
493        let toml_cfg = format!(
494            "[platform]\n\
495             triple = \"my-custom-linux\"\n\
496             custom-cfg = '''\n\
497             {custom_cfg}'''"
498        );
499        valid.push((
500            &toml_cfg,
501            PlatformSummary {
502                triple: "my-custom-linux".into(),
503                custom_json: None,
504                custom_cfg: Some(custom_cfg.to_owned()),
505                target_features: TargetFeaturesSummary::Unknown,
506                flags: BTreeSet::new(),
507            },
508        ));
509
510        for (input, expected) in valid {
511            let actual: Wrapper =
512                toml::from_str(input).unwrap_or_else(|err| panic!("input {input} is valid: {err}"));
513            assert_eq!(actual.platform, expected, "for input: {input}");
514
515            // Serialize and deserialize again.
516            let serialized = toml::to_string(&actual).expect("serialized correctly");
517            let actual_2: Wrapper = toml::from_str(&serialized)
518                .unwrap_or_else(|err| panic!("serialized input: {input} is valid: {err}"));
519            assert_eq!(actual, actual_2, "for input: {input}");
520
521            if actual.platform.custom_json.is_none() && actual.platform.custom_cfg.is_none() {
522                let platform = actual
523                    .platform
524                    .to_platform()
525                    .unwrap_or_else(|err| panic!("for input {input}, platform is valid: {err}"));
526                assert_eq!(platform.custom_source(), None, "for input: {input}");
527            }
528
529            // Check that custom JSON functionality works.
530            if actual.platform.custom_json.is_some() {
531                #[cfg(feature = "custom")]
532                {
533                    let platform = actual
534                        .platform
535                        .to_platform()
536                        .expect("custom platform parsed successfully");
537                    assert!(platform.is_custom(), "this is a custom platform");
538                    assert_eq!(
539                        platform.custom_source(),
540                        Some(CustomSource::Json(
541                            actual
542                                .platform
543                                .custom_json
544                                .as_deref()
545                                .expect("checked above")
546                        )),
547                        "for input: {input}"
548                    );
549                    assert_eq!(platform.to_summary(), actual.platform, "for input: {input}");
550                    assert_eq!(
551                        platform
552                            .to_summary()
553                            .to_platform()
554                            .expect("summary round-trips"),
555                        platform,
556                        "for input: {input}"
557                    );
558                }
559
560                #[cfg(not(feature = "custom"))]
561                {
562                    use crate::errors::CustomTripleCreateError;
563
564                    let error = actual
565                        .platform
566                        .to_platform()
567                        .expect_err("custom platforms are disabled");
568                    assert!(matches!(
569                        error,
570                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomJsonUnavailable)
571                    ));
572                }
573            }
574
575            // Check that custom cfg functionality works.
576            if actual.platform.custom_cfg.is_some() {
577                #[cfg(feature = "custom-cfg")]
578                {
579                    let platform = actual
580                        .platform
581                        .to_platform()
582                        .expect("custom cfg platform parsed successfully");
583                    assert!(platform.is_custom(), "this is a custom platform");
584                    assert_eq!(
585                        platform.custom_source(),
586                        Some(CustomSource::Cfg(
587                            actual
588                                .platform
589                                .custom_cfg
590                                .as_deref()
591                                .expect("checked above")
592                        )),
593                        "for input: {input}"
594                    );
595                    assert_eq!(platform.to_summary(), actual.platform, "for input: {input}");
596                }
597
598                #[cfg(not(feature = "custom-cfg"))]
599                {
600                    use crate::errors::CustomTripleCreateError;
601
602                    let error = actual
603                        .platform
604                        .to_platform()
605                        .expect_err("custom cfg platforms are disabled");
606                    assert!(matches!(
607                        error,
608                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomCfgUnavailable)
609                    ));
610                }
611            }
612        }
613    }
614    #[test]
615    fn platform_deserialize_errors() {
616        #[derive(Debug, Deserialize)]
617        struct Wrapper {
618            #[allow(dead_code)]
619            platform: PlatformSummary,
620        }
621
622        for (input, expected) in [
623            (r#"platform = """#, "a platform triple cannot be empty"),
624            (
625                r#"platform = { triple = "" }"#,
626                "a platform triple cannot be empty",
627            ),
628            (
629                r#"platform = { target-features = [] }"#,
630                "missing field `triple`",
631            ),
632            (
633                r#"platform = { tripel = "x86_64-unknown-linux-gnu" }"#,
634                "unknown field `tripel`",
635            ),
636            (
637                r#"platform = { triple = "x86_64-unknown-linux-gnu", target-feature = ["sse2"] }"#,
638                "unknown field `target-feature`",
639            ),
640            (
641                r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "bogus" }"#,
642                "unknown string for target features: bogus",
643            ),
644            (
645                "platform = 5",
646                "expected a platform (a triple string or a table with a `triple` key)",
647            ),
648            (
649                r#"platform = ["x86_64-unknown-linux-gnu"]"#,
650                "expected a platform (a triple string or a table with a `triple` key)",
651            ),
652        ] {
653            let message = toml::from_str::<Wrapper>(input)
654                .expect_err("input rejected")
655                .to_string();
656            assert!(
657                message.contains(expected),
658                "for input {input}: error `{message}` contains `{expected}`",
659            );
660        }
661    }
662}
663
664#[cfg(all(test, feature = "proptest1"))]
665mod proptests {
666    use super::*;
667    use proptest::prelude::*;
668    use std::collections::HashSet;
669
670    proptest! {
671        #[test]
672        fn summary_roundtrip(platform in Platform::strategy(any::<TargetFeatures>())) {
673            let summary = PlatformSummary::from_platform(&platform);
674            let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
675
676            let deserialized: PlatformSummary = toml::from_str(&serialized).expect("deserialization succeeded");
677            assert_eq!(summary, deserialized, "summary and deserialized should match");
678            let platform2 = deserialized.to_platform().expect("conversion to Platform succeeded");
679
680            assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
681            assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
682            assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
683        }
684    }
685}