use crate::format::Format;
use crate::install_scope::{InstallScope, effective_scope, note_once, set_cli_install_scope};
use crate::util::{fs_ctx, parse_target_cpu_arg};
use crate::{
Config, PluginDef, Res, deployment_target, detect_default_features, load_config, project_root,
};
#[cfg(target_os = "macos")]
use crate::{run_sudo, tmp_lv2};
#[cfg(not(target_os = "macos"))]
use crate::release_lib;
#[cfg(target_os = "macos")]
use crate::tmp_manifests;
#[cfg(target_os = "macos")]
use crate::{codesign_bundle, dirs};
#[cfg(target_os = "macos")]
use std::ffi::OsStr;
#[cfg(target_os = "macos")]
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_ios;
#[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 ios = false;
let mut ios_device = false;
let mut no_build = false;
let mut shell_mode = false;
let mut debug = false;
let mut target_cpu_arg: Option<String> = None;
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,
"--ios" => ios = true,
"--ios-device" => {
ios = true;
ios_device = true;
}
"--no-build" => no_build = true,
"--shell" => shell_mode = true,
"--debug" => debug = true,
"--target-cpu" => {
target_cpu_arg =
Some(crate::util::arg_value(args, &mut i, "--target-cpu")?.to_string());
}
"--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;
}
if ios {
#[cfg(target_os = "macos")]
{
let target = if ios_device {
au_ios::IosTarget::Device
} else {
au_ios::IosTarget::Simulator
};
return install_ios(plugin_filter.as_deref(), target);
}
#[cfg(not(target_os = "macos"))]
{
let _ = ios_device;
return Err("iOS plugins build only on macOS (Xcode-only).".into());
}
}
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 target_cpu = target_cpu_arg
.as_deref()
.map(parse_target_cpu_arg)
.unwrap_or_default();
crate::set_target_cpu(target_cpu);
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, None)?;
}
}
if shell_mode {
build_logic_dylibs(&plugins, logic_profile, dt)?;
}
}
for p in &plugins {
if clap {
let s = scope_for(Format::Clap, cli_scope);
install_clap(&root, p, &config, s)?;
}
if vst3 {
let s = scope_for(Format::Vst3, cli_scope);
install_vst3(&root, p, &config, s)?;
}
if vst2 {
let s = scope_for(Format::Vst2, cli_scope);
install_vst2(&root, p, &config, s)?;
}
if lv2 {
let s = scope_for(Format::Lv2, cli_scope);
install_lv2(&root, p, &config, s)?;
}
if au2 {
#[cfg(target_os = "macos")]
{
let s = scope_for(Format::Au2, cli_scope);
install_au(&root, p, &config, s)?;
}
}
if aax {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
let _ = scope_for(Format::Aax, cli_scope);
install_aax(&root, p, &config)?;
}
}
}
if au3 {
#[cfg(target_os = "macos")]
{
let _ = scope_for(Format::Au3, cli_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]
[--ios|--ios-device]
[--user|--system] [--shell] [--debug] [--no-build] [-p <crate>]
[--target-cpu <value>]
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. VST3 on
Windows defaults to system scope (the directory every commercial host
scans); pass --user for the per-user `%LOCALAPPDATA%\\Programs\\Common\\VST3`
location.
x86_64 builds default to `-C target-cpu=x86-64-v3` (AVX2 + FMA + BMI2)
so `wide`'s compile-time SIMD dispatch picks the wider path. aarch64
builds use NEON unconditionally and get no extra flag. Override with
`--target-cpu`.
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)
--ios AUv3 on the booted iOS Simulator (ad-hoc-signed)
--ios-device AUv3 on a tethered iOS device (needs team ID +
provisioning profile in .cargo/config.toml [env])
--user Install per-user (default; exception: VST3 on Windows
defaults to system - pass --user to override).
--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.
--target-cpu <value>
Override the x86_64 default. Accepted values:
baseline no flag (rustc default = x86-64 / SSE2)
v2|v3|v4 x86-64-v<N> (v3 is the implicit default)
native -C target-cpu=native (local-dev only;
won't run on machines without the
build host's exact feature set)
<literal> passed verbatim to rustc (apple-m1, znver4)
-h, --help Show this message"
);
}
fn scope_for(format: Format, requested: Option<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 {
#[cfg(not(target_os = "macos"))]
let dylib = release_lib(root, &format!("{}_clap", p.dylib_stem()));
#[cfg(target_os = "macos")]
let dylib = crate::release_bundle_bin(root, &p.dylib_stem(), "_clap");
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let clap_dir = scope.clap_dir();
let bundle = clap_dir.join(format!("{}.clap", p.file_stem()));
#[cfg(target_os = "macos")]
{
let contents = bundle.join("Contents");
let macos_dir = contents.join("MacOS");
let exec_name = p.file_stem();
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
let plist_tmp = tmp_manifests()
.join(format!("{}_clap.plist", p.bundle_id))
.to_string_lossy()
.to_string();
fs_ctx::write(&plist_tmp, &plist)?;
if scope.needs_sudo() {
if bundle.exists() && !bundle.is_dir() {
run_sudo("rm", &[OsStr::new("-f"), bundle.as_os_str()])?;
}
run_sudo("mkdir", &[OsStr::new("-p"), macos_dir.as_os_str()])?;
let dst_dylib = macos_dir.join(&exec_name);
run_sudo("cp", &[dylib.as_os_str(), dst_dylib.as_os_str()])?;
let dst_plist = contents.join("Info.plist");
run_sudo("cp", &[OsStr::new(&plist_tmp), dst_plist.as_os_str()])?;
} else {
if bundle.exists() && !bundle.is_dir() {
fs::remove_file(&bundle)?;
}
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&exec_name))?;
fs_ctx::copy(&plist_tmp, contents.join("Info.plist"))?;
}
codesign_bundle(
bundle.to_str().unwrap(),
&crate::application_identity(),
scope.needs_sudo(),
)?;
}
#[cfg(not(target_os = "macos"))]
{
fs_ctx::create_dir_all(&clap_dir)?;
fs_ctx::copy(&dylib, &bundle)?;
}
crate::log_output(format!("CLAP: {}", bundle.display()));
Ok(())
}
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
fn install_vst3(root: &Path, p: &PluginDef, config: &Config, scope: InstallScope) -> Res {
#[cfg(not(target_os = "macos"))]
let dylib = release_lib(root, &format!("{}_vst3", p.dylib_stem()));
#[cfg(target_os = "macos")]
let dylib = crate::release_bundle_bin(root, &p.dylib_stem(), "_vst3");
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = scope.vst3_dir().join(format!("{}.vst3", p.file_stem()));
#[cfg(target_os = "macos")]
{
let contents = bundle.join("Contents");
let macos_dir = contents.join("MacOS");
let exec_name = p.file_stem();
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
let plist_tmp = tmp_manifests()
.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", &[OsStr::new("-p"), macos_dir.as_os_str()])?;
let dst_dylib = macos_dir.join(&exec_name);
run_sudo("cp", &[dylib.as_os_str(), dst_dylib.as_os_str()])?;
let dst_plist = contents.join("Info.plist");
run_sudo("cp", &[OsStr::new(&plist_tmp), dst_plist.as_os_str()])?;
} else {
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&exec_name))?;
fs_ctx::copy(&plist_tmp, contents.join("Info.plist"))?;
}
codesign_bundle(
bundle.to_str().unwrap(),
&crate::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.file_stem()));
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.file_stem()));
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 {
#[cfg(not(target_os = "macos"))]
let dylib = release_lib(root, &format!("{}_vst2", p.dylib_stem()));
#[cfg(target_os = "macos")]
let dylib = crate::release_bundle_bin(root, &p.dylib_stem(), "_vst2");
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.file_stem()));
let macos_dir = bundle.join("Contents/MacOS");
let exec_name = p.file_stem();
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.vst2</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
let plist_tmp = tmp_manifests()
.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", &[OsStr::new("-rf"), bundle.as_os_str()])?;
run_sudo("mkdir", &[OsStr::new("-p"), macos_dir.as_os_str()])?;
let dst_dylib = macos_dir.join(&exec_name);
run_sudo("cp", &[dylib.as_os_str(), dst_dylib.as_os_str()])?;
let dst_plist = bundle.join("Contents/Info.plist");
run_sudo("cp", &[OsStr::new(&plist_tmp), dst_plist.as_os_str()])?;
let pkginfo_tmp = tmp_manifests().join(format!("{}_vst2.pkginfo", p.bundle_id));
fs_ctx::write(&pkginfo_tmp, "BNDL????")?;
let dst_pkginfo = bundle.join("Contents/PkgInfo");
run_sudo("cp", &[pkginfo_tmp.as_os_str(), dst_pkginfo.as_os_str()])?;
} else {
let _ = fs::remove_dir_all(&bundle);
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&exec_name))?;
fs_ctx::write(bundle.join("Contents/Info.plist"), &plist)?;
fs_ctx::write(bundle.join("Contents/PkgInfo"), "BNDL????")?;
}
codesign_bundle(
bundle.to_str().unwrap(),
&crate::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.file_stem()));
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.file_stem()));
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();
#[cfg(target_os = "macos")]
if scope.needs_sudo() {
run_sudo("mkdir", &[OsStr::new("-p"), lv2_dir.as_os_str()])?;
let staging = tmp_lv2(&p.bundle_id);
let _ = fs::remove_dir_all(&staging);
fs_ctx::create_dir_all(&staging)?;
crate::commands::package::stage::stage_lv2(
root,
p,
&staging,
&crate::application_identity(),
None,
)?;
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", &[OsStr::new("-rf"), dst_bundle.as_os_str()])?;
run_sudo(
"cp",
&[
OsStr::new("-R"),
staged_bundle.as_os_str(),
dst_bundle.as_os_str(),
],
)?;
crate::log_output(format!("LV2: {}", dst_bundle.display()));
return Ok(());
}
fs_ctx::create_dir_all(&lv2_dir)?;
crate::commands::package::stage::stage_lv2(
root,
p,
&lv2_dir,
&crate::application_identity(),
None,
)?;
let slug = crate::commands::package::stage::lv2_slug(&p.name);
crate::log_output(format!(
"LV2: {}",
lv2_dir.join(format!("{slug}.lv2")).display()
));
let _ = scope;
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.file_stem()));
let bundle_str = bundle.to_str().unwrap().to_string();
let contents = bundle.join("Contents");
let macos_dir = contents.join("MacOS");
let exec_name = p.file_stem();
if scope.needs_sudo() {
let _ = run_sudo("rm", &[OsStr::new("-rf"), bundle.as_os_str()]);
run_sudo("mkdir", &[OsStr::new("-p"), macos_dir.as_os_str()])?;
let dst_dylib = macos_dir.join(&exec_name);
run_sudo("cp", &[dylib.as_os_str(), dst_dylib.as_os_str()])?;
} else {
let _ = fs::remove_dir_all(&bundle);
fs_ctx::create_dir_all(&macos_dir)?;
fs_ctx::copy(&dylib, macos_dir.join(&exec_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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.component</string>
<key>CFBundleName</key>
<string>{display_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}: {display_name}</string>
<key>description</key>
<string>{display_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>"#,
display_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_manifests()
.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", &[OsStr::new(&plist_tmp), info_plist.as_os_str()])?;
} else {
fs_ctx::copy(&plist_tmp, &info_plist)?;
}
codesign_bundle(
&bundle_str,
&crate::application_identity(),
scope.needs_sudo(),
)?;
crate::log_output(format!("AU: {}", bundle.display()));
Ok(())
}
#[cfg(target_os = "macos")]
fn install_ios(plugin_filter: Option<&str>, target: au_ios::IosTarget) -> Res {
let root = crate::project_root();
let config = crate::load_config()?;
let plugins = super::pick_plugins(&config, plugin_filter)?;
let total = plugins.len();
for (i, p) in plugins.into_iter().enumerate() {
if total > 1 {
eprintln!("==> [{}/{total}] {}", i + 1, p.crate_name);
}
au_ios::install_one(&root, p, target)?;
}
Ok(())
}