use crate::templates;
#[cfg(target_os = "windows")]
use crate::tmp_scripts;
use crate::util::fs_ctx;
use crate::{Config, PluginDef, Res, release_lib, resolve_aax_sdk_path, tmp_aax_template};
#[cfg(target_os = "macos")]
use crate::{codesign_bundle, run_sudo};
#[cfg(target_os = "windows")]
use crate::{common_program_files, locate_cmake, locate_ninja, locate_vcvars64};
#[cfg(target_os = "macos")]
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[allow(clippy::too_many_lines)]
pub(crate) fn build_aax_template(_root: &Path, sdk_path: &Path, universal_mac: bool) -> Res {
#[cfg(target_os = "windows")]
let _ = universal_mac;
#[cfg(not(target_os = "windows"))]
let memo_key = format!("{}|{}", sdk_path.display(), universal_mac);
#[cfg(target_os = "windows")]
let memo_key = sdk_path.display().to_string();
{
use std::sync::{Mutex, OnceLock};
static MEMO: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
let set = MEMO.get_or_init(|| Mutex::new(std::collections::HashSet::default()));
if set.lock().is_ok_and(|s| s.contains(&memo_key)) {
return Ok(());
}
let _ = set.lock().map(|mut s| s.insert(memo_key.clone()));
}
#[cfg(not(target_os = "windows"))]
let aax_lib_path = ensure_aax_sdk_library(sdk_path, universal_mac)?;
#[cfg(target_os = "windows")]
let aax_lib_path = ensure_aax_sdk_library(sdk_path)?;
let template_dir = tmp_aax_template();
let src_dir = template_dir.join("src");
let cmake_lists = template_dir.join("CMakeLists.txt");
fs_ctx::create_dir_all(&src_dir)?;
fs_ctx::write_if_changed(&cmake_lists, templates::aax::CMAKE_LISTS)?;
fs_ctx::write_if_changed(
src_dir.join("TruceAAX_Bridge.cpp"),
templates::aax::BRIDGE_CPP,
)?;
fs_ctx::write_if_changed(src_dir.join("TruceAAX_Bridge.h"), templates::aax::BRIDGE_H)?;
fs_ctx::write_if_changed(
src_dir.join("TruceAAX_Describe.cpp"),
templates::aax::DESCRIBE_CPP,
)?;
fs_ctx::write_if_changed(src_dir.join("TruceAAX_GUI.cpp"), templates::aax::GUI_CPP)?;
fs_ctx::write_if_changed(src_dir.join("TruceAAX_GUI.h"), templates::aax::GUI_H)?;
fs_ctx::write_if_changed(
src_dir.join("TruceAAX_Parameters.cpp"),
templates::aax::PARAMETERS_CPP,
)?;
fs_ctx::write_if_changed(
src_dir.join("TruceAAX_Parameters.h"),
templates::aax::PARAMETERS_H,
)?;
fs_ctx::write_if_changed(src_dir.join("Info.plist.in"), templates::aax::INFO_PLIST_IN)?;
fs_ctx::write_if_changed(
src_dir.join("truce_aax_bridge.h"),
templates::aax::BRIDGE_HEADER,
)?;
let build_dir = template_dir.join("build");
#[cfg(not(target_os = "windows"))]
{
let mut configure = Command::new("cmake");
configure
.arg("-B")
.arg(&build_dir)
.arg(format!("-DAAX_SDK_PATH={}", sdk_path.display()))
.arg(format!("-DAAX_LIB_PATH={}", aax_lib_path.display()));
if universal_mac {
configure.arg("-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64");
}
let status = configure.current_dir(&template_dir).status()?;
if !status.success() {
return Err("cmake configure failed for AAX template".into());
}
let status = Command::new("cmake")
.arg("--build")
.arg(&build_dir)
.status()?;
if !status.success() {
return Err("cmake build failed for AAX template".into());
}
}
#[cfg(target_os = "windows")]
{
let vcvars = locate_vcvars64()
.ok_or("could not locate vcvars64.bat — install VS 2022+ with the C++ workload")?;
let cmake = locate_cmake().ok_or(
"could not locate cmake.exe — install cmake or the VS \"C++ CMake tools\" component",
)?;
let ninja = locate_ninja()
.ok_or("could not locate ninja.exe — install ninja or the VS \"C++ CMake tools\" component (which bundles it)")?;
let cmake_dir = cmake.parent().unwrap().display().to_string();
let ninja_dir = ninja.parent().unwrap().display().to_string();
let to_fwd = |p: &Path| p.display().to_string().replace('\\', "/");
let bat_path = tmp_scripts().join("truce_aax_build.bat");
let bat = format!(
"@echo off\r\n\
call \"{vcvars}\" >nul || exit /b 1\r\n\
set \"PATH={cmake_dir};{ninja_dir};%PATH%\"\r\n\
cmake -S \"{src}\" -B \"{build}\" -G Ninja -DCMAKE_BUILD_TYPE=Release \"-DAAX_SDK_PATH={sdk}\" \"-DAAX_LIB_PATH={lib}\" || exit /b 1\r\n\
cmake --build \"{build}\" || exit /b 1\r\n",
vcvars = vcvars.display(),
cmake_dir = cmake_dir,
ninja_dir = ninja_dir,
src = to_fwd(&template_dir),
build = to_fwd(&build_dir),
sdk = to_fwd(sdk_path),
lib = to_fwd(&aax_lib_path),
);
fs_ctx::write(&bat_path, bat)?;
let status = Command::new("cmd").arg("/c").arg(&bat_path).status()?;
if !status.success() {
return Err("AAX cmake+ninja build failed".into());
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn ensure_aax_sdk_library(sdk_path: &Path, universal_mac: bool) -> Result<PathBuf, crate::BoxErr> {
let build_dir = sdk_path.join("build-truce");
let lib_path = build_dir.join("Libs/AAXLibrary/libAAXLibrary.a");
let required_archs: &[&str] = if universal_mac {
&["arm64", "x86_64"]
} else if cfg!(target_arch = "aarch64") {
&["arm64"]
} else {
&["x86_64"]
};
if lib_path.exists() && lipo_has_archs(&lib_path, required_archs) {
return Ok(lib_path);
}
let _ = fs::remove_dir_all(&build_dir);
crate::vprintln!(
"AAX: building SDK library ({}) at {}",
required_archs.join("+"),
lib_path.display()
);
let osx_arches = required_archs.join(";");
let status = Command::new("cmake")
.arg("-S")
.arg(sdk_path)
.arg("-B")
.arg(&build_dir)
.arg("-DCMAKE_BUILD_TYPE=Release")
.arg(format!("-DCMAKE_OSX_ARCHITECTURES={osx_arches}"))
.arg("-DAAX_BUILD_EXAMPLES=OFF")
.arg("-DAAX_BUILD_PTSL_EXAMPLES=OFF")
.arg("-DAAX_BUILD_JUCE_GUI_EXTENSION=OFF")
.status()?;
if !status.success() {
return Err("cmake configure failed for AAX SDK library".into());
}
let status = Command::new("cmake")
.arg("--build")
.arg(&build_dir)
.arg("--target")
.arg("AAXLibrary")
.status()?;
if !status.success() {
return Err("cmake build failed for AAX SDK library".into());
}
if !lib_path.exists() {
return Err(format!(
"cmake succeeded but libAAXLibrary.a is missing at {}",
lib_path.display()
)
.into());
}
if !lipo_has_archs(&lib_path, required_archs) {
return Err(format!(
"built libAAXLibrary.a at {} does not cover required archs {required_archs:?}",
lib_path.display()
)
.into());
}
Ok(lib_path)
}
#[cfg(target_os = "windows")]
fn ensure_aax_sdk_library(sdk_path: &Path) -> Result<PathBuf, crate::BoxErr> {
let build_dir = sdk_path.join("build-truce");
let lib_path = build_dir.join("Libs/AAXLibrary/AAXLibrary.lib");
if lib_path.exists() {
return Ok(lib_path);
}
crate::vprintln!("AAX: building SDK library at {}", lib_path.display());
let vcvars = locate_vcvars64()
.ok_or("could not locate vcvars64.bat — install VS 2022+ with the C++ workload")?;
let cmake = locate_cmake().ok_or(
"could not locate cmake.exe — install cmake or the VS \"C++ CMake tools\" component",
)?;
let ninja = locate_ninja().ok_or(
"could not locate ninja.exe — install ninja or the VS \"C++ CMake tools\" component",
)?;
let cmake_dir = cmake.parent().unwrap().display().to_string();
let ninja_dir = ninja.parent().unwrap().display().to_string();
let to_fwd = |p: &Path| p.display().to_string().replace('\\', "/");
let bat_path = tmp_scripts().join("truce_aax_sdk_build.bat");
let bat = format!(
"@echo off\r\n\
call \"{vcvars}\" >nul || exit /b 1\r\n\
set \"PATH={cmake_dir};{ninja_dir};%PATH%\"\r\n\
cmake -S \"{src}\" -B \"{build}\" -G Ninja -DCMAKE_BUILD_TYPE=Release -DAAX_BUILD_EXAMPLES=OFF -DAAX_BUILD_PTSL_EXAMPLES=OFF -DAAX_BUILD_JUCE_GUI_EXTENSION=OFF || exit /b 1\r\n\
cmake --build \"{build}\" --target AAXLibrary || exit /b 1\r\n",
vcvars = vcvars.display(),
cmake_dir = cmake_dir,
ninja_dir = ninja_dir,
src = to_fwd(sdk_path),
build = to_fwd(&build_dir),
);
fs_ctx::write(&bat_path, bat)?;
let status = Command::new("cmd").arg("/c").arg(&bat_path).status()?;
if !status.success() {
return Err("AAX SDK cmake+ninja build failed".into());
}
if !lib_path.exists() {
return Err(format!(
"cmake succeeded but AAXLibrary.lib is missing at {}",
lib_path.display()
)
.into());
}
Ok(lib_path)
}
#[cfg(target_os = "macos")]
fn lipo_has_archs(lib: &Path, required: &[&str]) -> bool {
let out = match Command::new("lipo").arg("-info").arg(lib).output() {
Ok(o) if o.status.success() => o.stdout,
_ => return false,
};
let text = String::from_utf8_lossy(&out);
required.iter().all(|a| text.contains(a))
}
#[cfg(target_os = "macos")]
fn template_binary() -> PathBuf {
tmp_aax_template().join("build/TruceAAXTemplate.aaxplugin/Contents/MacOS/TruceAAXTemplate")
}
#[cfg(target_os = "windows")]
fn template_binary() -> PathBuf {
tmp_aax_template().join("build/TruceAAXTemplate.aaxplugin")
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn ensure_template(root: &Path, universal_mac: bool) -> Result<Option<PathBuf>, crate::BoxErr> {
let template = template_binary();
if let Some(sdk_path) = resolve_aax_sdk_path() {
if !template.exists() {
crate::vprintln!("AAX: building template with SDK at {}", sdk_path.display());
}
build_aax_template(root, &sdk_path, universal_mac)?;
} else if !template.exists() {
return Ok(None);
}
if !template.exists() {
return Err(format!(
"AAX template build succeeded but binary not found at {}",
template.display()
)
.into());
}
Ok(Some(template))
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub(crate) fn emit_aax_bundle(
_root: &Path,
_p: &PluginDef,
_config: &Config,
_universal_mac: bool,
) -> Res {
crate::log_skip(
"AAX: not supported on this platform. Use macOS or Windows to build AAX.".to_string(),
);
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn emit_aax_bundle(
root: &Path,
p: &PluginDef,
_config: &Config,
universal_mac: bool,
) -> Res {
let Some(template) = ensure_template(root, universal_mac)? else {
crate::log_skip(format!(
"AAX: skipped {} — SDK not configured. \
Set AAX_SDK_PATH in .cargo/config.toml [env].",
p.name
));
return Ok(());
};
let dylib = release_lib(root, &format!("{}_aax", p.dylib_stem()));
if !dylib.exists() {
crate::log_skip(format!(
"AAX: build artifact missing for {} at {}. \
Re-run `cargo truce build --aax -p {}`.",
p.name,
dylib.display(),
p.crate_name,
));
return Ok(());
}
let bundles_dir = truce_build::target_dir(root).join("bundles");
fs_ctx::create_dir_all(&bundles_dir)?;
let bundle = bundles_dir.join(format!("{}.aaxplugin", p.name));
let _ = fs::remove_dir_all(&bundle);
#[cfg(target_os = "macos")]
{
let contents = bundle.join("Contents");
fs_ctx::create_dir_all(contents.join("MacOS"))?;
fs_ctx::create_dir_all(contents.join("Resources"))?;
fs_ctx::copy(&template, contents.join("MacOS").join(&p.name))?;
fs_ctx::copy(
&dylib,
contents
.join("Resources")
.join(format!("lib{}_aax.dylib", p.dylib_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>{name}</string>
<key>CFBundleIdentifier</key>
<string>com.truce.{bundle_id}.aax</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundlePackageType</key>
<string>TDMw</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
name = p.name,
bundle_id = p.bundle_id,
);
fs_ctx::write(contents.join("Info.plist"), plist)?;
codesign_bundle(
bundle.to_str().unwrap(),
&crate::application_identity(),
false,
)?;
crate::vprintln!(" AAX: {}", bundle.display());
}
#[cfg(target_os = "windows")]
{
let contents = bundle.join("Contents");
let x64_dir = contents.join("x64");
let resources_dir = contents.join("Resources");
fs_ctx::create_dir_all(&x64_dir)?;
fs_ctx::create_dir_all(&resources_dir)?;
fs_ctx::copy(&template, x64_dir.join(format!("{}.aaxplugin", p.name)))?;
fs_ctx::copy(
&dylib,
resources_dir.join(format!("{}_aax.dll", p.dylib_stem())),
)?;
crate::vprintln!(" AAX: {}", bundle.display());
}
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub(crate) fn install_aax(_root: &Path, _p: &PluginDef, _config: &Config) -> Res {
crate::log_skip(
"AAX: not supported on this platform. Use macOS or Windows to install AAX.".to_string(),
);
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(crate) fn install_aax(root: &Path, p: &PluginDef, _config: &Config) -> Res {
let bundle_name = format!("{}.aaxplugin", p.name);
let built = truce_build::target_dir(root)
.join("bundles")
.join(&bundle_name);
if !built.exists() {
if resolve_aax_sdk_path().is_none() {
return Ok(());
}
return Err(format!(
"AAX: bundle missing for {} at {}. Run `cargo truce build --aax -p {}` to produce it.",
p.name,
built.display(),
p.crate_name,
)
.into());
}
#[cfg(target_os = "macos")]
{
let aax_dir = Path::new("/Library/Application Support/Avid/Audio/Plug-Ins");
let dst = aax_dir.join(bundle_name);
run_sudo("rm", &[OsStr::new("-rf"), dst.as_os_str()])?;
run_sudo("ditto", &[built.as_os_str(), dst.as_os_str()])?;
crate::log_output(format!("AAX: {}", dst.display()));
}
#[cfg(target_os = "windows")]
{
let aax_dir = common_program_files()
.join("Avid")
.join("Audio")
.join("Plug-Ins");
fs_ctx::create_dir_all(&aax_dir)?;
let dst = aax_dir.join(&bundle_name);
let _ = fs::remove_dir_all(&dst);
crate::util::copy_dir_recursive(&built, &dst)?;
crate::log_output(format!("AAX: {}", dst.display()));
}
Ok(())
}