use crate::util::fs_ctx;
use crate::{Config, PluginDef, Res, cargo_build, release_lib_for_target};
use std::path::Path;
use truce_utils::shell_sidecar::sidecar_path;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum BuildFormat {
Clap,
Vst3,
Vst2,
Lv2,
Au2,
Aax,
}
impl BuildFormat {
pub(crate) fn feature(self) -> &'static str {
match self {
BuildFormat::Clap => "clap",
BuildFormat::Vst3 => "vst3",
BuildFormat::Vst2 => "vst2",
BuildFormat::Lv2 => "lv2",
BuildFormat::Au2 => "au",
BuildFormat::Aax => "aax",
}
}
pub(crate) fn label(self) -> &'static str {
match self {
BuildFormat::Clap => "CLAP",
BuildFormat::Vst3 => "VST3",
BuildFormat::Vst2 => "VST2",
BuildFormat::Lv2 => "LV2",
BuildFormat::Au2 => "AU v2",
BuildFormat::Aax => "AAX",
}
}
pub(crate) fn dylib_suffix(self) -> &'static str {
match self {
BuildFormat::Clap => "_clap",
BuildFormat::Vst3 => "_vst3",
BuildFormat::Vst2 => "_vst2",
BuildFormat::Lv2 => "_lv2",
BuildFormat::Au2 => "_au",
BuildFormat::Aax => "_aax",
}
}
}
#[cfg_attr(
not(any(target_os = "macos", target_os = "windows")),
allow(clippy::unnecessary_wraps)
)]
fn aax_skip_reason(_config: &Config) -> Option<String> {
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
Some("AAX: not supported on this platform. Use macOS or Windows to build AAX.".to_string())
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
if crate::resolve_aax_sdk_path().is_some() {
return None;
}
Some("AAX: SDK not configured. Set AAX_SDK_PATH in .cargo/config.toml [env].".to_string())
}
}
pub(crate) fn build_format_dylibs(
format: BuildFormat,
plugins: &[&PluginDef],
extra_features: &[&str],
config: &Config,
root: &Path,
deployment_target: &str,
target: Option<&str>,
) -> Res {
match format {
BuildFormat::Au2 => {
#[cfg(not(target_os = "macos"))]
{
crate::log_skip(
"AU v2: not supported on this platform. Audio Unit is macOS-only.".to_string(),
);
return Ok(());
}
}
BuildFormat::Aax => {
if let Some(reason) = aax_skip_reason(config) {
crate::log_skip(reason);
return Ok(());
}
}
_ => {}
}
if extra_features.is_empty() {
crate::vprintln!("Building {}...", format.label());
} else {
let extras = extra_features.join(" + ");
crate::vprintln!("Building {} ({extras})...", format.label());
}
let mut format_features: Vec<&str> = vec![format.feature()];
format_features.extend_from_slice(extra_features);
let combined = format_features.join(",");
let batched = format != BuildFormat::Au2;
if batched {
let env_pairs: &[(&str, &str)] = &[];
let mut cargo_args: Vec<String> = Vec::with_capacity(plugins.len() * 2 + 5);
for p in plugins {
cargo_args.push("-p".into());
cargo_args.push(p.crate_name.clone());
}
cargo_args.push("--no-default-features".into());
cargo_args.push("--features".into());
cargo_args.push(combined.clone());
if let Some(t) = target {
cargo_args.push("--target".into());
cargo_args.push(t.into());
}
let cargo_arg_refs: Vec<&str> = cargo_args.iter().map(String::as_str).collect();
cargo_build(env_pairs, &cargo_arg_refs, deployment_target)?;
} else {
for p in plugins {
let env_pairs: &[(&str, &str)] = &[("TRUCE_AU_PLUGIN_ID", p.bundle_id.as_str())];
let mut cargo_args: Vec<String> = Vec::with_capacity(7);
cargo_args.push("-p".into());
cargo_args.push(p.crate_name.clone());
cargo_args.push("--no-default-features".into());
cargo_args.push("--features".into());
cargo_args.push(combined.clone());
if let Some(t) = target {
cargo_args.push("--target".into());
cargo_args.push(t.into());
}
let cargo_arg_refs: Vec<&str> = cargo_args.iter().map(String::as_str).collect();
cargo_build(env_pairs, &cargo_arg_refs, deployment_target)?;
}
}
for p in plugins {
let src = release_lib_for_target(root, &p.dylib_stem(), target);
let dst = release_lib_for_target(
root,
&format!("{}{}", p.dylib_stem(), format.dylib_suffix()),
target,
);
if src.exists() {
fs_ctx::copy(&src, &dst)?;
}
#[cfg(target_os = "macos")]
if matches!(
format,
BuildFormat::Clap | BuildFormat::Vst3 | BuildFormat::Vst2
) {
link_macos_bundle_for_plugin(root, p, format, target)?;
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
if format == BuildFormat::Aax {
crate::commands::install::aax::emit_aax_bundle(root, p, config, false)?;
}
}
Ok(())
}
pub(crate) fn build_logic_dylibs(
plugins: &[&PluginDef],
logic_profile: &str,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] deployment_target: &str,
) -> Res {
use std::process::Command;
let root = crate::project_root();
for p in plugins {
crate::vprintln!(
"Building {} logic dylib for {}...",
logic_profile,
p.crate_name
);
let mut cmd = Command::new("cargo");
cmd.arg("build").arg("-p").arg(&p.crate_name);
match logic_profile {
"debug" => {} "release" => {
cmd.arg("--release");
}
other => {
cmd.arg("--profile").arg(other);
}
}
#[cfg(target_os = "macos")]
cmd.env("MACOSX_DEPLOYMENT_TARGET", deployment_target);
if let Some(wrapper) = crate::util::sccache_wrapper() {
cmd.env("RUSTC_WRAPPER", wrapper);
}
let status = cmd.status()?;
if !status.success() {
return Err(format!("{logic_profile} build of {} failed", p.crate_name).into());
}
write_shell_sidecar(&root, &p.crate_name, logic_profile)?;
}
Ok(())
}
fn write_shell_sidecar(root: &std::path::Path, crate_name: &str, logic_profile: &str) -> Res {
use std::fs;
let stem = crate_name.replace('-', "_");
let dylib_path = truce_build::target_dir(root)
.join(logic_profile)
.join(crate::util::shared_lib_name(&stem));
let canonical = dylib_path.canonicalize().unwrap_or(dylib_path);
let sidecar = sidecar_path(crate_name).ok_or_else(|| -> crate::CargoTruceError {
"could not resolve $HOME (or %USERPROFILE% on Windows) for the \
shell sidecar - the runtime needs $HOME to locate the logic \
dylib without it"
.into()
})?;
if let Some(parent) = sidecar.parent() {
fs::create_dir_all(parent).map_err(|e| -> crate::CargoTruceError {
format!("failed to create {}: {e}", parent.display()).into()
})?;
}
let tmp = sidecar.with_extension(format!("path.tmp.{}", std::process::id()));
fs::write(&tmp, format!("{}\n", canonical.display())).map_err(
|e| -> crate::CargoTruceError {
format!("failed to write shell sidecar {}: {e}", tmp.display()).into()
},
)?;
fs::rename(&tmp, &sidecar).map_err(|e| -> crate::CargoTruceError {
let _ = fs::remove_file(&tmp);
format!(
"failed to rename {} -> {}: {e}",
tmp.display(),
sidecar.display()
)
.into()
})?;
crate::vprintln!(
"Wrote shell sidecar {} -> {}",
sidecar.display(),
canonical.display(),
);
Ok(())
}
#[cfg(target_os = "macos")]
fn link_macos_bundle_for_plugin(
root: &Path,
p: &PluginDef,
format: BuildFormat,
target: Option<&str>,
) -> Res {
let staticlib = crate::release_static_for_target(root, &p.dylib_stem(), target);
if !staticlib.exists() {
return Err(crate::missing_staticlib_error(&staticlib).into());
}
let arch = match target {
Some(t) if t.starts_with("aarch64") => crate::MacArch::Arm64,
Some(t) if t.starts_with("x86_64") => crate::MacArch::X86_64,
Some(other) => {
return Err(
format!("unrecognized cargo target triple {other} for macOS bundle link").into(),
);
}
None => crate::MacArch::host(),
};
let exports = match format {
BuildFormat::Clap => crate::CLAP_EXPORTS,
BuildFormat::Vst3 => crate::VST3_EXPORTS,
BuildFormat::Vst2 => crate::VST2_EXPORTS,
_ => unreachable!("caller gates on bundle formats"),
};
let out = crate::release_bundle_bin(root, &p.dylib_stem(), format.dylib_suffix());
crate::link_macos_bundle(
&[(arch, staticlib)],
exports,
&crate::deployment_target(),
&out,
)?;
Ok(())
}