#[cfg(target_os = "macos")]
use std::fs;
#[cfg(target_os = "macos")]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use super::build_profile_name;
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn rustup_has_target(triple: &str) -> bool {
installed_rustup_targets().is_some_and(|set| set.contains(triple))
}
fn installed_rustup_targets() -> Option<&'static std::collections::HashSet<String>> {
static CACHE: OnceLock<Option<std::collections::HashSet<String>>> = OnceLock::new();
CACHE
.get_or_init(|| {
let out = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
)
})
.as_ref()
}
pub(crate) fn ensure_rustup_target(triple: &str) -> crate::Res {
let Some(installed) = installed_rustup_targets() else {
return Err(format!(
"rustup not available - can't verify target `{triple}` is installed. \
Either `rustup` isn't on PATH, or `cargo` is resolving to a non-rustup \
toolchain (e.g. Homebrew's). Install rustup from https://rustup.rs and \
make sure `which cargo` points at `~/.cargo/bin/cargo`."
)
.into());
};
if installed.contains(triple) {
return Ok(());
}
eprintln!("rustup: installing target {triple}...");
let status = Command::new("rustup")
.args(["target", "add", triple])
.status()?;
if !status.success() {
return Err(format!("`rustup target add {triple}` failed").into());
}
Ok(())
}
#[allow(unused_variables)]
pub(crate) fn cargo_build(
env_vars: &[(&str, &str)],
extra_args: &[&str],
deployment_target: &str,
) -> crate::Res {
cargo_build_with_profile(
env_vars,
extra_args,
deployment_target,
&build_profile_name(),
)
}
pub(crate) fn cargo_build_debug(
env_vars: &[(&str, &str)],
extra_args: &[&str],
deployment_target: &str,
) -> crate::Res {
cargo_build_with_profile(env_vars, extra_args, deployment_target, "debug")
}
pub(crate) fn cargo_build_with_profile(
env_vars: &[(&str, &str)],
extra_args: &[&str],
deployment_target: &str,
profile: &str,
) -> crate::Res {
cargo_build_inner(env_vars, extra_args, deployment_target, profile)
}
fn cargo_build_inner(
env_vars: &[(&str, &str)],
extra_args: &[&str],
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] deployment_target: &str,
profile: &str,
) -> crate::Res {
let targets = extract_target_triples(extra_args);
for triple in &targets {
ensure_rustup_target(triple)?;
}
let mut cmd = Command::new("cargo");
cmd.arg("build");
match profile {
"debug" => {} "release" => {
cmd.arg("--release");
}
custom => {
cmd.arg("--profile").arg(custom);
}
}
#[cfg(target_os = "macos")]
cmd.env("MACOSX_DEPLOYMENT_TARGET", deployment_target);
apply_target_cpu(&mut cmd, &targets);
if let Some(wrapper) = sccache_wrapper() {
cmd.env("RUSTC_WRAPPER", wrapper);
}
for (k, v) in env_vars {
cmd.env(k, v);
}
for arg in extra_args {
cmd.arg(arg);
}
let status = cmd.status()?;
if !status.success() {
return Err("cargo build failed".into());
}
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn cargo_rustc_bin(
env_vars: &[(&str, &str)],
base_args: &[&str],
package: &str,
bin_name: &str,
link_args: &[&str],
) -> crate::Res {
let targets = extract_target_triples(base_args);
for triple in &targets {
ensure_rustup_target(triple)?;
}
let mut cmd = Command::new("cargo");
cmd.arg("rustc");
match build_profile_name().as_str() {
"debug" => {}
"release" => {
cmd.arg("--release");
}
custom => {
cmd.arg("--profile").arg(custom);
}
}
cmd.arg("-p").arg(package);
cmd.arg("--bin").arg(bin_name);
apply_target_cpu(&mut cmd, &targets);
if let Some(wrapper) = sccache_wrapper() {
cmd.env("RUSTC_WRAPPER", wrapper);
}
for (k, v) in env_vars {
cmd.env(k, v);
}
for arg in base_args {
cmd.arg(arg);
}
if !link_args.is_empty() {
cmd.arg("--");
for a in link_args {
cmd.arg(a);
}
}
let status = cmd.status()?;
if !status.success() {
return Err("cargo rustc failed".into());
}
Ok(())
}
fn extract_target_triples<'a>(args: &'a [&'a str]) -> Vec<&'a str> {
let mut out = Vec::new();
let mut it = args.iter();
while let Some(a) = it.next() {
if *a == "--target" {
if let Some(t) = it.next() {
out.push(*t);
}
} else if let Some(t) = a.strip_prefix("--target=") {
out.push(t);
}
}
out
}
fn apply_target_cpu(cmd: &mut Command, targets: &[&str]) {
use crate::util::resolve_target_cpu;
if targets.is_empty() {
if let Some(cpu) = resolve_target_cpu(truce_build::host_triple()) {
append_rustflags_env(cmd, "RUSTFLAGS", &format!("-C target-cpu={cpu}"));
}
return;
}
for triple in targets {
let Some(cpu) = resolve_target_cpu(triple) else {
continue;
};
let var = cargo_target_rustflags_var(triple);
append_rustflags_env(cmd, &var, &format!("-C target-cpu={cpu}"));
}
}
fn cargo_target_rustflags_var(triple: &str) -> String {
let normalised: String = triple
.chars()
.map(|c| {
if c == '-' || c == '.' {
'_'
} else {
c.to_ascii_uppercase()
}
})
.collect();
format!("CARGO_TARGET_{normalised}_RUSTFLAGS")
}
fn append_rustflags_env(cmd: &mut Command, var: &str, flag: &str) {
let prior = std::env::var(var).unwrap_or_default();
let combined = if prior.is_empty() {
flag.to_string()
} else {
format!("{prior} {flag}")
};
cmd.env(var, combined);
}
pub(crate) fn sccache_wrapper() -> Option<std::ffi::OsString> {
if std::env::var_os("RUSTC_WRAPPER").is_some()
|| std::env::var_os("RUSTC_WORKSPACE_WRAPPER").is_some()
|| std::env::var_os("TRUCE_DISABLE_SCCACHE").is_some()
{
return None;
}
which("sccache")
}
fn which(name: &str) -> Option<std::ffi::OsString> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if let Ok(meta) = std::fs::metadata(&candidate)
&& meta.is_file()
{
return Some(candidate.into_os_string());
}
}
None
}
#[cfg(target_os = "macos")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MacArch {
X86_64,
Arm64,
}
#[cfg(target_os = "macos")]
impl MacArch {
pub(crate) fn triple(self) -> &'static str {
match self {
MacArch::X86_64 => "x86_64-apple-darwin",
MacArch::Arm64 => "aarch64-apple-darwin",
}
}
pub(crate) fn host() -> Self {
if cfg!(target_arch = "aarch64") {
MacArch::Arm64
} else {
MacArch::X86_64
}
}
}
#[cfg(target_os = "macos")]
pub(crate) fn lipo_into(inputs: &[PathBuf], output: &Path) -> crate::Res {
if inputs.is_empty() {
return Err("lipo_into: no inputs".into());
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)?;
}
if inputs.len() == 1 {
fs::copy(&inputs[0], output)?;
return Ok(());
}
let mut cmd = Command::new("lipo");
cmd.arg("-create");
for i in inputs {
cmd.arg(i);
}
cmd.arg("-output").arg(output);
let status = cmd.status()?;
if !status.success() {
return Err(format!(
"lipo -create failed combining {} slices into {}",
inputs.len(),
output.display()
)
.into());
}
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn cargo_build_for_arch(
env_vars: &[(&str, &str)],
base_args: &[&str],
arch: MacArch,
dt: &str,
) -> crate::Res {
let mut args: Vec<String> = vec!["--target".into(), arch.triple().into()];
for a in base_args {
args.push((*a).into());
}
let arg_refs: Vec<&str> = args.iter().map(std::string::String::as_str).collect();
cargo_build(env_vars, &arg_refs, dt)
}
#[cfg(target_os = "macos")]
pub(crate) fn cargo_build_multi_arch(
archs: &[MacArch],
base_args: &[&str],
dt: &str,
) -> crate::Res {
let mut args: Vec<String> = Vec::with_capacity(archs.len() * 2 + base_args.len());
for arch in archs {
args.push("--target".into());
args.push(arch.triple().into());
}
for a in base_args {
args.push((*a).into());
}
let arg_refs: Vec<&str> = args.iter().map(std::string::String::as_str).collect();
cargo_build(&[], &arg_refs, dt)
}