use crate::target::TargetTriple;
use cfg_expr::{Expression, Predicate};
use color_eyre::eyre::{self, WrapErr};
use std::collections::{HashMap, HashSet};
use std::process::Command;
pub trait CfgEvaluator {
fn matches(&mut self, cfg_expr: &str, target: &TargetTriple) -> eyre::Result<bool>;
}
#[derive(Debug, Default, Clone)]
struct RustcCfgSet {
flags: HashSet<String>,
key_values: HashMap<String, HashSet<String>>,
}
impl RustcCfgSet {
fn from_rustc_print_cfg(output: &str) -> Self {
let mut set = Self::default();
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let mut val = val.trim();
if let Some(stripped) = val.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
val = stripped;
}
set.key_values
.entry(key.to_string())
.or_default()
.insert(val.to_string());
} else {
set.flags.insert(line.to_string());
}
}
set
}
fn has_flag(&self, flag: &str) -> bool {
self.flags.contains(flag)
}
fn has_kv(&self, key: &str, val: &str) -> bool {
self.key_values
.get(key)
.is_some_and(|vals| vals.contains(val))
}
}
#[derive(Debug, Default)]
pub struct RustcCfgEvaluator {
cache: HashMap<String, RustcCfgSet>,
}
impl RustcCfgEvaluator {
fn cfg_set_for(&mut self, target: &TargetTriple) -> eyre::Result<&RustcCfgSet> {
let key = target.as_str();
if !self.cache.contains_key(key) {
let output = Command::new("rustc")
.args(["--print", "cfg", "--target", key])
.output()
.wrap_err_with(|| {
format!("failed to invoke rustc to obtain cfg set for target `{key}`")
})?;
if !output.status.success() {
eyre::bail!(
"rustc --print cfg --target {} failed with exit code {:?}",
key,
output.status.code()
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let set = RustcCfgSet::from_rustc_print_cfg(&stdout);
self.cache.insert(key.to_string(), set);
}
self.cache
.get(key)
.ok_or_else(|| eyre::eyre!("failed to cache rustc cfg set"))
}
fn validate_supported(expr: &Expression) -> eyre::Result<()> {
for pred in expr.predicates() {
if let Predicate::Feature(_) = pred {
eyre::bail!(
"cfg expressions using `feature = \"...\"` are not supported in cargo-feature-combinations target overrides"
)
}
}
Ok(())
}
}
fn endian_str(e: cfg_expr::targets::Endian) -> &'static str {
match e {
cfg_expr::targets::Endian::big => "big",
cfg_expr::targets::Endian::little => "little",
}
}
impl CfgEvaluator for RustcCfgEvaluator {
fn matches(&mut self, cfg_expr: &str, target: &TargetTriple) -> eyre::Result<bool> {
let set = self.cfg_set_for(target)?;
let expr = Expression::parse(cfg_expr)
.wrap_err_with(|| format!("failed to parse cfg expression `{cfg_expr}`"))?;
Self::validate_supported(&expr)?;
Ok(expr.eval(|pred| match pred {
Predicate::Target(tp) => {
match tp {
cfg_expr::expr::TargetPredicate::Arch(a) => {
set.has_kv("target_arch", a.as_ref())
}
cfg_expr::expr::TargetPredicate::Os(o) => set.has_kv("target_os", o.as_ref()),
cfg_expr::expr::TargetPredicate::Env(e) => set.has_kv("target_env", e.as_ref()),
cfg_expr::expr::TargetPredicate::Family(f) => {
set.has_kv("target_family", f.as_ref())
}
cfg_expr::expr::TargetPredicate::Vendor(v) => {
set.has_kv("target_vendor", v.as_ref())
}
cfg_expr::expr::TargetPredicate::Abi(a) => set.has_kv("target_abi", a.as_ref()),
cfg_expr::expr::TargetPredicate::Endian(e) => {
set.has_kv("target_endian", endian_str(*e))
}
cfg_expr::expr::TargetPredicate::Panic(p) => set.has_kv("panic", p.as_ref()),
cfg_expr::expr::TargetPredicate::PointerWidth(w) => {
set.has_kv("target_pointer_width", &w.to_string())
}
cfg_expr::expr::TargetPredicate::HasAtomic(a) => {
set.has_kv("target_has_atomic", &a.to_string())
}
}
}
Predicate::TargetFeature(feat) => set.has_kv("target_feature", feat),
Predicate::Flag(name) => set.has_flag(name),
Predicate::KeyValue { key, val } => set.has_kv(key, val),
Predicate::Test => set.has_flag("test"),
Predicate::DebugAssertions => set.has_flag("debug_assertions"),
Predicate::ProcMacro => set.has_flag("proc_macro"),
Predicate::Feature(_feat) => false,
}))
}
}
#[cfg(test)]
mod test {
use super::{CfgEvaluator, RustcCfgEvaluator};
use crate::target::TargetDetector;
use color_eyre::eyre;
#[test]
fn matches_simple_true_for_target_arch() -> eyre::Result<()> {
let mut eval = RustcCfgEvaluator::default();
let host = crate::target::RustcTargetDetector::default().detect_target(&Vec::new())?;
let cfg_set = std::process::Command::new("rustc")
.args(["--print", "cfg"])
.output()?;
assert!(cfg_set.status.success());
let stdout = String::from_utf8_lossy(&cfg_set.stdout);
let arch = stdout
.lines()
.find_map(|l| {
l.strip_prefix("target_arch=\"")
.and_then(|r| r.strip_suffix("\""))
})
.ok_or_else(|| {
eyre::eyre!("expected rustc --print cfg output to contain target_arch")
})?;
let expr = format!("cfg(target_arch = \"{arch}\")");
assert!(eval.matches(&expr, &host)?);
Ok(())
}
#[test]
fn rejects_feature_predicate() -> eyre::Result<()> {
let mut eval = RustcCfgEvaluator::default();
let host = crate::target::RustcTargetDetector::default().detect_target(&Vec::new())?;
let err = match eval.matches(r#"cfg(feature = "foo")"#, &host) {
Ok(v) => eyre::bail!("expected cfg(feature=...) to be rejected, got {v}"),
Err(err) => err,
};
assert!(err.to_string().contains("not supported"));
Ok(())
}
}