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
114pub fn resolve_partial_target(config: &Option<PartialConfig>) -> Result<PartialTarget> {
126 resolve_partial_target_with_env(config, &crate::ProcessEnvSource)
127}
128
129pub fn resolve_partial_target_with_env<E: EnvSource + ?Sized>(
134 config: &Option<PartialConfig>,
135 env: &E,
136) -> Result<PartialTarget> {
137 if let Some(t) = env.var("TARGET")
139 && !t.is_empty()
140 {
141 return Ok(PartialTarget::Exact(t));
142 }
143
144 let os = env
147 .var("ANODIZER_OS")
148 .filter(|s| !s.is_empty())
149 .or_else(|| env.var("GGOOS").filter(|s| !s.is_empty()));
150 if let Some(os) = os {
151 let arch = env
152 .var("ANODIZER_ARCH")
153 .filter(|a| !a.is_empty())
154 .or_else(|| env.var("GGOARCH").filter(|a| !a.is_empty()));
155 return Ok(PartialTarget::OsArch { os, arch });
156 }
157
158 let host = detect_host_target()?;
160 let by = config
161 .as_ref()
162 .and_then(|c| c.by.as_deref())
163 .unwrap_or("os");
164
165 match by {
166 "os" => {
167 let (os, _) = target::map_target(&host);
168 Ok(PartialTarget::OsArch { os, arch: None })
169 }
170 "target" => Ok(PartialTarget::Exact(host)),
171 other => anyhow::bail!(
172 "partial.by: unknown value '{}' (expected 'os' or 'target')",
173 other
174 ),
175 }
176}
177
178fn run_rustc_vv() -> Result<String> {
185 let mut cmd = std::process::Command::new("rustc");
186 cmd.args(["-vV"]);
187 cmd.current_dir(crate::path_util::probe_dir());
191 tracing::debug!(args = ?cmd.get_args(), "spawning rustc -vV for host/version detection");
192 let output = cmd.output().context("failed to run `rustc -vV`")?;
193
194 if !output.status.success() {
195 anyhow::bail!(
196 "rustc -vV failed: {}",
197 String::from_utf8_lossy(&output.stderr)
198 );
199 }
200 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
201}
202
203pub(crate) fn parse_host_from_output(output: &str) -> Option<String> {
205 output
206 .lines()
207 .find_map(|line| line.strip_prefix("host: ").map(|h| h.trim().to_string()))
208}
209
210pub(crate) fn parse_rustc_version_from_output(output: &str) -> Option<String> {
212 output
213 .lines()
214 .find_map(|line| line.strip_prefix("release: ").map(|v| v.trim().to_string()))
215}
216
217pub fn detect_host_target() -> Result<String> {
219 let stdout = run_rustc_vv()?;
220 parse_host_from_output(&stdout).context("could not detect host target from `rustc -vV` output")
221}
222
223pub fn detect_rustc_version() -> Option<String> {
229 let stdout = run_rustc_vv().ok()?;
230 parse_rustc_version_from_output(&stdout)
231}
232
233pub fn resolve_host_target_with_env<E: EnvSource + ?Sized>(env: &E) -> Result<String> {
251 if let Some(t) = env.var("TARGET")
253 && !t.trim().is_empty()
254 {
255 return Ok(t);
256 }
257
258 let host = detect_host_target()?;
262 let ggoos = env.var("GGOOS").filter(|s| !s.trim().is_empty());
263 let ggoarch = env.var("GGOARCH").filter(|s| !s.trim().is_empty());
264 if ggoos.is_some() || ggoarch.is_some() {
265 return Ok(synthesize_triple_with_overrides(
266 &host,
267 ggoos.as_deref(),
268 ggoarch.as_deref(),
269 ));
270 }
271 Ok(host)
272}
273
274pub fn resolve_host_target() -> Result<String> {
276 resolve_host_target_with_env(&crate::ProcessEnvSource)
277}
278
279pub fn find_runtime_target(host: &str, configured: &[String]) -> Option<String> {
290 let (host_os, host_arch) = crate::target::map_target(host);
291 configured
292 .iter()
293 .find(|t| {
294 let (t_os, t_arch) = crate::target::map_target(t);
295 t_os == host_os && t_arch == host_arch
296 })
297 .cloned()
298}
299
300fn synthesize_triple_with_overrides(
309 host_triple: &str,
310 goos: Option<&str>,
311 goarch: Option<&str>,
312) -> String {
313 let arch_token = goarch.map(|a| match a {
315 "amd64" | "x86_64" => "x86_64",
316 "arm64" | "aarch64" => "aarch64",
317 "386" | "i686" => "i686",
318 other => other,
319 });
320 let os_token = goos.map(|o| match o {
321 "darwin" | "macos" => "apple-darwin",
322 "linux" => "unknown-linux-gnu",
323 "windows" => "pc-windows-msvc",
324 other => other,
325 });
326
327 let parts: Vec<&str> = host_triple.split('-').collect();
329 let original_arch = parts.first().copied().unwrap_or("");
330 let original_rest = if parts.len() > 1 {
331 parts[1..].join("-")
332 } else {
333 String::new()
334 };
335
336 let new_arch = arch_token.unwrap_or(original_arch);
337 let new_rest = os_token.map(str::to_string).unwrap_or(original_rest);
338
339 if new_rest.is_empty() {
340 new_arch.to_string()
341 } else {
342 format!("{}-{}", new_arch, new_rest)
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354enum HostConstraint {
355 NeedsAppleHost,
359 NeedsWindowsHost,
364}
365
366impl HostConstraint {
367 fn reason(self) -> &'static str {
370 match self {
371 HostConstraint::NeedsAppleHost => "apple targets require a macOS host",
372 HostConstraint::NeedsWindowsHost => "windows-msvc targets require a Windows host",
373 }
374 }
375}
376
377fn target_host_constraint(host: &str, triple: &str) -> Option<HostConstraint> {
384 if crate::target::is_darwin(triple) && !host_is_apple(host) {
385 Some(HostConstraint::NeedsAppleHost)
386 } else if crate::target::is_windows_msvc(triple) && !host_is_windows(host) {
387 Some(HostConstraint::NeedsWindowsHost)
388 } else {
389 None
390 }
391}
392
393pub fn host_is_apple(host: &str) -> bool {
396 crate::target::is_darwin(host)
397}
398
399pub fn host_is_windows(host: &str) -> bool {
402 crate::target::is_windows(host)
403}
404
405pub fn host_buildable_targets(host: &str, configured: &[String]) -> (Vec<String>, Vec<String>) {
440 let mut kept = Vec::new();
441 let mut skipped = Vec::new();
442 for t in configured {
443 if target_host_constraint(host, t).is_some() {
444 skipped.push(t.clone());
445 } else {
446 kept.push(t.clone());
447 }
448 }
449 (kept, skipped)
450}
451
452pub fn host_targets_skip_message(host: &str, skipped: &[String]) -> Option<String> {
462 if skipped.is_empty() {
463 return None;
464 }
465 let (host_os, _) = crate::target::map_target(host);
466 Some(format!(
467 "skipped {} target(s) — not buildable on this {} host (--host-targets): {}",
468 skipped.len(),
469 host_os,
470 host_targets_skip_reasons(host, skipped),
471 ))
472}
473
474pub fn host_targets_skip_reasons(host: &str, skipped: &[String]) -> String {
483 [
484 HostConstraint::NeedsAppleHost,
485 HostConstraint::NeedsWindowsHost,
486 ]
487 .into_iter()
488 .filter_map(|constraint| {
489 let triples: Vec<&str> = skipped
490 .iter()
491 .filter(|t| target_host_constraint(host, t) == Some(constraint))
492 .map(String::as_str)
493 .collect();
494 if triples.is_empty() {
495 None
496 } else {
497 Some(format!("{} ({})", triples.join(", "), constraint.reason()))
498 }
499 })
500 .collect::<Vec<_>>()
501 .join("; ")
502}
503
504pub fn suggest_runner(os: &str) -> &'static str {
506 match os {
507 "linux" => "ubuntu-latest",
508 "darwin" => "macos-latest",
509 "windows" => "windows-latest",
510 _ => "ubuntu-latest", }
512}
513
514#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::config::PartialConfig;
522 use serial_test::serial;
523
524 #[test]
529 fn test_exact_filter_matches_one() {
530 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
531 let targets = vec![
532 "x86_64-unknown-linux-gnu".to_string(),
533 "aarch64-unknown-linux-gnu".to_string(),
534 "x86_64-apple-darwin".to_string(),
535 ];
536 let filtered = target.filter_targets(&targets);
537 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
538 }
539
540 #[test]
541 fn test_exact_filter_no_match() {
542 let target = PartialTarget::Exact("riscv64gc-unknown-linux-gnu".to_string());
543 let targets = vec![
544 "x86_64-unknown-linux-gnu".to_string(),
545 "aarch64-apple-darwin".to_string(),
546 ];
547 let filtered = target.filter_targets(&targets);
548 assert!(filtered.is_empty());
549 }
550
551 #[test]
552 fn test_os_filter_matches_all_linux() {
553 let target = PartialTarget::OsArch {
554 os: "linux".to_string(),
555 arch: None,
556 };
557 let targets = vec![
558 "x86_64-unknown-linux-gnu".to_string(),
559 "aarch64-unknown-linux-gnu".to_string(),
560 "x86_64-apple-darwin".to_string(),
561 "x86_64-pc-windows-msvc".to_string(),
562 ];
563 let filtered = target.filter_targets(&targets);
564 assert_eq!(
565 filtered,
566 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",]
567 );
568 }
569
570 #[test]
571 fn test_os_arch_filter() {
572 let target = PartialTarget::OsArch {
573 os: "linux".to_string(),
574 arch: Some("arm64".to_string()),
575 };
576 let targets = vec![
577 "x86_64-unknown-linux-gnu".to_string(),
578 "aarch64-unknown-linux-gnu".to_string(),
579 ];
580 let filtered = target.filter_targets(&targets);
581 assert_eq!(filtered, vec!["aarch64-unknown-linux-gnu"]);
582 }
583
584 #[test]
585 fn test_os_filter_darwin() {
586 let target = PartialTarget::OsArch {
587 os: "darwin".to_string(),
588 arch: None,
589 };
590 let targets = vec![
591 "x86_64-apple-darwin".to_string(),
592 "aarch64-apple-darwin".to_string(),
593 "x86_64-unknown-linux-gnu".to_string(),
594 ];
595 let filtered = target.filter_targets(&targets);
596 assert_eq!(
597 filtered,
598 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"]
599 );
600 }
601
602 #[test]
603 fn test_os_filter_windows() {
604 let target = PartialTarget::OsArch {
605 os: "windows".to_string(),
606 arch: None,
607 };
608 let targets = vec![
609 "x86_64-pc-windows-msvc".to_string(),
610 "aarch64-pc-windows-msvc".to_string(),
611 "x86_64-unknown-linux-gnu".to_string(),
612 ];
613 let filtered = target.filter_targets(&targets);
614 assert_eq!(
615 filtered,
616 vec!["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]
617 );
618 }
619
620 #[test]
625 fn test_dist_subdir_exact() {
626 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
627 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
628 }
629
630 #[test]
631 fn test_dist_subdir_os_only() {
632 let target = PartialTarget::OsArch {
633 os: "linux".to_string(),
634 arch: None,
635 };
636 assert_eq!(target.dist_subdir(), "linux");
637 }
638
639 #[test]
640 fn test_dist_subdir_os_arch() {
641 let target = PartialTarget::OsArch {
642 os: "linux".to_string(),
643 arch: Some("amd64".to_string()),
644 };
645 assert_eq!(target.dist_subdir(), "linux_amd64");
646 }
647
648 #[test]
653 fn dist_subdir_os_only_matches_goreleaser_layout() {
654 let target = PartialTarget::OsArch {
655 os: "linux".to_string(),
656 arch: None,
657 };
658 assert_eq!(target.dist_subdir(), "linux");
659 }
660
661 #[test]
666 fn dist_subdir_exact_uses_full_rust_triple_not_goos_goarch() {
667 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
668 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
669 assert_ne!(target.dist_subdir(), "linux_amd64");
670 }
671
672 #[test]
677 fn test_targets_filter_matches_intersection() {
678 let target = PartialTarget::Targets(vec![
679 "x86_64-unknown-linux-gnu".to_string(),
680 "aarch64-unknown-linux-gnu".to_string(),
681 ]);
682 let configured = vec![
683 "x86_64-unknown-linux-gnu".to_string(),
684 "aarch64-unknown-linux-gnu".to_string(),
685 "x86_64-apple-darwin".to_string(),
686 "aarch64-apple-darwin".to_string(),
687 ];
688 let filtered = target.filter_targets(&configured);
689 assert_eq!(
690 filtered,
691 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
692 );
693 }
694
695 #[test]
696 fn test_targets_filter_drops_non_configured_entries() {
697 let target = PartialTarget::Targets(vec![
700 "x86_64-unknown-linux-gnu".to_string(),
701 "x86_64-pc-windows-msvc".to_string(),
702 ]);
703 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
704 let filtered = target.filter_targets(&configured);
705 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
706 }
707
708 #[test]
709 fn test_targets_filter_empty_list_yields_empty() {
710 let target = PartialTarget::Targets(Vec::new());
711 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
712 assert!(target.filter_targets(&configured).is_empty());
713 }
714
715 #[test]
716 fn test_dist_subdir_targets_uses_first_triple() {
717 let target = PartialTarget::Targets(vec![
718 "x86_64-apple-darwin".to_string(),
719 "aarch64-apple-darwin".to_string(),
720 ]);
721 assert_eq!(target.dist_subdir(), "targets-x86_64-apple-darwin");
722 }
723
724 #[test]
725 fn test_dist_subdir_targets_empty_list_has_stable_name() {
726 let target = PartialTarget::Targets(Vec::new());
727 assert_eq!(target.dist_subdir(), "targets-empty");
728 }
729
730 #[test]
735 #[serial(path_env)]
736 fn test_detect_host_target() {
737 let host = detect_host_target().unwrap();
740 assert!(!host.is_empty());
741 assert!(host.contains('-'), "host triple should contain '-': {host}");
743 }
744
745 #[test]
754 #[serial(cwd, path_env)]
755 #[cfg(unix)]
756 fn detect_host_target_survives_deleted_cwd() {
757 let scratch = tempfile::tempdir().unwrap();
758 let _cwd = crate::test_helpers::CwdGuard::new(scratch.path()).unwrap();
761 scratch.close().unwrap();
764
765 let result = detect_host_target();
766
767 let host = result.expect("host detection must succeed despite a deleted cwd");
768 assert!(host.contains('-'), "host triple should contain '-': {host}");
769 }
770
771 #[test]
776 fn test_resolve_with_os_default() {
777 let env = crate::MapEnvSource::new();
779
780 let config = None; let target = resolve_partial_target_with_env(&config, &env).unwrap();
782
783 match target {
785 PartialTarget::OsArch { os, arch } => {
786 assert!(!os.is_empty());
787 assert!(arch.is_none()); }
789 other => panic!("expected OsArch, got: {other:?}"),
790 }
791 }
792
793 #[test]
794 fn test_resolve_with_by_target() {
795 let env = crate::MapEnvSource::new();
796
797 let config = Some(PartialConfig {
798 by: Some("target".to_string()),
799 });
800 let target = resolve_partial_target_with_env(&config, &env).unwrap();
801
802 match target {
804 PartialTarget::Exact(t) => {
805 assert!(t.contains('-'), "should be full triple: {t}");
806 }
807 other => panic!("expected Exact, got: {other:?}"),
808 }
809 }
810
811 #[test]
812 fn test_resolve_invalid_by_value() {
813 let env = crate::MapEnvSource::new();
814
815 let config = Some(PartialConfig {
816 by: Some("invalid".to_string()),
817 });
818 let err = resolve_partial_target_with_env(&config, &env).unwrap_err();
819 assert!(err.to_string().contains("unknown value"), "got: {}", err);
820 }
821
822 #[test]
823 fn test_resolve_by_os_works_and_legacy_goos_rejected() {
824 let env = crate::MapEnvSource::new();
825
826 let ok = resolve_partial_target_with_env(
827 &Some(PartialConfig {
828 by: Some("os".to_string()),
829 }),
830 &env,
831 )
832 .unwrap();
833 assert!(matches!(ok, PartialTarget::OsArch { arch: None, .. }));
834
835 let err = resolve_partial_target_with_env(
838 &Some(PartialConfig {
839 by: Some("goos".to_string()),
840 }),
841 &env,
842 )
843 .unwrap_err();
844 assert!(err.to_string().contains("unknown value"), "got: {}", err);
845 }
846
847 #[test]
852 fn test_suggest_runner() {
853 assert_eq!(suggest_runner("linux"), "ubuntu-latest");
854 assert_eq!(suggest_runner("darwin"), "macos-latest");
855 assert_eq!(suggest_runner("windows"), "windows-latest");
856 assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
857 }
858
859 #[test]
864 fn resolve_host_target_honours_target_env_override() {
865 let env = crate::MapEnvSource::new().with("TARGET", "x86_64-unknown-linux-musl");
866 let triple = resolve_host_target_with_env(&env).unwrap();
867 assert_eq!(triple, "x86_64-unknown-linux-musl");
868 }
869
870 #[test]
871 fn resolve_host_target_target_env_wins_over_ggoos() {
872 let env = crate::MapEnvSource::new()
873 .with("TARGET", "aarch64-apple-darwin")
874 .with("GGOOS", "linux")
875 .with("GGOARCH", "amd64");
876 let triple = resolve_host_target_with_env(&env).unwrap();
877 assert_eq!(triple, "aarch64-apple-darwin");
878 }
879
880 #[test]
881 #[serial(path_env)]
882 fn resolve_host_target_blank_target_falls_through() {
883 let env = crate::MapEnvSource::new().with("TARGET", " ");
886 let triple = resolve_host_target_with_env(&env).unwrap();
887 assert!(triple.contains('-'), "fell back to rustc -vV: {triple}");
888 }
889
890 #[test]
891 fn ggoos_overrides_host_os_component() {
892 let synthesized = synthesize_triple_with_overrides(
895 "x86_64-unknown-linux-gnu",
896 Some("darwin"),
897 Some("arm64"),
898 );
899 assert_eq!(synthesized, "aarch64-apple-darwin");
900 }
901
902 #[test]
903 fn ggoos_alone_keeps_host_arch() {
904 let synthesized =
905 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", Some("windows"), None);
906 assert_eq!(synthesized, "x86_64-pc-windows-msvc");
907 }
908
909 #[test]
910 fn ggoarch_alone_keeps_host_os() {
911 let synthesized =
912 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", None, Some("arm64"));
913 assert_eq!(synthesized, "aarch64-unknown-linux-gnu");
914 }
915
916 #[test]
921 fn find_runtime_matches_exact() {
922 let configured = vec![
923 "x86_64-unknown-linux-gnu".to_string(),
924 "aarch64-apple-darwin".to_string(),
925 ];
926 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
927 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
928 }
929
930 #[test]
931 fn find_runtime_matches_by_alias() {
932 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
936 let m = find_runtime_target("x86_64-unknown-linux-musl", &configured);
937 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
938 }
939
940 #[test]
941 fn find_runtime_returns_none_when_no_match() {
942 let configured = vec!["aarch64-apple-darwin".to_string()];
943 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
944 assert!(m.is_none());
945 }
946
947 const LINUX_HOST: &str = "x86_64-unknown-linux-gnu";
952 const MAC_HOST: &str = "aarch64-apple-darwin";
953 const WINDOWS_HOST: &str = "x86_64-pc-windows-msvc";
954
955 fn mixed_targets() -> Vec<String> {
958 vec![
959 "x86_64-unknown-linux-gnu".to_string(),
960 "aarch64-unknown-linux-gnu".to_string(),
961 "x86_64-pc-windows-gnu".to_string(),
962 "x86_64-pc-windows-msvc".to_string(),
963 "x86_64-apple-darwin".to_string(),
964 "aarch64-apple-darwin".to_string(),
965 ]
966 }
967
968 #[test]
969 fn host_buildable_linux_keeps_cross_buildable_skips_apple_and_msvc() {
970 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &mixed_targets());
971 assert_eq!(
972 kept,
973 vec![
974 "x86_64-unknown-linux-gnu",
975 "aarch64-unknown-linux-gnu",
976 "x86_64-pc-windows-gnu",
977 ],
978 "linux + windows-gnu targets are cross-buildable from a linux host"
979 );
980 assert_eq!(
981 skipped,
982 vec![
983 "x86_64-pc-windows-msvc",
984 "x86_64-apple-darwin",
985 "aarch64-apple-darwin",
986 ],
987 "windows-msvc (needs Windows) and apple (needs macOS) are skipped on linux"
988 );
989 }
990
991 #[test]
992 fn host_buildable_apple_host_keeps_apple_still_skips_msvc() {
993 let (kept, skipped) = host_buildable_targets(MAC_HOST, &mixed_targets());
996 assert_eq!(
997 kept,
998 vec![
999 "x86_64-unknown-linux-gnu",
1000 "aarch64-unknown-linux-gnu",
1001 "x86_64-pc-windows-gnu",
1002 "x86_64-apple-darwin",
1003 "aarch64-apple-darwin",
1004 ],
1005 "apple host keeps apple + linux + windows-gnu: {kept:?}"
1006 );
1007 assert_eq!(
1008 skipped,
1009 vec!["x86_64-pc-windows-msvc"],
1010 "windows-msvc still needs a Windows host, even from macOS"
1011 );
1012 }
1013
1014 #[test]
1015 fn host_buildable_windows_host_keeps_msvc_skips_apple() {
1016 let (kept, skipped) = host_buildable_targets(WINDOWS_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 "x86_64-pc-windows-msvc",
1025 ],
1026 "windows host keeps windows-msvc + linux + windows-gnu: {kept:?}"
1027 );
1028 assert_eq!(
1029 skipped,
1030 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"],
1031 "apple targets still need a macOS host, even from Windows"
1032 );
1033 }
1034
1035 #[test]
1036 fn host_buildable_linux_only_config_keeps_all() {
1037 let configured = vec![
1038 "x86_64-unknown-linux-gnu".to_string(),
1039 "x86_64-pc-windows-gnu".to_string(),
1040 ];
1041 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1042 assert_eq!(kept, configured);
1043 assert!(skipped.is_empty());
1044 }
1045
1046 #[test]
1047 fn host_buildable_linux_apple_only_config_skips_all() {
1048 let configured = vec![
1049 "x86_64-apple-darwin".to_string(),
1050 "aarch64-apple-darwin".to_string(),
1051 ];
1052 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1053 assert!(kept.is_empty(), "a linux host can build no apple targets");
1054 assert_eq!(skipped, configured);
1055 }
1056
1057 #[test]
1058 fn host_buildable_linux_msvc_only_config_skips_all() {
1059 let configured = vec!["x86_64-pc-windows-msvc".to_string()];
1060 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1061 assert!(
1062 kept.is_empty(),
1063 "a linux host can build no windows-msvc targets"
1064 );
1065 assert_eq!(skipped, configured);
1066 }
1067
1068 #[test]
1069 fn host_targets_skip_message_names_both_reasons_on_linux() {
1070 let skipped = vec![
1072 "aarch64-apple-darwin".to_string(),
1073 "x86_64-apple-darwin".to_string(),
1074 "x86_64-pc-windows-msvc".to_string(),
1075 ];
1076 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1077 assert!(msg.contains("3 target(s)"), "names the count: {msg}");
1078 assert!(msg.contains("linux host"), "names the host OS: {msg}");
1079 assert!(
1080 msg.contains("apple targets require a macOS host"),
1081 "names the apple reason: {msg}"
1082 );
1083 assert!(
1084 msg.contains("windows-msvc targets require a Windows host"),
1085 "names the msvc reason: {msg}"
1086 );
1087 assert!(msg.contains("aarch64-apple-darwin"), "lists triple: {msg}");
1088 assert!(msg.contains("x86_64-apple-darwin"), "lists triple: {msg}");
1089 assert!(
1090 msg.contains("x86_64-pc-windows-msvc"),
1091 "lists triple: {msg}"
1092 );
1093 assert_eq!(msg.lines().count(), 1, "stays a single line: {msg}");
1095 }
1096
1097 #[test]
1098 fn host_targets_skip_message_msvc_only_omits_apple_clause() {
1099 let skipped = vec!["x86_64-pc-windows-msvc".to_string()];
1101 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1102 assert!(
1103 msg.contains("windows-msvc targets require a Windows host"),
1104 "names the msvc reason: {msg}"
1105 );
1106 assert!(
1107 !msg.contains("macOS"),
1108 "msvc-only skip must not mention macOS: {msg}"
1109 );
1110 }
1111
1112 #[test]
1113 fn host_targets_skip_message_is_none_when_nothing_skipped() {
1114 assert!(host_targets_skip_message(LINUX_HOST, &[]).is_none());
1115 }
1116
1117 #[test]
1118 fn parse_rustc_version_from_output_parses_release_line() {
1119 let sample = "\
1120rustc 1.96.0 (ac68faa20 2026-05-25)\n\
1121binary: rustc\n\
1122commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\n\
1123commit-date: 2026-05-25\n\
1124host: x86_64-unknown-linux-gnu\n\
1125release: 1.96.0\n\
1126LLVM version: 22.1.2\n";
1127 assert_eq!(
1128 parse_rustc_version_from_output(sample),
1129 Some("1.96.0".to_string())
1130 );
1131 assert_eq!(
1133 parse_host_from_output(sample),
1134 Some("x86_64-unknown-linux-gnu".to_string())
1135 );
1136 }
1137
1138 #[test]
1139 fn parse_rustc_version_from_output_parses_prerelease_line() {
1140 let sample = "\
1141rustc 1.97.0-nightly (abc123 2026-06-01)\n\
1142release: 1.97.0-nightly\n\
1143host: aarch64-apple-darwin\n";
1144 assert_eq!(
1145 parse_rustc_version_from_output(sample),
1146 Some("1.97.0-nightly".to_string())
1147 );
1148 }
1149
1150 #[test]
1151 fn parse_rustc_version_from_output_returns_none_when_line_absent() {
1152 let sample = "binary: rustc\nhost: x86_64-unknown-linux-gnu\n";
1153 assert_eq!(parse_rustc_version_from_output(sample), None);
1154 }
1155
1156 #[test]
1157 #[serial(path_env)]
1158 fn detect_rustc_version_live_returns_nonempty() {
1159 if let Some(ver) = detect_rustc_version() {
1161 assert!(!ver.is_empty(), "live rustc version should not be empty");
1162 assert!(
1163 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
1164 "live rustc version should start with a digit: {ver}"
1165 );
1166 }
1167 }
1168}