use anyhow::{Context as _, Result};
use crate::EnvSource;
use crate::config::PartialConfig;
use crate::target;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartialTarget {
Exact(String),
OsArch { os: String, arch: Option<String> },
Targets(Vec<String>),
}
impl PartialTarget {
pub fn filter_targets(&self, targets: &[String]) -> Vec<String> {
match self {
PartialTarget::Exact(t) => targets.iter().filter(|tt| *tt == t).cloned().collect(),
PartialTarget::OsArch { os, arch } => targets
.iter()
.filter(|tt| {
let (t_os, t_arch) = target::map_target(tt);
t_os == *os && arch.as_ref().is_none_or(|a| t_arch == *a)
})
.cloned()
.collect(),
PartialTarget::Targets(list) => targets
.iter()
.filter(|tt| list.iter().any(|wanted| wanted == *tt))
.cloned()
.collect(),
}
}
pub fn dist_subdir(&self) -> String {
match self {
PartialTarget::Exact(t) => t.clone(),
PartialTarget::OsArch { os, arch } => {
if let Some(a) = arch {
format!("{}_{}", os, a)
} else {
os.clone()
}
}
PartialTarget::Targets(list) => {
match list.first() {
Some(first) => format!("targets-{}", first),
None => "targets-empty".to_string(),
}
}
}
}
}
pub fn resolve_partial_target(config: &Option<PartialConfig>) -> Result<PartialTarget> {
resolve_partial_target_with_env(config, &crate::ProcessEnvSource)
}
pub fn resolve_partial_target_with_env<E: EnvSource + ?Sized>(
config: &Option<PartialConfig>,
env: &E,
) -> Result<PartialTarget> {
if let Some(t) = env.var("TARGET")
&& !t.is_empty()
{
return Ok(PartialTarget::Exact(t));
}
let os = env
.var("ANODIZER_OS")
.filter(|s| !s.is_empty())
.or_else(|| env.var("GGOOS").filter(|s| !s.is_empty()));
if let Some(os) = os {
let arch = env
.var("ANODIZER_ARCH")
.filter(|a| !a.is_empty())
.or_else(|| env.var("GGOARCH").filter(|a| !a.is_empty()));
return Ok(PartialTarget::OsArch { os, arch });
}
let host = detect_host_target()?;
let by = config
.as_ref()
.and_then(|c| c.by.as_deref())
.unwrap_or("os");
match by {
"os" => {
let (os, _) = target::map_target(&host);
Ok(PartialTarget::OsArch { os, arch: None })
}
"target" => Ok(PartialTarget::Exact(host)),
other => anyhow::bail!(
"partial.by: unknown value '{}' (expected 'os' or 'target')",
other
),
}
}
fn run_rustc_vv() -> Result<String> {
let mut cmd = std::process::Command::new("rustc");
cmd.args(["-vV"]);
cmd.current_dir(crate::path_util::probe_dir());
tracing::debug!(args = ?cmd.get_args(), "spawning rustc -vV for host/version detection");
let output = cmd.output().context("failed to run `rustc -vV`")?;
if !output.status.success() {
anyhow::bail!(
"rustc -vV failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub(crate) fn parse_host_from_output(output: &str) -> Option<String> {
output
.lines()
.find_map(|line| line.strip_prefix("host: ").map(|h| h.trim().to_string()))
}
pub(crate) fn parse_rustc_version_from_output(output: &str) -> Option<String> {
output
.lines()
.find_map(|line| line.strip_prefix("release: ").map(|v| v.trim().to_string()))
}
pub fn detect_host_target() -> Result<String> {
let stdout = run_rustc_vv()?;
parse_host_from_output(&stdout).context("could not detect host target from `rustc -vV` output")
}
pub fn detect_rustc_version() -> Option<String> {
let stdout = run_rustc_vv().ok()?;
parse_rustc_version_from_output(&stdout)
}
pub fn resolve_host_target_with_env<E: EnvSource + ?Sized>(env: &E) -> Result<String> {
if let Some(t) = env.var("TARGET")
&& !t.trim().is_empty()
{
return Ok(t);
}
let host = detect_host_target()?;
let ggoos = env.var("GGOOS").filter(|s| !s.trim().is_empty());
let ggoarch = env.var("GGOARCH").filter(|s| !s.trim().is_empty());
if ggoos.is_some() || ggoarch.is_some() {
return Ok(synthesize_triple_with_overrides(
&host,
ggoos.as_deref(),
ggoarch.as_deref(),
));
}
Ok(host)
}
pub fn resolve_host_target() -> Result<String> {
resolve_host_target_with_env(&crate::ProcessEnvSource)
}
pub fn find_runtime_target(host: &str, configured: &[String]) -> Option<String> {
let (host_os, host_arch) = crate::target::map_target(host);
configured
.iter()
.find(|t| {
let (t_os, t_arch) = crate::target::map_target(t);
t_os == host_os && t_arch == host_arch
})
.cloned()
}
fn synthesize_triple_with_overrides(
host_triple: &str,
goos: Option<&str>,
goarch: Option<&str>,
) -> String {
let arch_token = goarch.map(|a| match a {
"amd64" | "x86_64" => "x86_64",
"arm64" | "aarch64" => "aarch64",
"386" | "i686" => "i686",
other => other,
});
let os_token = goos.map(|o| match o {
"darwin" | "macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
"windows" => "pc-windows-msvc",
other => other,
});
let parts: Vec<&str> = host_triple.split('-').collect();
let original_arch = parts.first().copied().unwrap_or("");
let original_rest = if parts.len() > 1 {
parts[1..].join("-")
} else {
String::new()
};
let new_arch = arch_token.unwrap_or(original_arch);
let new_rest = os_token.map(str::to_string).unwrap_or(original_rest);
if new_rest.is_empty() {
new_arch.to_string()
} else {
format!("{}-{}", new_arch, new_rest)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HostConstraint {
NeedsAppleHost,
NeedsWindowsHost,
}
impl HostConstraint {
fn reason(self) -> &'static str {
match self {
HostConstraint::NeedsAppleHost => "apple targets require a macOS host",
HostConstraint::NeedsWindowsHost => "windows-msvc targets require a Windows host",
}
}
}
fn target_host_constraint(host: &str, triple: &str) -> Option<HostConstraint> {
if crate::target::is_darwin(triple) && !host_is_apple(host) {
Some(HostConstraint::NeedsAppleHost)
} else if crate::target::is_windows_msvc(triple) && !host_is_windows(host) {
Some(HostConstraint::NeedsWindowsHost)
} else {
None
}
}
pub fn host_is_apple(host: &str) -> bool {
crate::target::is_darwin(host)
}
pub fn host_is_windows(host: &str) -> bool {
crate::target::is_windows(host)
}
pub fn host_buildable_targets(host: &str, configured: &[String]) -> (Vec<String>, Vec<String>) {
let mut kept = Vec::new();
let mut skipped = Vec::new();
for t in configured {
if target_host_constraint(host, t).is_some() {
skipped.push(t.clone());
} else {
kept.push(t.clone());
}
}
(kept, skipped)
}
pub fn host_targets_skip_message(host: &str, skipped: &[String]) -> Option<String> {
if skipped.is_empty() {
return None;
}
let (host_os, _) = crate::target::map_target(host);
Some(format!(
"host-targets: skipping {} target(s) not buildable on this {} host: {}",
skipped.len(),
host_os,
host_targets_skip_reasons(host, skipped),
))
}
pub fn host_targets_skip_reasons(host: &str, skipped: &[String]) -> String {
[
HostConstraint::NeedsAppleHost,
HostConstraint::NeedsWindowsHost,
]
.into_iter()
.filter_map(|constraint| {
let triples: Vec<&str> = skipped
.iter()
.filter(|t| target_host_constraint(host, t) == Some(constraint))
.map(String::as_str)
.collect();
if triples.is_empty() {
None
} else {
Some(format!("{} ({})", triples.join(", "), constraint.reason()))
}
})
.collect::<Vec<_>>()
.join("; ")
}
pub fn suggest_runner(os: &str) -> &'static str {
match os {
"linux" => "ubuntu-latest",
"darwin" => "macos-latest",
"windows" => "windows-latest",
_ => "ubuntu-latest", }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::PartialConfig;
use serial_test::serial;
#[test]
fn test_exact_filter_matches_one() {
let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
let targets = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
"x86_64-apple-darwin".to_string(),
];
let filtered = target.filter_targets(&targets);
assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
}
#[test]
fn test_exact_filter_no_match() {
let target = PartialTarget::Exact("riscv64gc-unknown-linux-gnu".to_string());
let targets = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-apple-darwin".to_string(),
];
let filtered = target.filter_targets(&targets);
assert!(filtered.is_empty());
}
#[test]
fn test_os_filter_matches_all_linux() {
let target = PartialTarget::OsArch {
os: "linux".to_string(),
arch: None,
};
let targets = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
"x86_64-apple-darwin".to_string(),
"x86_64-pc-windows-msvc".to_string(),
];
let filtered = target.filter_targets(&targets);
assert_eq!(
filtered,
vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",]
);
}
#[test]
fn test_os_arch_filter() {
let target = PartialTarget::OsArch {
os: "linux".to_string(),
arch: Some("arm64".to_string()),
};
let targets = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
];
let filtered = target.filter_targets(&targets);
assert_eq!(filtered, vec!["aarch64-unknown-linux-gnu"]);
}
#[test]
fn test_os_filter_darwin() {
let target = PartialTarget::OsArch {
os: "darwin".to_string(),
arch: None,
};
let targets = vec![
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
"x86_64-unknown-linux-gnu".to_string(),
];
let filtered = target.filter_targets(&targets);
assert_eq!(
filtered,
vec!["x86_64-apple-darwin", "aarch64-apple-darwin"]
);
}
#[test]
fn test_os_filter_windows() {
let target = PartialTarget::OsArch {
os: "windows".to_string(),
arch: None,
};
let targets = vec![
"x86_64-pc-windows-msvc".to_string(),
"aarch64-pc-windows-msvc".to_string(),
"x86_64-unknown-linux-gnu".to_string(),
];
let filtered = target.filter_targets(&targets);
assert_eq!(
filtered,
vec!["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]
);
}
#[test]
fn test_dist_subdir_exact() {
let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
}
#[test]
fn test_dist_subdir_os_only() {
let target = PartialTarget::OsArch {
os: "linux".to_string(),
arch: None,
};
assert_eq!(target.dist_subdir(), "linux");
}
#[test]
fn test_dist_subdir_os_arch() {
let target = PartialTarget::OsArch {
os: "linux".to_string(),
arch: Some("amd64".to_string()),
};
assert_eq!(target.dist_subdir(), "linux_amd64");
}
#[test]
fn dist_subdir_os_only_matches_goreleaser_layout() {
let target = PartialTarget::OsArch {
os: "linux".to_string(),
arch: None,
};
assert_eq!(target.dist_subdir(), "linux");
}
#[test]
fn dist_subdir_exact_uses_full_rust_triple_not_goos_goarch() {
let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
assert_ne!(target.dist_subdir(), "linux_amd64");
}
#[test]
fn test_targets_filter_matches_intersection() {
let target = PartialTarget::Targets(vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
]);
let configured = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
];
let filtered = target.filter_targets(&configured);
assert_eq!(
filtered,
vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
);
}
#[test]
fn test_targets_filter_drops_non_configured_entries() {
let target = PartialTarget::Targets(vec![
"x86_64-unknown-linux-gnu".to_string(),
"x86_64-pc-windows-msvc".to_string(),
]);
let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
let filtered = target.filter_targets(&configured);
assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
}
#[test]
fn test_targets_filter_empty_list_yields_empty() {
let target = PartialTarget::Targets(Vec::new());
let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
assert!(target.filter_targets(&configured).is_empty());
}
#[test]
fn test_dist_subdir_targets_uses_first_triple() {
let target = PartialTarget::Targets(vec![
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
]);
assert_eq!(target.dist_subdir(), "targets-x86_64-apple-darwin");
}
#[test]
fn test_dist_subdir_targets_empty_list_has_stable_name() {
let target = PartialTarget::Targets(Vec::new());
assert_eq!(target.dist_subdir(), "targets-empty");
}
#[test]
#[serial]
fn test_detect_host_target() {
let host = detect_host_target().unwrap();
assert!(!host.is_empty());
assert!(host.contains('-'), "host triple should contain '-': {host}");
}
#[test]
#[serial]
#[cfg(unix)]
fn detect_host_target_survives_deleted_cwd() {
let original = std::env::current_dir().unwrap();
let scratch = tempfile::tempdir().unwrap();
std::env::set_current_dir(scratch.path()).unwrap();
scratch.close().unwrap();
let result = detect_host_target();
std::env::set_current_dir(&original).unwrap();
let host = result.expect("host detection must succeed despite a deleted cwd");
assert!(host.contains('-'), "host triple should contain '-': {host}");
}
#[test]
#[serial]
fn test_resolve_with_os_default() {
unsafe {
std::env::remove_var("TARGET");
std::env::remove_var("ANODIZER_OS");
std::env::remove_var("ANODIZER_ARCH");
}
let config = None; let target = resolve_partial_target(&config).unwrap();
match target {
PartialTarget::OsArch { os, arch } => {
assert!(!os.is_empty());
assert!(arch.is_none()); }
other => panic!("expected OsArch, got: {other:?}"),
}
}
#[test]
#[serial]
fn test_resolve_with_by_target() {
unsafe {
std::env::remove_var("TARGET");
std::env::remove_var("ANODIZER_OS");
std::env::remove_var("ANODIZER_ARCH");
}
let config = Some(PartialConfig {
by: Some("target".to_string()),
});
let target = resolve_partial_target(&config).unwrap();
match target {
PartialTarget::Exact(t) => {
assert!(t.contains('-'), "should be full triple: {t}");
}
other => panic!("expected Exact, got: {other:?}"),
}
}
#[test]
#[serial]
fn test_resolve_invalid_by_value() {
unsafe {
std::env::remove_var("TARGET");
std::env::remove_var("ANODIZER_OS");
std::env::remove_var("ANODIZER_ARCH");
}
let config = Some(PartialConfig {
by: Some("invalid".to_string()),
});
let err = resolve_partial_target(&config).unwrap_err();
assert!(err.to_string().contains("unknown value"), "got: {}", err);
}
#[test]
#[serial]
fn test_resolve_by_os_works_and_legacy_goos_rejected() {
unsafe {
std::env::remove_var("TARGET");
std::env::remove_var("ANODIZER_OS");
std::env::remove_var("ANODIZER_ARCH");
}
let ok = resolve_partial_target(&Some(PartialConfig {
by: Some("os".to_string()),
}))
.unwrap();
assert!(matches!(ok, PartialTarget::OsArch { arch: None, .. }));
let err = resolve_partial_target(&Some(PartialConfig {
by: Some("goos".to_string()),
}))
.unwrap_err();
assert!(err.to_string().contains("unknown value"), "got: {}", err);
}
#[test]
fn test_suggest_runner() {
assert_eq!(suggest_runner("linux"), "ubuntu-latest");
assert_eq!(suggest_runner("darwin"), "macos-latest");
assert_eq!(suggest_runner("windows"), "windows-latest");
assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
}
#[test]
fn resolve_host_target_honours_target_env_override() {
let env = crate::MapEnvSource::new().with("TARGET", "x86_64-unknown-linux-musl");
let triple = resolve_host_target_with_env(&env).unwrap();
assert_eq!(triple, "x86_64-unknown-linux-musl");
}
#[test]
fn resolve_host_target_target_env_wins_over_ggoos() {
let env = crate::MapEnvSource::new()
.with("TARGET", "aarch64-apple-darwin")
.with("GGOOS", "linux")
.with("GGOARCH", "amd64");
let triple = resolve_host_target_with_env(&env).unwrap();
assert_eq!(triple, "aarch64-apple-darwin");
}
#[test]
#[serial]
fn resolve_host_target_blank_target_falls_through() {
let env = crate::MapEnvSource::new().with("TARGET", " ");
let triple = resolve_host_target_with_env(&env).unwrap();
assert!(triple.contains('-'), "fell back to rustc -vV: {triple}");
}
#[test]
fn ggoos_overrides_host_os_component() {
let synthesized = synthesize_triple_with_overrides(
"x86_64-unknown-linux-gnu",
Some("darwin"),
Some("arm64"),
);
assert_eq!(synthesized, "aarch64-apple-darwin");
}
#[test]
fn ggoos_alone_keeps_host_arch() {
let synthesized =
synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", Some("windows"), None);
assert_eq!(synthesized, "x86_64-pc-windows-msvc");
}
#[test]
fn ggoarch_alone_keeps_host_os() {
let synthesized =
synthesize_triple_with_overrides("x86_64-unknown-linux-gnu", None, Some("arm64"));
assert_eq!(synthesized, "aarch64-unknown-linux-gnu");
}
#[test]
fn find_runtime_matches_exact() {
let configured = vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-apple-darwin".to_string(),
];
let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
}
#[test]
fn find_runtime_matches_by_alias() {
let configured = vec!["x86_64-unknown-linux-gnu".to_string()];
let m = find_runtime_target("x86_64-unknown-linux-musl", &configured);
assert_eq!(m.as_deref(), Some("x86_64-unknown-linux-gnu"));
}
#[test]
fn find_runtime_returns_none_when_no_match() {
let configured = vec!["aarch64-apple-darwin".to_string()];
let m = find_runtime_target("x86_64-unknown-linux-gnu", &configured);
assert!(m.is_none());
}
const LINUX_HOST: &str = "x86_64-unknown-linux-gnu";
const MAC_HOST: &str = "aarch64-apple-darwin";
const WINDOWS_HOST: &str = "x86_64-pc-windows-msvc";
fn mixed_targets() -> Vec<String> {
vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
"x86_64-pc-windows-gnu".to_string(),
"x86_64-pc-windows-msvc".to_string(),
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
]
}
#[test]
fn host_buildable_linux_keeps_cross_buildable_skips_apple_and_msvc() {
let (kept, skipped) = host_buildable_targets(LINUX_HOST, &mixed_targets());
assert_eq!(
kept,
vec![
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-gnu",
],
"linux + windows-gnu targets are cross-buildable from a linux host"
);
assert_eq!(
skipped,
vec![
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
],
"windows-msvc (needs Windows) and apple (needs macOS) are skipped on linux"
);
}
#[test]
fn host_buildable_apple_host_keeps_apple_still_skips_msvc() {
let (kept, skipped) = host_buildable_targets(MAC_HOST, &mixed_targets());
assert_eq!(
kept,
vec![
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
],
"apple host keeps apple + linux + windows-gnu: {kept:?}"
);
assert_eq!(
skipped,
vec!["x86_64-pc-windows-msvc"],
"windows-msvc still needs a Windows host, even from macOS"
);
}
#[test]
fn host_buildable_windows_host_keeps_msvc_skips_apple() {
let (kept, skipped) = host_buildable_targets(WINDOWS_HOST, &mixed_targets());
assert_eq!(
kept,
vec![
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-gnu",
"x86_64-pc-windows-msvc",
],
"windows host keeps windows-msvc + linux + windows-gnu: {kept:?}"
);
assert_eq!(
skipped,
vec!["x86_64-apple-darwin", "aarch64-apple-darwin"],
"apple targets still need a macOS host, even from Windows"
);
}
#[test]
fn host_buildable_linux_only_config_keeps_all() {
let configured = vec![
"x86_64-unknown-linux-gnu".to_string(),
"x86_64-pc-windows-gnu".to_string(),
];
let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
assert_eq!(kept, configured);
assert!(skipped.is_empty());
}
#[test]
fn host_buildable_linux_apple_only_config_skips_all() {
let configured = vec![
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
];
let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
assert!(kept.is_empty(), "a linux host can build no apple targets");
assert_eq!(skipped, configured);
}
#[test]
fn host_buildable_linux_msvc_only_config_skips_all() {
let configured = vec!["x86_64-pc-windows-msvc".to_string()];
let (kept, skipped) = host_buildable_targets(LINUX_HOST, &configured);
assert!(
kept.is_empty(),
"a linux host can build no windows-msvc targets"
);
assert_eq!(skipped, configured);
}
#[test]
fn host_targets_skip_message_names_both_reasons_on_linux() {
let skipped = vec![
"aarch64-apple-darwin".to_string(),
"x86_64-apple-darwin".to_string(),
"x86_64-pc-windows-msvc".to_string(),
];
let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
assert!(msg.contains("3 target(s)"), "names the count: {msg}");
assert!(msg.contains("linux host"), "names the host OS: {msg}");
assert!(
msg.contains("apple targets require a macOS host"),
"names the apple reason: {msg}"
);
assert!(
msg.contains("windows-msvc targets require a Windows host"),
"names the msvc reason: {msg}"
);
assert!(msg.contains("aarch64-apple-darwin"), "lists triple: {msg}");
assert!(msg.contains("x86_64-apple-darwin"), "lists triple: {msg}");
assert!(
msg.contains("x86_64-pc-windows-msvc"),
"lists triple: {msg}"
);
assert_eq!(msg.lines().count(), 1, "stays a single line: {msg}");
}
#[test]
fn host_targets_skip_message_msvc_only_omits_apple_clause() {
let skipped = vec!["x86_64-pc-windows-msvc".to_string()];
let msg = host_targets_skip_message(LINUX_HOST, &skipped).unwrap();
assert!(
msg.contains("windows-msvc targets require a Windows host"),
"names the msvc reason: {msg}"
);
assert!(
!msg.contains("macOS"),
"msvc-only skip must not mention macOS: {msg}"
);
}
#[test]
fn host_targets_skip_message_is_none_when_nothing_skipped() {
assert!(host_targets_skip_message(LINUX_HOST, &[]).is_none());
}
#[test]
fn parse_rustc_version_from_output_parses_release_line() {
let sample = "\
rustc 1.96.0 (ac68faa20 2026-05-25)\n\
binary: rustc\n\
commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\n\
commit-date: 2026-05-25\n\
host: x86_64-unknown-linux-gnu\n\
release: 1.96.0\n\
LLVM version: 22.1.2\n";
assert_eq!(
parse_rustc_version_from_output(sample),
Some("1.96.0".to_string())
);
assert_eq!(
parse_host_from_output(sample),
Some("x86_64-unknown-linux-gnu".to_string())
);
}
#[test]
fn parse_rustc_version_from_output_parses_prerelease_line() {
let sample = "\
rustc 1.97.0-nightly (abc123 2026-06-01)\n\
release: 1.97.0-nightly\n\
host: aarch64-apple-darwin\n";
assert_eq!(
parse_rustc_version_from_output(sample),
Some("1.97.0-nightly".to_string())
);
}
#[test]
fn parse_rustc_version_from_output_returns_none_when_line_absent() {
let sample = "binary: rustc\nhost: x86_64-unknown-linux-gnu\n";
assert_eq!(parse_rustc_version_from_output(sample), None);
}
#[test]
#[serial]
fn detect_rustc_version_live_returns_nonempty() {
if let Some(ver) = detect_rustc_version() {
assert!(!ver.is_empty(), "live rustc version should not be empty");
assert!(
ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
"live rustc version should start with a digit: {ver}"
);
}
}
}