use crate::util::{fs_ctx, parse_target_cpu_arg};
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 target_cpu_arg: Option<String> = None;
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,
"--target-cpu" => {
target_cpu_arg =
Some(crate::util::arg_value(args, &mut i, "--target-cpu")?.to_string());
}
"--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 target_cpu = target_cpu_arg
.as_deref()
.map(parse_target_cpu_arg)
.unwrap_or_default();
crate::set_target_cpu(target_cpu);
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.file_stem()));
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)?;
let icon = plugin
.windows_icon
.as_ref()
.map(|s| project_root().join(s))
.filter(|p| p.exists());
if let Some(icon) = &icon {
crate::windows_manifest::embed_icon(&staged, icon)?;
}
}
let exec_path = exec_path_inside_stage(&staged, &bin_stem);
super::install::presets::emit_standalone_factory(&root, plugin, &config, &exec_path)?;
}
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 macos = staged.join("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 icon_present = if let Some(icon_rel) = &plugin.macos_icon {
let icon_src = crate::project_root().join(icon_rel);
if !icon_src.exists() {
return Err(format!(
"macos_icon for `{}` points to {} but no file is there.",
plugin.name,
icon_src.display()
)
.into());
}
let resources_dir = staged.join("Contents").join("Resources");
fs_ctx::create_dir_all(&resources_dir)?;
fs_ctx::copy(&icon_src, resources_dir.join("icon.icns"))?;
true
} else {
false
};
crate::commands::package::stage::write_standalone_info_plist(
staged,
plugin,
&exe_name,
vendor,
icon_present,
)
}
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]
[--target-cpu <value>] [-- <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`).
x86_64 builds default to `-C target-cpu=x86-64-v3` (AVX2 + FMA + BMI2);
override with `--target-cpu` (see `cargo truce build --help` for the
full value list).
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).
--target-cpu <value>
Override the x86_64 default. baseline|v2|v3|v4|native
or any literal rustc target-cpu name.
-h, --help Show this message."
);
}
fn standalone_bundle_name(plugin_name: &str) -> String {
if cfg!(target_os = "macos") {
format!("{plugin_name}.app")
} else if cfg!(windows) {
format!("{plugin_name}.standalone.exe")
} else {
format!("{plugin_name}.standalone")
}
}