#[cfg(target_os = "windows")]
use crate::commands::package::windows;
use crate::format::Format;
use crate::install_scope::InstallScope;
#[cfg(target_os = "macos")]
use crate::locate_wraptool_macos;
use crate::read_build_env;
#[cfg(target_os = "macos")]
use crate::rustup_has_target;
use crate::{
Res, check_cmd, dirs, find_on_path, load_config, project_root, resolve_aax_sdk_path, tag_fail,
tag_info, tag_ok, tag_warn,
};
#[cfg(target_os = "windows")]
use crate::{common_program_files, locate_cmake, locate_msvc_cl, locate_ninja, which_exe};
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
fn print_help() {
eprintln!(
"\
Usage: cargo truce doctor
Run environment diagnostics: Rust toolchain, code-signing identities,
AAX SDK detection, installed-plugin scan. Prints a summary report and
exits 0; investigates rather than fixes. Run when something feels
broken about your machine setup before reaching for a build.
Options:
-h, --help Show this message."
);
}
#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
pub(crate) fn cmd_doctor(args: &[String]) -> Res {
if args.iter().any(|a| a == "--help" || a == "-h") {
print_help();
return Ok(());
}
if let Some(unknown) = args.iter().find(|a| !a.is_empty()) {
return Err(format!("unknown flag: {unknown}").into());
}
eprintln!("truce doctor");
eprintln!("─────────────────────────────────────────");
eprintln!();
let root = project_root();
eprintln!(" Toolchain");
check_cmd("rustc", &[OsStr::new("--version")], "rustc");
check_cmd("cargo", &[OsStr::new("--version")], "cargo");
if root.join("rust-toolchain.toml").exists() {
eprintln!(" {} rust-toolchain.toml present", tag_ok());
}
match crate::util::sccache_wrapper() {
Some(p) => eprintln!(
" {} sccache active at {} - caches rustc invocations across builds",
tag_ok(),
p.to_string_lossy()
),
None if env::var_os("RUSTC_WRAPPER").is_some()
|| env::var_os("RUSTC_WORKSPACE_WRAPPER").is_some() =>
{
eprintln!(
" {} RUSTC_WRAPPER already set - skipping sccache auto-detect",
tag_info()
);
}
None => eprintln!(
" {} sccache not on PATH - optional. `brew install sccache` (or cargo install) caches rustc \
invocations at the input-hash level and speeds up env-flipping rebuilds",
tag_info()
),
}
#[cfg(target_os = "macos")]
{
eprintln!();
eprintln!(" macOS");
check_cmd("xcode-select", &[OsStr::new("-p")], "Xcode CLI tools");
check_cmd(
"xcodebuild",
&[OsStr::new("-version")],
"xcodebuild (AU v3)",
);
check_cmd("codesign", &[OsStr::new("--help")], "codesign");
match locate_wraptool_macos() {
Some(p) => eprintln!(" {} wraptool (PACE) at {}", tag_ok(), p.display()),
None => eprintln!(
" {} wraptool not found - only needed for signed AAX builds. \
Install Eden via the iLok License Manager, then optionally \
`sudo ln -s /Applications/PACEAntiPiracy/Eden/Fusion/Current/bin/wraptool /usr/local/bin/wraptool`",
tag_info()
),
}
let has_x64 = rustup_has_target("x86_64-apple-darwin");
let has_arm = rustup_has_target("aarch64-apple-darwin");
match (has_x64, has_arm) {
(true, true) => eprintln!(
" {} Rust targets: x86_64-apple-darwin + aarch64-apple-darwin - `cargo truce package` will produce universal Mach-O binaries",
tag_ok()
),
(false, true) => eprintln!(
" {} Rust target x86_64-apple-darwin missing - run: rustup target add x86_64-apple-darwin (or pass `--host-only` to skip)",
tag_warn()
),
(true, false) => eprintln!(
" {} Rust target aarch64-apple-darwin missing - run: rustup target add aarch64-apple-darwin (or pass `--host-only` to skip)",
tag_warn()
),
(false, false) => eprintln!(
" {} No Apple Rust targets installed - run: rustup target add x86_64-apple-darwin aarch64-apple-darwin (or pass `--host-only` to skip)",
tag_warn()
),
}
}
#[cfg(target_os = "windows")]
{
eprintln!();
eprintln!(" Windows");
match locate_cmake() {
Some(p) => eprintln!(
" {} cmake (AAX template build): {}",
tag_ok(),
p.display()
),
None => eprintln!(
" {} cmake.exe not found - install cmake or VS \"C++ CMake tools\"",
tag_fail()
),
}
match locate_ninja() {
Some(p) => eprintln!(
" {} ninja (AAX template build): {}",
tag_ok(),
p.display()
),
None => eprintln!(
" {} ninja.exe not found - install ninja or VS \"C++ CMake tools\"",
tag_fail()
),
}
eprintln!();
eprintln!(" Packaging (Windows)");
windows::doctor();
}
eprintln!();
eprintln!(" Compilers");
#[cfg(not(target_os = "windows"))]
{
check_cmd("cc", &[OsStr::new("--version")], "C compiler");
check_cmd(
"c++",
&[OsStr::new("--version")],
"C++ compiler (VST3 shim)",
);
}
#[cfg(target_os = "windows")]
{
if which_exe("cl.exe").is_some() {
check_cmd("cl", &[OsStr::new("/?")], "MSVC compiler (in current PATH)");
} else {
match locate_msvc_cl() {
Some(p) => eprintln!(
" {} MSVC compiler at {} (not in PATH - Rust/CMake auto-discover it via vswhere)",
tag_ok(),
p.display()
),
None => eprintln!(
" {} MSVC compiler: not found - install VS \"Desktop development with C++\" workload",
tag_fail()
),
}
}
}
eprintln!();
eprintln!(" Validation Tools");
#[cfg(target_os = "macos")]
check_cmd("auval", &[OsStr::new("-h")], "auval");
check_which_with_env("pluginval", Some("PLUGINVAL"));
check_which_with_env("clap-validator", Some("CLAP_VALIDATOR"));
eprintln!();
eprintln!(" Configuration");
if root.join("truce.toml").exists() {
match load_config() {
Ok(c) => eprintln!(
" {} truce.toml: {} plugins configured",
tag_ok(),
c.plugin.len(),
),
Err(e) => eprintln!(" {} truce.toml parse error: {e}", tag_fail()),
}
} else {
eprintln!(" {} truce.toml not found", tag_fail());
}
eprintln!();
eprintln!(" SDKs");
let aax_sdk = resolve_aax_sdk_path();
if let Some(p) = aax_sdk {
eprintln!(" {} AAX SDK at {}", tag_ok(), p.display());
} else {
eprintln!(
" {} AAX SDK not configured (set AAX_SDK_PATH env var, e.g. in .cargo/config.toml [env])",
tag_warn()
);
}
eprintln!();
eprintln!(" Plugin install paths");
show_scope_paths();
eprintln!();
eprintln!("─────────────────────────────────────────");
Ok(())
}
#[derive(Clone, Copy)]
struct PathFormat {
format: Format,
ext: &'static str,
}
const PATH_FORMATS_MACOS: &[PathFormat] = &[
PathFormat {
format: Format::Clap,
ext: "clap",
},
PathFormat {
format: Format::Vst3,
ext: "vst3",
},
PathFormat {
format: Format::Vst2,
ext: "vst",
},
PathFormat {
format: Format::Lv2,
ext: "lv2",
},
PathFormat {
format: Format::Au2,
ext: "component",
},
];
const PATH_FORMATS_WINDOWS: &[PathFormat] = &[
PathFormat {
format: Format::Clap,
ext: "clap",
},
PathFormat {
format: Format::Vst3,
ext: "vst3",
},
PathFormat {
format: Format::Vst2,
ext: "dll",
},
PathFormat {
format: Format::Lv2,
ext: "lv2",
},
];
const PATH_FORMATS_LINUX: &[PathFormat] = &[
PathFormat {
format: Format::Clap,
ext: "clap",
},
PathFormat {
format: Format::Vst3,
ext: "vst3",
},
PathFormat {
format: Format::Vst2,
ext: "so",
},
PathFormat {
format: Format::Lv2,
ext: "lv2",
},
];
fn show_scope_paths() {
let formats: &[PathFormat] = if cfg!(target_os = "macos") {
PATH_FORMATS_MACOS
} else if cfg!(target_os = "windows") {
PATH_FORMATS_WINDOWS
} else {
PATH_FORMATS_LINUX
};
for f in formats {
let Some(user_path) = f.format.dir(InstallScope::User) else {
continue;
};
let Some(system_path) = f.format.dir(InstallScope::System) else {
continue;
};
report_scope_line(f, "user", InstallScope::User, &user_path);
if user_path != system_path {
report_scope_line(f, "system", InstallScope::System, &system_path);
}
}
#[cfg(target_os = "macos")]
{
report_fixed(
"AAX",
"system",
true,
Path::new("/Library/Application Support/Avid/Audio/Plug-Ins"),
"aaxplugin",
);
report_fixed(
"AU v3",
"system",
true,
Path::new("/Applications"),
"",
);
}
#[cfg(target_os = "windows")]
{
let aax_dir = common_program_files()
.join("Avid")
.join("Audio")
.join("Plug-Ins");
report_fixed("AAX", "system", true, &aax_dir, "aaxplugin");
}
}
fn report_scope_line(f: &PathFormat, scope_label: &str, scope: InstallScope, path: &Path) {
let label = format!("{} {}:", f.format.label(), scope_label);
report_path_line(&label, scope.needs_sudo(), path, f.ext);
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn report_fixed(format_label: &str, scope_label: &str, needs_sudo: bool, path: &Path, ext: &str) {
let label = format!("{format_label} {scope_label}:");
report_path_line(&label, needs_sudo, path, ext);
}
fn report_path_line(label: &str, needs_sudo: bool, path: &Path, ext: &str) {
if !path.exists() {
eprintln!(
" {label:<14} {}{}- not present",
display_path(path),
spacer(path)
);
return;
}
let count_str = if ext.is_empty() {
String::new()
} else {
let count = count_bundles_with_ext(path, ext);
let plural = if count == 1 { "" } else { "s" };
format!(" ({count} plug-in{plural})")
};
let state = if needs_sudo {
format!("{} needs sudo{count_str}", tag_warn())
} else if path_is_writable(path) {
format!("{} writable{count_str}", tag_ok())
} else {
format!("{} not writable{count_str}", tag_warn())
};
eprintln!(
" {label:<14} {}{}{state}",
display_path(path),
spacer(path)
);
}
fn spacer(path: &Path) -> String {
let width = display_path(path).chars().count();
let target = 46usize;
if width < target {
" ".repeat(target - width)
} else {
" ".to_string()
}
}
fn display_path(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(rel) = path.strip_prefix(&home)
{
return format!("~/{}", rel.display());
}
path.display().to_string()
}
fn count_bundles_with_ext(dir: &Path, ext: &str) -> usize {
let Ok(entries) = fs::read_dir(dir) else {
return 0;
};
let want = format!(".{ext}");
entries
.flatten()
.filter(|e| {
e.file_name()
.to_string_lossy()
.to_lowercase()
.ends_with(&want.to_lowercase())
})
.count()
}
fn path_is_writable(dir: &Path) -> bool {
let probe = dir.join(format!(".truce-doctor-write-probe-{}", std::process::id()));
match fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&probe)
{
Ok(_) => {
let _ = fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
fn check_which_with_env(name: &str, env_var: Option<&str>) {
if let Some(var) = env_var
&& let Some(path) = read_build_env(var)
{
let p = PathBuf::from(&path);
if p.is_file() {
eprintln!(" {} {name}: {path} (via ${var})", tag_ok());
return;
}
eprintln!(
" {} {name}: ${var}={path} but file not found - falling back to $PATH",
tag_warn()
);
}
if let Some(path) = find_on_path(name) {
eprintln!(" {} {name}: {}", tag_ok(), path.display());
} else {
let hint = env_var
.map(|v| format!(" (or set ${v} in shell or .cargo/config.toml [env])"))
.unwrap_or_default();
eprintln!(" {} {name}: not found{hint}", tag_warn());
}
}