use crate::BoxErr;
use std::env;
use std::fs;
#[cfg(target_os = "macos")]
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
pub(crate) mod fs_ctx {
use crate::BoxErr;
use std::fs;
use std::path::Path;
pub(crate) fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64, BoxErr> {
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<(), BoxErr> {
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<(), BoxErr> {
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, BoxErr> {
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| -> BoxErr { format!("write {}: {e}", path.display()).into() })?;
Ok(true)
}
}
pub(crate) fn arg_value<'a>(
args: &'a [String],
i: &mut usize,
flag: &str,
) -> Result<&'a str, BoxErr> {
*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" });
}
pub(crate) fn verify_shell_profile_declared() -> Result<(), BoxErr> {
let cargo_toml = project_root().join("Cargo.toml");
let content = fs::read_to_string(&cargo_toml).map_err(|e| -> BoxErr {
format!("failed to read {}: {e}", cargo_toml.display()).into()
})?;
let doc: toml::Table = content.parse().map_err(|e| -> BoxErr {
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))
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
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 = "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")
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn read_workspace_version(root: &Path) -> Option<String> {
let content = fs::read_to_string(root.join("Cargo.toml")).ok()?;
let doc: toml::Table = content.parse().ok()?;
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 Some(v.to_string());
}
doc.get("package")?
.get("version")?
.as_str()
.map(std::string::ToString::to_string)
}
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: &[&str]) -> crate::Res {
announce_sudo_once();
let status = Command::new("sudo").arg(cmd).args(args).status()?;
if !status.success() {
return Err(format!("sudo {cmd} failed with {status}").into());
}
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: &[&str], use_sudo: bool) -> crate::Res {
use std::process::Stdio;
let target = args.last().copied().unwrap_or("?");
let target_label = std::path::Path::new(target)
.file_name()
.map_or_else(|| target.to_string(), |n| n.to_string_lossy().into_owned());
let is_verify = args.contains(&"--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("codesign failed".into())
}
}
#[cfg(target_os = "macos")]
pub(crate) fn run_silent(cmd: &str, args: &[&str]) {
use std::process::Stdio;
let _ = Command::new(cmd)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(target_os = "macos")]
pub(crate) fn run_quiet(cmd: &str, args: &[&str]) -> std::result::Result<String, BoxErr> {
let output = Command::new(cmd).args(args).output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "macos")]
pub(crate) fn is_production_identity(identity: &str) -> bool {
identity != "-"
}
pub(crate) fn tmp_dir() -> PathBuf {
let dir = truce_build::target_dir(&project_root()).join("tmp");
let _ = fs::create_dir_all(&dir);
dir
}
#[cfg(target_os = "macos")]
pub(crate) fn write_entitlements_plist() -> PathBuf {
let path = tmp_dir().join("entitlements.plist");
let content = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>"#;
let _ = fs::write(&path, content);
path
}
#[cfg_attr(
not(target_os = "macos"),
allow(unused_variables, clippy::unnecessary_wraps)
)]
pub(crate) fn codesign_bundle(bundle: &str, identity: &str, use_sudo: bool) -> crate::Res {
#[cfg(target_os = "macos")]
{
let production = is_production_identity(identity);
let entitlements = write_entitlements_plist();
let ent_path = entitlements.to_str().unwrap();
let bundle_path = Path::new(bundle);
let sign_one = |target: &str| -> crate::Res {
let mut args: Vec<&str> = vec!["--force", "--sign", identity];
if production {
args.extend_from_slice(&["--options", "runtime", "--timestamp"]);
args.extend_from_slice(&["--entitlements", ent_path]);
}
args.push(target);
run_codesign(&args, use_sudo)
};
if bundle_path.is_dir() {
let mach_os = enumerate_mach_os(bundle_path);
for mach_o in &mach_os {
let mach_o_str = mach_o.to_str().ok_or("Mach-O path is not UTF-8")?;
sign_one(mach_o_str)?;
}
}
sign_one(bundle)?;
if production {
run_codesign(&["--verify", "--strict", bundle], use_sudo)?;
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn is_mach_o_file(path: &Path) -> bool {
let Ok(mut f) = fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 4];
if f.read_exact(&mut buf).is_err() {
return false;
}
let magic_be = u32::from_be_bytes(buf);
matches!(
magic_be,
0xFEED_FACE | 0xFEED_FACF | 0xCEFA_EDFE | 0xCFFA_EDFE | 0xCAFE_BABE | 0xBEBA_FECA )
}
#[cfg(target_os = "macos")]
fn enumerate_mach_os(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
walk_mach_os(dir, &mut out);
out
}
#[cfg(target_os = "macos")]
fn walk_mach_os(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = entry.metadata() else {
continue;
};
if metadata.is_dir() {
walk_mach_os(&path, out);
} else if metadata.is_file() && is_mach_o_file(&path) {
out.push(path);
}
}
}
#[cfg(target_os = "macos")]
pub(crate) fn verify_signed_for_notarization(path: &Path, identity: &str) -> crate::Res {
if !is_production_identity(identity) {
return Ok(());
}
let mach_os = enumerate_mach_os(path);
if mach_os.is_empty() {
return Ok(());
}
let mut failures: Vec<(PathBuf, Vec<String>)> = Vec::new();
for mach_o in &mach_os {
let issues = check_mach_o_signing(mach_o)?;
if !issues.is_empty() {
failures.push((mach_o.clone(), issues));
}
}
if failures.is_empty() {
return Ok(());
}
eprintln!();
eprintln!(
"{} Notarization-readiness check failed for {} Mach-O(s) under {}:",
tag_fail(),
failures.len(),
path.display()
);
for (path, issues) in &failures {
eprintln!(" {}", path.display());
for issue in issues {
eprintln!(" - {issue}");
}
}
eprintln!();
eprintln!(
"These issues mirror Apple's notarization-server checks. \
Submitting now would fail the same way, with a ~6-minute \
round-trip per attempt."
);
Err("notarization-readiness check failed".into())
}
#[cfg(target_os = "macos")]
fn check_mach_o_signing(path: &Path) -> Result<Vec<String>, BoxErr> {
let path_str = path.to_str().ok_or("Mach-O path is not UTF-8")?;
let output = Command::new("codesign")
.args(["-d", "-vvvv", path_str])
.output()?;
let report = String::from_utf8_lossy(&output.stderr);
let mut issues = Vec::new();
if report.contains("code object is not signed at all")
|| report.contains("is not signed at all")
{
issues.push("not signed".to_string());
return Ok(issues);
}
if !report.contains("Authority=Developer ID Application:") {
if report.contains("Signature=adhoc") {
issues.push("ad-hoc signature (not a Developer ID cert)".to_string());
} else {
issues.push("not signed with a Developer ID Application certificate".to_string());
}
}
let has_timestamp = report
.lines()
.any(|l| l.starts_with("Timestamp=") && !l.contains("Timestamp=none"));
if !has_timestamp {
issues.push("missing secure timestamp (--timestamp)".to_string());
}
if !report.contains("(runtime)") {
issues.push("hardened runtime not enabled (--options runtime)".to_string());
}
Ok(issues)
}
#[cfg(target_os = "macos")]
pub(crate) fn locate_wraptool_macos() -> Option<PathBuf> {
if let Ok(p) = which_unix("wraptool") {
return Some(p);
}
for canonical in [
"/Applications/PACEAntiPiracy/Eden/Fusion/Current/bin/wraptool",
"/Applications/PACEAntiPiracy/Eden/Fusion/Versions/5/bin/wraptool",
] {
let p = PathBuf::from(canonical);
if p.exists() {
return Some(p);
}
}
None
}
#[cfg(target_os = "macos")]
pub(crate) fn which_unix(name: &str) -> std::result::Result<PathBuf, std::io::Error> {
let path = std::env::var_os("PATH")
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "PATH not set"))?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
name.to_string(),
))
}
#[cfg(target_os = "macos")]
pub(crate) fn pace_sign_aax_macos(bundle: &Path) -> crate::Res {
let Some(wraptool) = locate_wraptool_macos() else {
eprintln!(
" wraptool not found — AAX bundle is unsigned for PACE. \
Pro Tools Developer will load it; retail Pro Tools won't."
);
return Ok(());
};
let Ok(account) = std::env::var("PACE_ACCOUNT") else {
eprintln!(" PACE_ACCOUNT not set — skipping PACE signing.");
return Ok(());
};
let Ok(signid) = std::env::var("PACE_SIGN_ID") else {
eprintln!(" PACE_SIGN_ID not set — skipping PACE signing.");
return Ok(());
};
eprintln!(" wraptool: PACE-signing {}", bundle.display());
let bundle_str = bundle
.to_str()
.ok_or("AAX bundle path is not valid UTF-8")?;
let status = Command::new(&wraptool)
.args([
"sign",
"--account",
&account,
"--signid",
&signid,
"--allowsigningservice",
"--dsigharden",
"--dsig1-compat",
"off",
"--in",
bundle_str,
"--out",
bundle_str,
])
.status()?;
if !status.success() {
return Err("wraptool failed".into());
}
Ok(())
}
#[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))
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
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()
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
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 {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
let mut it = extra_args.iter();
while let Some(a) = it.next() {
if *a == "--target" {
if let Some(triple) = it.next() {
ensure_rustup_target(triple)?;
}
} else if let Some(triple) = a.strip_prefix("--target=") {
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);
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 = "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(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()
}
#[cfg(target_os = "windows")]
pub(crate) fn which_exe(name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
for dir in env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub(crate) fn find_on_path(name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
let exts: &[&str] = if cfg!(windows) { &["", ".exe"] } else { &[""] };
for dir in env::split_paths(&path) {
for ext in exts {
let mut candidate = dir.join(name);
if !ext.is_empty() {
let mut s = candidate.into_os_string();
s.push(ext);
candidate = PathBuf::from(s);
}
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
#[cfg(target_os = "windows")]
pub(crate) fn locate_cmake() -> Option<PathBuf> {
if let Some(p) = which_exe("cmake.exe") {
return Some(p);
}
for vs_install in vs_install_paths() {
let bundled =
vs_install.join(r"Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe");
if bundled.is_file() {
return Some(bundled);
}
}
for c in [
r"C:\Program Files\CMake\bin\cmake.exe",
r"C:\Program Files (x86)\CMake\bin\cmake.exe",
] {
let p = PathBuf::from(c);
if p.is_file() {
return Some(p);
}
}
None
}
#[cfg(target_os = "windows")]
pub(crate) fn locate_ninja() -> Option<PathBuf> {
if let Some(p) = which_exe("ninja.exe") {
return Some(p);
}
for vs_install in vs_install_paths() {
let bundled =
vs_install.join(r"Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe");
if bundled.is_file() {
return Some(bundled);
}
}
None
}
#[cfg(target_os = "windows")]
pub(crate) fn locate_msvc_cl() -> Option<PathBuf> {
if let Some(p) = which_exe("cl.exe") {
return Some(p);
}
let mut candidates: Vec<(String, PathBuf)> = Vec::new();
for vs_install in vs_install_paths() {
let msvc_root = vs_install.join(r"VC\Tools\MSVC");
let Ok(entries) = fs::read_dir(&msvc_root) else {
continue;
};
for entry in entries.flatten() {
let cl = entry.path().join(r"bin\Hostx64\x64\cl.exe");
if cl.is_file() {
let ver = entry.file_name().to_string_lossy().into_owned();
candidates.push((ver, cl));
}
}
}
candidates.sort_by(|a, b| b.0.cmp(&a.0));
candidates.into_iter().next().map(|(_, p)| p)
}
#[cfg(target_os = "windows")]
pub(crate) fn vs_install_paths() -> Vec<PathBuf> {
let vswhere =
PathBuf::from(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe");
if !vswhere.exists() {
return Vec::new();
}
let out = Command::new(&vswhere)
.args(["-all", "-property", "installationPath", "-format", "value"])
.output();
match out {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(PathBuf::from)
.collect(),
_ => Vec::new(),
}
}
#[cfg(target_os = "windows")]
pub(crate) fn locate_vcvars64() -> Option<PathBuf> {
let vswhere =
PathBuf::from(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe");
if !vswhere.exists() {
return None;
}
let out = Command::new(&vswhere)
.args([
"-latest",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"installationPath",
"-format",
"value",
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let install = String::from_utf8(out.stdout).ok()?;
let install = install.trim();
if install.is_empty() {
return None;
}
let vcvars = PathBuf::from(install).join(r"VC\Auxiliary\Build\vcvars64.bat");
vcvars.exists().then_some(vcvars)
}
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")
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
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: &[&str], 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()),
}
}