use crate::{BoxErr, project_root};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
#[derive(Deserialize)]
pub(crate) struct Config {
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) macos: MacosConfig,
#[serde(default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) windows: WindowsConfig,
pub(crate) vendor: VendorConfig,
pub(crate) plugin: Vec<PluginDef>,
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) packaging: PackagingConfig,
#[serde(default, rename = "suite")]
pub(crate) suites: Vec<SuiteDef>,
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) struct WindowsConfig {
#[serde(default)]
pub(crate) packaging: WindowsPackagingConfig,
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) struct WindowsPackagingConfig {
pub(crate) publisher: Option<String>,
pub(crate) publisher_url: Option<String>,
pub(crate) installer_icon: Option<String>,
pub(crate) welcome_bmp: Option<String>,
pub(crate) license_rtf: Option<String>,
pub(crate) app_id: Option<String>,
}
#[derive(Deserialize, Default)]
pub(crate) struct MacosConfig {
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) packaging: MacosPackagingConfig,
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) struct MacosPackagingConfig {
#[serde(default)]
pub(crate) notarize: bool,
pub(crate) welcome_html: Option<String>,
pub(crate) license_html: Option<String>,
}
#[derive(Deserialize, Default)]
pub(crate) struct PackagingConfig {
#[serde(default)]
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
pub(crate) formats: Vec<String>,
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
pub(crate) preferred_scope: Option<String>,
}
#[derive(Deserialize)]
pub(crate) struct VendorConfig {
pub(crate) name: String,
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
pub(crate) id: String,
#[serde(default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) url: Option<String>,
pub(crate) au_manufacturer: String,
}
#[derive(Deserialize)]
pub(crate) struct PluginDef {
#[serde(flatten)]
pub(crate) shared: truce_build::PluginDef,
#[serde(default)]
pub(crate) au3_subtype: Option<String>,
#[serde(default = "default_au_tag")]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) au_tag: String,
#[serde(default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) windows_icon: Option<String>,
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) macos_icon: Option<String>,
}
impl std::ops::Deref for PluginDef {
type Target = truce_build::PluginDef;
fn deref(&self) -> &Self::Target {
&self.shared
}
}
impl PluginDef {
pub(crate) fn resolved_fourcc(&self) -> &str {
self.fourcc
.as_deref()
.or(self.au_subtype.as_deref())
.expect("truce.toml: each [[plugin]] requires `fourcc` or `au_subtype`")
}
pub(crate) fn resolved_au_type(&self) -> &str {
self.au_type
.as_deref()
.unwrap_or(match self.category.as_str() {
"instrument" => "aumu",
"midi" | "note_effect" => "aumi",
_ => "aufx",
})
}
pub(crate) fn au3_sub(&self) -> &str {
self.au3_subtype
.as_deref()
.unwrap_or(self.resolved_fourcc())
}
#[cfg(target_os = "macos")]
pub(crate) fn au3_app_name(&self) -> String {
match self.au3_name.as_deref() {
Some(n) if !n.is_empty() => n.to_string(),
_ => format!("{} v3", self.name),
}
}
#[cfg(target_os = "macos")]
pub(crate) fn fw_name(&self) -> String {
let cap = format!(
"{}{}",
self.bundle_id[..1].to_uppercase(),
&self.bundle_id[1..]
);
format!("Truce{cap}AU")
}
pub(crate) fn dylib_stem(&self) -> String {
self.crate_name.replace('-', "_")
}
}
fn default_au_tag() -> String {
"Effects".to_string()
}
#[derive(Deserialize, Debug)]
pub(crate) struct SuiteDef {
pub(crate) name: String,
pub(crate) bundle_id: String,
#[serde(default)]
pub(crate) plugins: Option<Vec<String>>,
#[serde(default)]
pub(crate) exclude_plugins: Option<Vec<String>>,
#[serde(default)]
pub(crate) version: Option<String>,
#[serde(default)]
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
pub(crate) description: Option<String>,
}
impl SuiteDef {
pub(crate) fn resolve<'a>(
&'a self,
workspace_plugins: &'a [PluginDef],
) -> Result<ResolvedSuite<'a>, BoxErr> {
if self.plugins.is_some() && self.exclude_plugins.is_some() {
return Err(format!(
"[[suite]] '{}' sets both `plugins` and `exclude_plugins` — \
these are mutually exclusive",
self.name,
)
.into());
}
let resolve_one = |needle: &str| -> Result<&'a PluginDef, BoxErr> {
workspace_plugins
.iter()
.find(|p| p.crate_name == needle || p.bundle_id == needle)
.ok_or_else(|| {
format!(
"[[suite]] '{}': plugin '{}' is not in the workspace. \
Available: {}",
self.name,
needle,
workspace_plugins
.iter()
.map(|p| p.crate_name.as_str())
.collect::<Vec<_>>()
.join(", "),
)
.into()
})
};
let plugins: Vec<&PluginDef> = if let Some(list) = &self.plugins {
list.iter()
.map(|s| resolve_one(s))
.collect::<Result<Vec<_>, _>>()?
} else if let Some(excl) = &self.exclude_plugins {
let exclude_set: Vec<&PluginDef> = excl
.iter()
.map(|s| resolve_one(s))
.collect::<Result<Vec<_>, _>>()?;
workspace_plugins
.iter()
.filter(|p| !exclude_set.iter().any(|e| std::ptr::eq(*e, *p)))
.collect()
} else {
workspace_plugins.iter().collect()
};
if plugins.is_empty() {
return Err(format!(
"[[suite]] '{}' resolves to zero plugins after \
plugins/exclude_plugins resolution",
self.name,
)
.into());
}
Ok(ResolvedSuite { def: self, plugins })
}
}
pub(crate) struct ResolvedSuite<'a> {
pub(crate) def: &'a SuiteDef,
pub(crate) plugins: Vec<&'a PluginDef>,
}
pub(crate) fn read_build_env(key: &str) -> Option<String> {
if let Ok(v) = std::env::var(key)
&& !v.is_empty()
{
return Some(v);
}
let root = project_root();
let path = root.join(".cargo/config.toml");
let content = fs::read_to_string(&path).ok()?;
let doc: toml::Table = content.parse().ok()?;
let env = doc.get("env")?.as_table()?;
let raw = match env.get(key)? {
toml::Value::String(s) => s.clone(),
toml::Value::Table(t) => t.get("value")?.as_str()?.to_string(),
_ => return None,
};
if raw.is_empty() { None } else { Some(raw) }
}
pub(crate) fn application_identity() -> String {
read_build_env("TRUCE_SIGNING_IDENTITY").unwrap_or_else(|| "-".to_string())
}
#[cfg(target_os = "macos")]
pub(crate) fn installer_identity() -> Option<String> {
read_build_env("TRUCE_INSTALLER_SIGNING_IDENTITY")
}
pub(crate) fn deployment_target() -> String {
read_build_env("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|| "11.0".to_string())
}
pub(crate) fn resolve_aax_sdk_path() -> Option<PathBuf> {
let raw = read_build_env("AAX_SDK_PATH")?;
let path = PathBuf::from(&raw);
if path.exists() {
return Some(path);
}
eprintln!(
"warning: AAX_SDK_PATH={raw} (from .cargo/config.toml [env] or shell env) but \
directory does not exist"
);
None
}
pub(crate) fn load_config() -> std::result::Result<Config, BoxErr> {
let root = project_root();
let path = root.join("truce.toml");
if !path.exists() {
return Err(format!(
"truce.toml not found at {}. Run 'cargo truce new' to scaffold a project, or create truce.toml manually.",
path.display()
)
.into());
}
let content = fs::read_to_string(&path)?;
let config: Config = toml::from_str(&content)?;
if config.plugin.is_empty() {
return Err("No [[plugin]] entries in truce.toml".into());
}
Ok(config)
}
#[cfg(test)]
mod suite_tests {
use super::*;
fn plugin(crate_name: &str, bundle_id: &str) -> PluginDef {
PluginDef {
shared: truce_build::PluginDef {
name: crate_name.into(),
bundle_id: bundle_id.into(),
crate_name: crate_name.into(),
version: None,
fourcc: None,
category: "effect".into(),
au_type: None,
au_subtype: None,
aax_category: None,
vst3_name: None,
clap_name: None,
vst2_name: None,
au_name: None,
au3_name: None,
aax_name: None,
lv2_name: None,
},
au3_subtype: None,
au_tag: default_au_tag(),
windows_icon: None,
macos_icon: None,
}
}
fn suite(name: &str) -> SuiteDef {
SuiteDef {
name: name.into(),
bundle_id: name.to_lowercase(),
plugins: None,
exclude_plugins: None,
version: None,
description: None,
}
}
#[test]
fn default_resolves_to_all_workspace_plugins() {
let plugins = vec![plugin("a", "a"), plugin("b", "b"), plugin("c", "c")];
let s = suite("Studio");
let r = match s.resolve(&plugins) {
Ok(r) => r,
Err(e) => panic!("resolve failed: {e}"),
};
assert_eq!(r.plugins.len(), 3);
}
#[test]
fn explicit_plugin_list_narrows() {
let plugins = vec![plugin("a", "a"), plugin("b", "b"), plugin("c", "c")];
let mut s = suite("Studio");
s.plugins = Some(vec!["a".into(), "c".into()]);
let r = match s.resolve(&plugins) {
Ok(r) => r,
Err(e) => panic!("resolve failed: {e}"),
};
let names: Vec<_> = r.plugins.iter().map(|p| p.crate_name.as_str()).collect();
assert_eq!(names, vec!["a", "c"]);
}
#[test]
fn exclude_plugins_inverts() {
let plugins = vec![plugin("a", "a"), plugin("b", "b"), plugin("c", "c")];
let mut s = suite("Studio");
s.exclude_plugins = Some(vec!["b".into()]);
let r = match s.resolve(&plugins) {
Ok(r) => r,
Err(e) => panic!("resolve failed: {e}"),
};
let names: Vec<_> = r.plugins.iter().map(|p| p.crate_name.as_str()).collect();
assert_eq!(names, vec!["a", "c"]);
}
#[test]
fn bundle_id_resolves_alongside_crate_name() {
let plugins = vec![plugin("acme-gain", "gain")];
let mut s = suite("Studio");
s.plugins = Some(vec!["gain".into()]);
let r = match s.resolve(&plugins) {
Ok(r) => r,
Err(e) => panic!("resolve failed: {e}"),
};
assert_eq!(r.plugins.len(), 1);
}
#[test]
fn unknown_plugin_errors() {
let plugins = vec![plugin("a", "a")];
let mut s = suite("Studio");
s.plugins = Some(vec!["does-not-exist".into()]);
let err = match s.resolve(&plugins) {
Err(e) => e.to_string(),
Ok(_) => panic!("expected resolve to error"),
};
assert!(err.contains("does-not-exist"), "got: {err}");
assert!(err.contains("Studio"), "got: {err}");
}
#[test]
fn plugins_and_exclude_plugins_both_set_errors() {
let plugins = vec![plugin("a", "a")];
let mut s = suite("Studio");
s.plugins = Some(vec!["a".into()]);
s.exclude_plugins = Some(vec!["a".into()]);
let err = match s.resolve(&plugins) {
Err(e) => e.to_string(),
Ok(_) => panic!("expected resolve to error"),
};
assert!(err.contains("mutually exclusive"));
}
#[test]
fn empty_resolution_errors() {
let plugins = vec![plugin("a", "a"), plugin("b", "b")];
let mut s = suite("Studio");
s.exclude_plugins = Some(vec!["a".into(), "b".into()]);
let err = match s.resolve(&plugins) {
Err(e) => e.to_string(),
Ok(_) => panic!("expected resolve to error"),
};
assert!(err.contains("zero plugins"), "got: {err}");
}
}