use crate::{BoxErr, project_root};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
#[derive(Deserialize)]
pub(crate) struct Config {
#[serde(default)]
pub(crate) macos: MacosConfig,
#[serde(default)]
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,
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) struct WindowsConfig {
pub(crate) aax_sdk_path: Option<String>,
#[serde(default)]
pub(crate) signing: WindowsSigningConfig,
#[serde(default)]
pub(crate) packaging: WindowsPackagingConfig,
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) struct WindowsSigningConfig {
pub(crate) azure_account: Option<String>,
pub(crate) azure_profile: Option<String>,
pub(crate) azure_dlib: Option<String>,
pub(crate) sha1: Option<String>,
pub(crate) cert_store: Option<String>,
pub(crate) pfx_path: Option<String>,
pub(crate) timestamp_url: Option<String>,
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
impl WindowsSigningConfig {
pub(crate) fn is_configured(&self) -> bool {
self.azure_account.is_some() || self.sha1.is_some() || self.pfx_path.is_some()
}
pub(crate) fn resolved_timestamp_url(&self) -> &str {
self.timestamp_url
.as_deref()
.unwrap_or("http://timestamp.digicert.com")
}
}
#[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 {
pub(crate) aax_sdk_path: Option<String>,
#[serde(default)]
pub(crate) signing: MacosSigningConfig,
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) packaging: MacosPackagingConfig,
}
#[derive(Deserialize, Default)]
pub(crate) struct MacosSigningConfig {
pub(crate) application_identity: Option<String>,
pub(crate) installer_identity: Option<String>,
}
impl MacosConfig {
pub(crate) fn application_identity(&self) -> &str {
self.signing.application_identity.as_deref().unwrap_or("-")
}
#[cfg(target_os = "macos")]
pub(crate) fn installer_identity(&self) -> Option<&str> {
self.signing.installer_identity.as_deref()
}
}
#[derive(Deserialize, Default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) struct MacosPackagingConfig {
#[serde(default)]
pub(crate) notarize: bool,
pub(crate) apple_id: Option<String>,
pub(crate) team_id: Option<String>,
}
#[derive(Deserialize, Default)]
pub(crate) struct PackagingConfig {
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) formats: Vec<String>,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) welcome_html: Option<String>,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) license_html: Option<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,
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 {
pub(crate) name: String,
pub(crate) bundle_id: String,
#[serde(rename = "crate")]
pub(crate) crate_name: String,
#[serde(default)]
pub(crate) fourcc: Option<String>,
pub(crate) category: String,
#[serde(default)]
pub(crate) au_type: Option<String>,
#[serde(default)]
pub(crate) au_subtype: Option<String>,
#[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)]
pub(crate) clap_name: Option<String>,
#[serde(default)]
pub(crate) vst3_name: Option<String>,
#[serde(default)]
pub(crate) vst2_name: Option<String>,
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) au_name: Option<String>,
#[serde(default)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) au3_name: Option<String>,
#[serde(default)]
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
pub(crate) aax_name: Option<String>,
#[serde(default)]
pub(crate) lv2_name: Option<String>,
}
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()
}
fn resolve_signing_identity(config: &Config) -> String {
if let Some(id) = &config.macos.signing.application_identity
&& !id.is_empty()
&& id != "-"
{
return id.clone();
}
if let Ok(id) = std::env::var("TRUCE_SIGNING_IDENTITY")
&& !id.is_empty()
{
return id;
}
if let Some(id) = read_cargo_config_env("TRUCE_SIGNING_IDENTITY") {
return id;
}
"-".to_string()
}
pub(crate) fn read_cargo_config_env(key: &str) -> Option<String> {
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()?;
match env.get(key)? {
toml::Value::String(s) => Some(s.clone()),
toml::Value::Table(t) => t
.get("value")?
.as_str()
.map(std::string::ToString::to_string),
_ => None,
}
}
fn resolve_installer_identity(config: &Config) -> Option<String> {
if let Some(ref id) = config.macos.signing.installer_identity
&& !id.is_empty()
{
return Some(id.clone());
}
if let Ok(id) = std::env::var("TRUCE_INSTALLER_SIGNING_IDENTITY")
&& !id.is_empty()
{
return Some(id);
}
if let Some(id) = read_cargo_config_env("TRUCE_INSTALLER_SIGNING_IDENTITY") {
return Some(id);
}
None
}
pub(crate) fn deployment_target() -> String {
std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "11.0".to_string())
}
pub(crate) fn resolve_aax_sdk_path(config: &Config) -> Option<PathBuf> {
let toml_path = if cfg!(target_os = "windows") {
(&config.windows.aax_sdk_path, "[windows].aax_sdk_path")
} else {
(&config.macos.aax_sdk_path, "[macos].aax_sdk_path")
};
if let Some(p) = toml_path.0 {
let path = PathBuf::from(p);
if path.exists() {
return Some(path);
}
eprintln!(
"warning: {} = {:?} in truce.toml but directory not found",
toml_path.1, p
);
}
if let Ok(p) = std::env::var("AAX_SDK_PATH") {
let path = PathBuf::from(&p);
if path.exists() {
return Some(path);
}
eprintln!("warning: AAX_SDK_PATH={p} but directory not found");
}
if let Some(p) = read_cargo_config_env("AAX_SDK_PATH") {
let path = PathBuf::from(&p);
if path.exists() {
return Some(path);
}
eprintln!("warning: AAX_SDK_PATH={p} in .cargo/config.toml but directory not found");
}
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 mut config: Config = toml::from_str(&content)?;
if config.plugin.is_empty() {
return Err("No [[plugin]] entries in truce.toml".into());
}
let resolved_app = resolve_signing_identity(&config);
config.macos.signing.application_identity = Some(resolved_app);
if config.macos.signing.installer_identity.is_none() {
config.macos.signing.installer_identity = resolve_installer_identity(&config);
}
Ok(config)
}