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