use crate::util::fs_ctx;
use crate::{Res, cargo_build, deployment_target, load_config, project_root};
use std::path::PathBuf;
use std::process::Command;
pub(crate) fn cmd_run(args: &[String]) -> Res {
let config = load_config()?;
let root = project_root();
let dt = &deployment_target();
let mut plugin_filter: Option<String> = None;
let mut no_build = false;
let mut debug = false;
let mut extra_args: Vec<String> = Vec::new();
let mut past_separator = false;
let mut i = 0;
while i < args.len() {
if past_separator {
extra_args.push(args[i].clone());
} else {
match args[i].as_str() {
"-p" => {
i += 1;
plugin_filter = Some(
args.get(i)
.cloned()
.ok_or("-p requires a plugin crate name")?,
);
}
"--no-build" => no_build = true,
"--debug" => debug = true,
"--help" | "-h" => {
print_help();
return Ok(());
}
"--" => past_separator = true,
other => return Err(format!("unknown flag: {other}").into()),
}
}
i += 1;
}
crate::set_debug_profile(debug);
let plugin = if let Some(ref f) = plugin_filter {
config
.plugin
.iter()
.find(|p| p.crate_name == *f)
.ok_or_else(|| {
format!(
"No plugin with crate name '{f}'. Available: {}",
config
.plugin
.iter()
.map(|p| p.crate_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?
} else {
config.plugin.first().ok_or("no plugins in truce.toml")?
};
let bundles_dir = crate::target_dir(&root).join("bundles");
fs_ctx::create_dir_all(&bundles_dir)?;
let staged = bundles_dir.join(standalone_bundle_name(&plugin.name));
if !no_build {
eprintln!("Building {} standalone...", plugin.name);
cargo_build(
&[],
&["-p", &plugin.crate_name, "--features", "standalone"],
dt,
)?;
let built = standalone_built_path(&root, &plugin.crate_name);
if !built.exists() {
let bin_name = standalone_bin_name(&plugin.crate_name);
return Err(format!(
"standalone binary not found at {}. \
Does your plugin have a [[bin]] target named '{bin_name}'?",
built.display()
)
.into());
}
#[cfg(target_os = "macos")]
stage_macos_app_bundle(&built, &staged, plugin, &config.vendor)?;
#[cfg(not(target_os = "macos"))]
{
fs_ctx::copy(&built, &staged)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let exec_path = exec_path_inside_stage(&staged, plugin);
let mut perms = std::fs::metadata(&exec_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&exec_path, perms)?;
}
#[cfg(target_os = "windows")]
crate::windows_manifest::embed_dpi_manifest(&staged)?;
}
if !staged.exists() {
return Err(format!(
"standalone bundle missing at {}. Drop `--no-build` to build it.",
staged.display()
)
.into());
}
let exec_path = exec_path_inside_stage(&staged, plugin);
eprintln!("Running {}...", exec_path.display());
let status = Command::new(&exec_path).args(&extra_args).status()?;
if !status.success() {
return Err(format!("{} exited with {status}", exec_path.display()).into());
}
Ok(())
}
#[cfg(target_os = "macos")]
fn stage_macos_app_bundle(
built: &std::path::Path,
staged: &std::path::Path,
plugin: &crate::config::PluginDef,
vendor: &crate::config::VendorConfig,
) -> Res {
let _ = std::fs::remove_dir_all(staged);
let contents = staged.join("Contents");
let macos = contents.join("MacOS");
fs_ctx::create_dir_all(&macos)?;
let exe_name = standalone_bin_name(&plugin.crate_name);
fs_ctx::copy(built, macos.join(&exe_name))?;
let mic_usage = format!(
"{} would like to use the microphone for plugin audio input.",
plugin.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>CFBundleName</key>
<string>{name}</string>
<key>CFBundleDisplayName</key>
<string>{name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.standalone</string>
<key>CFBundleExecutable</key>
<string>{exe}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>{mic_usage}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.music</string>
</dict>
</plist>
"#,
name = plugin.name,
vendor_id = vendor.id,
bundle_id = plugin.bundle_id,
exe = exe_name,
mic_usage = mic_usage,
);
fs_ctx::write(contents.join("Info.plist"), plist)?;
Ok(())
}
fn exec_path_inside_stage(
staged: &std::path::Path,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
plugin: &crate::config::PluginDef,
) -> PathBuf {
#[cfg(target_os = "macos")]
{
staged
.join("Contents")
.join("MacOS")
.join(standalone_bin_name(&plugin.crate_name))
}
#[cfg(not(target_os = "macos"))]
{
staged.to_path_buf()
}
}
fn standalone_built_path(root: &std::path::Path, crate_name: &str) -> PathBuf {
let profile = if crate::is_debug_profile() {
"debug"
} else {
"release"
};
crate::target_dir(root)
.join(profile)
.join(standalone_bin_name(crate_name))
}
fn standalone_bin_name(crate_name: &str) -> String {
if cfg!(windows) {
format!("{crate_name}-standalone.exe")
} else {
format!("{crate_name}-standalone")
}
}
fn print_help() {
eprintln!(
"\
Usage: cargo truce run [-p <crate>] [--no-build] [--debug] [-- <args>]
Build and run a plugin standalone. Pass `--debug` for a faster-compile
dev-profile build (fine when iterating outside a DAW); release otherwise.
Anything after `--` is forwarded verbatim to the standalone binary
(e.g. `cargo truce run -- --headless --bpm 140`).
Options:
-p <crate> Build and run only the plugin with this cargo crate name.
--no-build Skip rebuild; run the existing staged binary.
--debug Cargo dev profile (faster compile).
-h, --help Show this message."
);
}
fn standalone_bundle_name(plugin_name: &str) -> String {
if cfg!(target_os = "macos") {
format!("{plugin_name}.standalone.app")
} else if cfg!(windows) {
format!("{plugin_name}.standalone.exe")
} else {
format!("{plugin_name}.standalone")
}
}