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" => {
plugin_filter = Some(crate::util::arg_value(args, &mut i, "-p")?.to_string());
}
"--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 matched = super::pick_plugins(&config, plugin_filter.as_deref())?;
let plugin = *matched.first().ok_or("no plugins in truce.toml")?;
let bin_stem = crate::read_standalone_bin_name(&plugin.crate_name)
.unwrap_or_else(|| format!("{}-standalone", plugin.crate_name));
let bundles_dir = truce_build::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, &bin_stem);
if !built.exists() {
let bin_name = bin_filename(&bin_stem);
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, &bin_stem, &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, &bin_stem);
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, &bin_stem);
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,
bin_stem: &str,
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 = bin_filename(bin_stem);
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))] bin_stem: &str,
) -> PathBuf {
#[cfg(target_os = "macos")]
{
staged
.join("Contents")
.join("MacOS")
.join(bin_filename(bin_stem))
}
#[cfg(not(target_os = "macos"))]
{
staged.to_path_buf()
}
}
fn standalone_built_path(root: &std::path::Path, bin_stem: &str) -> PathBuf {
let profile = if crate::is_debug_profile() {
"debug"
} else {
"release"
};
truce_build::target_dir(root)
.join(profile)
.join(bin_filename(bin_stem))
}
fn bin_filename(stem: &str) -> String {
if cfg!(windows) {
format!("{stem}.exe")
} else {
stem.to_string()
}
}
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")
}
}