pub mod android;
pub mod darwin;
pub mod freebsd;
pub mod ios;
pub mod linux;
pub mod netbsd;
pub mod windows;
use crate::cli::Args;
use crate::config::{Arch, HostPlatform, Libc, Os, TargetConfig};
use crate::env::{CMakeToolchain, CrossEnv};
use crate::error::{CrossError, Result};
use path_slash::PathExt as _;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tokio::process::Command;
#[must_use]
pub fn to_cmake_path(path: &Path) -> String {
path.to_slash_lossy().into_owned()
}
pub fn setup_generic_cmake_toolchain(env: &mut CrossEnv) {
env.set_generic_cmake_toolchain();
}
#[must_use]
pub fn cmake_toolchain_env_key(target: &str) -> String {
format!("CMAKE_TOOLCHAIN_FILE_{}", target.replace('-', "_"))
}
#[must_use]
pub fn has_preconfigured_cmake_toolchain(env: &HashMap<String, String>, target: &str) -> bool {
let target_lower = target.replace('-', "_");
let target_hyphen = format!("CMAKE_TOOLCHAIN_FILE_{target}");
let target_underscore = format!("CMAKE_TOOLCHAIN_FILE_{target_lower}");
let vars = [
target_hyphen.as_str(),
target_underscore.as_str(),
"TARGET_CMAKE_TOOLCHAIN_FILE",
"CMAKE_TOOLCHAIN_FILE",
];
vars.iter().any(|key| {
env.get(*key).is_some_and(|value| !value.is_empty())
|| std::env::var_os(key).is_some_and(|value| !value.is_empty())
})
}
pub fn resolve_path_from_current_dir(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
let base = std::env::current_dir().map_err(|source| CrossError::IoError {
message: "Failed to determine current working directory".to_string(),
source,
})?;
Ok(base.join(path))
}
pub fn prepare_cmake_toolchain_file(
args: &Args,
target_config: &TargetConfig,
cross_env: &CrossEnv,
) -> Result<Option<PathBuf>> {
match cross_env.cmake_toolchain.as_ref() {
Some(CMakeToolchain::Custom(path)) => Ok(Some(path.clone())),
Some(CMakeToolchain::Generic) => {
let path = write_generic_cmake_toolchain_file(args, target_config, cross_env)?;
Ok(Some(path))
}
None => Ok(None),
}
}
fn write_generic_cmake_toolchain_file(
args: &Args,
target_config: &TargetConfig,
cross_env: &CrossEnv,
) -> Result<PathBuf> {
let output_dir = resolve_path_from_current_dir(&args.cross_compiler_dir)?.join("cmake");
fs::create_dir_all(&output_dir).map_err(|source| CrossError::IoError {
message: format!(
"Failed to create CMake toolchain directory at {}",
output_dir.display()
),
source,
})?;
let path = output_dir.join(format!("{}.cmake", target_config.target));
let content = render_cmake_toolchain_file(target_config, cross_env);
fs::write(&path, content).map_err(|source| CrossError::IoError {
message: format!("Failed to write CMake toolchain file at {}", path.display()),
source,
})?;
Ok(path)
}
#[must_use]
pub fn render_cmake_toolchain_file(target_config: &TargetConfig, cross_env: &CrossEnv) -> String {
let mut lines = vec![format!(
"# Auto-generated by cargo-cross for {}",
target_config.target
)];
let (system_name, system_processor) = cmake_system_name_and_processor(target_config);
push_cmake_set(&mut lines, "CMAKE_SYSTEM_NAME", system_name);
push_cmake_set(&mut lines, "CMAKE_SYSTEM_PROCESSOR", system_processor);
if let Some(osx_arch) = cmake_osx_architecture(target_config) {
push_cmake_set(&mut lines, "CMAKE_OSX_ARCHITECTURES", osx_arch);
}
let c_compiler = cross_env
.cc
.as_deref()
.map(|tool| resolve_tool_path(tool, &cross_env.path));
let cxx_compiler = cross_env
.cxx
.as_deref()
.map(|tool| resolve_tool_path(tool, &cross_env.path));
let ar = cross_env
.ar
.as_deref()
.map(|tool| resolve_tool_path(tool, &cross_env.path));
let linker = cross_env
.linker
.as_deref()
.map(|tool| resolve_tool_path(tool, &cross_env.path));
if let Some(path) = c_compiler.as_deref() {
push_cmake_set_path(&mut lines, "CMAKE_C_COMPILER", path);
}
if let Some(path) = cxx_compiler.as_deref() {
push_cmake_set_path(&mut lines, "CMAKE_CXX_COMPILER", path);
}
if let Some(path) = ar.as_deref() {
push_cmake_set_path(&mut lines, "CMAKE_AR", path);
}
if let Some(path) = linker.as_deref() {
push_cmake_set_path(&mut lines, "CMAKE_LINKER", path);
}
if let Some(root) = cross_env
.sysroot
.as_deref()
.or(cross_env.sdkroot.as_deref())
{
push_cmake_set_path(&mut lines, "CMAKE_SYSROOT", root);
}
if let Some(sdkroot) = cross_env.sdkroot.as_deref() {
push_cmake_set_path(&mut lines, "CMAKE_OSX_SYSROOT", sdkroot);
}
let mut content = lines.join("\n");
content.push('\n');
content
}
fn cmake_system_name_and_processor(target_config: &TargetConfig) -> (&'static str, &'static str) {
let os = rust_cfg_target_os(target_config);
let arch = rust_cfg_target_arch(target_config);
match (os, arch) {
("android", "arm") => ("Android", "armv7-a"),
("android", "x86") => ("Android", "i686"),
("android", arch) => ("Android", arch),
("dragonfly", arch) => ("DragonFly", arch),
("macos", "aarch64") => ("Darwin", "arm64"),
("macos", arch) => ("Darwin", arch),
("freebsd", "x86_64") => ("FreeBSD", "amd64"),
("freebsd", arch) => ("FreeBSD", arch),
("fuchsia", arch) => ("Fuchsia", arch),
("haiku", arch) => ("Haiku", arch),
("ios", "aarch64") => ("iOS", "arm64"),
("ios", arch) => ("iOS", arch),
("linux", arch) => {
let name = "Linux";
match arch {
"powerpc" => (name, "ppc"),
"powerpc64" => (name, "ppc64"),
"powerpc64le" => (name, "ppc64le"),
_ => (name, arch),
}
}
("netbsd", arch) => ("NetBSD", arch),
("openbsd", "x86_64") => ("OpenBSD", "amd64"),
("openbsd", arch) => ("OpenBSD", arch),
("solaris", arch) => ("SunOS", arch),
("tvos", "aarch64") => ("tvOS", "arm64"),
("tvos", arch) => ("tvOS", arch),
("visionos", "aarch64") => ("visionOS", "arm64"),
("visionos", arch) => ("visionOS", arch),
("watchos", "aarch64") => ("watchOS", "arm64"),
("watchos", arch) => ("watchOS", arch),
("windows", "x86_64") => ("Windows", "AMD64"),
("windows", "x86") => ("Windows", "X86"),
("windows", "aarch64") => ("Windows", "ARM64"),
("none", arch) => ("Generic", arch),
(os, arch) => (os, arch),
}
}
fn rust_cfg_target_os(target_config: &TargetConfig) -> &'static str {
match target_config.os {
Os::Linux => "linux",
Os::Windows => "windows",
Os::FreeBsd => "freebsd",
Os::NetBsd => "netbsd",
Os::Darwin => "macos",
Os::Ios | Os::IosSim => "ios",
Os::Android => "android",
}
}
fn rust_cfg_target_arch(target_config: &TargetConfig) -> &'static str {
match target_config.arch {
Arch::Aarch64 | Arch::Aarch64Be | Arch::Arm64e => "aarch64",
Arch::Armv5 | Arch::Armv6 | Arch::Armv7 => "arm",
Arch::I586 | Arch::I686 => "x86",
Arch::Mips | Arch::Mipsel => "mips",
Arch::Mipsisa32r6 | Arch::Mipsisa32r6el => "mips32r6",
Arch::Mipsisa64r6 | Arch::Mipsisa64r6el => "mips64r6",
Arch::Mips64 | Arch::Mips64el => "mips64",
Arch::Powerpc64 => "powerpc64",
Arch::Powerpc64le => "powerpc64le",
Arch::X86_64 | Arch::X86_64h => "x86_64",
_ => target_config.arch.as_str(),
}
}
fn cmake_osx_architecture(target_config: &TargetConfig) -> Option<&'static str> {
match (target_config.os, target_config.arch) {
(Os::Darwin, Arch::Aarch64) => Some("arm64"),
(Os::Darwin, Arch::X86_64 | Arch::X86_64h) => Some("x86_64"),
_ => None,
}
}
fn push_cmake_set(lines: &mut Vec<String>, key: &str, value: &str) {
lines.push(format!("set({key} \"{}\")", escape_cmake_value(value)));
}
fn push_cmake_set_path(lines: &mut Vec<String>, key: &str, value: &Path) {
push_cmake_set(lines, key, &to_cmake_path(value));
}
fn escape_cmake_value(value: &str) -> String {
value.replace('"', "\\\"")
}
fn resolve_tool_path(tool: &str, extra_path: &[PathBuf]) -> PathBuf {
let tool_path = Path::new(tool);
if tool_path.is_absolute() || tool_path.parent().is_some() {
return tool_path.to_path_buf();
}
let path_dirs = std::env::var_os("PATH")
.map(|paths| std::env::split_paths(&paths).collect::<Vec<_>>())
.unwrap_or_default();
extra_path
.iter()
.map(|dir| dir.join(tool))
.chain(path_dirs.into_iter().map(|dir| dir.join(tool)))
.find(|candidate| candidate.exists())
.unwrap_or_else(|| tool_path.to_path_buf())
}
pub async fn setup_cross_env(
target_config: &TargetConfig,
args: &Args,
host: &HostPlatform,
) -> Result<CrossEnv> {
if args.no_toolchain_setup {
return Ok(CrossEnv::new());
}
match target_config.os {
Os::Linux => linux::setup(target_config, args, host).await,
Os::Windows => windows::setup(target_config, args, host).await,
Os::FreeBsd => freebsd::setup(target_config, args, host).await,
Os::NetBsd => netbsd::setup(target_config, args, host).await,
Os::Darwin => darwin::setup(target_config, args, host).await,
Os::Ios | Os::IosSim => ios::setup(target_config, args, host).await,
Os::Android => android::setup(target_config, args, host).await,
}
}
#[must_use]
pub fn get_linux_bin_prefix(arch: Arch, libc: Libc, abi: Option<crate::config::Abi>) -> String {
let arch_str = arch.as_str();
if let Some(abi_val) = abi {
if abi_val.is_gnu_abi_variant() && libc == crate::config::Libc::Gnu {
return format!("{arch_str}-linux-gnu{}", abi_val.as_str());
}
}
let libc_str = libc.as_str();
let abi_str = abi.map_or("", |a| a.as_str());
format!("{arch_str}-linux-{libc_str}{abi_str}")
}
#[must_use]
pub fn get_linux_folder_name(
arch: Arch,
libc: Libc,
abi: Option<crate::config::Abi>,
glibc_version: &str,
default_glibc_version: &str,
) -> String {
let arch_str = arch.as_str();
if let Some(abi_val) = abi {
if abi_val.is_gnu_abi_variant() && libc == crate::config::Libc::Gnu {
let abi_suffix = abi_val.as_str();
let folder_suffix = if glibc_version == default_glibc_version {
format!("gnu{abi_suffix}")
} else {
format!("gnu{abi_suffix}-{glibc_version}")
};
return format!("{arch_str}-linux-{folder_suffix}-cross");
}
}
let libc_str = libc.as_str();
let abi_str = abi.map_or("", |a| a.as_str());
let folder_suffix = if libc == Libc::Gnu && glibc_version != default_glibc_version {
format!("{libc_str}{abi_str}-{glibc_version}")
} else {
format!("{libc_str}{abi_str}")
};
format!("{arch_str}-linux-{folder_suffix}-cross")
}
pub fn setup_cmake(env: &mut CrossEnv, cmake_generator: Option<&str>, is_windows: bool) {
if let Some(g) = cmake_generator {
env.extra_env
.insert("CMAKE_GENERATOR".to_string(), g.to_string());
return;
}
if !is_windows {
return;
}
let generator = if which::which("ninja").is_ok() {
"Ninja"
} else if which::which("mingw32-make").is_ok() {
"MinGW Makefiles"
} else {
"Unix Makefiles"
};
env.extra_env
.insert("CMAKE_GENERATOR".to_string(), generator.to_string());
}
pub fn setup_cross_compile_prefix(env: &mut CrossEnv, bin_prefix: &str) {
env.extra_env
.insert("CROSS_COMPILE".to_string(), format!("{bin_prefix}-"));
}
pub fn setup_darwin_linker_library_path(env: &mut CrossEnv, compiler_dir: &Path) {
let lib_dir = compiler_dir.join("lib");
if lib_dir.exists() {
env.add_library_path(&lib_dir);
}
}
pub async fn get_ubuntu_version() -> Option<String> {
let output = Command::new("lsb_release").arg("-rs").output().await.ok()?;
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
if version.contains('.') {
return Some(version);
}
}
None
}
pub async fn find_apple_sdk(sdk_type: AppleSdkType, version: &str) -> Option<PathBuf> {
let (sdk_name, platform_name) = sdk_type.names(version);
if let Some(path) = try_xcrun_sdk(&sdk_name).await {
return Some(path);
}
if let Some(path) = try_xcode_select_sdk(platform_name, version).await {
return Some(path);
}
search_xcode_apps_for_sdk(platform_name, version)
}
#[derive(Debug, Clone, Copy)]
pub enum AppleSdkType {
MacOS,
IPhoneOS,
IPhoneSimulator,
}
impl AppleSdkType {
fn names(&self, version: &str) -> (String, &'static str) {
match self {
Self::MacOS => (format!("macosx{version}"), "MacOSX"),
Self::IPhoneOS => (format!("iphoneos{version}"), "iPhoneOS"),
Self::IPhoneSimulator => (format!("iphonesimulator{version}"), "iPhoneSimulator"),
}
}
}
async fn try_xcrun_sdk(sdk_name: &str) -> Option<PathBuf> {
let output = Command::new("xcrun")
.args(["--sdk", sdk_name, "--show-sdk-path"])
.output()
.await
.ok()?;
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let path = PathBuf::from(&path);
if path.exists() {
return Some(path);
}
}
None
}
async fn try_xcode_select_sdk(platform_name: &str, version: &str) -> Option<PathBuf> {
let output = Command::new("xcode-select").arg("-p").output().await.ok()?;
if output.status.success() {
let xcode_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let sdk_path = PathBuf::from(&xcode_path)
.join(format!("Platforms/{platform_name}.platform/Developer/SDKs"))
.join(format!("{platform_name}{version}.sdk"));
if sdk_path.exists() {
return Some(sdk_path);
}
}
None
}
fn search_xcode_apps_for_sdk(platform_name: &str, version: &str) -> Option<PathBuf> {
let entries = std::fs::read_dir("/Applications").ok()?;
for entry in entries.filter_map(std::result::Result::ok) {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("Xcode") && name_str.ends_with(".app") {
let sdk_path = entry
.path()
.join(format!(
"Contents/Developer/Platforms/{platform_name}.platform/Developer/SDKs"
))
.join(format!("{platform_name}{version}.sdk"));
if sdk_path.exists() {
return Some(sdk_path);
}
}
}
None
}
pub async fn find_file_by_pattern(dir: &Path, pattern: &str) -> Option<PathBuf> {
let matcher = globset::Glob::new(pattern).ok()?.compile_matcher();
let mut entries = tokio::fs::read_dir(dir).await.ok()?;
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name();
if matcher.is_match(&*name.to_string_lossy()) {
return Some(entry.path());
}
}
None
}
#[cfg(test)]
fn glob_matches(pattern: &str, filename: &str) -> bool {
globset::Glob::new(pattern)
.map(|g| g.compile_matcher().is_match(filename))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Abi;
#[test]
fn test_to_cmake_path_unix() {
let path = Path::new("/home/user/project");
assert_eq!(to_cmake_path(path), "/home/user/project");
}
#[test]
fn test_to_cmake_path_relative() {
let path = Path::new("src/main.rs");
assert_eq!(to_cmake_path(path), "src/main.rs");
}
#[test]
fn test_to_cmake_path_with_dots() {
let path = Path::new("../project/./src");
assert_eq!(to_cmake_path(path), "../project/./src");
}
#[test]
fn test_linux_bin_prefix_musl() {
let prefix = get_linux_bin_prefix(Arch::Aarch64, Libc::Musl, None);
assert_eq!(prefix, "aarch64-linux-musl");
}
#[test]
fn test_linux_bin_prefix_gnu() {
let prefix = get_linux_bin_prefix(Arch::X86_64, Libc::Gnu, None);
assert_eq!(prefix, "x86_64-linux-gnu");
}
#[test]
fn test_linux_bin_prefix_with_abi() {
let prefix = get_linux_bin_prefix(Arch::Armv7, Libc::Musl, Some(Abi::Eabihf));
assert_eq!(prefix, "armv7-linux-musleabihf");
}
#[test]
fn test_linux_folder_name_musl() {
let name = get_linux_folder_name(Arch::Aarch64, Libc::Musl, None, "2.28", "2.28");
assert_eq!(name, "aarch64-linux-musl-cross");
}
#[test]
fn test_linux_folder_name_gnu_default() {
let name = get_linux_folder_name(Arch::X86_64, Libc::Gnu, None, "2.28", "2.28");
assert_eq!(name, "x86_64-linux-gnu-cross");
}
#[test]
fn test_linux_folder_name_gnu_custom_version() {
let name = get_linux_folder_name(Arch::X86_64, Libc::Gnu, None, "2.31", "2.28");
assert_eq!(name, "x86_64-linux-gnu-2.31-cross");
}
#[test]
fn test_linux_folder_name_with_abi() {
let name = get_linux_folder_name(Arch::Armv7, Libc::Gnu, Some(Abi::Eabihf), "2.28", "2.28");
assert_eq!(name, "armv7-linux-gnueabihf-cross");
}
#[test]
fn test_glob_matches_clang_exact() {
assert!(glob_matches(
"x86_64-apple-darwin*-clang",
"x86_64-apple-darwin25.2-clang"
));
}
#[test]
fn test_glob_does_not_match_clang_plus_plus() {
assert!(!glob_matches(
"x86_64-apple-darwin*-clang",
"x86_64-apple-darwin25.2-clang++"
));
}
#[test]
fn test_glob_does_not_match_clang_with_libc_suffix() {
assert!(!glob_matches(
"x86_64-apple-darwin*-clang",
"x86_64-apple-darwin25.2-clang++-libc++"
));
}
#[test]
fn test_glob_matches_clang_plus_plus_exact() {
assert!(glob_matches(
"x86_64-apple-darwin*-clang++",
"x86_64-apple-darwin25.2-clang++"
));
}
#[test]
fn test_glob_does_not_match_clang_plus_plus_with_suffix() {
assert!(!glob_matches(
"x86_64-apple-darwin*-clang++",
"x86_64-apple-darwin25.2-clang++-libc++"
));
}
#[test]
fn test_glob_matches_aarch64_darwin_clang() {
assert!(glob_matches(
"aarch64-apple-darwin*-clang",
"aarch64-apple-darwin25.2-clang"
));
assert!(!glob_matches(
"aarch64-apple-darwin*-clang",
"aarch64-apple-darwin25.2-clang++"
));
}
#[test]
fn test_glob_matches_different_darwin_versions() {
let pattern = "x86_64-apple-darwin*-clang";
assert!(glob_matches(pattern, "x86_64-apple-darwin24.0-clang"));
assert!(glob_matches(pattern, "x86_64-apple-darwin25.2-clang"));
assert!(glob_matches(pattern, "x86_64-apple-darwin26.0-clang"));
assert!(!glob_matches(pattern, "x86_64-apple-darwin24.0-clang++"));
assert!(!glob_matches(pattern, "x86_64-apple-darwin25.2-clang++"));
}
#[test]
fn test_glob_matches_ios_compiler() {
assert!(glob_matches(
"arm64-apple-darwin*-clang",
"arm64-apple-darwin11-clang"
));
assert!(!glob_matches(
"arm64-apple-darwin*-clang",
"arm64-apple-darwin11-clang++"
));
}
#[test]
fn test_x32_folder_names() {
use crate::config::{Abi, Arch, Libc};
let folder = get_linux_folder_name(Arch::X86_64, Libc::Gnu, Some(Abi::X32), "2.17", "");
assert_eq!(folder, "x86_64-linux-gnux32-2.17-cross");
let folder = get_linux_folder_name(Arch::X86_64, Libc::Gnu, Some(Abi::X32), "", "");
assert_eq!(folder, "x86_64-linux-gnux32-cross");
}
#[test]
fn test_x32_bin_prefix() {
use crate::config::{Abi, Arch, Libc};
let bin_prefix = get_linux_bin_prefix(Arch::X86_64, Libc::Gnu, Some(Abi::X32));
assert_eq!(bin_prefix, "x86_64-linux-gnux32");
}
#[test]
fn test_aarch64_be_targets() {
use crate::config::{Arch, Libc};
let bin_prefix = get_linux_bin_prefix(Arch::Aarch64Be, Libc::Musl, None);
assert_eq!(bin_prefix, "aarch64_be-linux-musl");
let folder = get_linux_folder_name(Arch::Aarch64Be, Libc::Musl, None, "", "");
assert_eq!(folder, "aarch64_be-linux-musl-cross");
let bin_prefix = get_linux_bin_prefix(Arch::Aarch64Be, Libc::Gnu, None);
assert_eq!(bin_prefix, "aarch64_be-linux-gnu");
let folder = get_linux_folder_name(Arch::Aarch64Be, Libc::Gnu, None, "2.17", "");
assert_eq!(folder, "aarch64_be-linux-gnu-2.17-cross");
}
}