use crate::install_scope::InstallScope;
#[cfg(target_os = "macos")]
use crate::{deployment_target, project_root};
use crate::{dirs, load_config, tag_warn, tmp_dir, PluginDef, Res};
use std::fs;
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
use std::process::Command;
fn warn_on_scope_collision(format: &str, user_path: &Path, system_path: &Path) {
if user_path.exists() && system_path.exists() {
eprintln!(" {} {format} installed in both scopes:", tag_warn());
eprintln!(" • user: {}", user_path.display());
eprintln!(" • system: {}", system_path.display());
eprintln!(
" Hosts pick one at scan time; remove the stale copy with \
`cargo truce remove --user` or `--system`."
);
}
}
#[cfg(target_os = "macos")]
fn plist_extract(plist: &Path, key_path: &str) -> Option<String> {
let out = Command::new("plutil")
.args(["-extract", key_path, "raw", "-o", "-", plist.to_str()?])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() {
None
} else {
Some(s)
}
}
#[cfg(target_os = "macos")]
fn find_au_collisions(au_type: &str, subtype: &str, manufacturer: &str) -> Vec<PathBuf> {
let mut hits = Vec::new();
let home = dirs::home_dir().unwrap_or_default();
for apps_dir in [PathBuf::from("/Applications"), home.join("Applications")] {
let Ok(apps) = fs::read_dir(&apps_dir) else {
continue;
};
for app in apps.flatten() {
let plugins_dir = app.path().join("Contents/PlugIns");
let Ok(appexes) = fs::read_dir(&plugins_dir) else {
continue;
};
for appex in appexes.flatten() {
let plist = appex.path().join("Contents/Info.plist");
if matches_au_v3_descriptor(&plist, au_type, subtype, manufacturer) {
hits.push(appex.path());
}
}
}
}
for dir in [
PathBuf::from("/Library/Audio/Plug-Ins/Components"),
home.join("Library/Audio/Plug-Ins/Components"),
] {
let Ok(comps) = fs::read_dir(&dir) else {
continue;
};
for comp in comps.flatten() {
let plist = comp.path().join("Contents/Info.plist");
if matches_au_v2_descriptor(&plist, au_type, subtype, manufacturer) {
hits.push(comp.path());
}
}
}
hits
}
#[cfg(target_os = "macos")]
fn matches_au_v3_descriptor(
plist: &Path,
au_type: &str,
subtype: &str,
manufacturer: &str,
) -> bool {
if !plist.is_file() {
return false;
}
let prefix = "NSExtension.NSExtensionAttributes.AudioComponents.0";
plist_extract(plist, &format!("{prefix}.subtype")).as_deref() == Some(subtype)
&& plist_extract(plist, &format!("{prefix}.type")).as_deref() == Some(au_type)
&& plist_extract(plist, &format!("{prefix}.manufacturer")).as_deref() == Some(manufacturer)
}
#[cfg(target_os = "macos")]
fn matches_au_v2_descriptor(
plist: &Path,
au_type: &str,
subtype: &str,
manufacturer: &str,
) -> bool {
if !plist.is_file() {
return false;
}
plist_extract(plist, "AudioComponents.0.subtype").as_deref() == Some(subtype)
&& plist_extract(plist, "AudioComponents.0.type").as_deref() == Some(au_type)
&& plist_extract(plist, "AudioComponents.0.manufacturer").as_deref() == Some(manufacturer)
}
#[cfg(target_os = "macos")]
fn warn_on_au_collision(au_type: &str, subtype: &str, manufacturer: &str, expected: &Path) {
let hits = find_au_collisions(au_type, subtype, manufacturer);
if hits.len() <= 1 {
return;
}
eprintln!(
" {} collision: {} other bundle(s) also claim {}/{}/{}",
tag_warn(),
hits.len() - 1,
au_type,
subtype,
manufacturer,
);
for h in &hits {
let marker = if h.starts_with(expected) || expected.starts_with(h) {
"← expected"
} else {
"← stale, remove this"
};
eprintln!(" • {} {}", h.display(), marker);
}
eprintln!(" macOS will pick one at load time; the rest are shadowed.");
}
pub(crate) fn cmd_validate(args: &[String]) -> Res {
let config = load_config()?;
let mut run_auval = false;
let mut run_auval_v3 = false;
let mut run_pluginval = false;
let mut run_clap = false;
let mut run_vst2 = false;
let mut plugin_filter: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--auval" => run_auval = true,
"--auval3" => run_auval_v3 = true,
"--pluginval" => run_pluginval = true,
"--clap" => run_clap = true,
"--vst2" => run_vst2 = true,
"--all" => {
run_auval = true;
run_auval_v3 = true;
run_pluginval = true;
run_clap = true;
run_vst2 = true;
}
"-p" => {
i += 1;
if i >= args.len() {
return Err("-p requires a plugin crate name".into());
}
plugin_filter = Some(args[i].clone());
}
other => return Err(format!("Unknown flag: {other}").into()),
}
i += 1;
}
if !run_auval && !run_auval_v3 && !run_pluginval && !run_clap && !run_vst2 {
run_auval = true;
run_auval_v3 = true;
run_pluginval = true;
run_clap = true;
run_vst2 = true;
}
let plugins: Vec<&PluginDef> = if let Some(ref filter) = plugin_filter {
let matched: Vec<_> = config
.plugin
.iter()
.filter(|p| p.crate_name == *filter)
.collect();
if matched.is_empty() {
return Err(format!(
"No plugin with crate name '{filter}'. Available: {}",
config
.plugin
.iter()
.map(|p| p.crate_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
.into());
}
matched
} else {
config.plugin.iter().collect()
};
let mut failures = 0;
if run_auval {
eprintln!("=== auval (AU v2) ===\n");
if Command::new("auval").arg("-h").output().is_ok() {
for p in &plugins {
#[cfg(target_os = "macos")]
{
let user_path = InstallScope::User
.au_v2_dir()
.join(format!("{}.component", p.name));
let system_path = InstallScope::System
.au_v2_dir()
.join(format!("{}.component", p.name));
warn_on_scope_collision("AU v2", &user_path, &system_path);
}
eprint!(
" {} ({} {} {}) ... ",
p.name,
p.resolved_au_type(),
p.resolved_fourcc(),
config.vendor.au_manufacturer
);
let output = Command::new("auval")
.args([
"-v",
p.resolved_au_type(),
p.resolved_fourcc(),
&config.vendor.au_manufacturer,
])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("VALIDATION SUCCEEDED") {
eprintln!("PASS");
} else {
eprintln!("FAIL");
#[cfg(target_os = "macos")]
{
let expected = PathBuf::from(format!(
"/Library/Audio/Plug-Ins/Components/{}.component",
p.name
));
warn_on_au_collision(
p.resolved_au_type(),
p.resolved_fourcc(),
&config.vendor.au_manufacturer,
&expected,
);
}
failures += 1;
}
}
} else {
eprintln!(" auval not found (macOS only)");
}
}
if run_auval_v3 {
eprintln!("\n=== auval (AU v3) ===\n");
if Command::new("auval").arg("-h").output().is_ok() {
for p in &plugins {
let sub = p.au3_sub();
eprint!(
" {} ({} {} {}) ... ",
p.name,
p.resolved_au_type(),
sub,
config.vendor.au_manufacturer
);
let output = Command::new("auval")
.args([
"-v",
p.resolved_au_type(),
sub,
&config.vendor.au_manufacturer,
])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("VALIDATION SUCCEEDED") {
eprintln!("PASS");
} else {
eprintln!("FAIL");
#[cfg(target_os = "macos")]
{
let expected = PathBuf::from(format!(
"/Applications/{}.app/Contents/PlugIns/AUExt.appex",
p.au3_app_name()
));
warn_on_au_collision(
p.resolved_au_type(),
sub,
&config.vendor.au_manufacturer,
&expected,
);
}
failures += 1;
}
}
} else {
eprintln!(" auval not found (macOS only)");
}
}
if run_pluginval {
eprintln!("\n=== pluginval (VST3) ===\n");
let pluginval = find_pluginval();
if let Some(pv) = pluginval {
for p in &plugins {
let user_path = InstallScope::User
.vst3_dir()
.join(format!("{}.vst3", p.name));
let system_path = InstallScope::System
.vst3_dir()
.join(format!("{}.vst3", p.name));
let validate_path = if system_path.exists() {
system_path.clone()
} else if user_path.exists() {
user_path.clone()
} else {
eprintln!(" {} ... SKIP (not installed)", p.name);
continue;
};
eprint!(" {} ... ", p.name);
let output = Command::new(&pv)
.args([
"--validate",
validate_path.to_str().unwrap(),
"--strictness-level",
"5",
])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("SUCCESS") || output.status.success() {
eprintln!("PASS");
} else {
eprintln!("FAIL");
failures += 1;
}
warn_on_scope_collision("VST3", &user_path, &system_path);
}
} else {
eprintln!(" pluginval not found. Install from https://github.com/Tracktion/pluginval");
}
}
if run_clap {
eprintln!("\n=== clap-validator (CLAP) ===\n");
let clap_validator = find_clap_validator();
if let Some(cv) = clap_validator {
let scratch = tmp_dir().join("clap-validate");
let _ = fs::create_dir_all(&scratch);
for p in &plugins {
let clap_name = format!("{}.clap", p.name);
let user_path = InstallScope::User.clap_dir().join(&clap_name);
let system_path = InstallScope::System.clap_dir().join(&clap_name);
let installed = if user_path.exists() {
user_path.clone()
} else {
system_path.clone()
};
if !installed.exists() {
eprintln!(" {} ... SKIP (not installed)", p.name);
continue;
}
let validate_path = if installed.join("Contents/MacOS").is_dir() {
installed.clone()
} else {
let bundle = scratch.join(&clap_name);
let macos = bundle.join("Contents/MacOS");
let _ = fs::create_dir_all(&macos);
let bin_name = clap_name.trim_end_matches(".clap");
let _ = fs::copy(&installed, macos.join(bin_name));
bundle
};
eprint!(" {} ... ", p.name);
let output = Command::new(&cv)
.args(["validate", &validate_path.to_string_lossy()])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}{}", stdout, stderr);
if output.status.success() && !combined.contains("FAILED") {
let passed = combined.matches("passed").count();
eprintln!("PASS ({} tests)", passed);
} else {
eprintln!("FAIL");
if !stdout.is_empty() {
eprintln!("{}", stdout);
}
if !stderr.is_empty() {
eprintln!("{}", stderr);
}
failures += 1;
}
warn_on_scope_collision("CLAP", &user_path, &system_path);
}
let _ = fs::remove_dir_all(&scratch);
} else {
eprintln!(" clap-validator not found.");
eprintln!(
" Install: cargo install --git https://github.com/free-audio/clap-validator"
);
eprintln!(" Or set CLAP_VALIDATOR=/path/to/clap-validator");
}
}
if run_vst2 {
eprintln!("=== VST2 binary smoke ===\n");
#[cfg(target_os = "macos")]
{
failures += validate_vst2_macos(&plugins);
}
#[cfg(not(target_os = "macos"))]
{
eprintln!(" Skipping: VST2 binary smoke is currently macOS-only.");
let _ = &plugins;
}
}
eprintln!();
if failures > 0 {
Err(format!("{failures} validation(s) failed").into())
} else {
eprintln!("All validations passed.");
Ok(())
}
}
#[cfg(target_os = "macos")]
fn validate_vst2_macos(plugins: &[&PluginDef]) -> usize {
let root = project_root();
let test_src = root.join("crates/truce-vst2/validate/binary_smoke.c");
if !test_src.exists() {
eprintln!(
" Skipping: smoke source missing at {}.",
test_src.display()
);
return 0;
}
let test_bin = crate::target_dir(&root).join("vst2_binary_smoke");
let cc_status = match Command::new("cc")
.args(["-o", test_bin.to_str().unwrap(), test_src.to_str().unwrap()])
.status()
{
Ok(s) => s,
Err(e) => {
eprintln!(" Skipping: failed to invoke cc: {e}");
return 0;
}
};
if !cc_status.success() {
eprintln!(" Skipping: cc failed to build the smoke binary.");
return 0;
}
let mut failures = 0;
for p in plugins {
let user_path = InstallScope::User
.vst2_dir()
.join(format!("{}.vst", p.name));
let system_path = InstallScope::System
.vst2_dir()
.join(format!("{}.vst", p.name));
warn_on_scope_collision("VST2", &user_path, &system_path);
eprint!(" {} ... ", p.name);
let build = Command::new("cargo")
.args([
"build",
"--release",
"-p",
&p.crate_name,
"--no-default-features",
"--features",
"vst2",
])
.env("MACOSX_DEPLOYMENT_TARGET", deployment_target())
.output();
let build = match build {
Ok(o) => o,
Err(e) => {
eprintln!("BUILD ERROR ({e})");
failures += 1;
continue;
}
};
if !build.status.success() {
eprintln!("BUILD FAILED");
eprint!("{}", String::from_utf8_lossy(&build.stderr));
failures += 1;
continue;
}
let dylib = crate::target_dir(&root).join(format!("release/lib{}.dylib", p.dylib_stem()));
let kind_flag: Option<&str> = match p.resolved_au_type() {
"aumu" => Some("--synth"),
"aumi" => Some("--midi-effect"),
_ => None,
};
let mut cmd = Command::new(test_bin.to_str().unwrap());
cmd.arg(dylib.to_str().unwrap());
if let Some(flag) = kind_flag {
cmd.arg(flag);
}
match cmd.output() {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
if out.status.success() {
if let Some(line) = stdout.lines().last() {
eprintln!("{line}");
} else {
eprintln!("PASS");
}
} else {
eprintln!("FAIL");
eprint!("{stdout}");
failures += 1;
}
}
Err(e) => {
eprintln!("INVOKE ERROR ({e})");
failures += 1;
}
}
}
failures
}
fn find_pluginval() -> Option<String> {
let candidates = [
"/Applications/pluginval.app/Contents/MacOS/pluginval",
"/usr/local/bin/pluginval",
];
for c in candidates {
if Path::new(c).exists() {
return Some(c.to_string());
}
}
if Command::new("pluginval").arg("--help").output().is_ok() {
return Some("pluginval".to_string());
}
None
}
fn find_clap_validator() -> Option<String> {
if let Ok(path) = std::env::var("CLAP_VALIDATOR") {
if Path::new(&path).exists() {
return Some(path);
}
}
if Command::new("clap-validator")
.arg("--version")
.output()
.is_ok()
{
return Some("clap-validator".to_string());
}
if let Some(home) = dirs::home_dir() {
let cargo_bin = home.join(".cargo/bin/clap-validator");
if cargo_bin.exists() {
return Some(cargo_bin.to_string_lossy().into());
}
}
None
}