1use anyhow::{Context as _, Result};
7
8use crate::EnvSource;
9use crate::config::PartialConfig;
10use crate::target;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum PartialTarget {
19 Exact(String),
21 OsArch { os: String, arch: Option<String> },
23 Targets(Vec<String>),
29}
30
31impl PartialTarget {
32 pub fn filter_targets(&self, targets: &[String]) -> Vec<String> {
34 match self {
35 PartialTarget::Exact(t) => targets.iter().filter(|tt| *tt == t).cloned().collect(),
36 PartialTarget::OsArch { os, arch } => targets
37 .iter()
38 .filter(|tt| {
39 let (t_os, t_arch) = target::map_target(tt);
40 t_os == *os && arch.as_ref().is_none_or(|a| t_arch == *a)
41 })
42 .cloned()
43 .collect(),
44 PartialTarget::Targets(list) => targets
45 .iter()
46 .filter(|tt| list.iter().any(|wanted| wanted == *tt))
47 .cloned()
48 .collect(),
49 }
50 }
51
52 pub fn dist_subdir(&self) -> String {
58 match self {
59 PartialTarget::Exact(t) => t.clone(),
60 PartialTarget::OsArch { os, arch } => {
61 if let Some(a) = arch {
62 format!("{}_{}", os, a)
63 } else {
64 os.clone()
65 }
66 }
67 PartialTarget::Targets(list) => {
68 match list.first() {
73 Some(first) => format!("targets-{}", first),
74 None => "targets-empty".to_string(),
75 }
76 }
77 }
78 }
79}
80
81pub fn resolve_partial_target(config: &Option<PartialConfig>) -> Result<PartialTarget> {
93 resolve_partial_target_with_env(config, &crate::ProcessEnvSource)
94}
95
96pub fn resolve_partial_target_with_env<E: EnvSource + ?Sized>(
101 config: &Option<PartialConfig>,
102 env: &E,
103) -> Result<PartialTarget> {
104 if let Some(t) = env.var("TARGET")
106 && !t.is_empty()
107 {
108 return Ok(PartialTarget::Exact(t));
109 }
110
111 let os = env
114 .var("ANODIZER_OS")
115 .filter(|s| !s.is_empty())
116 .or_else(|| env.var("GGOOS").filter(|s| !s.is_empty()));
117 if let Some(os) = os {
118 let arch = env
119 .var("ANODIZER_ARCH")
120 .filter(|a| !a.is_empty())
121 .or_else(|| env.var("GGOARCH").filter(|a| !a.is_empty()));
122 return Ok(PartialTarget::OsArch { os, arch });
123 }
124
125 let host = detect_host_target()?;
127 let by = config
128 .as_ref()
129 .and_then(|c| c.by.as_deref())
130 .unwrap_or("goos");
131
132 match by {
133 "goos" => {
134 let (os, _) = target::map_target(&host);
135 Ok(PartialTarget::OsArch { os, arch: None })
136 }
137 "target" => Ok(PartialTarget::Exact(host)),
138 other => anyhow::bail!(
139 "partial.by: unknown value '{}' (expected 'goos' or 'target')",
140 other
141 ),
142 }
143}
144
145pub fn detect_host_target() -> Result<String> {
147 let mut cmd = std::process::Command::new("rustc");
148 cmd.args(["-vV"]);
149 tracing::debug!(args = ?cmd.get_args(), "spawning rustc for host target detection");
150 let output = cmd
151 .output()
152 .context("failed to run `rustc -vV` for host target detection")?;
153
154 if !output.status.success() {
155 anyhow::bail!(
156 "rustc -vV failed: {}",
157 String::from_utf8_lossy(&output.stderr)
158 );
159 }
160
161 let stdout = String::from_utf8_lossy(&output.stdout);
162 for line in stdout.lines() {
163 if let Some(host) = line.strip_prefix("host: ") {
164 return Ok(host.trim().to_string());
165 }
166 }
167 anyhow::bail!("could not detect host target from `rustc -vV` output")
168}
169
170pub fn suggest_runner(os: &str) -> &'static str {
172 match os {
173 "linux" => "ubuntu-latest",
174 "darwin" => "macos-latest",
175 "windows" => "windows-latest",
176 _ => "ubuntu-latest", }
178}
179
180#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::config::PartialConfig;
188 use serial_test::serial;
189
190 #[test]
195 fn test_exact_filter_matches_one() {
196 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
197 let targets = vec![
198 "x86_64-unknown-linux-gnu".to_string(),
199 "aarch64-unknown-linux-gnu".to_string(),
200 "x86_64-apple-darwin".to_string(),
201 ];
202 let filtered = target.filter_targets(&targets);
203 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
204 }
205
206 #[test]
207 fn test_exact_filter_no_match() {
208 let target = PartialTarget::Exact("riscv64gc-unknown-linux-gnu".to_string());
209 let targets = vec![
210 "x86_64-unknown-linux-gnu".to_string(),
211 "aarch64-apple-darwin".to_string(),
212 ];
213 let filtered = target.filter_targets(&targets);
214 assert!(filtered.is_empty());
215 }
216
217 #[test]
218 fn test_os_filter_matches_all_linux() {
219 let target = PartialTarget::OsArch {
220 os: "linux".to_string(),
221 arch: None,
222 };
223 let targets = vec![
224 "x86_64-unknown-linux-gnu".to_string(),
225 "aarch64-unknown-linux-gnu".to_string(),
226 "x86_64-apple-darwin".to_string(),
227 "x86_64-pc-windows-msvc".to_string(),
228 ];
229 let filtered = target.filter_targets(&targets);
230 assert_eq!(
231 filtered,
232 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",]
233 );
234 }
235
236 #[test]
237 fn test_os_arch_filter() {
238 let target = PartialTarget::OsArch {
239 os: "linux".to_string(),
240 arch: Some("arm64".to_string()),
241 };
242 let targets = vec![
243 "x86_64-unknown-linux-gnu".to_string(),
244 "aarch64-unknown-linux-gnu".to_string(),
245 ];
246 let filtered = target.filter_targets(&targets);
247 assert_eq!(filtered, vec!["aarch64-unknown-linux-gnu"]);
248 }
249
250 #[test]
251 fn test_os_filter_darwin() {
252 let target = PartialTarget::OsArch {
253 os: "darwin".to_string(),
254 arch: None,
255 };
256 let targets = vec![
257 "x86_64-apple-darwin".to_string(),
258 "aarch64-apple-darwin".to_string(),
259 "x86_64-unknown-linux-gnu".to_string(),
260 ];
261 let filtered = target.filter_targets(&targets);
262 assert_eq!(
263 filtered,
264 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"]
265 );
266 }
267
268 #[test]
269 fn test_os_filter_windows() {
270 let target = PartialTarget::OsArch {
271 os: "windows".to_string(),
272 arch: None,
273 };
274 let targets = vec![
275 "x86_64-pc-windows-msvc".to_string(),
276 "aarch64-pc-windows-msvc".to_string(),
277 "x86_64-unknown-linux-gnu".to_string(),
278 ];
279 let filtered = target.filter_targets(&targets);
280 assert_eq!(
281 filtered,
282 vec!["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]
283 );
284 }
285
286 #[test]
291 fn test_dist_subdir_exact() {
292 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
293 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
294 }
295
296 #[test]
297 fn test_dist_subdir_os_only() {
298 let target = PartialTarget::OsArch {
299 os: "linux".to_string(),
300 arch: None,
301 };
302 assert_eq!(target.dist_subdir(), "linux");
303 }
304
305 #[test]
306 fn test_dist_subdir_os_arch() {
307 let target = PartialTarget::OsArch {
308 os: "linux".to_string(),
309 arch: Some("amd64".to_string()),
310 };
311 assert_eq!(target.dist_subdir(), "linux_amd64");
312 }
313
314 #[test]
319 fn test_targets_filter_matches_intersection() {
320 let target = PartialTarget::Targets(vec![
321 "x86_64-unknown-linux-gnu".to_string(),
322 "aarch64-unknown-linux-gnu".to_string(),
323 ]);
324 let configured = vec![
325 "x86_64-unknown-linux-gnu".to_string(),
326 "aarch64-unknown-linux-gnu".to_string(),
327 "x86_64-apple-darwin".to_string(),
328 "aarch64-apple-darwin".to_string(),
329 ];
330 let filtered = target.filter_targets(&configured);
331 assert_eq!(
332 filtered,
333 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
334 );
335 }
336
337 #[test]
338 fn test_targets_filter_drops_non_configured_entries() {
339 let target = PartialTarget::Targets(vec![
342 "x86_64-unknown-linux-gnu".to_string(),
343 "x86_64-pc-windows-msvc".to_string(),
344 ]);
345 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
346 let filtered = target.filter_targets(&configured);
347 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
348 }
349
350 #[test]
351 fn test_targets_filter_empty_list_yields_empty() {
352 let target = PartialTarget::Targets(Vec::new());
353 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
354 assert!(target.filter_targets(&configured).is_empty());
355 }
356
357 #[test]
358 fn test_dist_subdir_targets_uses_first_triple() {
359 let target = PartialTarget::Targets(vec![
360 "x86_64-apple-darwin".to_string(),
361 "aarch64-apple-darwin".to_string(),
362 ]);
363 assert_eq!(target.dist_subdir(), "targets-x86_64-apple-darwin");
364 }
365
366 #[test]
367 fn test_dist_subdir_targets_empty_list_has_stable_name() {
368 let target = PartialTarget::Targets(Vec::new());
369 assert_eq!(target.dist_subdir(), "targets-empty");
370 }
371
372 #[test]
377 fn test_detect_host_target() {
378 let host = detect_host_target().unwrap();
381 assert!(!host.is_empty());
382 assert!(host.contains('-'), "host triple should contain '-': {host}");
384 }
385
386 #[test]
391 #[serial]
392 fn test_resolve_with_goos_default() {
393 unsafe {
396 std::env::remove_var("TARGET");
397 std::env::remove_var("ANODIZER_OS");
398 std::env::remove_var("ANODIZER_ARCH");
399 }
400
401 let config = None; let target = resolve_partial_target(&config).unwrap();
403
404 match target {
406 PartialTarget::OsArch { os, arch } => {
407 assert!(!os.is_empty());
408 assert!(arch.is_none()); }
410 other => panic!("expected OsArch, got: {other:?}"),
411 }
412 }
413
414 #[test]
415 #[serial]
416 fn test_resolve_with_by_target() {
417 unsafe {
419 std::env::remove_var("TARGET");
420 std::env::remove_var("ANODIZER_OS");
421 std::env::remove_var("ANODIZER_ARCH");
422 }
423
424 let config = Some(PartialConfig {
425 by: Some("target".to_string()),
426 });
427 let target = resolve_partial_target(&config).unwrap();
428
429 match target {
431 PartialTarget::Exact(t) => {
432 assert!(t.contains('-'), "should be full triple: {t}");
433 }
434 other => panic!("expected Exact, got: {other:?}"),
435 }
436 }
437
438 #[test]
439 #[serial]
440 fn test_resolve_invalid_by_value() {
441 unsafe {
443 std::env::remove_var("TARGET");
444 std::env::remove_var("ANODIZER_OS");
445 std::env::remove_var("ANODIZER_ARCH");
446 }
447
448 let config = Some(PartialConfig {
449 by: Some("invalid".to_string()),
450 });
451 let err = resolve_partial_target(&config).unwrap_err();
452 assert!(err.to_string().contains("unknown value"), "got: {}", err);
453 }
454
455 #[test]
460 fn test_suggest_runner() {
461 assert_eq!(suggest_runner("linux"), "ubuntu-latest");
462 assert_eq!(suggest_runner("darwin"), "macos-latest");
463 assert_eq!(suggest_runner("windows"), "windows-latest");
464 assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
465 }
466}