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!(
182 target.os.as_str(),
183 "linux" | "macos" | "freebsd" | "netbsd" | "openbsd"
184 ),
185 "windows" => target.os == "windows",
186 "macos" => target.os == "macos",
187 "linux" => target.os == "linux",
188 s if s.starts_with("target_os") => {
189 s.split('"').nth(1).is_some_and(|os| os == target.os)
191 }
192 s if s.starts_with("target_arch") => {
193 s.split('"').nth(1).is_some_and(|cpu| cpu == target.cpu)
194 }
195 _ => true,
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn linux_x86() -> Target {
207 Target {
208 os: "linux".into(),
209 cpu: "x86_64".into(),
210 libc: Some("gnu".into()),
211 engines: indexmap::IndexMap::new(),
212 python_env_markers: indexmap::IndexMap::new(),
213 }
214 }
215
216 fn macos_arm() -> Target {
217 Target {
218 os: "macos".into(),
219 cpu: "aarch64".into(),
220 libc: None,
221 engines: indexmap::IndexMap::new(),
222 python_env_markers: indexmap::IndexMap::new(),
223 }
224 }
225
226 #[test]
227 fn cargo_cfg_unix_matches_linux_and_macos() {
228 let p = TargetPredicate::cargo_cfg("cfg(unix)");
229 assert!(p.matches(&linux_x86()));
230 assert!(p.matches(&macos_arm()));
231 }
232
233 #[test]
234 fn cargo_cfg_target_os_matches_correct_os() {
235 let p = TargetPredicate::cargo_cfg("cfg(target_os = \"linux\")");
236 assert!(p.matches(&linux_x86()));
237 assert!(!p.matches(&macos_arm()));
238 }
239
240 #[test]
241 fn os_list_matches_when_target_in_list() {
242 let p = TargetPredicate::os_list(["linux", "macos"]);
243 assert!(p.matches(&linux_x86()));
244 assert!(p.matches(&macos_arm()));
245 }
246
247 #[test]
248 fn cpu_list_matches_when_target_in_list() {
249 let p = TargetPredicate::cpu_list(["aarch64"]);
250 assert!(p.matches(&macos_arm()));
251 assert!(!p.matches(&linux_x86()));
252 }
253
254 #[test]
255 fn compound_all_requires_every_sub_predicate() {
256 let p = CompoundTargetPredicate {
257 combinator: PredicateCombinator::All,
258 atoms: vec![
259 TargetPredicate::os_list(["linux"]),
260 TargetPredicate::cpu_list(["x86_64"]),
261 ],
262 };
263 assert!(p.matches(&linux_x86()));
264 assert!(!p.matches(&macos_arm()));
265 }
266
267 #[test]
268 fn compound_any_requires_at_least_one_sub_predicate() {
269 let p = CompoundTargetPredicate {
270 combinator: PredicateCombinator::Any,
271 atoms: vec![
272 TargetPredicate::os_list(["linux"]),
273 TargetPredicate::os_list(["macos"]),
274 ],
275 };
276 assert!(p.matches(&linux_x86()));
277 assert!(p.matches(&macos_arm()));
278 }
279
280 #[test]
281 fn compound_none_inverts_all() {
282 let p = CompoundTargetPredicate {
283 combinator: PredicateCombinator::None,
284 atoms: vec![TargetPredicate::os_list(["windows"])],
285 };
286 assert!(p.matches(&linux_x86()));
287 }
288
289 #[test]
290 fn engine_min_compares_string_versions() {
291 let mut t = linux_x86();
292 t.engines.insert("node".into(), "20".into());
293 let p = TargetPredicate::EngineMin {
294 engine: "node".into(),
295 min: "18".into(),
296 };
297 assert!(p.matches(&t));
298 let p_too_high = TargetPredicate::EngineMin {
299 engine: "node".into(),
300 min: "21".into(),
301 };
302 assert!(!p_too_high.matches(&t));
303 }
304
305 #[test]
306 fn host_builder_returns_known_os() {
307 let h = Target::host();
308 assert!(matches!(
309 h.os.as_str(),
310 "linux" | "macos" | "windows" | "unknown"
311 ));
312 }
313}