use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum TargetPredicate {
CargoCfg { expr: String },
OsList { items: Vec<String> },
CpuList { items: Vec<String> },
EngineMin { engine: String, min: String },
PythonMarker { marker: String },
BundlerPlatforms { items: Vec<String> },
}
impl TargetPredicate {
#[must_use]
pub fn cargo_cfg(expr: impl Into<String>) -> Self {
Self::CargoCfg { expr: expr.into() }
}
#[must_use]
pub fn os_list(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::OsList {
items: items.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn cpu_list(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::CpuList {
items: items.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn bundler_platforms(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::BundlerPlatforms {
items: items.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompoundTargetPredicate {
pub combinator: PredicateCombinator,
pub atoms: Vec<TargetPredicate>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PredicateCombinator {
All,
Any,
None,
}
impl CompoundTargetPredicate {
#[must_use]
pub fn matches(&self, target: &Target) -> bool {
match self.combinator {
PredicateCombinator::All => self.atoms.iter().all(|p| p.matches(target)),
PredicateCombinator::Any => self.atoms.iter().any(|p| p.matches(target)),
PredicateCombinator::None => !self.atoms.iter().any(|p| p.matches(target)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Target {
pub os: String, pub cpu: String, pub libc: Option<String>, pub engines: indexmap::IndexMap<String, String>,
pub python_env_markers: indexmap::IndexMap<String, String>,
}
impl Target {
#[must_use]
pub fn host() -> Self {
Self {
#[cfg(target_os = "linux")]
os: "linux".to_string(),
#[cfg(target_os = "macos")]
os: "macos".to_string(),
#[cfg(target_os = "windows")]
os: "windows".to_string(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
os: "unknown".to_string(),
#[cfg(target_arch = "x86_64")]
cpu: "x86_64".to_string(),
#[cfg(target_arch = "aarch64")]
cpu: "aarch64".to_string(),
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
cpu: "unknown".to_string(),
libc: None,
engines: indexmap::IndexMap::new(),
python_env_markers: indexmap::IndexMap::new(),
}
}
}
impl TargetPredicate {
#[must_use]
pub fn matches(&self, target: &Target) -> bool {
match self {
Self::CargoCfg { expr } => eval_cfg_expr(expr, target),
Self::OsList { items } => items.iter().any(|o| o == &target.os),
Self::CpuList { items } => items.iter().any(|c| c == &target.cpu),
Self::EngineMin { engine, min } => target
.engines
.get(engine)
.map(|v| v.as_str() >= min.as_str())
.unwrap_or(false),
Self::PythonMarker { .. } => true, Self::BundlerPlatforms { .. } => true, }
}
}
fn eval_cfg_expr(expr: &str, target: &Target) -> bool {
let expr = expr.trim();
let inner = expr
.strip_prefix("cfg(")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or(expr)
.trim();
match inner {
"unix" => matches!(target.os.as_str(), "linux" | "macos" | "freebsd" | "netbsd" | "openbsd"),
"windows" => target.os == "windows",
"macos" => target.os == "macos",
"linux" => target.os == "linux",
s if s.starts_with("target_os") => {
s.split('"').nth(1).is_some_and(|os| os == target.os)
}
s if s.starts_with("target_arch") => s.split('"').nth(1).is_some_and(|cpu| cpu == target.cpu),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linux_x86() -> Target {
Target {
os: "linux".into(),
cpu: "x86_64".into(),
libc: Some("gnu".into()),
engines: indexmap::IndexMap::new(),
python_env_markers: indexmap::IndexMap::new(),
}
}
fn macos_arm() -> Target {
Target {
os: "macos".into(),
cpu: "aarch64".into(),
libc: None,
engines: indexmap::IndexMap::new(),
python_env_markers: indexmap::IndexMap::new(),
}
}
#[test]
fn cargo_cfg_unix_matches_linux_and_macos() {
let p = TargetPredicate::cargo_cfg("cfg(unix)");
assert!(p.matches(&linux_x86()));
assert!(p.matches(&macos_arm()));
}
#[test]
fn cargo_cfg_target_os_matches_correct_os() {
let p = TargetPredicate::cargo_cfg("cfg(target_os = \"linux\")");
assert!(p.matches(&linux_x86()));
assert!(!p.matches(&macos_arm()));
}
#[test]
fn os_list_matches_when_target_in_list() {
let p = TargetPredicate::os_list(["linux", "macos"]);
assert!(p.matches(&linux_x86()));
assert!(p.matches(&macos_arm()));
}
#[test]
fn cpu_list_matches_when_target_in_list() {
let p = TargetPredicate::cpu_list(["aarch64"]);
assert!(p.matches(&macos_arm()));
assert!(!p.matches(&linux_x86()));
}
#[test]
fn compound_all_requires_every_sub_predicate() {
let p = CompoundTargetPredicate {
combinator: PredicateCombinator::All,
atoms: vec![
TargetPredicate::os_list(["linux"]),
TargetPredicate::cpu_list(["x86_64"]),
],
};
assert!(p.matches(&linux_x86()));
assert!(!p.matches(&macos_arm()));
}
#[test]
fn compound_any_requires_at_least_one_sub_predicate() {
let p = CompoundTargetPredicate {
combinator: PredicateCombinator::Any,
atoms: vec![
TargetPredicate::os_list(["linux"]),
TargetPredicate::os_list(["macos"]),
],
};
assert!(p.matches(&linux_x86()));
assert!(p.matches(&macos_arm()));
}
#[test]
fn compound_none_inverts_all() {
let p = CompoundTargetPredicate {
combinator: PredicateCombinator::None,
atoms: vec![TargetPredicate::os_list(["windows"])],
};
assert!(p.matches(&linux_x86()));
}
#[test]
fn engine_min_compares_string_versions() {
let mut t = linux_x86();
t.engines.insert("node".into(), "20".into());
let p = TargetPredicate::EngineMin {
engine: "node".into(),
min: "18".into(),
};
assert!(p.matches(&t));
let p_too_high = TargetPredicate::EngineMin {
engine: "node".into(),
min: "21".into(),
};
assert!(!p_too_high.matches(&t));
}
#[test]
fn host_builder_returns_known_os() {
let h = Target::host();
assert!(matches!(h.os.as_str(), "linux" | "macos" | "windows" | "unknown"));
}
}