1use serde::{Deserialize, Serialize};
17
18#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "kind", rename_all = "kebab-case")]
27pub enum TargetPredicate {
28 CargoCfg { expr: String },
32 OsList { items: Vec<String> },
34 CpuList { items: Vec<String> },
36 EngineMin { engine: String, min: String },
39 PythonMarker { marker: String },
41 BundlerPlatforms { items: Vec<String> },
43}
44
45impl TargetPredicate {
46 #[must_use]
48 pub fn cargo_cfg(expr: impl Into<String>) -> Self {
49 Self::CargoCfg { expr: expr.into() }
50 }
51 #[must_use]
52 pub fn os_list(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
53 Self::OsList {
54 items: items.into_iter().map(Into::into).collect(),
55 }
56 }
57 #[must_use]
58 pub fn cpu_list(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
59 Self::CpuList {
60 items: items.into_iter().map(Into::into).collect(),
61 }
62 }
63 #[must_use]
64 pub fn bundler_platforms(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
65 Self::BundlerPlatforms {
66 items: items.into_iter().map(Into::into).collect(),
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct CompoundTargetPredicate {
77 pub combinator: PredicateCombinator,
78 pub atoms: Vec<TargetPredicate>,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "kebab-case")]
83pub enum PredicateCombinator {
84 All,
86 Any,
88 None,
90}
91
92impl CompoundTargetPredicate {
93 #[must_use]
94 pub fn matches(&self, target: &Target) -> bool {
95 match self.combinator {
96 PredicateCombinator::All => self.atoms.iter().all(|p| p.matches(target)),
97 PredicateCombinator::Any => self.atoms.iter().any(|p| p.matches(target)),
98 PredicateCombinator::None => !self.atoms.iter().any(|p| p.matches(target)),
99 }
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Target {
109 pub os: String, pub cpu: String, pub libc: Option<String>, pub engines: indexmap::IndexMap<String, String>,
113 pub python_env_markers: indexmap::IndexMap<String, String>,
114}
115
116impl Target {
117 #[must_use]
120 pub fn host() -> Self {
121 Self {
122 #[cfg(target_os = "linux")]
123 os: "linux".to_string(),
124 #[cfg(target_os = "macos")]
125 os: "macos".to_string(),
126 #[cfg(target_os = "windows")]
127 os: "windows".to_string(),
128 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
129 os: "unknown".to_string(),
130 #[cfg(target_arch = "x86_64")]
131 cpu: "x86_64".to_string(),
132 #[cfg(target_arch = "aarch64")]
133 cpu: "aarch64".to_string(),
134 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
135 cpu: "unknown".to_string(),
136 libc: None,
137 engines: indexmap::IndexMap::new(),
138 python_env_markers: indexmap::IndexMap::new(),
139 }
140 }
141}
142
143impl TargetPredicate {
144 #[must_use]
152 pub fn matches(&self, target: &Target) -> bool {
153 match self {
154 Self::CargoCfg { expr } => eval_cfg_expr(expr, target),
155 Self::OsList { items } => items.iter().any(|o| o == &target.os),
156 Self::CpuList { items } => items.iter().any(|c| c == &target.cpu),
157 Self::EngineMin { engine, min } => target
158 .engines
159 .get(engine)
160 .map(|v| v.as_str() >= min.as_str())
161 .unwrap_or(false),
162 Self::PythonMarker { .. } => true, Self::BundlerPlatforms { .. } => true, }
165 }
166}
167
168fn eval_cfg_expr(expr: &str, target: &Target) -> bool {
173 let expr = expr.trim();
174 let inner = expr
176 .strip_prefix("cfg(")
177 .and_then(|s| s.strip_suffix(')'))
178 .unwrap_or(expr)
179 .trim();
180 match inner {
181 "unix" => matches!(target.os.as_str(), "linux" | "macos" | "freebsd" | "netbsd" | "openbsd"),
182 "windows" => target.os == "windows",
183 "macos" => target.os == "macos",
184 "linux" => target.os == "linux",
185 s if s.starts_with("target_os") => {
186 s.split('"').nth(1).is_some_and(|os| os == target.os)
188 }
189 s if s.starts_with("target_arch") => s.split('"').nth(1).is_some_and(|cpu| cpu == target.cpu),
190 _ => true,
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn linux_x86() -> Target {
202 Target {
203 os: "linux".into(),
204 cpu: "x86_64".into(),
205 libc: Some("gnu".into()),
206 engines: indexmap::IndexMap::new(),
207 python_env_markers: indexmap::IndexMap::new(),
208 }
209 }
210
211 fn macos_arm() -> Target {
212 Target {
213 os: "macos".into(),
214 cpu: "aarch64".into(),
215 libc: None,
216 engines: indexmap::IndexMap::new(),
217 python_env_markers: indexmap::IndexMap::new(),
218 }
219 }
220
221 #[test]
222 fn cargo_cfg_unix_matches_linux_and_macos() {
223 let p = TargetPredicate::cargo_cfg("cfg(unix)");
224 assert!(p.matches(&linux_x86()));
225 assert!(p.matches(&macos_arm()));
226 }
227
228 #[test]
229 fn cargo_cfg_target_os_matches_correct_os() {
230 let p = TargetPredicate::cargo_cfg("cfg(target_os = \"linux\")");
231 assert!(p.matches(&linux_x86()));
232 assert!(!p.matches(&macos_arm()));
233 }
234
235 #[test]
236 fn os_list_matches_when_target_in_list() {
237 let p = TargetPredicate::os_list(["linux", "macos"]);
238 assert!(p.matches(&linux_x86()));
239 assert!(p.matches(&macos_arm()));
240 }
241
242 #[test]
243 fn cpu_list_matches_when_target_in_list() {
244 let p = TargetPredicate::cpu_list(["aarch64"]);
245 assert!(p.matches(&macos_arm()));
246 assert!(!p.matches(&linux_x86()));
247 }
248
249 #[test]
250 fn compound_all_requires_every_sub_predicate() {
251 let p = CompoundTargetPredicate {
252 combinator: PredicateCombinator::All,
253 atoms: vec![
254 TargetPredicate::os_list(["linux"]),
255 TargetPredicate::cpu_list(["x86_64"]),
256 ],
257 };
258 assert!(p.matches(&linux_x86()));
259 assert!(!p.matches(&macos_arm()));
260 }
261
262 #[test]
263 fn compound_any_requires_at_least_one_sub_predicate() {
264 let p = CompoundTargetPredicate {
265 combinator: PredicateCombinator::Any,
266 atoms: vec![
267 TargetPredicate::os_list(["linux"]),
268 TargetPredicate::os_list(["macos"]),
269 ],
270 };
271 assert!(p.matches(&linux_x86()));
272 assert!(p.matches(&macos_arm()));
273 }
274
275 #[test]
276 fn compound_none_inverts_all() {
277 let p = CompoundTargetPredicate {
278 combinator: PredicateCombinator::None,
279 atoms: vec![TargetPredicate::os_list(["windows"])],
280 };
281 assert!(p.matches(&linux_x86()));
282 }
283
284 #[test]
285 fn engine_min_compares_string_versions() {
286 let mut t = linux_x86();
287 t.engines.insert("node".into(), "20".into());
288 let p = TargetPredicate::EngineMin {
289 engine: "node".into(),
290 min: "18".into(),
291 };
292 assert!(p.matches(&t));
293 let p_too_high = TargetPredicate::EngineMin {
294 engine: "node".into(),
295 min: "21".into(),
296 };
297 assert!(!p_too_high.matches(&t));
298 }
299
300 #[test]
301 fn host_builder_returns_known_os() {
302 let h = Target::host();
303 assert!(matches!(h.os.as_str(), "linux" | "macos" | "windows" | "unknown"));
304 }
305}