use crate::format::Format;
use crate::install_scope::{InstallScope, effective_scope, note_once, set_cli_install_scope};
use crate::util::fs_ctx;
use crate::{
Config, PluginDef, Res, deployment_target, detect_default_features, load_config, project_root,
release_lib, run_sudo, tmp_dir,
};
#[cfg(target_os = "macos")]
use crate::{codesign_bundle, dirs};
use std::fs;
use std::path::Path;
#[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::install_aax;
#[cfg(target_os = "macos")]
use au_v3::build_and_install_au_v3;
#[allow(clippy::too_many_lines)]
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" => set_cli_install_scope(&mut cli_scope, InstallScope::User)?,
"--system" => set_cli_install_scope(&mut cli_scope, InstallScope::System)?,
"--ask" => {
return Err(
"--ask is not valid for `cargo truce install` (no end user to prompt). \
Use --user or --system."
.into(),
);
}
"-p" => {
plugin_filter = Some(crate::util::arg_value(args, &mut i, "-p")?.to_string());
}
"--help" | "-h" => {
print_help();
return Ok(());
}
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> = super::pick_plugins(&config, plugin_filter.as_deref())?;
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");
}
if !no_build {
use super::build_dylibs::{BuildFormat, build_format_dylibs, build_logic_dylibs};
let format_selection: &[(bool, BuildFormat)] = &[
(clap, BuildFormat::Clap),
(vst3, BuildFormat::Vst3),
(vst2, BuildFormat::Vst2),
(lv2, BuildFormat::Lv2),
(au2, BuildFormat::Au2),
(aax, BuildFormat::Aax),
];
for &(selected, format) in format_selection {
if selected {
build_format_dylibs(format, &plugins, &extra_features, &config, &root, dt)?;
}
}
if shell_mode {
build_logic_dylibs(&plugins, logic_profile, dt)?;
}
}
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 Some(home) = dirs::home_dir() {
let cache = home.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 print_help() {
eprintln!(
"\
Usage: cargo truce install [--clap] [--vst3] [--vst2] [--lv2] [--au2] [--au3] [--aax]
[--user|--system] [--shell] [--debug] [--no-build] [-p <crate>]
Build and install plugins into the host's plug-in directories. Defaults
to release. Defaults to whichever formats are in the plugin's Cargo.toml
default features (typically clap + vst3).
Per-format scope is per-user by default; pass --system for the shared
system directories. AAX and AU v3 are always system-scope.
Options:
--clap CLAP only
--vst3 VST3 only
--vst2 VST2 only (legacy format)
--lv2 LV2 only
--au2 AU v2 only (.component, macOS only)
--au3 AU v3 only (.appex, macOS only)
--aax AAX only (requires pre-built template)
--user Install per-user (default).
--system Install system-wide (sudo / admin required).
--shell Build dynamic shells + per-plugin logic dylibs.
--debug Cargo dev profile (faster compile, slower DSP).
--no-build Skip build, install existing artifacts.
-p <crate> Install only the plugin with this cargo crate name.
-h, --help Show this message"
);
}
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>{vendor_id}.{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,
vendor_id = config.vendor.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 =
truce_build::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(())
}