use std::path::{Path, PathBuf};
use crate::install_scope::InstallScope;
use crate::util::fs_ctx;
use crate::{Config, PluginDef, Res};
#[cfg(target_os = "macos")]
use crate::{run_sudo, tmp_manifests};
#[cfg(target_os = "macos")]
use std::ffi::OsStr;
use crate::preset_codec::vstpreset_bytes;
#[cfg(target_os = "macos")]
use crate::preset_codec::{aupreset_xml, fourcc_int};
use truce_utils::preset::{PRESET_FILE_EXT, PresetMeta, write_preset_file};
use truce_utils::{safe_filename, state};
pub(crate) struct EmittablePreset {
meta: PresetMeta,
blob: Vec<u8>,
stem: String,
lv2_ports: Vec<(String, f64)>,
}
impl EmittablePreset {
fn rel_path(&self, ext: &str) -> PathBuf {
self.rel(&self.stem, ext)
}
fn display_rel_path(&self, ext: &str) -> PathBuf {
self.rel(&safe_filename(&self.meta.name), ext)
}
fn rel(&self, file_stem: &str, ext: &str) -> PathBuf {
let file = format!("{file_stem}.{ext}");
if self.meta.category.is_empty() {
PathBuf::from(file)
} else {
PathBuf::from(safe_filename(&self.meta.category)).join(file)
}
}
}
pub(crate) struct FactoryPresets {
presets: Vec<EmittablePreset>,
}
pub(crate) fn authored_presets_dir(root: &Path, p: &PluginDef) -> Option<PathBuf> {
let manifest = crate::util::locate_plugin_manifest(root, &p.crate_name)?;
let crate_dir = manifest.parent()?;
let dir = p
.presets
.as_ref()
.map_or("presets", |c| c.factory_dir.as_str());
Some(crate_dir.join(dir))
}
pub(crate) fn validate_user_dir(p: &PluginDef) -> Res {
if let Some(raw) = p.presets.as_ref().and_then(|c| c.user_dir.as_deref())
&& truce_utils::presets::sanitize_preset_user_dir(raw).is_none()
{
return Err(format!(
"[plugin.presets] user_dir \"{raw}\" is not a usable relative path \
(needs at least one valid segment; `..` is rejected)"
)
.into());
}
Ok(())
}
pub(crate) fn load_factory_presets(
root: &Path,
p: &PluginDef,
config: &Config,
) -> Result<Option<FactoryPresets>, crate::CargoTruceError> {
validate_user_dir(p)?;
let configured_dir = p.presets.as_ref().map(|c| c.factory_dir.clone());
let Some(dir) = authored_presets_dir(root, p) else {
if configured_dir.is_some() {
return Err(format!(
"[plugin.presets] set for \"{}\" but its crate dir could not be located",
p.name
)
.into());
}
return Ok(None);
};
if !dir.is_dir() {
if configured_dir.is_some() {
return Err(format!(
"[plugin.presets] points at {} but it does not exist",
dir.display()
)
.into());
}
return Ok(None);
}
let sidecar_dir = truce_build::target_dir(root)
.join("lv2-meta")
.join(&p.crate_name);
let annotations = truce_build::presets::read_param_annotations(&sidecar_dir);
let names = truce_build::presets::ParamNameMap::from_annotations(&annotations);
let symbols = truce_build::presets::read_param_symbols(&sidecar_dir);
let authored = truce_build::presets::read_presets_dir(&dir, true, Some(&names))?;
if authored.is_empty() {
return Ok(None);
}
let hash = state::hash_plugin_id(&truce_build::plugin_id(&config.vendor.id, &p.name));
let presets = authored
.into_iter()
.map(|a| {
let blob = a.state_blob(hash);
let lv2_ports = state::deserialize_state(&blob, hash).map_or_else(Vec::new, |s| {
s.params
.iter()
.filter_map(|(id, v)| symbols.get(id).map(|sym| (sym.clone(), *v)))
.collect()
});
EmittablePreset {
blob,
meta: a.meta,
stem: a.stem,
lv2_ports,
}
})
.collect();
Ok(Some(FactoryPresets { presets }))
}
fn write_tree(
files: &[(PathBuf, Vec<u8>)],
dest_root: &Path,
replace: bool,
needs_sudo: bool,
tag: &str,
) -> Res {
#[cfg(target_os = "macos")]
if needs_sudo {
if replace {
run_sudo("rm", &[OsStr::new("-rf"), dest_root.as_os_str()])?;
}
let staging = tmp_manifests().join(format!("presets-{tag}"));
let _ = std::fs::remove_dir_all(&staging);
for (rel, bytes) in files {
let dst = staging.join(rel);
if let Some(parent) = dst.parent() {
fs_ctx::create_dir_all(parent)?;
}
fs_ctx::write(&dst, bytes)?;
}
run_sudo("mkdir", &[OsStr::new("-p"), dest_root.as_os_str()])?;
let staging_contents = staging.join(".");
run_sudo(
"cp",
&[
OsStr::new("-R"),
staging_contents.as_os_str(),
dest_root.as_os_str(),
],
)?;
return Ok(());
}
#[cfg(not(target_os = "macos"))]
let _ = (needs_sudo, tag);
if replace {
let _ = std::fs::remove_dir_all(dest_root);
}
for (rel, bytes) in files {
let dst = dest_root.join(rel);
if let Some(parent) = dst.parent() {
fs_ctx::create_dir_all(parent)?;
}
fs_ctx::write(&dst, bytes)?;
}
Ok(())
}
pub(crate) fn emit_trucepreset_tree(
fp: &FactoryPresets,
dest_root: &Path,
needs_sudo: bool,
tag: &str,
) -> Res {
let files: Vec<_> = fp
.presets
.iter()
.map(|p| {
(
p.rel_path(PRESET_FILE_EXT),
write_preset_file(&p.meta, &p.blob),
)
})
.collect();
write_tree(&files, dest_root, true, needs_sudo, tag)?;
crate::log_output(format!(
" {} factory presets -> {}",
files.len(),
dest_root.display()
));
Ok(())
}
pub(crate) fn emit_standalone_factory(
root: &Path,
p: &PluginDef,
config: &Config,
exec_path: &Path,
) -> Res {
let Some(fp) = load_factory_presets(root, p, config)? else {
return Ok(());
};
let dest = standalone_factory_root(exec_path);
emit_trucepreset_tree(&fp, &dest, false, &format!("{}-standalone", p.bundle_id))
}
fn standalone_factory_root(exec_path: &Path) -> PathBuf {
#[cfg(target_os = "macos")]
if let Some(macos) = exec_path.parent()
&& macos.file_name() == Some(OsStr::new("MacOS"))
&& let Some(contents) = macos.parent()
{
return contents.join("Resources/Presets");
}
let stem = exec_path.file_stem().map_or_else(
|| "plugin".to_string(),
|s| s.to_string_lossy().into_owned(),
);
exec_path
.parent()
.unwrap_or(Path::new("."))
.join(format!("{stem}.presets"))
}
pub(crate) fn vst3_preset_payload(
fp: &FactoryPresets,
p: &PluginDef,
config: &Config,
) -> Vec<(PathBuf, Vec<u8>)> {
let dir = PathBuf::from(safe_filename(&config.vendor.name)).join(safe_filename(resolved_name(
p.vst3_name.as_deref(),
&p.name,
)));
let cid = state::vst3_cid(&truce_build::plugin_id(&config.vendor.id, &p.name));
fp.presets
.iter()
.map(|pr| {
(
dir.join(pr.display_rel_path("vstpreset")),
vstpreset_bytes(&cid, &pr.blob),
)
})
.collect()
}
pub(crate) fn emit_vst3_presets(
fp: &FactoryPresets,
p: &PluginDef,
config: &Config,
scope: InstallScope,
) -> Res {
let Some(presets_root) = vst3_presets_root(scope) else {
return Err("cannot resolve the VST3 preset directory".into());
};
let dest_root = presets_root
.join(safe_filename(&config.vendor.name))
.join(safe_filename(resolved_name(
p.vst3_name.as_deref(),
&p.name,
)));
let cid = state::vst3_cid(&truce_build::plugin_id(&config.vendor.id, &p.name));
let files: Vec<_> = fp
.presets
.iter()
.map(|pr| {
(
pr.display_rel_path("vstpreset"),
vstpreset_bytes(&cid, &pr.blob),
)
})
.collect();
write_tree(
&files,
&dest_root,
false,
scope.needs_sudo(),
&format!("{}-vst3", p.bundle_id),
)?;
crate::log_output(format!(
" {} factory presets -> {}",
files.len(),
dest_root.display()
));
Ok(())
}
fn vst3_presets_root(scope: InstallScope) -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
match scope {
InstallScope::User => crate::dirs::home_dir().map(|h| h.join("Library/Audio/Presets")),
InstallScope::System => Some(PathBuf::from("/Library/Audio/Presets")),
}
}
#[cfg(target_os = "windows")]
{
match scope {
InstallScope::User => std::env::var_os("USERPROFILE")
.map(|p| PathBuf::from(p).join("Documents").join("VST3 Presets")),
InstallScope::System => {
std::env::var_os("PROGRAMDATA").map(|p| PathBuf::from(p).join("VST3 Presets"))
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let _ = scope;
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".vst3/presets"))
}
}
pub(crate) fn resolved_name<'a>(name_override: Option<&'a str>, name: &'a str) -> &'a str {
match name_override {
Some(n) if !n.is_empty() => n,
_ => name,
}
}
#[cfg(target_os = "macos")]
pub(crate) fn emit_au_presets(
fp: &FactoryPresets,
p: &PluginDef,
config: &Config,
scope: InstallScope,
) -> Res {
let presets_root = match scope {
InstallScope::User => {
let Some(home) = crate::dirs::home_dir() else {
return Err("cannot resolve home directory for AU presets".into());
};
home.join("Library/Audio/Presets")
}
InstallScope::System => PathBuf::from("/Library/Audio/Presets"),
};
let dest_root = presets_root
.join(safe_filename(&config.vendor.name))
.join(safe_filename(resolved_name(p.au_name.as_deref(), &p.name)));
let au_type = fourcc_int(p.resolved_au_type())?;
let subtype = fourcc_int(p.resolved_fourcc())?;
let manufacturer = fourcc_int(&config.vendor.au_manufacturer)?;
let files: Vec<_> = fp
.presets
.iter()
.map(|pr| {
(
pr.display_rel_path("aupreset"),
aupreset_xml(au_type, subtype, manufacturer, &pr.meta.name, &pr.blob).into_bytes(),
)
})
.collect();
write_tree(
&files,
&dest_root,
false,
scope.needs_sudo(),
&format!("{}-au", p.bundle_id),
)?;
crate::log_output(format!(
" {} factory presets -> {}",
files.len(),
dest_root.display()
));
Ok(())
}
pub(crate) fn emit_lv2_presets(fp: &FactoryPresets, bundle: &Path, plugin_uri: &str) -> Res {
let presets_dir = bundle.join("presets");
fs_ctx::create_dir_all(&presets_dir)?;
let mut manifest_additions = String::from(truce_build::lv2::PRESET_MANIFEST_PREFIXES);
for pr in &fp.presets {
let file_name = format!("{}.ttl", safe_filename(&pr.stem));
let label = if pr.meta.category.is_empty() {
pr.meta.name.clone()
} else {
format!("{}/{}", pr.meta.category, pr.meta.name)
};
let ttl = truce_build::lv2::render_preset_ttl(
plugin_uri,
&pr.meta.uuid,
&label,
&pr.blob,
&pr.lv2_ports,
);
fs_ctx::write(presets_dir.join(&file_name), &ttl)?;
manifest_additions.push_str(&truce_build::lv2::render_preset_manifest_entry(
plugin_uri,
&pr.meta.uuid,
&format!("presets/{file_name}"),
));
}
let manifest_path = bundle.join("manifest.ttl");
let mut manifest = std::fs::read_to_string(&manifest_path)
.map_err(|e| format!("reading {}: {e}", manifest_path.display()))?;
manifest.push_str(&manifest_additions);
fs_ctx::write(&manifest_path, &manifest)?;
crate::log_output(format!(
" {} factory presets -> {}",
fp.presets.len(),
presets_dir.display()
));
Ok(())
}