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 {
91 match self {
92 PartialTarget::Exact(t) => t.clone(),
93 PartialTarget::OsArch { os, arch } => {
94 if let Some(a) = arch {
95 format!("{}_{}", os, a)
96 } else {
97 os.clone()
98 }
99 }
100 PartialTarget::Targets(list) => {
101 match list.first() {
106 Some(first) => format!("targets-{}", first),
107 None => "targets-empty".to_string(),
108 }
109 }
110 }
111 }
112
113 pub fn from_dist_subdir(subdir: &str) -> PartialTarget {
136 if let Some(rest) = subdir.strip_prefix("targets-") {
137 return if rest.is_empty() || rest == "empty" {
138 PartialTarget::Targets(Vec::new())
139 } else {
140 PartialTarget::Targets(vec![rest.to_string()])
141 };
142 }
143 if subdir.contains('-') {
146 return PartialTarget::Exact(subdir.to_string());
147 }
148 match subdir.split_once('_') {
149 Some((os, arch)) => PartialTarget::OsArch {
150 os: os.to_string(),
151 arch: Some(arch.to_string()),
152 },
153 None => PartialTarget::OsArch {
154 os: subdir.to_string(),
155 arch: None,
156 },
157 }
158 }
159}
160
161pub fn resolve_partial_target(config: &Option<PartialConfig>) -> Result<PartialTarget> {
173 resolve_partial_target_with_env(config, &crate::ProcessEnvSource)
174}
175
176pub fn resolve_partial_target_with_env<E: EnvSource + ?Sized>(
181 config: &Option<PartialConfig>,
182 env: &E,
183) -> Result<PartialTarget> {
184 if let Some(t) = env.var("TARGET")
186 && !t.is_empty()
187 {
188 return Ok(PartialTarget::Exact(t));
189 }
190
191 let os = env
194 .var("ANODIZER_OS")
195 .filter(|s| !s.is_empty())
196 .or_else(|| env.var("GGOOS").filter(|s| !s.is_empty()));
197 if let Some(os) = os {
198 let arch = env
199 .var("ANODIZER_ARCH")
200 .filter(|a| !a.is_empty())
201 .or_else(|| env.var("GGOARCH").filter(|a| !a.is_empty()));
202 return Ok(PartialTarget::OsArch { os, arch });
203 }
204
205 let host = detect_host_target()?;
207 let by = config
208 .as_ref()
209 .and_then(|c| c.by.as_deref())
210 .unwrap_or("os");
211
212 match by {
213 "os" => {
214 let (os, _) = target::map_target(&host);
215 Ok(PartialTarget::OsArch { os, arch: None })
216 }
217 "target" => Ok(PartialTarget::Exact(host)),
218 other => anyhow::bail!(
219 "partial.by: unknown value '{}' (expected 'os' or 'target')",
220 other
221 ),
222 }
223}
224
225fn run_rustc_vv() -> Result<String> {
232 let mut cmd = std::process::Command::new("rustc");
233 cmd.args(["-vV"]);
234 cmd.current_dir(crate::path_util::probe_dir());
238 tracing::debug!(args = ?cmd.get_args(), "spawning rustc -vV for host/version detection");
239 let output = cmd.output().context("failed to run `rustc -vV`")?;
240
241 if !output.status.success() {
242 anyhow::bail!(
243 "rustc -vV failed: {}",
244 String::from_utf8_lossy(&output.stderr)
245 );
246 }
247 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
248}
249
250pub(crate) fn parse_host_from_output(output: &str) -> Option<String> {
252 output
253 .lines()
254 .find_map(|line| line.strip_prefix("host: ").map(|h| h.trim().to_string()))
255}
256
257pub(crate) fn parse_rustc_version_from_output(output: &str) -> Option<String> {
259 output
260 .lines()
261 .find_map(|line| line.strip_prefix("release: ").map(|v| v.trim().to_string()))
262}
263
264pub fn detect_host_target() -> Result<String> {
266 let stdout = run_rustc_vv()?;
267 parse_host_from_output(&stdout).context("could not detect host target from `rustc -vV` output")
268}
269
270pub fn detect_rustc_version() -> Option<String> {
276 let stdout = run_rustc_vv().ok()?;
277 parse_rustc_version_from_output(&stdout)
278}
279
280pub fn resolve_host_target_with_env<E: EnvSource + ?Sized>(env: &E) -> Result<String> {
298 if let Some(t) = env.var("TARGET")
300 && !t.trim().is_empty()
301 {
302 return Ok(t);
303 }
304
305 let host = detect_host_target()?;
309 let ggoos = env.var("GGOOS").filter(|s| !s.trim().is_empty());
310 let ggoarch = env.var("GGOARCH").filter(|s| !s.trim().is_empty());
311 if ggoos.is_some() || ggoarch.is_some() {
312 return Ok(synthesize_triple_with_overrides(
313 &host,
314 ggoos.as_deref(),
315 ggoarch.as_deref(),
316 ));
317 }
318 Ok(host)
319}
320
321pub fn resolve_host_target() -> Result<String> {
323 resolve_host_target_with_env(&crate::ProcessEnvSource)
324}
325
326pub fn find_runtime_target(host: &str, configured: &[String]) -> Option<String> {
337 let (host_os, host_arch) = crate::target::map_target(host);
338 configured
339 .iter()
340 .find(|t| {
341 let (t_os, t_arch) = crate::target::map_target(t);
342 t_os == host_os && t_arch == host_arch
343 })
344 .cloned()
345}
346
347fn synthesize_triple_with_overrides(
356 host_triple: &str,
357 goos: Option<&str>,
358 goarch: Option<&str>,
359) -> String {
360 let arch_token = goarch.map(|a| match a {
362 "amd64" | "x86_64" => "x86_64",
363 "arm64" | "aarch64" => "aarch64",
364 "386" | "i686" => "i686",
365 other => other,
366 });
367 let os_token = goos.map(|o| match o {
368 "darwin" | "macos" => "apple-darwin",
369 "linux" => "unknown-linux-gnu",
370 "windows" => "pc-windows-msvc",
371 other => other,
372 });
373
374 let parts: Vec<&str> = host_triple.split('-').collect();
376 let original_arch = parts.first().copied().unwrap_or("");
377 let original_rest = if parts.len() > 1 {
378 parts[1..].join("-")
379 } else {
380 String::new()
381 };
382
383 let new_arch = arch_token.unwrap_or(original_arch);
384 let new_rest = os_token.map(str::to_string).unwrap_or(original_rest);
385
386 if new_rest.is_empty() {
387 new_arch.to_string()
388 } else {
389 format!("{}-{}", new_arch, new_rest)
390 }
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401enum HostConstraint {
402 NeedsAppleHost,
406 NeedsWindowsHost,
411}
412
413impl HostConstraint {
414 fn reason(self) -> &'static str {
417 match self {
418 HostConstraint::NeedsAppleHost => "apple targets require a macOS host",
419 HostConstraint::NeedsWindowsHost => "windows-msvc targets require a Windows host",
420 }
421 }
422}
423
424fn target_host_constraint(host: &str, triple: &str) -> Option<HostConstraint> {
431 if crate::target::is_darwin(triple) && !host_is_apple(host) {
432 Some(HostConstraint::NeedsAppleHost)
433 } else if crate::target::is_windows_msvc(triple) && !host_is_windows(host) {
434 Some(HostConstraint::NeedsWindowsHost)
435 } else {
436 None
437 }
438}
439
440pub fn host_is_apple(host: &str) -> bool {
443 crate::target::is_darwin(host)
444}
445
446pub fn host_is_windows(host: &str) -> bool {
449 crate::target::is_windows(host)
450}
451
452pub fn host_buildable_targets(host: &str, configured: &[String]) -> (Vec<String>, Vec<String>) {
487 let mut kept = Vec::new();
488 let mut skipped = Vec::new();
489 for t in configured {
490 if target_host_constraint(host, t).is_some() {
491 skipped.push(t.clone());
492 } else {
493 kept.push(t.clone());
494 }
495 }
496 (kept, skipped)
497}
498
499pub fn host_targets_skip_message(host: &str, skipped: &[String]) -> Option<String> {
509 if skipped.is_empty() {
510 return None;
511 }
512 let (host_os, _) = crate::target::map_target(host);
513 Some(format!(
514 "skipped {} target(s) — not buildable on this {} host (--host-targets): {}",
515 skipped.len(),
516 host_os,
517 host_targets_skip_reasons(host, skipped),
518 ))
519}
520
521pub fn host_targets_skip_reasons(host: &str, skipped: &[String]) -> String {
530 [
531 HostConstraint::NeedsAppleHost,
532 HostConstraint::NeedsWindowsHost,
533 ]
534 .into_iter()
535 .filter_map(|constraint| {
536 let triples: Vec<&str> = skipped
537 .iter()
538 .filter(|t| target_host_constraint(host, t) == Some(constraint))
539 .map(String::as_str)
540 .collect();
541 if triples.is_empty() {
542 None
543 } else {
544 Some(format!("{} ({})", triples.join(", "), constraint.reason()))
545 }
546 })
547 .collect::<Vec<_>>()
548 .join("; ")
549}
550
551pub fn suggest_runner(os: &str) -> &'static str {
553 match os {
554 "linux" => "ubuntu-latest",
555 "darwin" => "macos-latest",
556 "windows" => "windows-latest",
557 _ => "ubuntu-latest", }
559}
560
561#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::config::PartialConfig;
569 use serial_test::serial;
570
571 #[test]
576 fn test_exact_filter_matches_one() {
577 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
578 let targets = vec![
579 "x86_64-unknown-linux-gnu".to_string(),
580 "aarch64-unknown-linux-gnu".to_string(),
581 "x86_64-apple-darwin".to_string(),
582 ];
583 let filtered = target.filter_targets(&targets);
584 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
585 }
586
587 #[test]
588 fn test_exact_filter_no_match() {
589 let target = PartialTarget::Exact("riscv64gc-unknown-linux-gnu".to_string());
590 let targets = vec![
591 "x86_64-unknown-linux-gnu".to_string(),
592 "aarch64-apple-darwin".to_string(),
593 ];
594 let filtered = target.filter_targets(&targets);
595 assert!(filtered.is_empty());
596 }
597
598 #[test]
599 fn test_os_filter_matches_all_linux() {
600 let target = PartialTarget::OsArch {
601 os: "linux".to_string(),
602 arch: None,
603 };
604 let targets = vec![
605 "x86_64-unknown-linux-gnu".to_string(),
606 "aarch64-unknown-linux-gnu".to_string(),
607 "x86_64-apple-darwin".to_string(),
608 "x86_64-pc-windows-msvc".to_string(),
609 ];
610 let filtered = target.filter_targets(&targets);
611 assert_eq!(
612 filtered,
613 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",]
614 );
615 }
616
617 #[test]
618 fn test_os_arch_filter() {
619 let target = PartialTarget::OsArch {
620 os: "linux".to_string(),
621 arch: Some("arm64".to_string()),
622 };
623 let targets = vec![
624 "x86_64-unknown-linux-gnu".to_string(),
625 "aarch64-unknown-linux-gnu".to_string(),
626 ];
627 let filtered = target.filter_targets(&targets);
628 assert_eq!(filtered, vec!["aarch64-unknown-linux-gnu"]);
629 }
630
631 #[test]
632 fn test_os_filter_darwin() {
633 let target = PartialTarget::OsArch {
634 os: "darwin".to_string(),
635 arch: None,
636 };
637 let targets = vec![
638 "x86_64-apple-darwin".to_string(),
639 "aarch64-apple-darwin".to_string(),
640 "x86_64-unknown-linux-gnu".to_string(),
641 ];
642 let filtered = target.filter_targets(&targets);
643 assert_eq!(
644 filtered,
645 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"]
646 );
647 }
648
649 #[test]
650 fn test_os_filter_windows() {
651 let target = PartialTarget::OsArch {
652 os: "windows".to_string(),
653 arch: None,
654 };
655 let targets = vec![
656 "x86_64-pc-windows-msvc".to_string(),
657 "aarch64-pc-windows-msvc".to_string(),
658 "x86_64-unknown-linux-gnu".to_string(),
659 ];
660 let filtered = target.filter_targets(&targets);
661 assert_eq!(
662 filtered,
663 vec!["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]
664 );
665 }
666
667 #[test]
672 fn test_dist_subdir_exact() {
673 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
674 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
675 }
676
677 #[test]
678 fn test_dist_subdir_os_only() {
679 let target = PartialTarget::OsArch {
680 os: "linux".to_string(),
681 arch: None,
682 };
683 assert_eq!(target.dist_subdir(), "linux");
684 }
685
686 #[test]
687 fn test_dist_subdir_os_arch() {
688 let target = PartialTarget::OsArch {
689 os: "linux".to_string(),
690 arch: Some("amd64".to_string()),
691 };
692 assert_eq!(target.dist_subdir(), "linux_amd64");
693 }
694
695 #[test]
700 fn dist_subdir_os_only_matches_goreleaser_layout() {
701 let target = PartialTarget::OsArch {
702 os: "linux".to_string(),
703 arch: None,
704 };
705 assert_eq!(target.dist_subdir(), "linux");
706 }
707
708 #[test]
713 fn dist_subdir_exact_uses_full_rust_triple_not_goos_goarch() {
714 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
715 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
716 assert_ne!(target.dist_subdir(), "linux_amd64");
717 }
718
719 #[test]
724 fn test_targets_filter_matches_intersection() {
725 let target = PartialTarget::Targets(vec![
726 "x86_64-unknown-linux-gnu".to_string(),
727 "aarch64-unknown-linux-gnu".to_string(),
728 ]);
729 let configured = vec![
730 "x86_64-unknown-linux-gnu".to_string(),
731 "aarch64-unknown-linux-gnu".to_string(),
732 "x86_64-apple-darwin".to_string(),
733 "aarch64-apple-darwin".to_string(),
734 ];
735 let filtered = target.filter_targets(&configured);
736 assert_eq!(
737 filtered,
738 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
739 );
740 }
741
742 #[test]
743 fn test_targets_filter_drops_non_configured_entries() {
744 let target = PartialTarget::Targets(vec![
747 "x86_64-unknown-linux-gnu".to_string(),
748 "x86_64-pc-windows-msvc".to_string(),
749 ]);
750 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
751 let filtered = target.filter_targets(&configured);
752 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
753 }
754
755 #[test]
756 fn test_targets_filter_empty_list_yields_empty() {
757 let target = PartialTarget::Targets(Vec::new());
758 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
759 assert!(target.filter_targets(&configured).is_empty());
760 }
761
762 #[test]
763 fn test_dist_subdir_targets_uses_first_triple() {
764 let target = PartialTarget::Targets(vec![
765 "x86_64-apple-darwin".to_string(),
766 "aarch64-apple-darwin".to_string(),
767 ]);
768 assert_eq!(target.dist_subdir(), "targets-x86_64-apple-darwin");
769 }
770
771 #[test]
772 fn test_dist_subdir_targets_empty_list_has_stable_name() {
773 let target = PartialTarget::Targets(Vec::new());
774 assert_eq!(target.dist_subdir(), "targets-empty");
775 }
776
777 #[test]
782 #[serial(path_env)]
783 fn test_detect_host_target() {
784 let host = detect_host_target().unwrap();
787 assert!(!host.is_empty());
788 assert!(host.contains('-'), "host triple should contain '-': {host}");
790 }
791
792 #[test]
801 #[serial(cwd, path_env)]
802 #[cfg(unix)]
803 fn detect_host_target_survives_deleted_cwd() {
804 let scratch = tempfile::tempdir().unwrap();
805 let _cwd = crate::test_helpers::CwdGuard::new(scratch.path()).unwrap();
808 scratch.close().unwrap();
811
812 let result = detect_host_target();
813
814 let host = result.expect("host detection must succeed despite a deleted cwd");
815 assert!(host.contains('-'), "host triple should contain '-': {host}");
816 }
817
818 #[test]
823 fn test_resolve_with_os_default() {
824 let env = crate::MapEnvSource::new();
826
827 let config = None; let target = resolve_partial_target_with_env(&config, &env).unwrap();
829
830 match target {
832 PartialTarget::OsArch { os, arch } => {
833 assert!(!os.is_empty());
834 assert!(arch.is_none()); }
836 other => panic!("expected OsArch, got: {other:?}"),
837 }
838 }
839
840 #[test]
841 fn test_resolve_with_by_target() {
842 let env = crate::MapEnvSource::new();
843
844 let config = Some(PartialConfig {
845 by: Some("target".to_string()),
846 });
847 let target = resolve_partial_target_with_env(&config, &env).unwrap();
848
849 match target {
851 PartialTarget::Exact(t) => {
852 assert!(t.contains('-'), "should be full triple: {t}");
853 }
854 other => panic!("expected Exact, got: {other:?}"),
855 }
856 }
857
858 #[test]
859 fn test_resolve_invalid_by_value() {
860 let env = crate::MapEnvSource::new();
861
862 let config = Some(PartialConfig {
863 by: Some("invalid".to_string()),
864 });
865 let err = resolve_partial_target_with_env(&config, &env).unwrap_err();
866 assert!(err.to_string().contains("unknown value"), "got: {}", err);
867 }
868
869 #[test]
870 fn test_resolve_by_os_works_and_legacy_goos_rejected() {
871 let env = crate::MapEnvSource::new();
872
873 let ok = resolve_partial_target_with_env(
874 &Some(PartialConfig {
875 by: Some("os".to_string()),
876 }),
877 &env,
878 )
879 .unwrap();
880 assert!(matches!(ok, PartialTarget::OsArch { arch: None, .. }));
881
882 let err = resolve_partial_target_with_env(
885 &Some(PartialConfig {
886 by: Some("goos".to_string()),
887 }),
888 &env,
889 )
890 .unwrap_err();
891 assert!(err.to_string().contains("unknown value"), "got: {}", err);
892 }
893
894 #[test]
899 fn test_suggest_runner() {
900 assert_eq!(suggest_runner("linux"), "ubuntu-latest");
901 assert_eq!(suggest_runner("darwin"), "macos-latest");
902 assert_eq!(suggest_runner("windows"), "windows-latest");
903 assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
904 }
905
906 #[test]
911 fn resolve_host_target_honours_target_env_override() {
912 let env = crate::MapEnvSource::new().with("TARGET", "x86_64-unknown-linux-musl");
913 let triple = resolve_host_target_with_env(&env).unwrap();
914 assert_eq!(triple, "x86_64-unknown-linux-musl");
915 }
916
917 #[test]
918 fn resolve_host_target_target_env_wins_over_ggoos() {
919 let env = crate::MapEnvSource::new()
920 .with("TARGET", "aarch64-apple-darwin")
921 .with("GGOOS", "linux")
922 .with("GGOARCH", "amd64");
923 let triple = resolve_host_target_with_env(&env).unwrap();
924 assert_eq!(triple, "aarch64-apple-darwin");
925 }
926
927 #[test]
928 #[serial(path_env)]
929 fn resolve_host_target_blank_target_falls_through() {
930 let env = crate::MapEnvSource::new().with("TARGET", " ");
933 let triple = resolve_host_target_with_env(&env).unwrap();
934 assert!(triple.contains('-'), "fell back to rustc -vV: {triple}");
935 }
936
937 #[test]
938 fn ggoos_overrides_host_os_component() {
939 let synthesized = synthesize_triple_with_overrides(
942 "x86_64-unknown-linux-gnu",
943 Some("darwin"),
944 Some("arm64"),
945 );
946 assert_eq!(synthesized, "aarch64-apple-darwin");
947 }
948
949 #[test]
950 fn ggoos_alone_keeps_host_arch() {
951 let synthesized =
952 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", Some("windows"), None);
953 assert_eq!(synthesized, "x86_64-pc-windows-msvc");
954 }
955
956 #[test]
957 fn ggoarch_alone_keeps_host_os() {
958 let synthesized =
959 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", None, Some("arm64"));
960 assert_eq!(synthesized, "aarch64-unknown-linux-gnu");
961 }
962
963 #[test]
968 fn find_runtime_matches_exact() {
969 let configured = vec![
970 "x86_64-unknown-linux-gnu".to_string(),
971 "aarch64-apple-darwin".to_string(),
972 ];
973 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
974 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
975 }
976
977 #[test]
978 fn find_runtime_matches_by_alias() {
979 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
983 let m = find_runtime_target("x86_64-unknown-linux-musl", &configured);
984 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
985 }
986
987 #[test]
988 fn find_runtime_returns_none_when_no_match() {
989 let configured = vec!["aarch64-apple-darwin".to_string()];
990 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
991 assert!(m.is_none());
992 }
993
994 const LINUX_HOST: &str = "x86_64-unknown-linux-gnu";
999 const MAC_HOST: &str = "aarch64-apple-darwin";
1000 const WINDOWS_HOST: &str = "x86_64-pc-windows-msvc";
1001
1002 fn mixed_targets() -> Vec<String> {
1005 vec![
1006 "x86_64-unknown-linux-gnu".to_string(),
1007 "aarch64-unknown-linux-gnu".to_string(),
1008 "x86_64-pc-windows-gnu".to_string(),
1009 "x86_64-pc-windows-msvc".to_string(),
1010 "x86_64-apple-darwin".to_string(),
1011 "aarch64-apple-darwin".to_string(),
1012 ]
1013 }
1014
1015 #[test]
1016 fn host_buildable_linux_keeps_cross_buildable_skips_apple_and_msvc() {
1017 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &mixed_targets());
1018 assert_eq!(
1019 kept,
1020 vec![
1021 "x86_64-unknown-linux-gnu",
1022 "aarch64-unknown-linux-gnu",
1023 "x86_64-pc-windows-gnu",
1024 ],
1025 "linux + windows-gnu targets are cross-buildable from a linux host"
1026 );
1027 assert_eq!(
1028 skipped,
1029 vec![
1030 "x86_64-pc-windows-msvc",
1031 "x86_64-apple-darwin",
1032 "aarch64-apple-darwin",
1033 ],
1034 "windows-msvc (needs Windows) and apple (needs macOS) are skipped on linux"
1035 );
1036 }
1037
1038 #[test]
1039 fn host_buildable_apple_host_keeps_apple_still_skips_msvc() {
1040 let (kept, skipped) = host_buildable_targets(MAC_HOST, &mixed_targets());
1043 assert_eq!(
1044 kept,
1045 vec![
1046 "x86_64-unknown-linux-gnu",
1047 "aarch64-unknown-linux-gnu",
1048 "x86_64-pc-windows-gnu",
1049 "x86_64-apple-darwin",
1050 "aarch64-apple-darwin",
1051 ],
1052 "apple host keeps apple + linux + windows-gnu: {kept:?}"
1053 );
1054 assert_eq!(
1055 skipped,
1056 vec!["x86_64-pc-windows-msvc"],
1057 "windows-msvc still needs a Windows host, even from macOS"
1058 );
1059 }
1060
1061 #[test]
1062 fn host_buildable_windows_host_keeps_msvc_skips_apple() {
1063 let (kept, skipped) = host_buildable_targets(WINDOWS_HOST, &mixed_targets());
1065 assert_eq!(
1066 kept,
1067 vec![
1068 "x86_64-unknown-linux-gnu",
1069 "aarch64-unknown-linux-gnu",
1070 "x86_64-pc-windows-gnu",
1071 "x86_64-pc-windows-msvc",
1072 ],
1073 "windows host keeps windows-msvc + linux + windows-gnu: {kept:?}"
1074 );
1075 assert_eq!(
1076 skipped,
1077 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"],
1078 "apple targets still need a macOS host, even from Windows"
1079 );
1080 }
1081
1082 #[test]
1083 fn host_buildable_linux_only_config_keeps_all() {
1084 let configured = vec![
1085 "x86_64-unknown-linux-gnu".to_string(),
1086 "x86_64-pc-windows-gnu".to_string(),
1087 ];
1088 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1089 assert_eq!(kept, configured);
1090 assert!(skipped.is_empty());
1091 }
1092
1093 #[test]
1094 fn host_buildable_linux_apple_only_config_skips_all() {
1095 let configured = vec![
1096 "x86_64-apple-darwin".to_string(),
1097 "aarch64-apple-darwin".to_string(),
1098 ];
1099 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1100 assert!(kept.is_empty(), "a linux host can build no apple targets");
1101 assert_eq!(skipped, configured);
1102 }
1103
1104 #[test]
1105 fn host_buildable_linux_msvc_only_config_skips_all() {
1106 let configured = vec!["x86_64-pc-windows-msvc".to_string()];
1107 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1108 assert!(
1109 kept.is_empty(),
1110 "a linux host can build no windows-msvc targets"
1111 );
1112 assert_eq!(skipped, configured);
1113 }
1114
1115 #[test]
1116 fn host_targets_skip_message_names_both_reasons_on_linux() {
1117 let skipped = vec![
1119 "aarch64-apple-darwin".to_string(),
1120 "x86_64-apple-darwin".to_string(),
1121 "x86_64-pc-windows-msvc".to_string(),
1122 ];
1123 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1124 assert!(msg.contains("3 target(s)"), "names the count: {msg}");
1125 assert!(msg.contains("linux host"), "names the host OS: {msg}");
1126 assert!(
1127 msg.contains("apple targets require a macOS host"),
1128 "names the apple reason: {msg}"
1129 );
1130 assert!(
1131 msg.contains("windows-msvc targets require a Windows host"),
1132 "names the msvc reason: {msg}"
1133 );
1134 assert!(msg.contains("aarch64-apple-darwin"), "lists triple: {msg}");
1135 assert!(msg.contains("x86_64-apple-darwin"), "lists triple: {msg}");
1136 assert!(
1137 msg.contains("x86_64-pc-windows-msvc"),
1138 "lists triple: {msg}"
1139 );
1140 assert_eq!(msg.lines().count(), 1, "stays a single line: {msg}");
1142 }
1143
1144 #[test]
1145 fn host_targets_skip_message_msvc_only_omits_apple_clause() {
1146 let skipped = vec!["x86_64-pc-windows-msvc".to_string()];
1148 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1149 assert!(
1150 msg.contains("windows-msvc targets require a Windows host"),
1151 "names the msvc reason: {msg}"
1152 );
1153 assert!(
1154 !msg.contains("macOS"),
1155 "msvc-only skip must not mention macOS: {msg}"
1156 );
1157 }
1158
1159 #[test]
1160 fn host_targets_skip_message_is_none_when_nothing_skipped() {
1161 assert!(host_targets_skip_message(LINUX_HOST, &[]).is_none());
1162 }
1163
1164 #[test]
1165 fn parse_rustc_version_from_output_parses_release_line() {
1166 let sample = "\
1167rustc 1.96.0 (ac68faa20 2026-05-25)\n\
1168binary: rustc\n\
1169commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\n\
1170commit-date: 2026-05-25\n\
1171host: x86_64-unknown-linux-gnu\n\
1172release: 1.96.0\n\
1173LLVM version: 22.1.2\n";
1174 assert_eq!(
1175 parse_rustc_version_from_output(sample),
1176 Some("1.96.0".to_string())
1177 );
1178 assert_eq!(
1180 parse_host_from_output(sample),
1181 Some("x86_64-unknown-linux-gnu".to_string())
1182 );
1183 }
1184
1185 #[test]
1186 fn parse_rustc_version_from_output_parses_prerelease_line() {
1187 let sample = "\
1188rustc 1.97.0-nightly (abc123 2026-06-01)\n\
1189release: 1.97.0-nightly\n\
1190host: aarch64-apple-darwin\n";
1191 assert_eq!(
1192 parse_rustc_version_from_output(sample),
1193 Some("1.97.0-nightly".to_string())
1194 );
1195 }
1196
1197 #[test]
1198 fn parse_rustc_version_from_output_returns_none_when_line_absent() {
1199 let sample = "binary: rustc\nhost: x86_64-unknown-linux-gnu\n";
1200 assert_eq!(parse_rustc_version_from_output(sample), None);
1201 }
1202
1203 #[test]
1204 #[serial(path_env)]
1205 fn detect_rustc_version_live_returns_nonempty() {
1206 if let Some(ver) = detect_rustc_version() {
1208 assert!(!ver.is_empty(), "live rustc version should not be empty");
1209 assert!(
1210 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
1211 "live rustc version should start with a digit: {ver}"
1212 );
1213 }
1214 }
1215}