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 "host-targets: skipping {} target(s) not buildable on this {} host: {}",
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]
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]
755 #[cfg(unix)]
756 fn detect_host_target_survives_deleted_cwd() {
757 let original = std::env::current_dir().unwrap();
758 let scratch = tempfile::tempdir().unwrap();
759 std::env::set_current_dir(scratch.path()).unwrap();
760 scratch.close().unwrap();
763
764 let result = detect_host_target();
765
766 std::env::set_current_dir(&original).unwrap();
768
769 let host = result.expect("host detection must succeed despite a deleted cwd");
770 assert!(host.contains('-'), "host triple should contain '-': {host}");
771 }
772
773 #[test]
778 #[serial]
779 fn test_resolve_with_os_default() {
780 unsafe {
783 std::env::remove_var("TARGET");
784 std::env::remove_var("ANODIZER_OS");
785 std::env::remove_var("ANODIZER_ARCH");
786 }
787
788 let config = None; let target = resolve_partial_target(&config).unwrap();
790
791 match target {
793 PartialTarget::OsArch { os, arch } => {
794 assert!(!os.is_empty());
795 assert!(arch.is_none()); }
797 other => panic!("expected OsArch, got: {other:?}"),
798 }
799 }
800
801 #[test]
802 #[serial]
803 fn test_resolve_with_by_target() {
804 unsafe {
806 std::env::remove_var("TARGET");
807 std::env::remove_var("ANODIZER_OS");
808 std::env::remove_var("ANODIZER_ARCH");
809 }
810
811 let config = Some(PartialConfig {
812 by: Some("target".to_string()),
813 });
814 let target = resolve_partial_target(&config).unwrap();
815
816 match target {
818 PartialTarget::Exact(t) => {
819 assert!(t.contains('-'), "should be full triple: {t}");
820 }
821 other => panic!("expected Exact, got: {other:?}"),
822 }
823 }
824
825 #[test]
826 #[serial]
827 fn test_resolve_invalid_by_value() {
828 unsafe {
830 std::env::remove_var("TARGET");
831 std::env::remove_var("ANODIZER_OS");
832 std::env::remove_var("ANODIZER_ARCH");
833 }
834
835 let config = Some(PartialConfig {
836 by: Some("invalid".to_string()),
837 });
838 let err = resolve_partial_target(&config).unwrap_err();
839 assert!(err.to_string().contains("unknown value"), "got: {}", err);
840 }
841
842 #[test]
843 #[serial]
844 fn test_resolve_by_os_works_and_legacy_goos_rejected() {
845 unsafe {
847 std::env::remove_var("TARGET");
848 std::env::remove_var("ANODIZER_OS");
849 std::env::remove_var("ANODIZER_ARCH");
850 }
851
852 let ok = resolve_partial_target(&Some(PartialConfig {
853 by: Some("os".to_string()),
854 }))
855 .unwrap();
856 assert!(matches!(ok, PartialTarget::OsArch { arch: None, .. }));
857
858 let err = resolve_partial_target(&Some(PartialConfig {
861 by: Some("goos".to_string()),
862 }))
863 .unwrap_err();
864 assert!(err.to_string().contains("unknown value"), "got: {}", err);
865 }
866
867 #[test]
872 fn test_suggest_runner() {
873 assert_eq!(suggest_runner("linux"), "ubuntu-latest");
874 assert_eq!(suggest_runner("darwin"), "macos-latest");
875 assert_eq!(suggest_runner("windows"), "windows-latest");
876 assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
877 }
878
879 #[test]
884 fn resolve_host_target_honours_target_env_override() {
885 let env = crate::MapEnvSource::new().with("TARGET", "x86_64-unknown-linux-musl");
886 let triple = resolve_host_target_with_env(&env).unwrap();
887 assert_eq!(triple, "x86_64-unknown-linux-musl");
888 }
889
890 #[test]
891 fn resolve_host_target_target_env_wins_over_ggoos() {
892 let env = crate::MapEnvSource::new()
893 .with("TARGET", "aarch64-apple-darwin")
894 .with("GGOOS", "linux")
895 .with("GGOARCH", "amd64");
896 let triple = resolve_host_target_with_env(&env).unwrap();
897 assert_eq!(triple, "aarch64-apple-darwin");
898 }
899
900 #[test]
901 #[serial]
902 fn resolve_host_target_blank_target_falls_through() {
903 let env = crate::MapEnvSource::new().with("TARGET", " ");
906 let triple = resolve_host_target_with_env(&env).unwrap();
907 assert!(triple.contains('-'), "fell back to rustc -vV: {triple}");
908 }
909
910 #[test]
911 fn ggoos_overrides_host_os_component() {
912 let synthesized = synthesize_triple_with_overrides(
915 "x86_64-unknown-linux-gnu",
916 Some("darwin"),
917 Some("arm64"),
918 );
919 assert_eq!(synthesized, "aarch64-apple-darwin");
920 }
921
922 #[test]
923 fn ggoos_alone_keeps_host_arch() {
924 let synthesized =
925 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", Some("windows"), None);
926 assert_eq!(synthesized, "x86_64-pc-windows-msvc");
927 }
928
929 #[test]
930 fn ggoarch_alone_keeps_host_os() {
931 let synthesized =
932 synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", None, Some("arm64"));
933 assert_eq!(synthesized, "aarch64-unknown-linux-gnu");
934 }
935
936 #[test]
941 fn find_runtime_matches_exact() {
942 let configured = vec![
943 "x86_64-unknown-linux-gnu".to_string(),
944 "aarch64-apple-darwin".to_string(),
945 ];
946 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
947 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
948 }
949
950 #[test]
951 fn find_runtime_matches_by_alias() {
952 let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
956 let m = find_runtime_target("x86_64-unknown-linux-musl", &configured);
957 assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
958 }
959
960 #[test]
961 fn find_runtime_returns_none_when_no_match() {
962 let configured = vec!["aarch64-apple-darwin".to_string()];
963 let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
964 assert!(m.is_none());
965 }
966
967 const LINUX_HOST: &str = "x86_64-unknown-linux-gnu";
972 const MAC_HOST: &str = "aarch64-apple-darwin";
973 const WINDOWS_HOST: &str = "x86_64-pc-windows-msvc";
974
975 fn mixed_targets() -> Vec<String> {
978 vec![
979 "x86_64-unknown-linux-gnu".to_string(),
980 "aarch64-unknown-linux-gnu".to_string(),
981 "x86_64-pc-windows-gnu".to_string(),
982 "x86_64-pc-windows-msvc".to_string(),
983 "x86_64-apple-darwin".to_string(),
984 "aarch64-apple-darwin".to_string(),
985 ]
986 }
987
988 #[test]
989 fn host_buildable_linux_keeps_cross_buildable_skips_apple_and_msvc() {
990 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &mixed_targets());
991 assert_eq!(
992 kept,
993 vec![
994 "x86_64-unknown-linux-gnu",
995 "aarch64-unknown-linux-gnu",
996 "x86_64-pc-windows-gnu",
997 ],
998 "linux + windows-gnu targets are cross-buildable from a linux host"
999 );
1000 assert_eq!(
1001 skipped,
1002 vec![
1003 "x86_64-pc-windows-msvc",
1004 "x86_64-apple-darwin",
1005 "aarch64-apple-darwin",
1006 ],
1007 "windows-msvc (needs Windows) and apple (needs macOS) are skipped on linux"
1008 );
1009 }
1010
1011 #[test]
1012 fn host_buildable_apple_host_keeps_apple_still_skips_msvc() {
1013 let (kept, skipped) = host_buildable_targets(MAC_HOST, &mixed_targets());
1016 assert_eq!(
1017 kept,
1018 vec![
1019 "x86_64-unknown-linux-gnu",
1020 "aarch64-unknown-linux-gnu",
1021 "x86_64-pc-windows-gnu",
1022 "x86_64-apple-darwin",
1023 "aarch64-apple-darwin",
1024 ],
1025 "apple host keeps apple + linux + windows-gnu: {kept:?}"
1026 );
1027 assert_eq!(
1028 skipped,
1029 vec!["x86_64-pc-windows-msvc"],
1030 "windows-msvc still needs a Windows host, even from macOS"
1031 );
1032 }
1033
1034 #[test]
1035 fn host_buildable_windows_host_keeps_msvc_skips_apple() {
1036 let (kept, skipped) = host_buildable_targets(WINDOWS_HOST, &mixed_targets());
1038 assert_eq!(
1039 kept,
1040 vec![
1041 "x86_64-unknown-linux-gnu",
1042 "aarch64-unknown-linux-gnu",
1043 "x86_64-pc-windows-gnu",
1044 "x86_64-pc-windows-msvc",
1045 ],
1046 "windows host keeps windows-msvc + linux + windows-gnu: {kept:?}"
1047 );
1048 assert_eq!(
1049 skipped,
1050 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"],
1051 "apple targets still need a macOS host, even from Windows"
1052 );
1053 }
1054
1055 #[test]
1056 fn host_buildable_linux_only_config_keeps_all() {
1057 let configured = vec![
1058 "x86_64-unknown-linux-gnu".to_string(),
1059 "x86_64-pc-windows-gnu".to_string(),
1060 ];
1061 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1062 assert_eq!(kept, configured);
1063 assert!(skipped.is_empty());
1064 }
1065
1066 #[test]
1067 fn host_buildable_linux_apple_only_config_skips_all() {
1068 let configured = vec![
1069 "x86_64-apple-darwin".to_string(),
1070 "aarch64-apple-darwin".to_string(),
1071 ];
1072 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1073 assert!(kept.is_empty(), "a linux host can build no apple targets");
1074 assert_eq!(skipped, configured);
1075 }
1076
1077 #[test]
1078 fn host_buildable_linux_msvc_only_config_skips_all() {
1079 let configured = vec!["x86_64-pc-windows-msvc".to_string()];
1080 let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
1081 assert!(
1082 kept.is_empty(),
1083 "a linux host can build no windows-msvc targets"
1084 );
1085 assert_eq!(skipped, configured);
1086 }
1087
1088 #[test]
1089 fn host_targets_skip_message_names_both_reasons_on_linux() {
1090 let skipped = vec![
1092 "aarch64-apple-darwin".to_string(),
1093 "x86_64-apple-darwin".to_string(),
1094 "x86_64-pc-windows-msvc".to_string(),
1095 ];
1096 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1097 assert!(msg.contains("3 target(s)"), "names the count: {msg}");
1098 assert!(msg.contains("linux host"), "names the host OS: {msg}");
1099 assert!(
1100 msg.contains("apple targets require a macOS host"),
1101 "names the apple reason: {msg}"
1102 );
1103 assert!(
1104 msg.contains("windows-msvc targets require a Windows host"),
1105 "names the msvc reason: {msg}"
1106 );
1107 assert!(msg.contains("aarch64-apple-darwin"), "lists triple: {msg}");
1108 assert!(msg.contains("x86_64-apple-darwin"), "lists triple: {msg}");
1109 assert!(
1110 msg.contains("x86_64-pc-windows-msvc"),
1111 "lists triple: {msg}"
1112 );
1113 assert_eq!(msg.lines().count(), 1, "stays a single line: {msg}");
1115 }
1116
1117 #[test]
1118 fn host_targets_skip_message_msvc_only_omits_apple_clause() {
1119 let skipped = vec!["x86_64-pc-windows-msvc".to_string()];
1121 let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
1122 assert!(
1123 msg.contains("windows-msvc targets require a Windows host"),
1124 "names the msvc reason: {msg}"
1125 );
1126 assert!(
1127 !msg.contains("macOS"),
1128 "msvc-only skip must not mention macOS: {msg}"
1129 );
1130 }
1131
1132 #[test]
1133 fn host_targets_skip_message_is_none_when_nothing_skipped() {
1134 assert!(host_targets_skip_message(LINUX_HOST, &[]).is_none());
1135 }
1136
1137 #[test]
1138 fn parse_rustc_version_from_output_parses_release_line() {
1139 let sample = "\
1140rustc 1.96.0 (ac68faa20 2026-05-25)\n\
1141binary: rustc\n\
1142commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\n\
1143commit-date: 2026-05-25\n\
1144host: x86_64-unknown-linux-gnu\n\
1145release: 1.96.0\n\
1146LLVM version: 22.1.2\n";
1147 assert_eq!(
1148 parse_rustc_version_from_output(sample),
1149 Some("1.96.0".to_string())
1150 );
1151 assert_eq!(
1153 parse_host_from_output(sample),
1154 Some("x86_64-unknown-linux-gnu".to_string())
1155 );
1156 }
1157
1158 #[test]
1159 fn parse_rustc_version_from_output_parses_prerelease_line() {
1160 let sample = "\
1161rustc 1.97.0-nightly (abc123 2026-06-01)\n\
1162release: 1.97.0-nightly\n\
1163host: aarch64-apple-darwin\n";
1164 assert_eq!(
1165 parse_rustc_version_from_output(sample),
1166 Some("1.97.0-nightly".to_string())
1167 );
1168 }
1169
1170 #[test]
1171 fn parse_rustc_version_from_output_returns_none_when_line_absent() {
1172 let sample = "binary: rustc\nhost: x86_64-unknown-linux-gnu\n";
1173 assert_eq!(parse_rustc_version_from_output(sample), None);
1174 }
1175
1176 #[test]
1177 #[serial]
1178 fn detect_rustc_version_live_returns_nonempty() {
1179 if let Some(ver) = detect_rustc_version() {
1181 assert!(!ver.is_empty(), "live rustc version should not be empty");
1182 assert!(
1183 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
1184 "live rustc version should start with a digit: {ver}"
1185 );
1186 }
1187 }
1188}