1use crate::{Error, Platform, TargetFeatures};
12use serde::{Deserialize, Serialize};
13use std::{borrow::Cow, collections::BTreeSet};
14
15impl Platform {
16 #[inline]
20 pub fn to_summary(&self) -> PlatformSummary {
21 PlatformSummary::from_platform(self)
22 }
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "kebab-case")]
32#[non_exhaustive]
33pub struct PlatformSummary {
34 pub triple: String,
36
37 #[serde(skip_serializing_if = "Option::is_none", default)]
39 pub custom_json: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none", default)]
43 pub custom_cfg: Option<String>,
44
45 pub target_features: TargetFeaturesSummary,
47
48 #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
50 pub flags: BTreeSet<String>,
51}
52
53impl PlatformSummary {
54 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 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 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 pub fn with_target_features(mut self, target_features: TargetFeaturesSummary) -> Self {
101 self.target_features = target_features;
102 self
103 }
104
105 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 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 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)] 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#[derive(Clone, Debug, Eq, PartialEq)]
177#[non_exhaustive]
178#[derive(Default)]
179pub enum TargetFeaturesSummary {
180 #[default]
184 Unknown,
185 Features(BTreeSet<String>),
187 All,
189}
190
191impl TargetFeaturesSummary {
192 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 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 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 #[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 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 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 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}