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::{
222        Deserializer,
223        de::{self, MapAccess, Visitor, value::MapAccessDeserializer},
224    };
225    use std::fmt;
226
227    impl<'de> Deserialize<'de> for PlatformSummary {
228        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229        where
230            D: Deserializer<'de>,
231        {
232            deserializer.deserialize_any(PlatformSummaryVisitor)
233        }
234    }
235
236    // This is a hand-written visitor and not serde(untagged) for better error
237    // messages.
238    struct PlatformSummaryVisitor;
239
240    impl<'de> Visitor<'de> for PlatformSummaryVisitor {
241        type Value = PlatformSummary;
242
243        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244            f.write_str("a platform (a triple string or a table with a `triple` key)")
245        }
246
247        fn visit_str<E>(self, triple: &str) -> Result<Self::Value, E>
248        where
249            E: de::Error,
250        {
251            validate_triple(triple)?;
252            Ok(PlatformSummary::new(triple))
253        }
254
255        fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
256        where
257            A: MapAccess<'de>,
258        {
259            let PlatformSummaryTable {
260                triple,
261                custom_json,
262                custom_cfg,
263                target_features,
264                flags,
265            } = PlatformSummaryTable::deserialize(MapAccessDeserializer::new(map))?;
266            validate_triple(&triple)?;
267            Ok(PlatformSummary {
268                triple,
269                custom_json,
270                custom_cfg,
271                target_features,
272                flags,
273            })
274        }
275    }
276
277    fn validate_triple<E: de::Error>(triple: &str) -> Result<(), E> {
278        if triple.is_empty() {
279            return Err(E::custom("a platform triple cannot be empty"));
280        }
281        Ok(())
282    }
283
284    #[derive(Deserialize)]
285    #[serde(rename_all = "kebab-case", deny_unknown_fields)]
286    struct PlatformSummaryTable {
287        triple: String,
288        #[serde(default)]
289        custom_json: Option<String>,
290        #[serde(default)]
291        custom_cfg: Option<String>,
292        #[serde(default)]
293        target_features: TargetFeaturesSummary,
294        #[serde(default)]
295        flags: BTreeSet<String>,
296    }
297}
298
299mod target_features_impl {
300    use super::*;
301    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
302
303    impl Serialize for TargetFeaturesSummary {
304        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
305        where
306            S: Serializer,
307        {
308            match self {
309                TargetFeaturesSummary::Unknown => "unknown".serialize(serializer),
310                TargetFeaturesSummary::All => "all".serialize(serializer),
311                TargetFeaturesSummary::Features(features) => features.serialize(serializer),
312            }
313        }
314    }
315
316    impl<'de> Deserialize<'de> for TargetFeaturesSummary {
317        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
318        where
319            D: Deserializer<'de>,
320        {
321            let d = TargetFeaturesDeserialize::deserialize(deserializer)?;
322            match d {
323                TargetFeaturesDeserialize::String(target_features) => {
324                    match target_features.as_str() {
325                        "unknown" => Ok(TargetFeaturesSummary::Unknown),
326                        "all" => Ok(TargetFeaturesSummary::All),
327                        other => Err(D::Error::custom(format!(
328                            "unknown string for target features: {other}",
329                        ))),
330                    }
331                }
332                TargetFeaturesDeserialize::List(target_features) => {
333                    Ok(TargetFeaturesSummary::Features(target_features))
334                }
335            }
336        }
337    }
338
339    #[derive(Deserialize)]
340    #[serde(untagged)]
341    enum TargetFeaturesDeserialize {
342        String(String),
343        List(BTreeSet<String>),
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    #![allow(clippy::vec_init_then_push)]
350
351    use super::*;
352
353    #[test]
354    fn platform_deserialize_valid() {
355        // Need a wrapper because of TOML restrictions
356        #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
357        struct Wrapper {
358            platform: PlatformSummary,
359        }
360
361        let mut valid = vec![];
362        valid.push((
363            r#"platform = "x86_64-unknown-linux-gnu""#,
364            PlatformSummary {
365                triple: "x86_64-unknown-linux-gnu".into(),
366                custom_json: None,
367                custom_cfg: None,
368                target_features: TargetFeaturesSummary::Unknown,
369                flags: BTreeSet::new(),
370            },
371        ));
372        valid.push((
373            r#"platform = { triple = "x86_64-unknown-linux-gnu" }"#,
374            PlatformSummary {
375                triple: "x86_64-unknown-linux-gnu".into(),
376                custom_json: None,
377                custom_cfg: None,
378                target_features: TargetFeaturesSummary::Unknown,
379                flags: BTreeSet::new(),
380            },
381        ));
382        valid.push((
383            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "unknown" }"#,
384            PlatformSummary {
385                triple: "x86_64-unknown-linux-gnu".into(),
386                custom_json: None,
387                custom_cfg: None,
388                target_features: TargetFeaturesSummary::Unknown,
389                flags: BTreeSet::new(),
390            },
391        ));
392        valid.push((
393            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "all" }"#,
394            PlatformSummary {
395                triple: "x86_64-unknown-linux-gnu".into(),
396                custom_json: None,
397                custom_cfg: None,
398                target_features: TargetFeaturesSummary::All,
399                flags: BTreeSet::new(),
400            },
401        ));
402        valid.push((
403            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = [] }"#,
404            PlatformSummary {
405                triple: "x86_64-unknown-linux-gnu".into(),
406                custom_json: None,
407                custom_cfg: None,
408                target_features: TargetFeaturesSummary::Features(BTreeSet::new()),
409                flags: BTreeSet::new(),
410            },
411        ));
412
413        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"}"#;
414        let toml = format!(
415            r#"platform = {{ triple = "x86_64-unknown-haiku", custom-json = '{custom_json}' }}"#
416        );
417
418        valid.push((
419            &toml,
420            PlatformSummary {
421                triple: "x86_64-unknown-haiku".into(),
422                custom_json: Some(custom_json.to_owned()),
423                custom_cfg: None,
424                target_features: TargetFeaturesSummary::Unknown,
425                flags: BTreeSet::new(),
426            },
427        ));
428
429        let mut flags = BTreeSet::new();
430        flags.insert("cargo_web".to_owned());
431        valid.push((
432            r#"platform = { triple = "x86_64-unknown-linux-gnu", flags = ["cargo_web"] }"#,
433            PlatformSummary {
434                triple: "x86_64-unknown-linux-gnu".into(),
435                custom_json: None,
436                custom_cfg: None,
437                target_features: TargetFeaturesSummary::Unknown,
438                flags,
439            },
440        ));
441
442        let custom_cfg = indoc::indoc! {r#"
443            panic="unwind"
444            target_arch="x86_64"
445            target_endian="little"
446            target_env="gnu"
447            target_family="unix"
448            target_os="linux"
449            target_pointer_width="64"
450            target_vendor="unknown"
451        "#};
452        let toml_cfg = format!(
453            "[platform]\n\
454             triple = \"my-custom-linux\"\n\
455             custom-cfg = '''\n\
456             {custom_cfg}'''"
457        );
458        valid.push((
459            &toml_cfg,
460            PlatformSummary {
461                triple: "my-custom-linux".into(),
462                custom_json: None,
463                custom_cfg: Some(custom_cfg.to_owned()),
464                target_features: TargetFeaturesSummary::Unknown,
465                flags: BTreeSet::new(),
466            },
467        ));
468
469        for (input, expected) in valid {
470            let actual: Wrapper =
471                toml::from_str(input).unwrap_or_else(|err| panic!("input {input} is valid: {err}"));
472            assert_eq!(actual.platform, expected, "for input: {input}");
473
474            // Serialize and deserialize again.
475            let serialized = toml::to_string(&actual).expect("serialized correctly");
476            let actual_2: Wrapper = toml::from_str(&serialized)
477                .unwrap_or_else(|err| panic!("serialized input: {input} is valid: {err}"));
478            assert_eq!(actual, actual_2, "for input: {input}");
479
480            // Check that custom JSON functionality works.
481            if actual.platform.custom_json.is_some() {
482                #[cfg(feature = "custom")]
483                {
484                    let platform = actual
485                        .platform
486                        .to_platform()
487                        .expect("custom platform parsed successfully");
488                    assert!(platform.is_custom(), "this is a custom platform");
489                }
490
491                #[cfg(not(feature = "custom"))]
492                {
493                    use crate::errors::CustomTripleCreateError;
494
495                    let error = actual
496                        .platform
497                        .to_platform()
498                        .expect_err("custom platforms are disabled");
499                    assert!(matches!(
500                        error,
501                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomJsonUnavailable)
502                    ));
503                }
504            }
505
506            // Check that custom cfg functionality works.
507            if actual.platform.custom_cfg.is_some() {
508                #[cfg(feature = "custom-cfg")]
509                {
510                    let platform = actual
511                        .platform
512                        .to_platform()
513                        .expect("custom cfg platform parsed successfully");
514                    assert!(platform.is_custom(), "this is a custom platform");
515                }
516
517                #[cfg(not(feature = "custom-cfg"))]
518                {
519                    use crate::errors::CustomTripleCreateError;
520
521                    let error = actual
522                        .platform
523                        .to_platform()
524                        .expect_err("custom cfg platforms are disabled");
525                    assert!(matches!(
526                        error,
527                        Error::CustomPlatformCreate(CustomTripleCreateError::CustomCfgUnavailable)
528                    ));
529                }
530            }
531        }
532    }
533    #[test]
534    fn platform_deserialize_errors() {
535        #[derive(Debug, Deserialize)]
536        struct Wrapper {
537            #[allow(dead_code)]
538            platform: PlatformSummary,
539        }
540
541        for (input, expected) in [
542            (r#"platform = """#, "a platform triple cannot be empty"),
543            (
544                r#"platform = { triple = "" }"#,
545                "a platform triple cannot be empty",
546            ),
547            (
548                r#"platform = { target-features = [] }"#,
549                "missing field `triple`",
550            ),
551            (
552                r#"platform = { tripel = "x86_64-unknown-linux-gnu" }"#,
553                "unknown field `tripel`",
554            ),
555            (
556                r#"platform = { triple = "x86_64-unknown-linux-gnu", target-feature = ["sse2"] }"#,
557                "unknown field `target-feature`",
558            ),
559            (
560                r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "bogus" }"#,
561                "unknown string for target features: bogus",
562            ),
563            (
564                "platform = 5",
565                "expected a platform (a triple string or a table with a `triple` key)",
566            ),
567            (
568                r#"platform = ["x86_64-unknown-linux-gnu"]"#,
569                "expected a platform (a triple string or a table with a `triple` key)",
570            ),
571        ] {
572            let message = toml::from_str::<Wrapper>(input)
573                .expect_err("input rejected")
574                .to_string();
575            assert!(
576                message.contains(expected),
577                "for input {input}: error `{message}` contains `{expected}`",
578            );
579        }
580    }
581}
582
583#[cfg(all(test, feature = "proptest1"))]
584mod proptests {
585    use super::*;
586    use proptest::prelude::*;
587    use std::collections::HashSet;
588
589    proptest! {
590        #[test]
591        fn summary_roundtrip(platform in Platform::strategy(any::<TargetFeatures>())) {
592            let summary = PlatformSummary::from_platform(&platform);
593            let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
594
595            let deserialized: PlatformSummary = toml::from_str(&serialized).expect("deserialization succeeded");
596            assert_eq!(summary, deserialized, "summary and deserialized should match");
597            let platform2 = deserialized.to_platform().expect("conversion to Platform succeeded");
598
599            assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
600            assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
601            assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
602        }
603    }
604}