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: {other}",
256                        ))),
257                    }
258                }
259                TargetFeaturesDeserialize::List(target_features) => {
260                    Ok(TargetFeaturesSummary::Features(target_features))
261                }
262            }
263        }
264    }
265
266    #[derive(Deserialize)]
267    #[serde(untagged)]
268    enum TargetFeaturesDeserialize {
269        String(String),
270        List(BTreeSet<String>),
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    #![allow(clippy::vec_init_then_push)]
277
278    use super::*;
279
280    #[test]
281    fn platform_deserialize_valid() {
282        // Need a wrapper because of TOML restrictions
283        #[derive(Debug, Deserialize, Serialize, Eq, PartialEq)]
284        struct Wrapper {
285            platform: PlatformSummary,
286        }
287
288        let mut valid = vec![];
289        valid.push((
290            r#"platform = "x86_64-unknown-linux-gnu""#,
291            PlatformSummary {
292                triple: "x86_64-unknown-linux-gnu".into(),
293                custom_json: None,
294                target_features: TargetFeaturesSummary::Unknown,
295                flags: BTreeSet::new(),
296            },
297        ));
298        valid.push((
299            r#"platform = { triple = "x86_64-unknown-linux-gnu" }"#,
300            PlatformSummary {
301                triple: "x86_64-unknown-linux-gnu".into(),
302                custom_json: None,
303                target_features: TargetFeaturesSummary::Unknown,
304                flags: BTreeSet::new(),
305            },
306        ));
307        valid.push((
308            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "unknown" }"#,
309            PlatformSummary {
310                triple: "x86_64-unknown-linux-gnu".into(),
311                custom_json: None,
312                target_features: TargetFeaturesSummary::Unknown,
313                flags: BTreeSet::new(),
314            },
315        ));
316        valid.push((
317            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = "all" }"#,
318            PlatformSummary {
319                triple: "x86_64-unknown-linux-gnu".into(),
320                custom_json: None,
321                target_features: TargetFeaturesSummary::All,
322                flags: BTreeSet::new(),
323            },
324        ));
325        valid.push((
326            r#"platform = { triple = "x86_64-unknown-linux-gnu", target-features = [] }"#,
327            PlatformSummary {
328                triple: "x86_64-unknown-linux-gnu".into(),
329                custom_json: None,
330                target_features: TargetFeaturesSummary::Features(BTreeSet::new()),
331                flags: BTreeSet::new(),
332            },
333        ));
334
335        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"}"#;
336        let toml = format!(
337            r#"platform = {{ triple = "x86_64-unknown-haiku", custom-json = '{custom_json}' }}"#
338        );
339
340        valid.push((
341            &toml,
342            PlatformSummary {
343                triple: "x86_64-unknown-haiku".into(),
344                custom_json: Some(custom_json.to_owned()),
345                target_features: TargetFeaturesSummary::Unknown,
346                flags: BTreeSet::new(),
347            },
348        ));
349
350        let mut flags = BTreeSet::new();
351        flags.insert("cargo_web".to_owned());
352        valid.push((
353            r#"platform = { triple = "x86_64-unknown-linux-gnu", flags = ["cargo_web"] }"#,
354            PlatformSummary {
355                triple: "x86_64-unknown-linux-gnu".into(),
356                custom_json: None,
357                target_features: TargetFeaturesSummary::Unknown,
358                flags,
359            },
360        ));
361
362        for (input, expected) in valid {
363            let actual: Wrapper =
364                toml::from_str(input).unwrap_or_else(|err| panic!("input {input} is valid: {err}"));
365            assert_eq!(actual.platform, expected, "for input: {input}");
366
367            // Serialize and deserialize again.
368            let serialized = toml::to_string(&actual).expect("serialized correctly");
369            let actual_2: Wrapper = toml::from_str(&serialized)
370                .unwrap_or_else(|err| panic!("serialized input: {input} is valid: {err}"));
371            assert_eq!(actual, actual_2, "for input: {input}");
372
373            // Check that custom JSON functionality works.
374            if actual.platform.custom_json.is_some() {
375                #[cfg(feature = "custom")]
376                {
377                    let platform = actual
378                        .platform
379                        .to_platform()
380                        .expect("custom platform parsed successfully");
381                    assert!(platform.is_custom(), "this is a custom platform");
382                }
383
384                #[cfg(not(feature = "custom"))]
385                {
386                    use crate::errors::CustomTripleCreateError;
387
388                    let error = actual
389                        .platform
390                        .to_platform()
391                        .expect_err("custom platforms are disabled");
392                    assert!(matches!(
393                        error,
394                        Error::CustomPlatformCreate(CustomTripleCreateError::Unavailable)
395                    ));
396                }
397            }
398        }
399    }
400}
401
402#[cfg(all(test, feature = "proptest1"))]
403mod proptests {
404    use super::*;
405    use proptest::prelude::*;
406    use std::collections::HashSet;
407
408    proptest! {
409        #[test]
410        fn summary_roundtrip(platform in Platform::strategy(any::<TargetFeatures>())) {
411            let summary = PlatformSummary::from_platform(&platform);
412            let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
413
414            let deserialized: PlatformSummary = toml::from_str(&serialized).expect("deserialization succeeded");
415            assert_eq!(summary, deserialized, "summary and deserialized should match");
416            let platform2 = deserialized.to_platform().expect("conversion to Platform succeeded");
417
418            assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
419            assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
420            assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
421        }
422    }
423}