use crate::buildcap::{InstallCommand, Shell};
use crate::platform::{arch_of, host_native_crossable};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CrossTool {
CargoNative,
CargoZigbuild,
MuslCross,
}
impl CrossTool {
pub fn cargo_subcommand(&self) -> &'static str {
match self {
CrossTool::CargoZigbuild => "zigbuild",
CrossTool::CargoNative | CrossTool::MuslCross => "build",
}
}
pub fn env_for(&self, target: &str) -> Vec<(String, String)> {
match self {
CrossTool::MuslCross => {
let prefix = musl_cc_prefix(target);
let key = cargo_target_env_key(target);
vec![
(
format!("CARGO_TARGET_{key}_LINKER"),
format!("{prefix}-gcc"),
),
(
format!("CC_{}", target.replace('-', "_")),
format!("{prefix}-gcc"),
),
(
format!("AR_{}", target.replace('-', "_")),
format!("{prefix}-ar"),
),
]
}
CrossTool::CargoNative | CrossTool::CargoZigbuild => Vec::new(),
}
}
pub fn probe_argv(&self) -> Vec<String> {
match self {
CrossTool::CargoZigbuild => {
vec!["cargo-zigbuild".into(), "--version".into()]
}
CrossTool::CargoNative => vec!["cargo".into(), "--version".into()],
CrossTool::MuslCross => {
vec!["x86_64-linux-musl-gcc".into(), "--version".into()]
}
}
}
pub fn install_hint(&self, target: &str) -> String {
let cmds = self
.install_commands(target, Shell::Posix)
.into_iter()
.map(|c| format!("`{}`", c.line))
.collect::<Vec<_>>()
.join(" and ");
match self {
CrossTool::CargoZigbuild => {
format!("install cargo-zigbuild + zig: {cmds} (or download zig from ziglang.org)")
}
CrossTool::MuslCross => format!(
"install a musl cross toolchain: {cmds} (macOS) or your platform's \
musl-cross package — or install cargo-zigbuild, which needs no \
per-target toolchain"
),
CrossTool::CargoNative => format!("add the rustup target: {cmds}"),
}
}
pub fn install_commands(&self, target: &str, shell: Shell) -> Vec<InstallCommand> {
match self {
CrossTool::CargoZigbuild => vec![
InstallCommand::new(shell, "cargo install cargo-zigbuild", false),
InstallCommand::new(shell, "brew install zig", false)
.with_note("or install zig from ziglang.org and put it on PATH"),
],
CrossTool::MuslCross => vec![InstallCommand::new(
shell,
"brew install FiloSottile/musl-cross/musl-cross",
false,
)
.with_note(format!(
"must end up providing `{}-gcc` on PATH; cargo-zigbuild covers every \
target with no per-target install",
musl_cc_prefix(target)
))],
CrossTool::CargoNative => vec![InstallCommand::new(
shell,
format!("rustup target add {target}"),
false,
)],
}
}
pub fn label(&self) -> &'static str {
match self {
CrossTool::CargoNative => "native (cargo build)",
CrossTool::CargoZigbuild => "cargo-zigbuild",
CrossTool::MuslCross => "musl-cross",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ToolAvailability {
pub zigbuild: bool,
pub musl_cross: bool,
}
impl ToolAvailability {
pub const FULL: ToolAvailability = ToolAvailability {
zigbuild: true,
musl_cross: true,
};
pub const NONE: ToolAvailability = ToolAvailability {
zigbuild: false,
musl_cross: false,
};
pub fn probe() -> ToolAvailability {
ToolAvailability {
zigbuild: probe_ok(&CrossTool::CargoZigbuild.probe_argv()),
musl_cross: probe_ok(&CrossTool::MuslCross.probe_argv()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeCrossPlan {
pub tool: CrossTool,
pub argv: Vec<String>,
pub env: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"no host-native cross toolchain for `{target}` on host `{host}`: {} is not installed — {}",
.preferred.label(),
.preferred.install_hint(.target)
)]
pub struct CrossToolUnavailable {
pub host: String,
pub target: String,
pub preferred: CrossTool,
}
impl CrossToolUnavailable {
pub fn install_hint(&self) -> String {
self.preferred.install_hint(&self.target)
}
}
pub fn select_cross_tool(
host: &str,
target: Option<&str>,
avail: &ToolAvailability,
) -> Result<CrossTool, CrossToolUnavailable> {
let target = match target.map(str::trim).filter(|t| !t.is_empty()) {
None => return Ok(CrossTool::CargoNative),
Some(t) => t,
};
let same_platform =
arch_of(target) == arch_of(host) && crate::platform::os_tag_of(target) == crate::platform::os_tag_of(host);
if same_platform || needs_no_foreign_linker(host, target) {
return Ok(CrossTool::CargoNative);
}
if avail.zigbuild {
return Ok(CrossTool::CargoZigbuild);
}
if is_musl_target(target) && avail.musl_cross {
return Ok(CrossTool::MuslCross);
}
Err(CrossToolUnavailable {
host: host.to_string(),
target: target.to_string(),
preferred: CrossTool::CargoZigbuild,
})
}
pub fn plan_native_cross(
original_argv: &[String],
host: &str,
target: &str,
avail: &ToolAvailability,
) -> Result<NativeCrossPlan, CrossToolUnavailable> {
let tool = select_cross_tool(host, Some(target), avail)?;
let argv = rewrite_build_argv(original_argv, &tool, target);
let env = tool.env_for(target);
Ok(NativeCrossPlan { tool, argv, env })
}
pub fn rewrite_build_argv(argv: &[String], tool: &CrossTool, target: &str) -> Vec<String> {
let prog = argv.first().map(String::as_str);
let sub = argv.get(1).map(String::as_str);
let head_is_build = matches!(prog, Some("cargo") | Some("cross"))
&& matches!(sub, Some("build") | Some("zigbuild"));
if !head_is_build {
return argv.to_vec();
}
let mut out: Vec<String> = Vec::with_capacity(argv.len() + 2);
out.push("cargo".to_string());
out.push(tool.cargo_subcommand().to_string());
out.extend(argv[2..].iter().cloned());
ensure_target_flag(&mut out, target);
out
}
fn ensure_target_flag(argv: &mut Vec<String>, target: &str) {
let has_target = argv
.iter()
.any(|a| a == "--target" || a.starts_with("--target="));
if !has_target {
argv.push("--target".to_string());
argv.push(target.to_string());
}
}
fn needs_no_foreign_linker(host: &str, target: &str) -> bool {
target_is_darwin(target) && target_is_darwin(host)
}
fn target_is_darwin(triple: &str) -> bool {
triple.contains("darwin") || triple.contains("apple")
}
fn is_musl_target(target: &str) -> bool {
target.contains("musl")
}
fn musl_cc_prefix(target: &str) -> String {
format!("{}-linux-musl", arch_of(target))
}
fn cargo_target_env_key(target: &str) -> String {
target.to_ascii_uppercase().replace('-', "_")
}
pub(crate) fn probe_ok<S: AsRef<std::ffi::OsStr>>(argv: &[S]) -> bool {
let Some((prog, args)) = argv.split_first() else {
return false;
};
std::process::Command::new(prog)
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn is_native_cross_target(host: &str, target: &str) -> bool {
let foreign = arch_of(target) != arch_of(host)
|| crate::platform::os_tag_of(target) != crate::platform::os_tag_of(host);
foreign && host_native_crossable(host, target)
}
#[cfg(test)]
mod tests {
use super::*;
const ARM_MAC: &str = "aarch64-apple-darwin";
const X64_LINUX: &str = "x86_64-unknown-linux-gnu";
const X64_MUSL: &str = "x86_64-unknown-linux-musl";
#[test]
fn no_target_is_native() {
assert_eq!(
select_cross_tool(ARM_MAC, None, &ToolAvailability::FULL),
Ok(CrossTool::CargoNative)
);
assert_eq!(
select_cross_tool(ARM_MAC, Some(" "), &ToolAvailability::NONE),
Ok(CrossTool::CargoNative)
);
}
#[test]
fn same_arch_foreign_os_is_not_native() {
let err =
select_cross_tool(ARM_MAC, Some("aarch64-unknown-linux-musl"), &ToolAvailability::NONE)
.unwrap_err();
assert_eq!(err.preferred, CrossTool::CargoZigbuild);
assert_eq!(
select_cross_tool(ARM_MAC, Some("aarch64-unknown-linux-musl"), &ToolAvailability::FULL),
Ok(CrossTool::CargoZigbuild)
);
}
#[test]
fn host_arch_target_is_native_when_os_also_matches() {
assert_eq!(
select_cross_tool(ARM_MAC, Some(ARM_MAC), &ToolAvailability::NONE),
Ok(CrossTool::CargoNative)
);
}
#[test]
fn darwin_cross_off_darwin_host_is_native() {
assert_eq!(
select_cross_tool(
ARM_MAC,
Some("x86_64-apple-darwin"),
&ToolAvailability::NONE
),
Ok(CrossTool::CargoNative)
);
}
#[test]
fn foreign_linux_prefers_zigbuild() {
assert_eq!(
select_cross_tool(ARM_MAC, Some(X64_MUSL), &ToolAvailability::FULL),
Ok(CrossTool::CargoZigbuild)
);
assert_eq!(
select_cross_tool(ARM_MAC, Some(X64_LINUX), &ToolAvailability::FULL),
Ok(CrossTool::CargoZigbuild)
);
}
#[test]
fn windows_gnu_foreign_arch_prefers_zigbuild() {
assert_eq!(
select_cross_tool(
ARM_MAC,
Some("x86_64-pc-windows-gnu"),
&ToolAvailability::FULL
),
Ok(CrossTool::CargoZigbuild)
);
}
#[test]
fn musl_falls_back_to_musl_cross_when_no_zig() {
let avail = ToolAvailability {
zigbuild: false,
musl_cross: true,
};
assert_eq!(
select_cross_tool(ARM_MAC, Some(X64_MUSL), &avail),
Ok(CrossTool::MuslCross)
);
}
#[test]
fn glibc_does_not_fall_back_to_musl_cross() {
let avail = ToolAvailability {
zigbuild: false,
musl_cross: true,
};
let err = select_cross_tool(ARM_MAC, Some(X64_LINUX), &avail).unwrap_err();
assert_eq!(err.target, X64_LINUX);
assert_eq!(err.preferred, CrossTool::CargoZigbuild);
}
#[test]
fn nothing_installed_errors_with_install_hint() {
let err = select_cross_tool(ARM_MAC, Some(X64_MUSL), &ToolAvailability::NONE).unwrap_err();
assert_eq!(err.preferred, CrossTool::CargoZigbuild);
let msg = err.to_string();
assert!(msg.contains("cargo-zigbuild"), "names the install: {msg}");
assert!(msg.contains(X64_MUSL), "names the target: {msg}");
}
#[test]
fn select_is_total_over_the_class_space() {
let hosts = [ARM_MAC, X64_LINUX, "x86_64-pc-windows-msvc"];
let targets = [
None,
Some(X64_MUSL),
Some(X64_LINUX),
Some("aarch64-apple-darwin"),
Some("x86_64-pc-windows-gnu"),
Some(""),
];
let avails = [
ToolAvailability::FULL,
ToolAvailability::NONE,
ToolAvailability {
zigbuild: true,
musl_cross: false,
},
ToolAvailability {
zigbuild: false,
musl_cross: true,
},
];
for h in hosts {
for t in targets {
for a in avails {
let _ = select_cross_tool(h, t, &a);
}
}
}
}
fn argv(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[test]
fn rewrites_cross_build_to_cargo_zigbuild() {
let original = argv(&["cross", "build", "--release"]);
let out = rewrite_build_argv(&original, &CrossTool::CargoZigbuild, X64_MUSL);
assert_eq!(
out,
argv(&["cargo", "zigbuild", "--release", "--target", X64_MUSL])
);
}
#[test]
fn rewrites_cargo_build_and_keeps_existing_target() {
let original = argv(&["cargo", "build", "--release", "--target", X64_MUSL]);
let out = rewrite_build_argv(&original, &CrossTool::CargoZigbuild, X64_MUSL);
assert_eq!(
out,
argv(&["cargo", "zigbuild", "--release", "--target", X64_MUSL])
);
assert_eq!(out.iter().filter(|a| *a == "--target").count(), 1);
}
#[test]
fn rewrites_equals_form_target_without_duplicating() {
let original = argv(&["cargo", "build", &format!("--target={X64_MUSL}")]);
let out = rewrite_build_argv(&original, &CrossTool::CargoZigbuild, X64_MUSL);
assert_eq!(out.iter().filter(|a| a.starts_with("--target")).count(), 1);
assert_eq!(out[1], "zigbuild");
}
#[test]
fn native_tool_keeps_build_subcommand() {
let original = argv(&["cargo", "build", "--release"]);
let out = rewrite_build_argv(&original, &CrossTool::CargoNative, "x86_64-apple-darwin");
assert_eq!(out[1], "build");
assert_eq!(out.last().unwrap(), "x86_64-apple-darwin");
}
#[test]
fn unrecognized_argv_passes_through_unchanged() {
let original = argv(&["./build.sh", "--fast"]);
let out = rewrite_build_argv(&original, &CrossTool::CargoZigbuild, X64_MUSL);
assert_eq!(out, original);
}
#[test]
fn zigbuild_and_native_need_no_env() {
assert!(CrossTool::CargoZigbuild.env_for(X64_MUSL).is_empty());
assert!(CrossTool::CargoNative.env_for(X64_MUSL).is_empty());
}
#[test]
fn musl_cross_wires_linker_cc_and_ar() {
let env = CrossTool::MuslCross.env_for(X64_MUSL);
let map: std::collections::HashMap<_, _> = env.into_iter().collect();
assert_eq!(
map.get("CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER")
.map(String::as_str),
Some("x86_64-linux-musl-gcc")
);
assert_eq!(
map.get("CC_x86_64_unknown_linux_musl").map(String::as_str),
Some("x86_64-linux-musl-gcc")
);
assert_eq!(
map.get("AR_x86_64_unknown_linux_musl").map(String::as_str),
Some("x86_64-linux-musl-ar")
);
}
#[test]
fn musl_cc_prefix_strips_vendor() {
assert_eq!(
musl_cc_prefix("x86_64-unknown-linux-musl"),
"x86_64-linux-musl"
);
assert_eq!(
musl_cc_prefix("aarch64-unknown-linux-musl"),
"aarch64-linux-musl"
);
}
#[test]
fn plan_routes_the_mesofact_step_to_zigbuild() {
let original = argv(&["cross", "build", "--release", "-p", "almanac-serve"]);
let plan =
plan_native_cross(&original, ARM_MAC, X64_MUSL, &ToolAvailability::FULL).unwrap();
assert_eq!(plan.tool, CrossTool::CargoZigbuild);
assert_eq!(
plan.argv,
argv(&[
"cargo",
"zigbuild",
"--release",
"-p",
"almanac-serve",
"--target",
X64_MUSL
])
);
assert!(plan.env.is_empty(), "zigbuild self-contains its sysroot");
}
#[test]
fn plan_falls_back_to_musl_cross_with_env() {
let avail = ToolAvailability {
zigbuild: false,
musl_cross: true,
};
let original = argv(&["cargo", "build"]);
let plan = plan_native_cross(&original, ARM_MAC, X64_MUSL, &avail).unwrap();
assert_eq!(plan.tool, CrossTool::MuslCross);
assert_eq!(plan.argv, argv(&["cargo", "build", "--target", X64_MUSL]));
assert!(!plan.env.is_empty(), "musl-cross needs linker env");
}
#[test]
fn plan_errors_when_no_toolchain() {
let err = plan_native_cross(
&argv(&["cross", "build"]),
ARM_MAC,
X64_MUSL,
&ToolAvailability::NONE,
)
.unwrap_err();
assert_eq!(err.preferred, CrossTool::CargoZigbuild);
}
#[test]
fn labels_and_hints_are_distinct_and_actionable() {
assert_eq!(CrossTool::CargoZigbuild.label(), "cargo-zigbuild");
assert_eq!(CrossTool::MuslCross.label(), "musl-cross");
assert_eq!(CrossTool::CargoNative.label(), "native (cargo build)");
assert!(CrossTool::CargoZigbuild
.install_hint(X64_MUSL)
.contains("cargo-zigbuild"));
assert!(CrossTool::MuslCross
.install_hint(X64_MUSL)
.contains("musl-cross"));
let native = CrossTool::CargoNative.install_hint(X64_MUSL);
assert!(native.contains(&format!("rustup target add {X64_MUSL}")), "{native}");
assert!(!native.contains("<triple>"), "{native}");
}
#[test]
fn install_commands_are_single_lines() {
for tool in [
CrossTool::CargoZigbuild,
CrossTool::MuslCross,
CrossTool::CargoNative,
] {
for target in [X64_MUSL, X64_LINUX, ARM_MAC] {
for shell in [crate::buildcap::Shell::Posix, crate::buildcap::Shell::Windows] {
for cmd in tool.install_commands(target, shell) {
assert!(!cmd.line.contains('\n'), "{:?}", cmd.line);
assert_eq!(cmd.line.trim(), cmd.line, "{:?}", cmd.line);
assert!(!cmd.line.contains("<triple>"), "{:?}", cmd.line);
}
}
}
}
}
#[test]
fn is_native_cross_target_matches_the_zigbuild_set() {
assert!(is_native_cross_target(ARM_MAC, X64_MUSL));
assert!(is_native_cross_target(ARM_MAC, "aarch64-unknown-linux-gnu"));
assert!(!is_native_cross_target(ARM_MAC, ARM_MAC));
assert!(!is_native_cross_target(X64_LINUX, "aarch64-apple-darwin"));
}
#[test]
fn cargo_subcommand_maps_each_tool() {
assert_eq!(CrossTool::CargoZigbuild.cargo_subcommand(), "zigbuild");
assert_eq!(CrossTool::CargoNative.cargo_subcommand(), "build");
assert_eq!(CrossTool::MuslCross.cargo_subcommand(), "build");
}
}