#![allow(unused_imports)]
use crate::install_scope::{Format, InstallScope, effective_scope, note_once};
use crate::util::fs_ctx;
use crate::{
Config, PluginDef, Res, cargo_build, codesign_bundle, deployment_target,
detect_default_features, dirs, load_config, project_root, release_lib, run_sudo, tmp_dir,
};
#[cfg(target_os = "windows")]
use crate::{common_program_files, program_files};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) mod aax;
#[cfg(target_os = "macos")]
pub(crate) mod au_v3;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use aax::{emit_aax_bundle, install_aax};
#[cfg(target_os = "macos")]
use au_v3::build_and_install_au_v3;
pub(crate) fn cmd_install(args: &[String]) -> Res {
let config = load_config()?;
let mut clap = false;
let mut vst3 = false;
let mut vst2 = false;
let mut lv2 = false;
let mut au2 = false;
let mut au3 = false;
let mut aax = false;
let mut no_build = false;
let mut shell_mode = false;
let mut debug = false;
let mut plugin_filter: Option<String> = None;
let mut cli_scope: Option<InstallScope> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--clap" => clap = true,
"--vst3" => vst3 = true,
"--vst2" => vst2 = true,
"--lv2" => lv2 = true,
"--au2" => au2 = true,
"--au3" => au3 = true,
"--aax" => aax = true,
"--no-build" => no_build = true,
"--shell" => shell_mode = true,
"--debug" => debug = true,
"--user" => {
if matches!(cli_scope, Some(InstallScope::System)) {
return Err("--user and --system are mutually exclusive".into());
}
cli_scope = Some(InstallScope::User);
}
"--system" => {
if matches!(cli_scope, Some(InstallScope::User)) {
return Err("--user and --system are mutually exclusive".into());
}
cli_scope = Some(InstallScope::System);
}
"--ask" => {
return Err(
"--ask is not valid for `cargo truce install` (no end user to prompt). \
Use --user or --system."
.into(),
);
}
"-p" => {
i += 1;
if i >= args.len() {
return Err(
"-p requires a plugin crate name (e.g. -p truce-example-gain)".into(),
);
}
plugin_filter = Some(args[i].clone());
}
other => return Err(format!("Unknown flag: {other}").into()),
}
i += 1;
}
let scope = cli_scope.unwrap_or_else(InstallScope::os_default);
if !clap && !vst3 && !vst2 && !lv2 && !au2 && !au3 && !aax {
let available = detect_default_features();
clap = available.contains("clap");
vst3 = available.contains("vst3");
vst2 = available.contains("vst2");
lv2 = available.contains("lv2");
au2 = available.contains("au");
au3 = available.contains("au");
aax = available.contains("aax");
}
if shell_mode {
crate::verify_shell_profile_declared()?;
}
if shell_mode && au3 && cfg!(target_os = "macos") {
eprintln!(
"note: AU v3 + --shell is unreliable. The appex sandbox blocks dlopen of \
target/<profile>/lib<crate>.dylib, so hot-reload won't fire. Use --au2 \
for hot-reload iteration; run `cargo truce install --au3` (no --shell) \
for AU v3 smoke tests."
);
}
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 logic_profile = if debug { "debug" } else { "release" };
if shell_mode {
crate::set_build_profile("shell");
} else {
crate::set_debug_profile(debug);
}
let root = project_root();
let dt = &deployment_target();
let mut extra_features = Vec::new();
if shell_mode {
extra_features.push("shell");
unsafe {
std::env::set_var("TRUCE_LOGIC_PROFILE", logic_profile);
}
}
if !no_build {
if clap {
let mut format_features: Vec<&str> = vec!["clap"];
for f in &extra_features {
format_features.push(f);
}
let features_combined = format_features.join(",");
if !extra_features.is_empty() {
let label = extra_features.join(" + ");
crate::vprintln!("Building CLAP ({label})...");
} else {
crate::vprintln!("Building CLAP...");
}
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = Vec::new();
if let Some(n) = p.clap_name.as_deref() {
env_pairs.push(("TRUCE_CLAP_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
&features_combined,
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_clap", p.dylib_stem()));
if src.exists() {
fs_ctx::copy(&src, &dst)?;
}
}
}
if vst3 {
let mut format_features: Vec<&str> = vec!["vst3"];
for f in &extra_features {
format_features.push(f);
}
let features_combined = format_features.join(",");
if !extra_features.is_empty() {
let label = extra_features.join(" + ");
crate::vprintln!("Building VST3 ({label})...");
} else {
crate::vprintln!("Building VST3...");
}
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = Vec::new();
if let Some(n) = p.vst3_name.as_deref() {
env_pairs.push(("TRUCE_VST3_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
&features_combined,
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_vst3", p.dylib_stem()));
if src.exists() {
fs_ctx::copy(&src, &dst)?;
}
}
}
if vst2 {
crate::vprintln!("Building VST2...");
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = Vec::new();
if let Some(n) = p.vst2_name.as_deref() {
env_pairs.push(("TRUCE_VST2_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
"vst2",
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_vst2", p.dylib_stem()));
fs_ctx::copy(&src, &dst)?;
}
}
if lv2 {
crate::vprintln!("Building LV2...");
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = Vec::new();
if let Some(n) = p.lv2_name.as_deref() {
env_pairs.push(("TRUCE_LV2_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
"lv2",
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_lv2", p.dylib_stem()));
fs_ctx::copy(&src, &dst)?;
}
}
if au2 {
#[cfg(target_os = "macos")]
{
crate::vprintln!("Building AU v2...");
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = vec![
("TRUCE_AU_VERSION", "2"),
("TRUCE_AU_PLUGIN_ID", &p.bundle_id),
];
if let Some(n) = p.au_name.as_deref() {
env_pairs.push(("TRUCE_AU_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
"au",
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_au", p.dylib_stem()));
fs_ctx::copy(&src, &dst)?;
}
}
#[cfg(not(target_os = "macos"))]
crate::log_skip(
"AU v2: not supported on this platform. Audio Unit is macOS-only.".to_string(),
);
}
if aax {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
if crate::resolve_aax_sdk_path(&config).is_none() {
let hint = if cfg!(target_os = "windows") {
"[windows].aax_sdk_path"
} else {
"[macos].aax_sdk_path"
};
crate::log_skip(format!(
"AAX: SDK not configured. Set {hint} in truce.toml or the AAX_SDK_PATH env var."
));
} else {
crate::vprintln!("Building AAX...");
for p in &plugins {
let mut env_pairs: Vec<(&str, &str)> = Vec::new();
if let Some(n) = p.aax_name.as_deref() {
env_pairs.push(("TRUCE_AAX_NAME_OVERRIDE", n));
}
cargo_build(
&env_pairs,
&[
"-p",
&p.crate_name,
"--no-default-features",
"--features",
"aax",
],
dt,
)?;
let src = release_lib(&root, &p.dylib_stem());
let dst = release_lib(&root, &format!("{}_aax", p.dylib_stem()));
fs_ctx::copy(&src, &dst)?;
emit_aax_bundle(&root, p, &config, false)?;
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
crate::log_skip(
"AAX: not supported on this platform. Use macOS or Windows to build AAX."
.to_string(),
);
}
if shell_mode {
for p in &plugins {
crate::vprintln!(
"Building {} logic dylib for {}...",
logic_profile,
p.crate_name
);
let mut cmd = Command::new("cargo");
cmd.arg("build").arg("-p").arg(&p.crate_name);
match logic_profile {
"debug" => {} "release" => {
cmd.arg("--release");
}
other => {
cmd.arg("--profile").arg(other);
}
}
#[cfg(target_os = "macos")]
cmd.env("MACOSX_DEPLOYMENT_TARGET", dt);
let status = cmd.status()?;
if !status.success() {
return Err(format!("{logic_profile} build of {} failed", p.crate_name).into());
}
}
}
}
for p in &plugins {
if clap {
let s = scope_for(Format::Clap, scope);
install_clap(&root, p, &config, s)?;
}
if vst3 {
let s = scope_for(Format::Vst3, scope);
install_vst3(&root, p, &config, s)?;
}
if vst2 {
let s = scope_for(Format::Vst2, scope);
install_vst2(&root, p, &config, s)?;
}
if lv2 {
let s = scope_for(Format::Lv2, scope);
install_lv2(&root, p, &config, s)?;
}
if au2 {
#[cfg(target_os = "macos")]
{
let s = scope_for(Format::Au2, scope);
install_au(&root, p, &config, s)?;
}
}
if aax {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
let _ = scope_for(Format::Aax, scope);
install_aax(&root, p, &config)?;
}
}
}
if au3 {
#[cfg(target_os = "macos")]
{
let _ = scope_for(Format::Au3, scope);
build_and_install_au_v3(&root, &config, &plugins, no_build)?;
}
#[cfg(not(target_os = "macos"))]
crate::log_skip(
"AU v3: not supported on this platform. Audio Unit is macOS-only.".to_string(),
);
}
#[cfg(target_os = "macos")]
if au2 {
let cache = dirs::home_dir()
.unwrap()
.join("Library/Caches/AudioUnitCache");
let _ = fs::remove_dir_all(&cache);
crate::vprintln!("Cleared AU cache.");
}
let installed = crate::take_outputs();
if !installed.is_empty() {
eprintln!("\nInstalled:");
for line in installed {
eprintln!(" {line}");
}
}
let skipped = crate::take_skipped();
if !skipped.is_empty() {
eprintln!("\nSkipped:");
for line in skipped {
eprintln!(" {line}");
}
}
eprintln!("\nDone. Restart your DAW to rescan.");
Ok(())
}
fn scope_for(format: Format, requested: InstallScope) -> InstallScope {
let (effective, note) = effective_scope(format, requested);
if let Some(msg) = note {
note_once(msg);
}
effective
}
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
pub(crate) fn install_clap(
root: &Path,
p: &PluginDef,
config: &Config,
scope: InstallScope,
) -> Res {
let dylib = release_lib(root, &format!("{}_clap", p.dylib_stem()));
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let clap_dir = scope.clap_dir();
let dst = clap_dir.join(format!("{}.clap", p.name));
if scope.needs_sudo() {
run_sudo("mkdir", &["-p", clap_dir.to_str().unwrap()])?;
run_sudo("cp", &[dylib.to_str().unwrap(), dst.to_str().unwrap()])?;
} else {
fs_ctx::create_dir_all(&clap_dir)?;
fs_ctx::copy(&dylib, &dst)?;
}
#[cfg(target_os = "macos")]
codesign_bundle(
dst.to_str().unwrap(),
config.macos.application_identity(),
scope.needs_sudo(),
)?;
crate::log_output(format!("CLAP: {}", dst.display()));
Ok(())
}
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
fn install_vst3(root: &Path, p: &PluginDef, config: &Config, scope: InstallScope) -> Res {
let dylib = release_lib(root, &format!("{}_vst3", p.dylib_stem()));
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = scope.vst3_dir().join(format!("{}.vst3", p.name));
#[cfg(target_os = "macos")]
{
let contents = bundle.join("Contents");
let macos_dir = contents.join("MacOS");
let plist = format!(
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>CFBundleExecutable</key>
<string>{name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
let plist_tmp = tmp_dir()
.join(format!("{}_vst3.plist", p.bundle_id))
.to_string_lossy()
.to_string();
fs_ctx::write(&plist_tmp, &plist)?;
if scope.needs_sudo() {
run_sudo("mkdir", &["-p", macos_dir.to_str().unwrap()])?;
run_sudo(
"cp",
&[
dylib.to_str().unwrap(),
&format!("{}/{}", macos_dir.display(), p.name),
],
)?;
run_sudo(
"cp",
&[&plist_tmp, &format!("{}/Info.plist", contents.display())],
)?;
} else {
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&p.name))?;
fs_ctx::copy(&plist_tmp, contents.join("Info.plist"))?;
}
codesign_bundle(
bundle.to_str().unwrap(),
config.macos.application_identity(),
scope.needs_sudo(),
)?;
crate::log_output(format!("VST3: {}", bundle.display()));
}
#[cfg(target_os = "windows")]
{
let arch_dir = bundle.join("Contents").join("x86_64-win");
let dst = arch_dir.join(format!("{}.vst3", p.name));
fs_ctx::create_dir_all(&arch_dir)?;
fs_ctx::copy(&dylib, &dst)?;
crate::log_output(format!("VST3: {}", bundle.display()));
}
#[cfg(target_os = "linux")]
{
let arch_dir = bundle.join("Contents").join("x86_64-linux");
let dst = arch_dir.join(format!("{}.so", p.name));
fs_ctx::create_dir_all(&arch_dir)?;
fs_ctx::copy(&dylib, &dst)?;
crate::log_output(format!("VST3: {}", bundle.display()));
}
Ok(())
}
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
fn install_vst2(root: &Path, p: &PluginDef, config: &Config, scope: InstallScope) -> Res {
let dylib = release_lib(root, &format!("{}_vst2", p.dylib_stem()));
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let vst_dir = scope.vst2_dir();
#[cfg(target_os = "macos")]
{
let bundle = vst_dir.join(format!("{}.vst", p.name));
let macos_dir = bundle.join("Contents/MacOS");
let plist = format!(
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>CFBundleExecutable</key>
<string>{name}</string>
<key>CFBundleIdentifier</key>
<string>com.truce.{bundle_id}.vst2</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
name = p.name,
bundle_id = p.bundle_id,
);
let plist_tmp = tmp_dir()
.join(format!("{}_vst2.plist", p.bundle_id))
.to_string_lossy()
.to_string();
fs_ctx::write(&plist_tmp, &plist)?;
if scope.needs_sudo() {
run_sudo("rm", &["-rf", bundle.to_str().unwrap()])?;
run_sudo("mkdir", &["-p", macos_dir.to_str().unwrap()])?;
run_sudo(
"cp",
&[
dylib.to_str().unwrap(),
&format!("{}/{}", macos_dir.display(), p.name),
],
)?;
run_sudo(
"cp",
&[
&plist_tmp,
&format!("{}/Contents/Info.plist", bundle.display()),
],
)?;
let pkginfo_tmp = tmp_dir().join(format!("{}_vst2.pkginfo", p.bundle_id));
fs_ctx::write(&pkginfo_tmp, "BNDL????")?;
run_sudo(
"cp",
&[
pkginfo_tmp.to_str().unwrap(),
&format!("{}/Contents/PkgInfo", bundle.display()),
],
)?;
} else {
let _ = fs::remove_dir_all(&bundle);
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&p.name))?;
fs_ctx::write(bundle.join("Contents/Info.plist"), &plist)?;
fs_ctx::write(bundle.join("Contents/PkgInfo"), "BNDL????")?;
}
codesign_bundle(
bundle.to_str().unwrap(),
config.macos.application_identity(),
scope.needs_sudo(),
)?;
crate::log_output(format!("VST2: {}", bundle.display()));
}
#[cfg(target_os = "windows")]
{
fs_ctx::create_dir_all(&vst_dir)?;
let dst = vst_dir.join(format!("{}.dll", p.name));
fs_ctx::copy(&dylib, &dst)?;
crate::log_output(format!("VST2: {}", dst.display()));
}
#[cfg(target_os = "linux")]
{
fs_ctx::create_dir_all(&vst_dir)?;
let dst = vst_dir.join(format!("{}.so", p.name));
fs_ctx::copy(&dylib, &dst)?;
crate::log_output(format!("VST2: {}", dst.display()));
}
Ok(())
}
fn install_lv2(root: &Path, p: &PluginDef, _config: &Config, scope: InstallScope) -> Res {
let lv2_dir = scope.lv2_dir();
if scope.needs_sudo() {
run_sudo("mkdir", &["-p", lv2_dir.to_str().unwrap()])?;
} else {
fs_ctx::create_dir_all(&lv2_dir)?;
}
if scope.needs_sudo() {
let staging = tmp_dir().join(format!("{}_lv2_stage", p.bundle_id));
let _ = fs::remove_dir_all(&staging);
fs_ctx::create_dir_all(&staging)?;
crate::commands::package::stage::stage_lv2(root, p, &staging)?;
let slug = crate::commands::package::stage::lv2_slug(&p.name);
let staged_bundle = staging.join(format!("{slug}.lv2"));
let dst_bundle = lv2_dir.join(format!("{slug}.lv2"));
run_sudo("rm", &["-rf", dst_bundle.to_str().unwrap()])?;
run_sudo(
"cp",
&[
"-R",
staged_bundle.to_str().unwrap(),
dst_bundle.to_str().unwrap(),
],
)?;
crate::log_output(format!("LV2: {}", dst_bundle.display()));
} else {
crate::commands::package::stage::stage_lv2(root, p, &lv2_dir)?;
let slug = crate::commands::package::stage::lv2_slug(&p.name);
crate::log_output(format!(
"LV2: {}",
lv2_dir.join(format!("{slug}.lv2")).display()
));
}
Ok(())
}
#[cfg(target_os = "macos")]
fn install_au(root: &Path, p: &PluginDef, config: &Config, scope: InstallScope) -> Res {
let dylib = crate::target_dir(root).join(format!("release/lib{}_au.dylib", p.dylib_stem()));
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = scope.au_v2_dir().join(format!("{}.component", p.name));
let bundle_str = bundle.to_str().unwrap().to_string();
let contents = bundle.join("Contents");
let macos_dir = contents.join("MacOS");
if scope.needs_sudo() {
let _ = run_sudo("rm", &["-rf", &bundle_str]);
run_sudo("mkdir", &["-p", macos_dir.to_str().unwrap()])?;
run_sudo(
"cp",
&[
dylib.to_str().unwrap(),
&format!("{}/{}", macos_dir.display(), p.name),
],
)?;
} else {
let _ = fs::remove_dir_all(&bundle);
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&p.name))?;
}
let plist = format!(
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>CFBundleExecutable</key>
<string>{name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.component</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>AudioComponents</key>
<array>
<dict>
<key>type</key>
<string>{au_type}</string>
<key>subtype</key>
<string>{au_subtype}</string>
<key>manufacturer</key>
<string>{au_mfr}</string>
<key>name</key>
<string>{vendor}: {name}</string>
<key>description</key>
<string>{name}</string>
<key>version</key>
<integer>65536</integer>
<key>factoryFunction</key>
<string>TruceAUFactory</string>
<key>sandboxSafe</key>
<true/>
<key>tags</key>
<array>
<string>{au_tag}</string>
</array>
</dict>
</array>
</dict>
</plist>"#,
name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
vendor = config.vendor.name,
au_type = p.resolved_au_type(),
au_subtype = p.resolved_fourcc(),
au_mfr = config.vendor.au_manufacturer,
au_tag = p.au_tag,
);
let plist_tmp = tmp_dir()
.join(format!("{}_au.plist", p.bundle_id))
.to_string_lossy()
.to_string();
fs_ctx::write(&plist_tmp, &plist)?;
let info_plist = contents.join("Info.plist");
if scope.needs_sudo() {
run_sudo("cp", &[&plist_tmp, info_plist.to_str().unwrap()])?;
} else {
fs_ctx::copy(&plist_tmp, &info_plist)?;
}
codesign_bundle(
&bundle_str,
config.macos.application_identity(),
scope.needs_sudo(),
)?;
crate::log_output(format!("AU: {}", bundle.display()));
Ok(())
}