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)]
352pub struct TargetPlatform {
353 pub os: String,
354 pub arch: String,
355 pub family: String,
356 pub env: String,
357 pub abi: String,
358 pub target: String,
359}
360
361impl TargetPlatform {
362 pub fn current() -> Self {
368 let os = normalize_os(std::env::consts::OS);
369 let arch = normalize_arch(std::env::consts::ARCH);
370 let family = normalize_family(std::env::consts::FAMILY, &os);
371 let env = normalize_env(&os);
372 let abi = "unknown".to_owned();
373 let target = format!("{arch}-{family}-{os}");
374 Self {
375 os,
376 arch,
377 family,
378 env,
379 abi,
380 target,
381 }
382 }
383}
384
385fn normalize_os(raw: &str) -> String {
386 match raw {
387 "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
388 | "android" | "ios" => raw.to_owned(),
389 "darwin" => "macos".to_owned(),
391 "" => "unknown".to_owned(),
392 other => other.to_owned(),
393 }
394}
395
396fn normalize_arch(raw: &str) -> String {
397 match raw {
398 "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
399 "" => "unknown".to_owned(),
400 other => other.to_owned(),
401 }
402}
403
404fn normalize_family(raw: &str, os: &str) -> String {
405 match raw {
406 "unix" | "windows" | "wasm" => raw.to_owned(),
407 _ => match os {
408 "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
409 | "ios" => "unix".to_owned(),
410 "windows" => "windows".to_owned(),
411 _ => "unknown".to_owned(),
412 },
413 }
414}
415
416fn normalize_env(os: &str) -> String {
417 match os {
423 "linux" => "gnu".to_owned(),
424 "macos" | "ios" => "apple".to_owned(),
425 "windows" => "msvc".to_owned(),
426 _ => "unknown".to_owned(),
427 }
428}
429
430#[derive(Debug, Clone, Copy)]
440pub struct ConditionContext<'a> {
441 pub platform: &'a TargetPlatform,
442 pub features: &'a BTreeSet<String>,
443 pub cc: Option<&'a crate::compiler::CompilerIdentity>,
446 pub cxx: Option<&'a crate::compiler::CompilerIdentity>,
448}
449
450static EMPTY_FEATURES: BTreeSet<String> = BTreeSet::new();
451
452impl<'a> ConditionContext<'a> {
453 pub fn platform_only(platform: &'a TargetPlatform) -> Self {
458 Self {
459 platform,
460 features: &EMPTY_FEATURES,
461 cc: None,
462 cxx: None,
463 }
464 }
465
466 pub fn with_features(platform: &'a TargetPlatform, features: &'a BTreeSet<String>) -> Self {
469 Self {
470 platform,
471 features,
472 cc: None,
473 cxx: None,
474 }
475 }
476
477 #[must_use]
479 pub fn with_compilers(
480 mut self,
481 cc: Option<&'a crate::compiler::CompilerIdentity>,
482 cxx: Option<&'a crate::compiler::CompilerIdentity>,
483 ) -> Self {
484 self.cc = cc;
485 self.cxx = cxx;
486 self
487 }
488
489 pub fn identity(&self, slot: CompilerSlot) -> Option<&'a crate::compiler::CompilerIdentity> {
491 match slot {
492 CompilerSlot::Cc => self.cc,
493 CompilerSlot::Cxx => self.cxx,
494 }
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Error)]
504pub enum ConditionParseError {
505 #[error("expected a `cfg(...)` expression but found {0:?}")]
506 ExpectedCfgPrefix(String),
507
508 #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
509 UnbalancedParens(String),
510
511 #[error(
512 "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, target, cc, cxx, cc_version, cxx_version, and feature"
513 )]
514 UnsupportedKey { key: String },
515
516 #[error(
517 "unknown compiler family {value:?} for cfg key {key:?}; supported families are clang, apple-clang, clang-cl, gcc, msvc, and unknown"
518 )]
519 UnknownCompilerFamily { key: String, value: String },
520
521 #[error("invalid version requirement {value:?} for cfg key {key:?}: {message}")]
522 InvalidCompilerVersionReq {
523 key: String,
524 value: String,
525 message: String,
526 },
527
528 #[error("expected `=` after key {key:?} in cfg expression")]
529 ExpectedEquals { key: String },
530
531 #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
532 ExpectedQuotedValue { key: String, found: String },
533
534 #[error("unterminated string literal in cfg expression: {0:?}")]
535 UnterminatedString(String),
536
537 #[error("trailing input after cfg expression: {0:?}")]
538 TrailingInput(String),
539
540 #[error("`all()` requires at least one condition")]
541 EmptyAll,
542
543 #[error("`any()` requires at least one condition")]
544 EmptyAny,
545
546 #[error("`not()` takes exactly one condition; found {0}")]
547 NotArity(usize),
548
549 #[error("expected `(` after {0}")]
550 ExpectedOpenParen(&'static str),
551
552 #[error("expected `)` to close {0}")]
553 ExpectedCloseParen(&'static str),
554
555 #[error("unexpected token in cfg expression: {0:?}")]
556 UnexpectedToken(String),
557
558 #[error("empty cfg expression")]
559 Empty,
560}
561
562struct Parser<'a> {
563 src: &'a str,
564 pos: usize,
565}
566
567impl<'a> Parser<'a> {
568 fn new(src: &'a str) -> Self {
569 Self { src, pos: 0 }
570 }
571
572 fn skip_whitespace(&mut self) {
573 while let Some(c) = self.peek_char() {
574 if c.is_whitespace() {
575 self.pos += c.len_utf8();
576 } else {
577 break;
578 }
579 }
580 }
581
582 fn peek_char(&self) -> Option<char> {
583 self.src[self.pos..].chars().next()
584 }
585
586 fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
587 self.skip_whitespace();
588 if self.pos < self.src.len() {
589 Err(ConditionParseError::TrailingInput(
590 self.src[self.pos..].to_owned(),
591 ))
592 } else {
593 Ok(())
594 }
595 }
596
597 fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
598 self.skip_whitespace();
599 if self.pos >= self.src.len() {
600 return Err(ConditionParseError::Empty);
601 }
602 let ident = self.read_ident()?;
605 self.skip_whitespace();
606 match ident.as_str() {
607 "all" => {
608 self.expect_open_paren("all")?;
609 let items = self.parse_condition_list()?;
610 self.expect_close_paren("all")?;
611 if items.is_empty() {
612 return Err(ConditionParseError::EmptyAll);
613 }
614 Ok(Condition::All(items))
615 }
616 "any" => {
617 self.expect_open_paren("any")?;
618 let items = self.parse_condition_list()?;
619 self.expect_close_paren("any")?;
620 if items.is_empty() {
621 return Err(ConditionParseError::EmptyAny);
622 }
623 Ok(Condition::Any(items))
624 }
625 "not" => {
626 self.expect_open_paren("not")?;
627 let items = self.parse_condition_list()?;
628 self.expect_close_paren("not")?;
629 if items.len() != 1 {
630 return Err(ConditionParseError::NotArity(items.len()));
631 }
632 let inner = items.into_iter().next().expect("len==1 above");
633 Ok(Condition::Not(Box::new(inner)))
634 }
635 other => self.parse_leaf(other),
636 }
637 }
638
639 fn parse_leaf(&mut self, key: &str) -> Result<Condition, ConditionParseError> {
645 self.skip_whitespace();
649 if self.peek_char() != Some('=') {
650 return Err(ConditionParseError::ExpectedEquals {
651 key: key.to_owned(),
652 });
653 }
654 self.pos += 1; self.skip_whitespace();
656 let value = self.read_quoted_string(key)?;
657 let family_slot = match key {
658 "cc" => Some(CompilerSlot::Cc),
659 "cxx" => Some(CompilerSlot::Cxx),
660 _ => None,
661 };
662 if let Some(slot) = family_slot {
663 let family = crate::compiler::CompilerKind::from_key(&value).ok_or_else(|| {
664 ConditionParseError::UnknownCompilerFamily {
665 key: key.to_owned(),
666 value: value.clone(),
667 }
668 })?;
669 return Ok(Condition::CompilerFamily { slot, family });
670 }
671 let version_slot = match key {
672 "cc_version" => Some(CompilerSlot::Cc),
673 "cxx_version" => Some(CompilerSlot::Cxx),
674 _ => None,
675 };
676 if let Some(slot) = version_slot {
677 if let Err(err) = crate::version_req::parse_lenient(&value) {
678 return Err(ConditionParseError::InvalidCompilerVersionReq {
679 key: key.to_owned(),
680 value,
681 message: err.to_string(),
682 });
683 }
684 return Ok(Condition::CompilerVersionReq { slot, req: value });
685 }
686 if key == "feature" {
687 Ok(Condition::Feature(value))
688 } else {
689 let key =
690 ConditionKey::from_str(key).map_err(|()| ConditionParseError::UnsupportedKey {
691 key: key.to_owned(),
692 })?;
693 Ok(Condition::KeyValue { key, value })
694 }
695 }
696
697 fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
698 let mut items = Vec::new();
699 self.skip_whitespace();
700 if self.peek_char() == Some(')') {
701 return Ok(items);
702 }
703 loop {
704 let cond = self.parse_condition()?;
705 items.push(cond);
706 self.skip_whitespace();
707 match self.peek_char() {
708 Some(',') => {
709 self.pos += 1;
710 self.skip_whitespace();
711 }
712 _ => break,
713 }
714 }
715 Ok(items)
716 }
717
718 fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
719 self.skip_whitespace();
720 if self.peek_char() == Some('(') {
721 self.pos += 1;
722 Ok(())
723 } else {
724 Err(ConditionParseError::ExpectedOpenParen(what))
725 }
726 }
727
728 fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
729 self.skip_whitespace();
730 if self.peek_char() == Some(')') {
731 self.pos += 1;
732 Ok(())
733 } else {
734 Err(ConditionParseError::ExpectedCloseParen(what))
735 }
736 }
737
738 fn read_ident(&mut self) -> Result<String, ConditionParseError> {
739 let start = self.pos;
740 while let Some(c) = self.peek_char() {
741 if c.is_ascii_alphanumeric() || c == '_' {
742 self.pos += c.len_utf8();
743 } else {
744 break;
745 }
746 }
747 if start == self.pos {
748 return Err(ConditionParseError::UnexpectedToken(
749 self.src[self.pos..].to_owned(),
750 ));
751 }
752 Ok(self.src[start..self.pos].to_owned())
753 }
754
755 fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
756 if self.peek_char() != Some('"') {
757 let rest_start = self.pos;
761 while let Some(c) = self.peek_char() {
762 if c == ',' || c == ')' || c.is_whitespace() {
763 break;
764 }
765 self.pos += c.len_utf8();
766 }
767 return Err(ConditionParseError::ExpectedQuotedValue {
768 key: key.to_owned(),
769 found: self.src[rest_start..self.pos].to_owned(),
770 });
771 }
772 self.pos += 1;
773 let start = self.pos;
774 while let Some(c) = self.peek_char() {
775 if c == '"' {
776 let value = self.src[start..self.pos].to_owned();
777 self.pos += 1;
778 return Ok(value);
779 }
780 self.pos += c.len_utf8();
781 }
782 Err(ConditionParseError::UnterminatedString(
783 self.src[start..].to_owned(),
784 ))
785 }
786}
787
788impl Serialize for Condition {
794 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
795 s.serialize_str(&self.to_string())
796 }
797}
798
799impl<'de> Deserialize<'de> for Condition {
800 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
801 let raw = String::deserialize(d)?;
802 Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use crate::compiler::{CompilerIdentity, CompilerKind, CompilerVersion};
810
811 fn identity(kind: CompilerKind, version: &str) -> CompilerIdentity {
812 CompilerIdentity {
813 kind,
814 version: CompilerVersion::parse(version),
815 target: None,
816 raw_version_line: format!("{kind} {version}"),
817 }
818 }
819
820 fn ctx_with_cxx<'a>(
821 platform: &'a TargetPlatform,
822 cxx: &'a CompilerIdentity,
823 ) -> ConditionContext<'a> {
824 ConditionContext::platform_only(platform).with_compilers(None, Some(cxx))
825 }
826
827 #[test]
828 fn parses_compiler_family_keys() {
829 let cond = Condition::parse_cfg(r#"cfg(cxx = "clang")"#).unwrap();
830 assert_eq!(
831 cond,
832 Condition::CompilerFamily {
833 slot: CompilerSlot::Cxx,
834 family: CompilerKind::Clang
835 }
836 );
837 let cond = Condition::parse_cfg(r#"cfg(cc = "gcc")"#).unwrap();
838 assert_eq!(
839 cond,
840 Condition::CompilerFamily {
841 slot: CompilerSlot::Cc,
842 family: CompilerKind::Gcc
843 }
844 );
845 }
846
847 #[test]
848 fn parses_compiler_version_keys() {
849 let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=18")"#).unwrap();
850 assert_eq!(
851 cond,
852 Condition::CompilerVersionReq {
853 slot: CompilerSlot::Cxx,
854 req: ">=18".to_owned()
855 }
856 );
857 assert!(Condition::parse_cfg(r#"cfg(cc_version = ">=12, <15")"#).is_ok());
858 }
859
860 #[test]
861 fn rejects_unknown_compiler_family_value() {
862 for bad in ["clang++", "Clang", "g++", "icc", ""] {
863 let raw = format!(r#"cfg(cxx = "{bad}")"#);
864 match Condition::parse_cfg(&raw).unwrap_err() {
865 ConditionParseError::UnknownCompilerFamily { key, value } => {
866 assert_eq!(key, "cxx");
867 assert_eq!(value, bad);
868 }
869 other => panic!("{raw}: unexpected {other:?}"),
870 }
871 }
872 }
873
874 #[test]
875 fn rejects_invalid_compiler_version_req() {
876 match Condition::parse_cfg(r#"cfg(cxx_version = "not a req")"#).unwrap_err() {
877 ConditionParseError::InvalidCompilerVersionReq { key, value, .. } => {
878 assert_eq!(key, "cxx_version");
879 assert_eq!(value, "not a req");
880 }
881 other => panic!("unexpected {other:?}"),
882 }
883 }
884
885 #[test]
886 fn evaluates_compiler_family_against_detected_identity() {
887 let platform = linux_x86_64();
888 let clang = identity(CompilerKind::Clang, "18.1.3");
889 let ctx = ctx_with_cxx(&platform, &clang);
890 assert!(
891 Condition::parse_cfg(r#"cfg(cxx = "clang")"#)
892 .unwrap()
893 .evaluate(&ctx)
894 );
895 assert!(
896 !Condition::parse_cfg(r#"cfg(cxx = "gcc")"#)
897 .unwrap()
898 .evaluate(&ctx)
899 );
900 assert!(
903 Condition::parse_cfg(r#"cfg(cc = "unknown")"#)
904 .unwrap()
905 .evaluate(&ctx)
906 );
907 assert!(
908 !Condition::parse_cfg(r#"cfg(cc = "clang")"#)
909 .unwrap()
910 .evaluate(&ctx)
911 );
912 }
913
914 #[test]
915 fn evaluates_compiler_version_with_semver_partial_semantics() {
916 let platform = linux_x86_64();
917 let clang18 = identity(CompilerKind::Clang, "18.1.3");
918 let ctx = ctx_with_cxx(&platform, &clang18);
919 for (req, expect) in [
920 (">=18", true),
921 (">18", false), ("18", true), ("=18", true),
924 (">=16, <19", true),
925 (">=19", false),
926 ("<18", false),
927 ] {
928 let raw = format!(r#"cfg(cxx_version = "{req}")"#);
929 let cond = Condition::parse_cfg(&raw).unwrap();
930 assert_eq!(cond.evaluate(&ctx), expect, "{req} vs 18.1.3");
931 }
932 }
933
934 #[test]
935 fn compiler_version_zero_pads_missing_components() {
936 let platform = linux_x86_64();
937 let gcc14 = identity(CompilerKind::Gcc, "14.2");
938 let ctx = ctx_with_cxx(&platform, &gcc14);
939 let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=14.2")"#).unwrap();
940 assert!(cond.evaluate(&ctx)); }
942
943 #[test]
944 fn compiler_version_without_detected_version_is_false() {
945 let platform = linux_x86_64();
946 let unparsed = CompilerIdentity::unknown("mystery output");
947 let ctx = ctx_with_cxx(&platform, &unparsed);
948 assert!(
949 !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
950 .unwrap()
951 .evaluate(&ctx)
952 );
953 let bare = ConditionContext::platform_only(&platform);
955 assert!(
956 !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
957 .unwrap()
958 .evaluate(&bare)
959 );
960 }
961
962 #[test]
963 fn compiler_conditions_compose_with_platform_and_feature_leaves() {
964 let platform = linux_x86_64();
965 let clang = identity(CompilerKind::Clang, "18.1.3");
966 let features: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
967 let ctx = ConditionContext::with_features(&platform, &features)
968 .with_compilers(None, Some(&clang));
969 let cond = Condition::parse_cfg(
970 r#"cfg(all(os = "linux", feature = "simd", cxx = "clang", cxx_version = ">=18"))"#,
971 )
972 .unwrap();
973 assert!(cond.evaluate(&ctx));
974 let not = Condition::parse_cfg(r#"cfg(not(cxx = "clang"))"#).unwrap();
975 assert!(!not.evaluate(&ctx));
976 }
977
978 #[test]
979 fn references_compiler_walks_combinators() {
980 for (raw, expect) in [
981 (r#"cxx = "clang""#, true),
982 (r#"cc_version = ">=12""#, true),
983 (r#"all(os = "linux", cxx = "clang")"#, true),
984 (r#"not(cc = "msvc")"#, true),
985 (r#"all(os = "linux", feature = "simd")"#, false),
986 (r#"os = "linux""#, false),
987 ] {
988 assert_eq!(
989 Condition::parse_inner(raw).unwrap().references_compiler(),
990 expect,
991 "{raw}"
992 );
993 }
994 }
995
996 fn linux_x86_64() -> TargetPlatform {
997 TargetPlatform {
998 os: "linux".into(),
999 arch: "x86_64".into(),
1000 family: "unix".into(),
1001 env: "gnu".into(),
1002 abi: "unknown".into(),
1003 target: "x86_64-unix-linux".into(),
1004 }
1005 }
1006
1007 fn macos_aarch64() -> TargetPlatform {
1008 TargetPlatform {
1009 os: "macos".into(),
1010 arch: "aarch64".into(),
1011 family: "unix".into(),
1012 env: "apple".into(),
1013 abi: "unknown".into(),
1014 target: "aarch64-unix-macos".into(),
1015 }
1016 }
1017
1018 #[test]
1019 fn parses_simple_key_value() {
1020 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1021 assert_eq!(
1022 cond,
1023 Condition::KeyValue {
1024 key: ConditionKey::Os,
1025 value: "linux".into()
1026 }
1027 );
1028 }
1029
1030 #[test]
1031 fn parses_each_supported_key() {
1032 for (raw, key) in [
1033 (r#"cfg(os = "linux")"#, ConditionKey::Os),
1034 (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
1035 (r#"cfg(family = "unix")"#, ConditionKey::Family),
1036 (r#"cfg(env = "gnu")"#, ConditionKey::Env),
1037 (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
1038 (
1039 r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
1040 ConditionKey::Target,
1041 ),
1042 ] {
1043 let cond = Condition::parse_cfg(raw).unwrap();
1044 match cond {
1045 Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
1046 other => panic!("{raw}: expected key/value, got {other:?}"),
1047 }
1048 }
1049 }
1050
1051 #[test]
1052 fn parses_all_any_not() {
1053 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1054 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1055 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1056 assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
1057 assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
1058 assert!(matches!(not, Condition::Not(_)));
1059 }
1060
1061 #[test]
1062 fn rejects_unquoted_value() {
1063 let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
1064 match err {
1065 ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
1066 other => panic!("unexpected: {other:?}"),
1067 }
1068 }
1069
1070 #[test]
1071 fn rejects_unsupported_key() {
1072 let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
1073 match err {
1074 ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
1075 other => panic!("unexpected: {other:?}"),
1076 }
1077 }
1078
1079 #[test]
1080 fn rejects_empty_all_and_any() {
1081 assert!(matches!(
1082 Condition::parse_cfg("cfg(all())").unwrap_err(),
1083 ConditionParseError::EmptyAll
1084 ));
1085 assert!(matches!(
1086 Condition::parse_cfg("cfg(any())").unwrap_err(),
1087 ConditionParseError::EmptyAny
1088 ));
1089 }
1090
1091 #[test]
1092 fn rejects_not_with_arity_other_than_one() {
1093 let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
1094 assert!(matches!(err, ConditionParseError::NotArity(2)));
1095 }
1096
1097 #[test]
1098 fn rejects_missing_cfg_prefix() {
1099 assert!(matches!(
1100 Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
1101 ConditionParseError::ExpectedCfgPrefix(_)
1102 ));
1103 }
1104
1105 #[test]
1106 fn rejects_unbalanced_parens() {
1107 assert!(matches!(
1108 Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
1109 ConditionParseError::UnbalancedParens(_)
1110 ));
1111 }
1112
1113 #[test]
1114 fn evaluates_simple_key_value() {
1115 let linux = linux_x86_64();
1116 let macos = macos_aarch64();
1117 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1118 assert!(cond.evaluate(&ConditionContext::platform_only(&linux)));
1119 assert!(!cond.evaluate(&ConditionContext::platform_only(&macos)));
1120 }
1121
1122 #[test]
1123 fn evaluates_all_any_not() {
1124 let linux = linux_x86_64();
1125 let macos = macos_aarch64();
1126 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1127 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1128 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1129 assert!(all.evaluate(&ConditionContext::platform_only(&linux)));
1130 assert!(!all.evaluate(&ConditionContext::platform_only(&macos)));
1131 assert!(any.evaluate(&ConditionContext::platform_only(&linux)));
1132 assert!(any.evaluate(&ConditionContext::platform_only(&macos)));
1133 assert!(not.evaluate(&ConditionContext::platform_only(&linux)));
1134 assert!(not.evaluate(&ConditionContext::platform_only(&macos)));
1135 }
1136
1137 #[test]
1138 fn parses_and_evaluates_feature_leaf() {
1139 let linux = linux_x86_64();
1140 let cond = Condition::parse_cfg(r#"cfg(feature = "simd")"#).unwrap();
1141 assert_eq!(cond, Condition::Feature("simd".to_owned()));
1142 assert!(cond.references_feature());
1143 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1144 assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1145 assert!(!cond.evaluate(&ConditionContext::platform_only(&linux)));
1146 }
1147
1148 #[test]
1149 fn evaluates_feature_combined_with_platform() {
1150 let linux = linux_x86_64();
1151 let macos = macos_aarch64();
1152 let cond = Condition::parse_cfg(r#"cfg(all(feature = "simd", arch = "x86_64"))"#).unwrap();
1153 assert!(cond.references_feature());
1154 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1155 assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1157 assert!(!cond.evaluate(&ConditionContext::with_features(&macos, &enabled))); assert!(!cond.evaluate(&ConditionContext::platform_only(&linux))); }
1160
1161 #[test]
1162 fn references_feature_is_false_for_platform_only_conditions() {
1163 for raw in [
1164 r#"cfg(os = "linux")"#,
1165 r#"cfg(all(os = "linux", arch = "x86_64"))"#,
1166 r#"cfg(not(os = "windows"))"#,
1167 ] {
1168 assert!(!Condition::parse_cfg(raw).unwrap().references_feature());
1169 }
1170 }
1171
1172 #[test]
1173 fn display_round_trips_through_parse_inner() {
1174 for raw in [
1175 r#"os = "linux""#,
1176 r#"feature = "simd""#,
1177 r#"all(feature = "simd", arch = "x86_64")"#,
1178 r#"all(os = "linux", arch = "x86_64")"#,
1179 r#"any(os = "macos", os = "linux")"#,
1180 r#"not(os = "windows")"#,
1181 r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
1182 r#"cxx = "clang""#,
1183 r#"cc = "gcc""#,
1184 r#"cxx_version = ">=18""#,
1185 r#"cc_version = ">=12, <15""#,
1186 r#"all(cxx = "clang", cxx_version = ">=18")"#,
1187 r#"not(cxx = "msvc")"#,
1188 r#"all(cxx = "apple-clang", os = "macos")"#,
1189 ] {
1190 let cond = Condition::parse_inner(raw).unwrap();
1191 let rendered = cond.to_string();
1192 assert_eq!(rendered, raw, "round-trip should be byte-identical");
1193 let again = Condition::parse_inner(&rendered).unwrap();
1194 assert_eq!(cond, again);
1195 }
1196 }
1197
1198 #[test]
1199 fn current_target_platform_is_internally_consistent() {
1200 let p = TargetPlatform::current();
1201 for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
1203 assert!(!v.is_empty());
1204 assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
1205 }
1206 }
1207
1208 #[test]
1209 fn deterministic_serialization_for_metadata_round_trip() {
1210 let cond = Condition::parse_cfg(
1211 r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
1212 )
1213 .unwrap();
1214 let json = serde_json::to_string(&cond).unwrap();
1215 let parsed: Condition = serde_json::from_str(&json).unwrap();
1216 assert_eq!(cond, parsed);
1217 }
1218}