1use crate::{CustomSource, 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 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 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)] 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#[derive(Clone, Debug, Eq, PartialEq)]
182#[non_exhaustive]
183#[derive(Default)]
184pub enum TargetFeaturesSummary {
185 #[default]
189 Unknown,
190 Features(BTreeSet<String>),
192 All,
194}
195
196impl TargetFeaturesSummary {
197 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 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 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 #[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 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 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 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}