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    /// The target features used.
42    pub target_features: TargetFeaturesSummary,
43
44    /// The flags enabled.
45    #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
46    pub flags: BTreeSet<String>,
47}
48
49impl PlatformSummary {
50    /// Creates a new `PlatformSummary` with the provided triple and default options.
51    ///
52    /// The default options are:
53    ///
54    /// * `custom_json` is set to None.
55    /// * `target_features` is set to [`TargetFeaturesSummary::Unknown`].
56    /// * `flags` is empty.
57    pub fn new(triple_str: impl Into<String>) -> Self {
58        Self {
59            triple: triple_str.into(),
60            custom_json: None,
61            target_features: TargetFeaturesSummary::Unknown,
62            flags: BTreeSet::new(),
63        }
64    }
65
66    /// If this represents a custom platform, sets the target definition JSON for it.
67    ///
68    /// For more about target definition JSON, see [Creating a custom
69    /// target](https://docs.rust-embedded.org/embedonomicon/custom-target.html) on the Rust
70    /// Embedonomicon.
71    pub fn with_custom_json(mut self, custom_json: impl Into<String>) -> Self {
72        self.custom_json = Some(custom_json.into());
73        self
74    }
75
76    /// Sets the target features for this platform.
77    pub fn with_target_features(mut self, target_features: TargetFeaturesSummary) -> Self {
78        self.target_features = target_features;
79        self
80    }
81
82    /// Adds flags for this platform.
83    pub fn with_added_flags(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
84        self.flags.extend(flags.into_iter().map(|flag| flag.into()));
85        self
86    }
87
88    /// Creates a new `PlatformSummary` instance from a platform.
89    pub fn from_platform(platform: &Platform) -> Self {
90        Self {
91            triple: platform.triple_str().to_string(),
92            custom_json: platform.custom_json().map(|s| s.to_owned()),
93            target_features: TargetFeaturesSummary::new(platform.target_features()),
94            flags: platform.flags().map(|flag| flag.to_string()).collect(),
95        }
96    }
97
98    /// Converts `self` to a `Platform`.
99    ///
100    /// Returns an `Error` if the platform was unknown.
101    pub fn to_platform(&self) -> Result<Platform, Error> {
102        #[allow(unused_variables)] // in some feature branches, json isn't used
103        let mut platform = if let Some(json) = &self.custom_json {
104            #[cfg(not(feature = "custom"))]
105            return Err(Error::CustomPlatformCreate(
106                crate::errors::CustomTripleCreateError::Unavailable,
107            ));
108
109            #[cfg(feature = "custom")]
110            Platform::new_custom(
111                self.triple.to_owned(),
112                json,
113                self.target_features.to_target_features(),
114            )?
115        } else {
116            Platform::new(
117                self.triple.to_owned(),
118                self.target_features.to_target_features(),
119            )?
120        };
121
122        platform.add_flags(self.flags.iter().cloned());
123        Ok(platform)
124    }
125}
126
127/// An owned, serializable version of [`TargetFeatures`].
128///
129/// This type can be serialized and deserialized using `serde`.
130///
131/// Requires the `summaries` feature to be enabled.
132#[derive(Clone, Debug, Eq, PartialEq)]
133#[non_exhaustive]
134#[derive(Default)]
135pub enum TargetFeaturesSummary {
136    /// The target features are unknown.
137    ///
138    /// This is the default.
139    #[default]
140    Unknown,
141    /// Only match the specified features.
142    Features(BTreeSet<String>),
143    /// Match all features.
144    All,
145}
146
147impl TargetFeaturesSummary {
148    /// Creates a new `TargetFeaturesSummary` from a `TargetFeatures`.
149    pub fn new(target_features: &TargetFeatures) -> Self {
150        match target_features {
151            TargetFeatures::Unknown => TargetFeaturesSummary::Unknown,
152            TargetFeatures::Features(features) => TargetFeaturesSummary::Features(
153                features.iter().map(|feature| feature.to_string()).collect(),
154            ),
155            TargetFeatures::All => TargetFeaturesSummary::All,
156        }
157    }
158
159    /// Converts `self` to a `TargetFeatures` instance.
160    pub fn to_target_features(&self) -> TargetFeatures {
161        match self {
162            TargetFeaturesSummary::Unknown => TargetFeatures::Unknown,
163            TargetFeaturesSummary::All => TargetFeatures::All,
164            TargetFeaturesSummary::Features(features) => {
165                let features = features
166                    .iter()
167                    .map(|feature| Cow::Owned(feature.clone()))
168                    .collect();
169                TargetFeatures::Features(features)
170            }
171        }
172    }
173}
174
175mod platform_impl {
176    use super::*;
177    use serde::Deserializer;
178
179    impl<'de> Deserialize<'de> for PlatformSummary {
180        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181        where
182            D: Deserializer<'de>,
183        {
184            let d = PlatformSummaryDeserialize::deserialize(deserializer)?;
185            match d {
186                PlatformSummaryDeserialize::String(triple) => Ok(PlatformSummary {
187                    triple,
188                    custom_json: None,
189                    target_features: TargetFeaturesSummary::default(),
190                    flags: BTreeSet::default(),
191                }),
192                PlatformSummaryDeserialize::Full {
193                    triple,
194                    custom_json,
195                    target_features,
196                    flags,
197                } => Ok(PlatformSummary {
198                    triple,
199                    custom_json,
200                    target_features,
201                    flags,
202                }),
203            }
204        }
205    }
206
207    #[derive(Deserialize)]
208    #[serde(untagged)]
209    enum PlatformSummaryDeserialize {
210        String(String),
211        #[serde(rename_all = "kebab-case")]
212        Full {
213            triple: String,
214            #[serde(default)]
215            custom_json: Option<String>,
216            /// The target features used.
217            #[serde(default)]
218            target_features: TargetFeaturesSummary,
219            /// The flags enabled.
220            #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
221            flags: BTreeSet<String>,
222        },
223    }
224}
225
226mod target_features_impl {
227    use super::*;
228    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
229
230    impl Serialize for TargetFeaturesSummary {
231        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
232        where
233            S: Serializer,
234        {
235            match self {
236                TargetFeaturesSummary::Unknown => "unknown".serialize(serializer),
237                TargetFeaturesSummary::All => "all".serialize(serializer),
238                TargetFeaturesSummary::Features(features) => features.serialize(serializer),
239            }
240        }
241    }
242
243    impl<'de> Deserialize<'de> for TargetFeaturesSummary {
244        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
245        where
246            D: Deserializer<'de>,
247        {
248            let d = TargetFeaturesDeserialize::deserialize(deserializer)?;
249            match d {
250                TargetFeaturesDeserialize::String(target_features) => {
251                    match target_features.as_str() {
252                        "unknown" => Ok(TargetFeaturesSummary::Unknown),
253                        "all" => Ok(TargetFeaturesSummary::All),
254                        other => Err(D::Error::custom(format!(
255                            "unknown string for target features: {}",
256                            other,
257                        ))),
258                    }
259                }
260                TargetFeaturesDeserialize::List(target_features) => {
261                    Ok(TargetFeaturesSummary::Features(target_features))
262                }
263            }
264        }
265    }
266
267    #[derive(Deserialize)]
268    #[serde(untagged)]
269    enum TargetFeaturesDeserialize {
270        String(String),
271        List(BTreeSet<String>),
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    #![allow(clippy::vec_init_then_push)]
278
279    use super::*;
280
281    #[test]
282    fn platform_deserialize_valid() {
283        // Need a wrapper because of TOML restrictions
284        #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
285        struct Wrapper {
286            platform: PlatformSummary,
287        }
288
289        let mut valid = vec![];
290        valid.push((
291            r#"platform = "x86_64-unknown-linux-gnu""#,
292            PlatformSummary {
293                triple: "x86_64-unknown-linux-gnu".into(),
294                custom_json: None,
295                target_features: TargetFeaturesSummary::Unknown,
296                flags: BTreeSet::new(),
297            },
298        ));
299        valid.push((
300            r#"platform = { triple = "x86_64-unknown-linux-gnu" }"#,
301            PlatformSummary {
302                triple: "x86_64-unknown-linux-gnu".into(),
303                custom_json: None,
304                target_features: TargetFeaturesSummary::Unknown,
305                flags: BTreeSet::new(),
306            },
307        ));
308        valid.push((
309            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "unknown" }"#,
310            PlatformSummary {
311                triple: "x86_64-unknown-linux-gnu".into(),
312                custom_json: None,
313                target_features: TargetFeaturesSummary::Unknown,
314                flags: BTreeSet::new(),
315            },
316        ));
317        valid.push((
318            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "all" }"#,
319            PlatformSummary {
320                triple: "x86_64-unknown-linux-gnu".into(),
321                custom_json: None,
322                target_features: TargetFeaturesSummary::All,
323                flags: BTreeSet::new(),
324            },
325        ));
326        valid.push((
327            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = [] }"#,
328            PlatformSummary {
329                triple: "x86_64-unknown-linux-gnu".into(),
330                custom_json: None,
331                target_features: TargetFeaturesSummary::Features(BTreeSet::new()),
332                flags: BTreeSet::new(),
333            },
334        ));
335
336        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"}"#;
337        let toml = format!(
338            r#"platform = {{ triple = "x86_64-unknown-haiku", custom-json = '{custom_json}' }}"#
339        );
340
341        valid.push((
342            &toml,
343            PlatformSummary {
344                triple: "x86_64-unknown-haiku".into(),
345                custom_json: Some(custom_json.to_owned()),
346                target_features: TargetFeaturesSummary::Unknown,
347                flags: BTreeSet::new(),
348            },
349        ));
350
351        let mut flags = BTreeSet::new();
352        flags.insert("cargo_web".to_owned());
353        valid.push((
354            r#"platform = { triple = "x86_64-unknown-linux-gnu", flags = ["cargo_web"] }"#,
355            PlatformSummary {
356                triple: "x86_64-unknown-linux-gnu".into(),
357                custom_json: None,
358                target_features: TargetFeaturesSummary::Unknown,
359                flags,
360            },
361        ));
362
363        for (input, expected) in valid {
364            let actual: Wrapper = toml::from_str(input)
365                .unwrap_or_else(|err| panic!("input {} is valid: {}", input, err));
366            assert_eq!(actual.platform, expected, "for input: {}", input);
367
368            // Serialize and deserialize again.
369            let serialized = toml::to_string(&actual).expect("serialized correctly");
370            let actual_2: Wrapper = toml::from_str(&serialized)
371                .unwrap_or_else(|err| panic!("serialized input: {} is valid: {}", input, err));
372            assert_eq!(actual, actual_2, "for input: {}", input);
373
374            // Check that custom JSON functionality works.
375            if actual.platform.custom_json.is_some() {
376                #[cfg(feature = "custom")]
377                {
378                    let platform = actual
379                        .platform
380                        .to_platform()
381                        .expect("custom platform parsed successfully");
382                    assert!(platform.is_custom(), "this is a custom platform");
383                }
384
385                #[cfg(not(feature = "custom"))]
386                {
387                    use crate::errors::CustomTripleCreateError;
388
389                    let error = actual
390                        .platform
391                        .to_platform()
392                        .expect_err("custom platforms are disabled");
393                    assert!(matches!(
394                        error,
395                        Error::CustomPlatformCreate(CustomTripleCreateError::Unavailable)
396                    ));
397                }
398            }
399        }
400    }
401}
402
403#[cfg(all(test, feature = "proptest1"))]
404mod proptests {
405    use super::*;
406    use proptest::prelude::*;
407    use std::collections::HashSet;
408
409    proptest! {
410        #[test]
411        fn summary_roundtrip(platform in Platform::strategy(any::<TargetFeatures>())) {
412            let summary = PlatformSummary::from_platform(&platform);
413            let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
414
415            let deserialized: PlatformSummary = toml::from_str(&serialized).expect("deserialization succeeded");
416            assert_eq!(summary, deserialized, "summary and deserialized should match");
417            let platform2 = deserialized.to_platform().expect("conversion to Platform succeeded");
418
419            assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
420            assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
421            assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
422        }
423    }
424}