use crate::CargoTruceError;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
mod build;
#[cfg(target_os = "macos")]
mod bundle_link;
mod codesign;
mod locate;
#[cfg(target_os = "windows")]
pub(crate) use build::cargo_rustc_bin;
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) use build::rustup_has_target;
#[cfg(target_os = "macos")]
pub(crate) use build::{MacArch, cargo_build_for_arch, cargo_build_multi_arch, lipo_into};
pub(crate) use build::{cargo_build, cargo_build_debug, sccache_wrapper};
#[cfg(target_os = "macos")]
pub(crate) use bundle_link::{
CLAP_EXPORTS, VST2_EXPORTS, VST3_EXPORTS, link_macos_bundle, missing_staticlib_error,
};
pub(crate) use codesign::codesign_bundle;
#[cfg(target_os = "macos")]
pub(crate) use codesign::{
is_production_identity, locate_wraptool_macos, pace_sign_aax_macos,
verify_signed_for_notarization,
};
pub(crate) use locate::find_on_path;
#[cfg(target_os = "windows")]
pub(crate) use locate::{
locate_cmake, locate_msvc_cl, locate_ninja, locate_vcvars64, vs_install_paths, which_exe,
};
pub(crate) mod fs_ctx {
use crate::CargoTruceError;
use std::fs;
use std::path::Path;
pub(crate) fn copy(
from: impl AsRef<Path>,
to: impl AsRef<Path>,
) -> Result<u64, CargoTruceError> {
let (from, to) = (from.as_ref(), to.as_ref());
fs::copy(from, to)
.map_err(|e| format!("copy {} -> {}: {e}", from.display(), to.display()).into())
}
pub(crate) fn create_dir_all(path: impl AsRef<Path>) -> Result<(), CargoTruceError> {
let path = path.as_ref();
fs::create_dir_all(path).map_err(|e| format!("mkdir -p {}: {e}", path.display()).into())
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn write(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> Result<(), CargoTruceError> {
let path = path.as_ref();
fs::write(path, contents).map_err(|e| format!("write {}: {e}", path.display()).into())
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn write_if_changed(
path: impl AsRef<Path>,
contents: impl AsRef<[u8]>,
) -> Result<bool, CargoTruceError> {
let path = path.as_ref();
let new = contents.as_ref();
if let Ok(existing) = fs::read(path)
&& existing == new
{
return Ok(false);
}
fs::write(path, new)
.map_err(|e| -> CargoTruceError { format!("write {}: {e}", path.display()).into() })?;
Ok(true)
}
}
#[cfg(target_os = "macos")]
#[track_caller]
pub(crate) fn path_str(path: &Path) -> &str {
path.to_str().unwrap_or_else(|| {
panic!(
"non-UTF-8 path can't be passed as a string: {}",
path.display()
)
})
}
pub(crate) fn arg_value<'a>(
args: &'a [String],
i: &mut usize,
flag: &str,
) -> Result<&'a str, CargoTruceError> {
*i += 1;
args.get(*i)
.map(String::as_str)
.ok_or_else(|| format!("{flag} requires a value").into())
}
pub(crate) fn shared_lib_name(stem: &str) -> String {
if cfg!(target_os = "windows") {
format!("{stem}.dll")
} else if cfg!(target_os = "linux") {
format!("lib{stem}.so")
} else {
format!("lib{stem}.dylib")
}
}
static PROFILE: OnceLock<String> = OnceLock::new();
pub(crate) fn set_build_profile(name: &str) {
PROFILE.get_or_init(|| name.to_string());
}
pub(crate) fn set_debug_profile(debug: bool) {
set_build_profile(if debug { "debug" } else { "release" });
}
#[derive(Clone, Debug, Default)]
pub(crate) enum TargetCpu {
#[default]
Default,
Baseline,
Named(String),
Native,
}
static TARGET_CPU: OnceLock<TargetCpu> = OnceLock::new();
pub(crate) fn set_target_cpu(choice: TargetCpu) {
TARGET_CPU.get_or_init(|| choice);
}
pub(crate) fn parse_target_cpu_arg(raw: &str) -> TargetCpu {
match raw {
"baseline" => TargetCpu::Baseline,
"native" => TargetCpu::Native,
"v2" => TargetCpu::Named("x86-64-v2".to_string()),
"v3" => TargetCpu::Named("x86-64-v3".to_string()),
"v4" => TargetCpu::Named("x86-64-v4".to_string()),
other => TargetCpu::Named(other.to_string()),
}
}
pub(crate) fn resolve_target_cpu(triple: &str) -> Option<String> {
let choice = TARGET_CPU.get().cloned().unwrap_or_default();
match choice {
TargetCpu::Default => {
if triple.starts_with("x86_64") || triple.starts_with("i686") {
Some("x86-64-v3".to_string())
} else {
None
}
}
TargetCpu::Baseline => None,
TargetCpu::Named(value) => Some(value),
TargetCpu::Native => Some("native".to_string()),
}
}
pub(crate) fn verify_shell_profile_declared() -> Result<(), CargoTruceError> {
let cargo_toml = project_root().join("Cargo.toml");
let content = fs::read_to_string(&cargo_toml).map_err(|e| -> CargoTruceError {
format!("failed to read {}: {e}", cargo_toml.display()).into()
})?;
let doc: toml::Table = content.parse().map_err(|e| -> CargoTruceError {
format!("failed to parse {}: {e}", cargo_toml.display()).into()
})?;
let has_profile_shell = doc
.get("profile")
.and_then(|v| v.as_table())
.and_then(|t| t.get("shell"))
.is_some();
if has_profile_shell {
return Ok(());
}
Err(format!(
"--shell requires `[profile.shell]` in {}.\n\
Add the following two lines and re-run:\n\
\n\
[profile.shell]\n\
inherits = \"release\"\n\
\n\
(Scaffolded plugins already include this.)",
cargo_toml.display()
)
.into())
}
pub(crate) fn build_profile_name() -> String {
PROFILE
.get()
.cloned()
.unwrap_or_else(|| "release".to_string())
}
pub(crate) fn is_debug_profile() -> bool {
build_profile_name() == "debug"
}
fn profile_subdir() -> String {
build_profile_name()
}
pub(crate) fn release_lib(root: &Path, stem: &str) -> PathBuf {
truce_build::target_dir(root)
.join(profile_subdir())
.join(shared_lib_name(stem))
}
pub(crate) fn release_lib_for_target(root: &Path, stem: &str, target: Option<&str>) -> PathBuf {
match target {
Some(t) => truce_build::target_dir(root)
.join(t)
.join(profile_subdir())
.join(shared_lib_name(stem)),
None => release_lib(root, stem),
}
}
#[cfg(target_os = "macos")]
pub(crate) fn release_static_for_target(root: &Path, stem: &str, target: Option<&str>) -> PathBuf {
let dir = match target {
Some(t) => truce_build::target_dir(root).join(t).join(profile_subdir()),
None => truce_build::target_dir(root).join(profile_subdir()),
};
dir.join(format!("lib{stem}.a"))
}
#[cfg(target_os = "macos")]
pub(crate) fn release_bundle_bin(root: &Path, stem: &str, format_suffix: &str) -> PathBuf {
truce_build::target_dir(root)
.join(profile_subdir())
.join(format!("{stem}{format_suffix}.bundle-bin"))
}
#[cfg(target_os = "windows")]
pub(crate) fn common_program_files() -> PathBuf {
if let Ok(v) = env::var("CommonProgramFiles") {
PathBuf::from(v)
} else {
PathBuf::from(r"C:\Program Files\Common Files")
}
}
#[cfg(target_os = "windows")]
pub(crate) fn program_files() -> PathBuf {
if let Ok(v) = env::var("ProgramFiles") {
PathBuf::from(v)
} else {
PathBuf::from(r"C:\Program Files")
}
}
pub(crate) fn read_workspace_version(root: &Path) -> Result<String, crate::CargoTruceError> {
let path = root.join("Cargo.toml");
let content = fs::read_to_string(&path).map_err(|e| -> crate::CargoTruceError {
format!("read {}: {e}", path.display()).into()
})?;
let doc: toml::Table = content.parse().map_err(|e| -> crate::CargoTruceError {
format!("parse {}: {e}", path.display()).into()
})?;
if let Some(v) = doc
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
{
return Ok(v.to_string());
}
if let Some(v) = doc
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
{
return Ok(v.to_string());
}
Err(format!(
"{} has no version (expected [workspace.package] version or [package] version)",
path.display()
)
.into())
}
fn locate_plugin_manifest(project_root: &Path, crate_name: &str) -> Option<PathBuf> {
let out = Command::new("cargo")
.args([
"metadata",
"--no-deps",
"--format-version=1",
"--manifest-path",
])
.arg(project_root.join("Cargo.toml"))
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let name_needle = format!("\"name\":\"{crate_name}\"");
let idx = text.find(&name_needle)?;
let after = &text[idx + name_needle.len()..];
let mp_marker = "\"manifest_path\":\"";
let mp_idx = after.find(mp_marker)?;
let rest = &after[mp_idx + mp_marker.len()..];
let end = rest.find('"')?;
Some(PathBuf::from(&rest[..end]))
}
pub(crate) fn read_standalone_bin_name(crate_name: &str) -> Option<String> {
let manifest = locate_plugin_manifest(&project_root(), crate_name)?;
let content = fs::read_to_string(&manifest).ok()?;
let doc: toml::Table = content.parse().ok()?;
let bins = doc.get("bin")?.as_array()?;
for bin in bins {
let table = bin.as_table()?;
let has_standalone = table
.get("required-features")
.and_then(toml::Value::as_array)
.is_some_and(|arr| arr.iter().any(|x| x.as_str() == Some("standalone")));
if has_standalone {
return table.get("name")?.as_str().map(str::to_string);
}
}
if bins.len() == 1 {
return bins[0]
.as_table()?
.get("name")?
.as_str()
.map(str::to_string);
}
None
}
pub(crate) fn detect_default_features() -> std::collections::HashSet<String> {
let root = project_root();
if let Ok(content) = fs::read_to_string(root.join("Cargo.toml"))
&& let Ok(doc) = content.parse::<toml::Table>()
&& let Some(toml::Value::Table(feat)) = doc.get("features")
&& let Some(toml::Value::Array(defaults)) = feat.get("default")
{
return defaults
.iter()
.filter_map(|v| v.as_str().map(std::string::ToString::to_string))
.collect();
}
let mut union = std::collections::HashSet::new();
if let Ok(config) = crate::load_config() {
for p in &config.plugin {
if let Some(manifest) = locate_plugin_manifest(&root, &p.crate_name)
&& let Ok(content) = fs::read_to_string(&manifest)
&& let Ok(doc) = content.parse::<toml::Table>()
&& let Some(toml::Value::Table(feat)) = doc.get("features")
&& let Some(toml::Value::Array(defaults)) = feat.get("default")
{
for v in defaults {
if let Some(s) = v.as_str() {
union.insert(s.to_string());
}
}
}
}
}
union
}
pub(crate) fn project_root() -> PathBuf {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut dir = cwd.as_path();
loop {
if dir.join("truce.toml").exists() {
return dir.to_path_buf();
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
if let Ok(manifest) = env::var("CARGO_MANIFEST_DIR") {
let p = Path::new(&manifest).parent().unwrap().to_path_buf();
if p.join("truce.toml").exists() {
return p;
}
}
cwd
}
pub(crate) fn run_sudo(cmd: &str, args: &[&OsStr]) -> crate::Res {
announce_sudo_once();
let status = Command::new("sudo").arg(cmd).args(args).status()?;
if !status.success() {
return Err(crate::CargoTruceError::Other(format!(
"sudo {cmd} failed with {status}"
)));
}
Ok(())
}
fn announce_sudo_once() {
static ANNOUNCED: AtomicBool = AtomicBool::new(false);
if !ANNOUNCED.swap(true, Ordering::Relaxed) {
eprintln!(
"→ Installing to system plugin directories (/Library/Audio/Plug-Ins/, \
/Library/Application Support/Avid/) - sudo required."
);
}
}
static VERBOSE: AtomicBool = AtomicBool::new(false);
pub fn set_verbose(v: bool) {
VERBOSE.store(v, Ordering::Relaxed);
}
pub(crate) fn is_verbose() -> bool {
VERBOSE.load(Ordering::Relaxed)
}
macro_rules! vprintln {
($($arg:tt)*) => {
if $crate::util::is_verbose() {
eprintln!($($arg)*);
}
};
}
pub(crate) use vprintln;
static OUTPUTS: Mutex<Vec<String>> = Mutex::new(Vec::new());
pub(crate) fn log_output(line: String) {
if is_verbose() {
eprintln!("{line}");
}
if let Ok(mut v) = OUTPUTS.lock() {
v.push(line);
}
}
pub(crate) fn take_outputs() -> Vec<String> {
OUTPUTS
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
static SKIPPED: Mutex<Vec<String>> = Mutex::new(Vec::new());
pub(crate) fn log_skip(line: String) {
if is_verbose() {
eprintln!("{line}");
}
if let Ok(mut v) = SKIPPED.lock() {
v.push(line);
}
}
pub(crate) fn take_skipped() -> Vec<String> {
SKIPPED
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
#[cfg(target_os = "macos")]
pub(crate) fn run_codesign(args: &[&OsStr], use_sudo: bool) -> crate::Res {
use std::process::Stdio;
let target = args.last().copied().unwrap_or(OsStr::new("?"));
let target_label = std::path::Path::new(target).file_name().map_or_else(
|| target.to_string_lossy().into_owned(),
|n| n.to_string_lossy().into_owned(),
);
let is_verify = args.iter().any(|a| *a == OsStr::new("--verify"));
let (verb_present, verb_past) = if is_verify {
("verify", "verified")
} else {
("sign", "signed")
};
let mut cmd = if use_sudo {
announce_sudo_once();
let mut c = Command::new("sudo");
c.arg("codesign");
c
} else {
Command::new("codesign")
};
cmd.args(args);
let (status, captured_stderr) = if is_verbose() {
(cmd.status()?, String::new())
} else {
let output = cmd.stderr(Stdio::piped()).output()?;
(
output.status,
String::from_utf8_lossy(&output.stderr).into_owned(),
)
};
if status.success() {
eprintln!(" ✓ {verb_past} {target_label}");
Ok(())
} else {
if !captured_stderr.is_empty() {
eprintln!("{captured_stderr}");
}
eprintln!(" ✗ failed to {verb_present} {target_label}");
Err(crate::CargoTruceError::Codesign(format!(
"failed to {verb_present} {target_label}"
)))
}
}
#[cfg(target_os = "macos")]
pub(crate) fn run_silent(cmd: &str, args: &[&OsStr]) {
use std::process::Stdio;
let _ = Command::new(cmd)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
pub(crate) fn tmp_dir() -> PathBuf {
let dir = truce_build::target_dir(&project_root()).join("tmp");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn tmp_manifests() -> PathBuf {
let dir = tmp_dir().join("manifests");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(target_os = "windows")]
pub(crate) fn tmp_scripts() -> PathBuf {
let dir = tmp_dir().join("scripts");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn tmp_verify() -> PathBuf {
let dir = tmp_dir().join("verify");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn tmp_aax_template() -> PathBuf {
let dir = tmp_dir().join("aax-template");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(target_os = "macos")]
pub(crate) fn tmp_au_v3(bundle_id: &str) -> PathBuf {
let dir = tmp_dir().join("au-v3").join(bundle_id);
let _ = fs::create_dir_all(&dir);
dir
}
pub(crate) fn tmp_lv2(bundle_id: &str) -> PathBuf {
let dir = tmp_dir().join("lv2").join(bundle_id);
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn copy_dir_recursive(src: &Path, dst: &Path) -> crate::Res {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
let ft = entry.file_type()?;
#[cfg(unix)]
if ft.is_symlink() {
let target = fs::read_link(&src_path)?;
let _ = fs::remove_file(&dst_path);
std::os::unix::fs::symlink(&target, &dst_path)?;
continue;
}
if ft.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn extract_team_id(sign_id: &str) -> String {
if let Some(start) = sign_id.rfind('(')
&& let Some(end) = sign_id.rfind(')')
{
return sign_id[start + 1..end].to_string();
}
String::new()
}
pub(crate) fn confirm_prompt(message: &str) -> bool {
eprint!("{message} [y/N] ");
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
matches!(input.trim(), "y" | "Y" | "yes" | "YES")
}
pub(crate) fn tag_ok() -> String {
paint("[ OK ]", "\x1b[1;32m")
}
pub(crate) fn tag_fail() -> String {
paint("[FAIL]", "\x1b[1;31m")
}
pub(crate) fn tag_warn() -> String {
paint("[WARN]", "\x1b[1;33m")
}
pub(crate) fn tag_info() -> String {
paint("[INFO]", "\x1b[1;36m")
}
fn paint(text: &str, ansi: &str) -> String {
if doctor_use_color() {
format!("{ansi}{text}\x1b[0m")
} else {
text.to_string()
}
}
fn doctor_use_color() -> bool {
use std::io::IsTerminal;
static USE: OnceLock<bool> = OnceLock::new();
*USE.get_or_init(|| {
if env::var_os("NO_COLOR").is_some() {
return false;
}
std::io::stderr().is_terminal()
})
}
pub(crate) fn check_cmd(cmd: &str, args: &[&OsStr], label: &str) {
match Command::new(cmd).args(args).output() {
Ok(o) if o.status.success() => {
let ver = String::from_utf8_lossy(&o.stdout);
let first_line = ver.lines().next().unwrap_or("").trim();
if first_line.is_empty() {
eprintln!(" {} {label}", tag_ok());
} else {
eprintln!(" {} {label}: {first_line}", tag_ok());
}
}
Ok(_) => eprintln!(" {} {label}", tag_ok()),
Err(_) => eprintln!(" {} {label}: not found", tag_fail()),
}
}