1use std::collections::BTreeSet;
34use std::fmt;
35use std::str::FromStr;
36
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use thiserror::Error;
39
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum Condition {
49 KeyValue { key: ConditionKey, value: String },
53 Feature(String),
62 CompilerFamily {
71 slot: CompilerSlot,
72 family: crate::compiler::CompilerKind,
73 },
74 CompilerVersionReq { slot: CompilerSlot, req: String },
81 All(Vec<Condition>),
84 Any(Vec<Condition>),
87 Not(Box<Condition>),
89}
90
91impl Condition {
92 pub fn parse_cfg(input: &str) -> Result<Self, ConditionParseError> {
102 let trimmed = input.trim();
103 let inner = trimmed
104 .strip_prefix("cfg")
105 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?
106 .trim_start();
107 let inner = inner
108 .strip_prefix('(')
109 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?;
110 let inner = inner
111 .strip_suffix(')')
112 .ok_or_else(|| ConditionParseError::UnbalancedParens(trimmed.to_owned()))?;
113 Self::parse_inner(inner)
114 }
115
116 pub fn parse_inner(input: &str) -> Result<Self, ConditionParseError> {
126 let mut parser = Parser::new(input);
127 let cond = parser.parse_condition()?;
128 parser.expect_eof()?;
129 Ok(cond)
130 }
131
132 pub fn evaluate(&self, ctx: &ConditionContext<'_>) -> bool {
147 match self {
148 Condition::KeyValue { key, value } => key.lookup(ctx.platform) == value,
149 Condition::Feature(name) => ctx.features.contains(name),
150 Condition::CompilerFamily { slot, family } => {
151 let detected = ctx
152 .identity(*slot)
153 .map_or(crate::compiler::CompilerKind::Unknown, |id| id.kind);
154 detected == *family
155 }
156 Condition::CompilerVersionReq { slot, req } => ctx
157 .identity(*slot)
158 .and_then(|id| id.version.as_ref())
159 .zip(crate::version_req::parse_lenient(req).ok())
160 .is_some_and(|(v, parsed)| {
161 parsed.matches(&semver::Version::new(
162 u64::from(v.major),
163 u64::from(v.minor.unwrap_or(0)),
164 u64::from(v.patch.unwrap_or(0)),
165 ))
166 }),
167 Condition::All(items) => items.iter().all(|c| c.evaluate(ctx)),
168 Condition::Any(items) => items.iter().any(|c| c.evaluate(ctx)),
169 Condition::Not(inner) => !inner.evaluate(ctx),
170 }
171 }
172
173 pub fn references_feature(&self) -> bool {
178 match self {
179 Condition::Feature(_) => true,
180 Condition::KeyValue { .. }
181 | Condition::CompilerFamily { .. }
182 | Condition::CompilerVersionReq { .. } => false,
183 Condition::All(items) | Condition::Any(items) => {
184 items.iter().any(Condition::references_feature)
185 }
186 Condition::Not(inner) => inner.references_feature(),
187 }
188 }
189
190 pub fn references_compiler(&self) -> bool {
196 match self {
197 Condition::CompilerFamily { .. } | Condition::CompilerVersionReq { .. } => true,
198 Condition::KeyValue { .. } | Condition::Feature(_) => false,
199 Condition::All(items) | Condition::Any(items) => {
200 items.iter().any(Condition::references_compiler)
201 }
202 Condition::Not(inner) => inner.references_compiler(),
203 }
204 }
205}
206
207impl fmt::Display for Condition {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Condition::KeyValue { key, value } => write!(f, "{} = \"{}\"", key.as_str(), value),
213 Condition::Feature(name) => write!(f, "feature = \"{name}\""),
214 Condition::CompilerFamily { slot, family } => {
215 write!(f, "{} = \"{}\"", slot.family_key(), family.as_key())
216 }
217 Condition::CompilerVersionReq { slot, req } => {
218 write!(f, "{} = \"{}\"", slot.version_key(), req)
219 }
220 Condition::All(items) => {
221 f.write_str("all(")?;
222 write_list(f, items)?;
223 f.write_str(")")
224 }
225 Condition::Any(items) => {
226 f.write_str("any(")?;
227 write_list(f, items)?;
228 f.write_str(")")
229 }
230 Condition::Not(inner) => write!(f, "not({inner})"),
231 }
232 }
233}
234
235fn write_list(f: &mut fmt::Formatter<'_>, items: &[Condition]) -> fmt::Result {
236 for (i, c) in items.iter().enumerate() {
237 if i > 0 {
238 f.write_str(", ")?;
239 }
240 write!(f, "{c}")?;
241 }
242 Ok(())
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
251pub enum CompilerSlot {
252 Cc,
253 Cxx,
254}
255
256impl CompilerSlot {
257 pub const fn family_key(self) -> &'static str {
259 match self {
260 CompilerSlot::Cc => "cc",
261 CompilerSlot::Cxx => "cxx",
262 }
263 }
264
265 pub const fn version_key(self) -> &'static str {
267 match self {
268 CompilerSlot::Cc => "cc_version",
269 CompilerSlot::Cxx => "cxx_version",
270 }
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub enum ConditionKey {
278 Os,
280 Arch,
282 Family,
284 Env,
286 Abi,
288 Target,
290}
291
292impl ConditionKey {
293 pub const fn as_str(self) -> &'static str {
294 match self {
295 ConditionKey::Os => "os",
296 ConditionKey::Arch => "arch",
297 ConditionKey::Family => "family",
298 ConditionKey::Env => "env",
299 ConditionKey::Abi => "abi",
300 ConditionKey::Target => "target",
301 }
302 }
303
304 pub const fn all() -> &'static [ConditionKey] {
306 &[
307 ConditionKey::Os,
308 ConditionKey::Arch,
309 ConditionKey::Family,
310 ConditionKey::Env,
311 ConditionKey::Abi,
312 ConditionKey::Target,
313 ]
314 }
315
316 fn lookup(self, platform: &TargetPlatform) -> &str {
317 match self {
318 ConditionKey::Os => platform.os.as_str(),
319 ConditionKey::Arch => platform.arch.as_str(),
320 ConditionKey::Family => platform.family.as_str(),
321 ConditionKey::Env => platform.env.as_str(),
322 ConditionKey::Abi => platform.abi.as_str(),
323 ConditionKey::Target => platform.target.as_str(),
324 }
325 }
326}
327
328impl FromStr for ConditionKey {
329 type Err = ();
330
331 fn from_str(s: &str) -> Result<Self, Self::Err> {
332 match s {
333 "os" => Ok(ConditionKey::Os),
334 "arch" => Ok(ConditionKey::Arch),
335 "family" => Ok(ConditionKey::Family),
336 "env" => Ok(ConditionKey::Env),
337 "abi" => Ok(ConditionKey::Abi),
338 "target" => Ok(ConditionKey::Target),
339 _ => Err(()),
340 }
341 }
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub struct TargetPlatform {
350 pub os: String,
351 pub arch: String,
352 pub family: String,
353 pub env: String,
354 pub abi: String,
355 pub target: String,
356}
357
358impl TargetPlatform {
359 pub fn current() -> Self {
365 let os = normalize_os(std::env::consts::OS);
366 let arch = normalize_arch(std::env::consts::ARCH);
367 let family = normalize_family(std::env::consts::FAMILY, &os);
368 let env = normalize_env(&os);
369 let abi = "unknown".to_owned();
370 let target = format!("{arch}-{family}-{os}");
371 Self {
372 os,
373 arch,
374 family,
375 env,
376 abi,
377 target,
378 }
379 }
380}
381
382fn normalize_os(raw: &str) -> String {
383 match raw {
384 "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
385 | "android" | "ios" => raw.to_owned(),
386 "darwin" => "macos".to_owned(),
388 "" => "unknown".to_owned(),
389 other => other.to_owned(),
390 }
391}
392
393fn normalize_arch(raw: &str) -> String {
394 match raw {
395 "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
396 "" => "unknown".to_owned(),
397 other => other.to_owned(),
398 }
399}
400
401fn normalize_family(raw: &str, os: &str) -> String {
402 match raw {
403 "unix" | "windows" | "wasm" => raw.to_owned(),
404 _ => match os {
405 "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
406 | "ios" => "unix".to_owned(),
407 "windows" => "windows".to_owned(),
408 _ => "unknown".to_owned(),
409 },
410 }
411}
412
413fn normalize_env(os: &str) -> String {
414 match os {
420 "linux" => "gnu".to_owned(),
421 "macos" | "ios" => "apple".to_owned(),
422 "windows" => "msvc".to_owned(),
423 _ => "unknown".to_owned(),
424 }
425}
426
427#[derive(Debug, Clone, Copy)]
437pub struct ConditionContext<'a> {
438 pub platform: &'a TargetPlatform,
439 pub features: &'a BTreeSet<String>,
440 pub cc: Option<&'a crate::compiler::CompilerIdentity>,
443 pub cxx: Option<&'a crate::compiler::CompilerIdentity>,
445}
446
447static EMPTY_FEATURES: BTreeSet<String> = BTreeSet::new();
448
449impl<'a> ConditionContext<'a> {
450 pub fn platform_only(platform: &'a TargetPlatform) -> Self {
455 Self {
456 platform,
457 features: &EMPTY_FEATURES,
458 cc: None,
459 cxx: None,
460 }
461 }
462
463 pub fn with_features(platform: &'a TargetPlatform, features: &'a BTreeSet<String>) -> Self {
466 Self {
467 platform,
468 features,
469 cc: None,
470 cxx: None,
471 }
472 }
473
474 #[must_use]
476 pub fn with_compilers(
477 mut self,
478 cc: Option<&'a crate::compiler::CompilerIdentity>,
479 cxx: Option<&'a crate::compiler::CompilerIdentity>,
480 ) -> Self {
481 self.cc = cc;
482 self.cxx = cxx;
483 self
484 }
485
486 pub fn identity(&self, slot: CompilerSlot) -> Option<&'a crate::compiler::CompilerIdentity> {
488 match slot {
489 CompilerSlot::Cc => self.cc,
490 CompilerSlot::Cxx => self.cxx,
491 }
492 }
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Error)]
501pub enum ConditionParseError {
502 #[error("expected a `cfg(...)` expression but found {0:?}")]
503 ExpectedCfgPrefix(String),
504
505 #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
506 UnbalancedParens(String),
507
508 #[error(
509 "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, target, cc, cxx, cc_version, cxx_version, and feature"
510 )]
511 UnsupportedKey { key: String },
512
513 #[error(
514 "unknown compiler family {value:?} for cfg key {key:?}; supported families are clang, apple-clang, clang-cl, gcc, msvc, and unknown"
515 )]
516 UnknownCompilerFamily { key: String, value: String },
517
518 #[error("invalid version requirement {value:?} for cfg key {key:?}: {message}")]
519 InvalidCompilerVersionReq {
520 key: String,
521 value: String,
522 message: String,
523 },
524
525 #[error("expected `=` after key {key:?} in cfg expression")]
526 ExpectedEquals { key: String },
527
528 #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
529 ExpectedQuotedValue { key: String, found: String },
530
531 #[error("unterminated string literal in cfg expression: {0:?}")]
532 UnterminatedString(String),
533
534 #[error("trailing input after cfg expression: {0:?}")]
535 TrailingInput(String),
536
537 #[error("`all()` requires at least one condition")]
538 EmptyAll,
539
540 #[error("`any()` requires at least one condition")]
541 EmptyAny,
542
543 #[error("`not()` takes exactly one condition; found {0}")]
544 NotArity(usize),
545
546 #[error("expected `(` after {0}")]
547 ExpectedOpenParen(&'static str),
548
549 #[error("expected `)` to close {0}")]
550 ExpectedCloseParen(&'static str),
551
552 #[error("unexpected token in cfg expression: {0:?}")]
553 UnexpectedToken(String),
554
555 #[error("empty cfg expression")]
556 Empty,
557}
558
559struct Parser<'a> {
560 src: &'a str,
561 pos: usize,
562}
563
564impl<'a> Parser<'a> {
565 fn new(src: &'a str) -> Self {
566 Self { src, pos: 0 }
567 }
568
569 fn skip_whitespace(&mut self) {
570 while let Some(c) = self.peek_char() {
571 if c.is_whitespace() {
572 self.pos += c.len_utf8();
573 } else {
574 break;
575 }
576 }
577 }
578
579 fn peek_char(&self) -> Option<char> {
580 self.src[self.pos..].chars().next()
581 }
582
583 fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
584 self.skip_whitespace();
585 if self.pos < self.src.len() {
586 Err(ConditionParseError::TrailingInput(
587 self.src[self.pos..].to_owned(),
588 ))
589 } else {
590 Ok(())
591 }
592 }
593
594 fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
595 self.skip_whitespace();
596 if self.pos >= self.src.len() {
597 return Err(ConditionParseError::Empty);
598 }
599 let ident = self.read_ident()?;
602 self.skip_whitespace();
603 match ident.as_str() {
604 "all" => {
605 self.expect_open_paren("all")?;
606 let items = self.parse_condition_list()?;
607 self.expect_close_paren("all")?;
608 if items.is_empty() {
609 return Err(ConditionParseError::EmptyAll);
610 }
611 Ok(Condition::All(items))
612 }
613 "any" => {
614 self.expect_open_paren("any")?;
615 let items = self.parse_condition_list()?;
616 self.expect_close_paren("any")?;
617 if items.is_empty() {
618 return Err(ConditionParseError::EmptyAny);
619 }
620 Ok(Condition::Any(items))
621 }
622 "not" => {
623 self.expect_open_paren("not")?;
624 let items = self.parse_condition_list()?;
625 self.expect_close_paren("not")?;
626 if items.len() != 1 {
627 return Err(ConditionParseError::NotArity(items.len()));
628 }
629 let inner = items.into_iter().next().expect("len==1 above");
630 Ok(Condition::Not(Box::new(inner)))
631 }
632 other => {
633 self.skip_whitespace();
637 if self.peek_char() != Some('=') {
638 return Err(ConditionParseError::ExpectedEquals {
639 key: other.to_owned(),
640 });
641 }
642 self.pos += 1; self.skip_whitespace();
644 let value = self.read_quoted_string(other)?;
645 let family_slot = match other {
646 "cc" => Some(CompilerSlot::Cc),
647 "cxx" => Some(CompilerSlot::Cxx),
648 _ => None,
649 };
650 if let Some(slot) = family_slot {
651 let family =
652 crate::compiler::CompilerKind::from_key(&value).ok_or_else(|| {
653 ConditionParseError::UnknownCompilerFamily {
654 key: other.to_owned(),
655 value: value.clone(),
656 }
657 })?;
658 return Ok(Condition::CompilerFamily { slot, family });
659 }
660 let version_slot = match other {
661 "cc_version" => Some(CompilerSlot::Cc),
662 "cxx_version" => Some(CompilerSlot::Cxx),
663 _ => None,
664 };
665 if let Some(slot) = version_slot {
666 if let Err(err) = crate::version_req::parse_lenient(&value) {
667 return Err(ConditionParseError::InvalidCompilerVersionReq {
668 key: other.to_owned(),
669 value,
670 message: err.to_string(),
671 });
672 }
673 return Ok(Condition::CompilerVersionReq { slot, req: value });
674 }
675 if other == "feature" {
676 Ok(Condition::Feature(value))
677 } else {
678 let key = ConditionKey::from_str(other).map_err(|()| {
679 ConditionParseError::UnsupportedKey {
680 key: other.to_owned(),
681 }
682 })?;
683 Ok(Condition::KeyValue { key, value })
684 }
685 }
686 }
687 }
688
689 fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
690 let mut items = Vec::new();
691 self.skip_whitespace();
692 if self.peek_char() == Some(')') {
693 return Ok(items);
694 }
695 loop {
696 let cond = self.parse_condition()?;
697 items.push(cond);
698 self.skip_whitespace();
699 match self.peek_char() {
700 Some(',') => {
701 self.pos += 1;
702 self.skip_whitespace();
703 }
704 _ => break,
705 }
706 }
707 Ok(items)
708 }
709
710 fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
711 self.skip_whitespace();
712 if self.peek_char() == Some('(') {
713 self.pos += 1;
714 Ok(())
715 } else {
716 Err(ConditionParseError::ExpectedOpenParen(what))
717 }
718 }
719
720 fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
721 self.skip_whitespace();
722 if self.peek_char() == Some(')') {
723 self.pos += 1;
724 Ok(())
725 } else {
726 Err(ConditionParseError::ExpectedCloseParen(what))
727 }
728 }
729
730 fn read_ident(&mut self) -> Result<String, ConditionParseError> {
731 let start = self.pos;
732 while let Some(c) = self.peek_char() {
733 if c.is_ascii_alphanumeric() || c == '_' {
734 self.pos += c.len_utf8();
735 } else {
736 break;
737 }
738 }
739 if start == self.pos {
740 return Err(ConditionParseError::UnexpectedToken(
741 self.src[self.pos..].to_owned(),
742 ));
743 }
744 Ok(self.src[start..self.pos].to_owned())
745 }
746
747 fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
748 if self.peek_char() != Some('"') {
749 let rest_start = self.pos;
753 while let Some(c) = self.peek_char() {
754 if c == ',' || c == ')' || c.is_whitespace() {
755 break;
756 }
757 self.pos += c.len_utf8();
758 }
759 return Err(ConditionParseError::ExpectedQuotedValue {
760 key: key.to_owned(),
761 found: self.src[rest_start..self.pos].to_owned(),
762 });
763 }
764 self.pos += 1;
765 let start = self.pos;
766 while let Some(c) = self.peek_char() {
767 if c == '"' {
768 let value = self.src[start..self.pos].to_owned();
769 self.pos += 1;
770 return Ok(value);
771 }
772 self.pos += c.len_utf8();
773 }
774 Err(ConditionParseError::UnterminatedString(
775 self.src[start..].to_owned(),
776 ))
777 }
778}
779
780impl Serialize for Condition {
786 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
787 s.serialize_str(&self.to_string())
788 }
789}
790
791impl<'de> Deserialize<'de> for Condition {
792 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
793 let raw = String::deserialize(d)?;
794 Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use crate::compiler::{CompilerIdentity, CompilerKind, CompilerVersion};
802
803 fn identity(kind: CompilerKind, version: &str) -> CompilerIdentity {
804 CompilerIdentity {
805 kind,
806 version: CompilerVersion::parse(version),
807 target: None,
808 raw_version_line: format!("{kind} {version}"),
809 }
810 }
811
812 fn ctx_with_cxx<'a>(
813 platform: &'a TargetPlatform,
814 cxx: &'a CompilerIdentity,
815 ) -> ConditionContext<'a> {
816 ConditionContext::platform_only(platform).with_compilers(None, Some(cxx))
817 }
818
819 #[test]
820 fn parses_compiler_family_keys() {
821 let cond = Condition::parse_cfg(r#"cfg(cxx = "clang")"#).unwrap();
822 assert_eq!(
823 cond,
824 Condition::CompilerFamily {
825 slot: CompilerSlot::Cxx,
826 family: CompilerKind::Clang
827 }
828 );
829 let cond = Condition::parse_cfg(r#"cfg(cc = "gcc")"#).unwrap();
830 assert_eq!(
831 cond,
832 Condition::CompilerFamily {
833 slot: CompilerSlot::Cc,
834 family: CompilerKind::Gcc
835 }
836 );
837 }
838
839 #[test]
840 fn parses_compiler_version_keys() {
841 let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=18")"#).unwrap();
842 assert_eq!(
843 cond,
844 Condition::CompilerVersionReq {
845 slot: CompilerSlot::Cxx,
846 req: ">=18".to_owned()
847 }
848 );
849 assert!(Condition::parse_cfg(r#"cfg(cc_version = ">=12, <15")"#).is_ok());
850 }
851
852 #[test]
853 fn rejects_unknown_compiler_family_value() {
854 for bad in ["clang++", "Clang", "g++", "icc", ""] {
855 let raw = format!(r#"cfg(cxx = "{bad}")"#);
856 match Condition::parse_cfg(&raw).unwrap_err() {
857 ConditionParseError::UnknownCompilerFamily { key, value } => {
858 assert_eq!(key, "cxx");
859 assert_eq!(value, bad);
860 }
861 other => panic!("{raw}: unexpected {other:?}"),
862 }
863 }
864 }
865
866 #[test]
867 fn rejects_invalid_compiler_version_req() {
868 match Condition::parse_cfg(r#"cfg(cxx_version = "not a req")"#).unwrap_err() {
869 ConditionParseError::InvalidCompilerVersionReq { key, value, .. } => {
870 assert_eq!(key, "cxx_version");
871 assert_eq!(value, "not a req");
872 }
873 other => panic!("unexpected {other:?}"),
874 }
875 }
876
877 #[test]
878 fn evaluates_compiler_family_against_detected_identity() {
879 let platform = linux_x86_64();
880 let clang = identity(CompilerKind::Clang, "18.1.3");
881 let ctx = ctx_with_cxx(&platform, &clang);
882 assert!(
883 Condition::parse_cfg(r#"cfg(cxx = "clang")"#)
884 .unwrap()
885 .evaluate(&ctx)
886 );
887 assert!(
888 !Condition::parse_cfg(r#"cfg(cxx = "gcc")"#)
889 .unwrap()
890 .evaluate(&ctx)
891 );
892 assert!(
895 Condition::parse_cfg(r#"cfg(cc = "unknown")"#)
896 .unwrap()
897 .evaluate(&ctx)
898 );
899 assert!(
900 !Condition::parse_cfg(r#"cfg(cc = "clang")"#)
901 .unwrap()
902 .evaluate(&ctx)
903 );
904 }
905
906 #[test]
907 fn evaluates_compiler_version_with_semver_partial_semantics() {
908 let platform = linux_x86_64();
909 let clang18 = identity(CompilerKind::Clang, "18.1.3");
910 let ctx = ctx_with_cxx(&platform, &clang18);
911 for (req, expect) in [
912 (">=18", true),
913 (">18", false), ("18", true), ("=18", true),
916 (">=16, <19", true),
917 (">=19", false),
918 ("<18", false),
919 ] {
920 let raw = format!(r#"cfg(cxx_version = "{req}")"#);
921 let cond = Condition::parse_cfg(&raw).unwrap();
922 assert_eq!(cond.evaluate(&ctx), expect, "{req} vs 18.1.3");
923 }
924 }
925
926 #[test]
927 fn compiler_version_zero_pads_missing_components() {
928 let platform = linux_x86_64();
929 let gcc14 = identity(CompilerKind::Gcc, "14.2");
930 let ctx = ctx_with_cxx(&platform, &gcc14);
931 let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=14.2")"#).unwrap();
932 assert!(cond.evaluate(&ctx)); }
934
935 #[test]
936 fn compiler_version_without_detected_version_is_false() {
937 let platform = linux_x86_64();
938 let unparsed = CompilerIdentity::unknown("mystery output");
939 let ctx = ctx_with_cxx(&platform, &unparsed);
940 assert!(
941 !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
942 .unwrap()
943 .evaluate(&ctx)
944 );
945 let bare = ConditionContext::platform_only(&platform);
947 assert!(
948 !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
949 .unwrap()
950 .evaluate(&bare)
951 );
952 }
953
954 #[test]
955 fn compiler_conditions_compose_with_platform_and_feature_leaves() {
956 let platform = linux_x86_64();
957 let clang = identity(CompilerKind::Clang, "18.1.3");
958 let features: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
959 let ctx = ConditionContext::with_features(&platform, &features)
960 .with_compilers(None, Some(&clang));
961 let cond = Condition::parse_cfg(
962 r#"cfg(all(os = "linux", feature = "simd", cxx = "clang", cxx_version = ">=18"))"#,
963 )
964 .unwrap();
965 assert!(cond.evaluate(&ctx));
966 let not = Condition::parse_cfg(r#"cfg(not(cxx = "clang"))"#).unwrap();
967 assert!(!not.evaluate(&ctx));
968 }
969
970 #[test]
971 fn references_compiler_walks_combinators() {
972 for (raw, expect) in [
973 (r#"cxx = "clang""#, true),
974 (r#"cc_version = ">=12""#, true),
975 (r#"all(os = "linux", cxx = "clang")"#, true),
976 (r#"not(cc = "msvc")"#, true),
977 (r#"all(os = "linux", feature = "simd")"#, false),
978 (r#"os = "linux""#, false),
979 ] {
980 assert_eq!(
981 Condition::parse_inner(raw).unwrap().references_compiler(),
982 expect,
983 "{raw}"
984 );
985 }
986 }
987
988 fn linux_x86_64() -> TargetPlatform {
989 TargetPlatform {
990 os: "linux".into(),
991 arch: "x86_64".into(),
992 family: "unix".into(),
993 env: "gnu".into(),
994 abi: "unknown".into(),
995 target: "x86_64-unix-linux".into(),
996 }
997 }
998
999 fn macos_aarch64() -> TargetPlatform {
1000 TargetPlatform {
1001 os: "macos".into(),
1002 arch: "aarch64".into(),
1003 family: "unix".into(),
1004 env: "apple".into(),
1005 abi: "unknown".into(),
1006 target: "aarch64-unix-macos".into(),
1007 }
1008 }
1009
1010 #[test]
1011 fn parses_simple_key_value() {
1012 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1013 assert_eq!(
1014 cond,
1015 Condition::KeyValue {
1016 key: ConditionKey::Os,
1017 value: "linux".into()
1018 }
1019 );
1020 }
1021
1022 #[test]
1023 fn parses_each_supported_key() {
1024 for (raw, key) in [
1025 (r#"cfg(os = "linux")"#, ConditionKey::Os),
1026 (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
1027 (r#"cfg(family = "unix")"#, ConditionKey::Family),
1028 (r#"cfg(env = "gnu")"#, ConditionKey::Env),
1029 (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
1030 (
1031 r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
1032 ConditionKey::Target,
1033 ),
1034 ] {
1035 let cond = Condition::parse_cfg(raw).unwrap();
1036 match cond {
1037 Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
1038 other => panic!("{raw}: expected key/value, got {other:?}"),
1039 }
1040 }
1041 }
1042
1043 #[test]
1044 fn parses_all_any_not() {
1045 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1046 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1047 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1048 assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
1049 assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
1050 assert!(matches!(not, Condition::Not(_)));
1051 }
1052
1053 #[test]
1054 fn rejects_unquoted_value() {
1055 let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
1056 match err {
1057 ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
1058 other => panic!("unexpected: {other:?}"),
1059 }
1060 }
1061
1062 #[test]
1063 fn rejects_unsupported_key() {
1064 let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
1065 match err {
1066 ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
1067 other => panic!("unexpected: {other:?}"),
1068 }
1069 }
1070
1071 #[test]
1072 fn rejects_empty_all_and_any() {
1073 assert!(matches!(
1074 Condition::parse_cfg("cfg(all())").unwrap_err(),
1075 ConditionParseError::EmptyAll
1076 ));
1077 assert!(matches!(
1078 Condition::parse_cfg("cfg(any())").unwrap_err(),
1079 ConditionParseError::EmptyAny
1080 ));
1081 }
1082
1083 #[test]
1084 fn rejects_not_with_arity_other_than_one() {
1085 let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
1086 assert!(matches!(err, ConditionParseError::NotArity(2)));
1087 }
1088
1089 #[test]
1090 fn rejects_missing_cfg_prefix() {
1091 assert!(matches!(
1092 Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
1093 ConditionParseError::ExpectedCfgPrefix(_)
1094 ));
1095 }
1096
1097 #[test]
1098 fn rejects_unbalanced_parens() {
1099 assert!(matches!(
1100 Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
1101 ConditionParseError::UnbalancedParens(_)
1102 ));
1103 }
1104
1105 #[test]
1106 fn evaluates_simple_key_value() {
1107 let linux = linux_x86_64();
1108 let macos = macos_aarch64();
1109 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1110 assert!(cond.evaluate(&ConditionContext::platform_only(&linux)));
1111 assert!(!cond.evaluate(&ConditionContext::platform_only(&macos)));
1112 }
1113
1114 #[test]
1115 fn evaluates_all_any_not() {
1116 let linux = linux_x86_64();
1117 let macos = macos_aarch64();
1118 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1119 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1120 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1121 assert!(all.evaluate(&ConditionContext::platform_only(&linux)));
1122 assert!(!all.evaluate(&ConditionContext::platform_only(&macos)));
1123 assert!(any.evaluate(&ConditionContext::platform_only(&linux)));
1124 assert!(any.evaluate(&ConditionContext::platform_only(&macos)));
1125 assert!(not.evaluate(&ConditionContext::platform_only(&linux)));
1126 assert!(not.evaluate(&ConditionContext::platform_only(&macos)));
1127 }
1128
1129 #[test]
1130 fn parses_and_evaluates_feature_leaf() {
1131 let linux = linux_x86_64();
1132 let cond = Condition::parse_cfg(r#"cfg(feature = "simd")"#).unwrap();
1133 assert_eq!(cond, Condition::Feature("simd".to_owned()));
1134 assert!(cond.references_feature());
1135 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1136 assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1137 assert!(!cond.evaluate(&ConditionContext::platform_only(&linux)));
1138 }
1139
1140 #[test]
1141 fn evaluates_feature_combined_with_platform() {
1142 let linux = linux_x86_64();
1143 let macos = macos_aarch64();
1144 let cond = Condition::parse_cfg(r#"cfg(all(feature = "simd", arch = "x86_64"))"#).unwrap();
1145 assert!(cond.references_feature());
1146 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1147 assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1149 assert!(!cond.evaluate(&ConditionContext::with_features(&macos, &enabled))); assert!(!cond.evaluate(&ConditionContext::platform_only(&linux))); }
1152
1153 #[test]
1154 fn references_feature_is_false_for_platform_only_conditions() {
1155 for raw in [
1156 r#"cfg(os = "linux")"#,
1157 r#"cfg(all(os = "linux", arch = "x86_64"))"#,
1158 r#"cfg(not(os = "windows"))"#,
1159 ] {
1160 assert!(!Condition::parse_cfg(raw).unwrap().references_feature());
1161 }
1162 }
1163
1164 #[test]
1165 fn display_round_trips_through_parse_inner() {
1166 for raw in [
1167 r#"os = "linux""#,
1168 r#"feature = "simd""#,
1169 r#"all(feature = "simd", arch = "x86_64")"#,
1170 r#"all(os = "linux", arch = "x86_64")"#,
1171 r#"any(os = "macos", os = "linux")"#,
1172 r#"not(os = "windows")"#,
1173 r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
1174 r#"cxx = "clang""#,
1175 r#"cc = "gcc""#,
1176 r#"cxx_version = ">=18""#,
1177 r#"cc_version = ">=12, <15""#,
1178 r#"all(cxx = "clang", cxx_version = ">=18")"#,
1179 r#"not(cxx = "msvc")"#,
1180 r#"all(cxx = "apple-clang", os = "macos")"#,
1181 ] {
1182 let cond = Condition::parse_inner(raw).unwrap();
1183 let rendered = cond.to_string();
1184 assert_eq!(rendered, raw, "round-trip should be byte-identical");
1185 let again = Condition::parse_inner(&rendered).unwrap();
1186 assert_eq!(cond, again);
1187 }
1188 }
1189
1190 #[test]
1191 fn current_target_platform_is_internally_consistent() {
1192 let p = TargetPlatform::current();
1193 for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
1195 assert!(!v.is_empty());
1196 assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
1197 }
1198 }
1199
1200 #[test]
1201 fn deterministic_serialization_for_metadata_round_trip() {
1202 let cond = Condition::parse_cfg(
1203 r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
1204 )
1205 .unwrap();
1206 let json = serde_json::to_string(&cond).unwrap();
1207 let parsed: Condition = serde_json::from_str(&json).unwrap();
1208 assert_eq!(cond, parsed);
1209 }
1210}