#[cfg(target_os = "macos")]
use super::PkgFormat;
use crate::commands::install::presets;
#[cfg(target_os = "macos")]
use crate::install_scope::PkgScope;
#[cfg(target_os = "macos")]
use crate::pace_sign_aax_macos;
use crate::{Config, PluginDef, Res, codesign_bundle};
#[cfg(target_os = "macos")]
use crate::{MacosPackagingConfig, copy_dir_recursive};
#[cfg(target_os = "macos")]
use std::fmt::Write;
use std::fs;
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::process::Command;
pub(crate) fn lv2_slug(name: &str) -> String {
truce_utils::slugify(name)
}
pub(crate) fn stage_lv2(
root: &Path,
p: &PluginDef,
staging: &Path,
identity: &str,
target: Option<&str>,
) -> Res {
let built = crate::release_lib_for_target(root, &format!("{}_lv2", p.dylib_stem()), target);
if !built.exists() {
return Err(format!("Missing: {}", built.display()).into());
}
let target_dir = truce_build::target_dir(root);
let sidecar_dir = target_dir.join("lv2-meta").join(&p.crate_name);
let manifest_ttl = sidecar_dir.join("manifest.ttl");
let plugin_ttl = sidecar_dir.join("plugin.ttl");
if !manifest_ttl.exists() || !plugin_ttl.exists() {
return Err(format!(
"no LV2 metadata sidecar at {} for {}. \
`derive(Params)` writes this during the cdylib's compile; \
missing it means either the params struct uses `#[nested]` \
(unsupported for the compile-time TTL path) or the plugin \
crate isn't listed under `[[plugin]]` in truce.toml.",
sidecar_dir.display(),
p.name,
)
.into());
}
let slug = lv2_slug(&p.name);
let bundle = staging.join(format!("{slug}.lv2"));
let _ = fs::remove_dir_all(&bundle);
fs::create_dir_all(&bundle)?;
let bin_ext = if cfg!(target_os = "windows") {
"dll"
} else {
"so"
};
let bin_name = format!("{slug}.{bin_ext}");
let bin_path = bundle.join(&bin_name);
fs::copy(&built, &bin_path)?;
fs::copy(&manifest_ttl, bundle.join("manifest.ttl"))?;
fs::copy(&plugin_ttl, bundle.join("plugin.ttl"))?;
codesign_bundle(&bin_path.to_string_lossy(), identity, false)?;
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn stage_lv2_packaged(
root: &Path,
p: &PluginDef,
config: &Config,
staging: &Path,
identity: &str,
target: Option<&str>,
) -> Res {
stage_lv2(root, p, staging, identity, target)?;
if let Some(fp) = presets::load_factory_presets(root, p, config)? {
let bundle = staging.join(format!("{}.lv2", lv2_slug(&p.name)));
let uri =
truce_build::lv2::plugin_uri(config.vendor.url.as_deref().unwrap_or(""), &p.bundle_id);
presets::emit_lv2_presets(&fp, &bundle, &uri)?;
}
Ok(())
}
#[cfg(target_os = "macos")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_preset_component(
staging: &Path,
components_dir: &Path,
file_stem: &str,
pkg_id: &str,
label: &str,
install_location: &str,
version: &str,
payload: &[(PathBuf, Vec<u8>)],
) -> Res {
let root = staging.join(format!("_presetroot_{label}"));
let _ = fs::remove_dir_all(&root);
for (rel, bytes) in payload {
let dst = root.join(rel);
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dst, bytes)?;
}
let component_pkg = components_dir.join(format!("{file_stem}-{label}.pkg"));
let status = Command::new("pkgbuild")
.args([
"--root",
root.to_str().unwrap(),
"--install-location",
install_location,
"--identifier",
pkg_id,
"--version",
version,
"--ownership",
"preserve",
component_pkg.to_str().unwrap(),
])
.status()?;
if !status.success() {
return Err(format!("pkgbuild failed for {file_stem} {label}").into());
}
Ok(())
}
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
pub(crate) fn stage_clap(
root: &Path,
p: &PluginDef,
config: &Config,
staging: &Path,
identity: &str,
target: Option<&str>,
) -> Res {
#[cfg(not(target_os = "macos"))]
let dylib = crate::release_lib_for_target(root, &format!("{}_clap", p.dylib_stem()), target);
#[cfg(target_os = "macos")]
let dylib = {
let _ = target;
crate::release_bundle_bin(root, &p.dylib_stem(), "_clap")
};
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = staging.join(format!("{}.clap", p.file_stem()));
#[cfg(target_os = "macos")]
{
let macos_dir = bundle.join("Contents/MacOS");
fs::create_dir_all(&macos_dir)?;
let exec_name = p.file_stem();
fs::copy(&dylib, macos_dir.join(&exec_name))?;
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
fs::write(bundle.join("Contents/Info.plist"), &plist)?;
if let Some(fp) = presets::load_factory_presets(root, p, config)? {
presets::emit_trucepreset_tree(
&fp,
&bundle.join("Contents/Resources/Presets"),
false,
&format!("{}-clap", p.bundle_id),
)?;
}
codesign_bundle(bundle.to_str().unwrap(), identity, false)?;
}
#[cfg(not(target_os = "macos"))]
{
fs::copy(&dylib, &bundle)?;
if let Some(fp) = presets::load_factory_presets(root, p, config)? {
presets::emit_trucepreset_tree(
&fp,
&staging.join(format!("{}.presets", p.file_stem())),
false,
&format!("{}-clap", p.bundle_id),
)?;
}
codesign_bundle(bundle.to_str().unwrap(), identity, false)?;
}
Ok(())
}
pub(crate) fn stage_vst3(
root: &Path,
p: &PluginDef,
config: &Config,
staging: &Path,
target: Option<&str>,
) -> Res {
#[cfg(not(target_os = "macos"))]
let dylib = crate::release_lib_for_target(root, &format!("{}_vst3", p.dylib_stem()), target);
#[cfg(target_os = "macos")]
let dylib = crate::release_bundle_bin(root, &p.dylib_stem(), "_vst3");
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = staging.join(format!("{}.vst3", p.file_stem()));
#[cfg(target_os = "macos")]
{
let macos_dir = bundle.join("Contents/MacOS");
fs::create_dir_all(&macos_dir)?;
let exec_name = p.file_stem();
fs::copy(&dylib, macos_dir.join(&exec_name))?;
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
);
fs::write(bundle.join("Contents/Info.plist"), &plist)?;
codesign_bundle(
bundle.to_str().unwrap(),
&crate::application_identity(),
false,
)?;
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
let _ = config; let triple: &str = match target {
Some(t) => t,
None => truce_build::host_triple(),
};
let arch_dir = bundle.join("Contents").join(vst3_arch_subdir(triple));
fs::create_dir_all(&arch_dir)?;
let inner_filename = format!("{}.{}", p.file_stem(), vst3_inner_extension(triple));
fs::copy(&dylib, arch_dir.join(inner_filename))?;
}
#[cfg(target_os = "macos")]
let _ = target; Ok(())
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn vst3_arch_subdir(triple: &str) -> &'static str {
match triple {
"x86_64-unknown-linux-gnu" | "x86_64-unknown-linux-musl" => "x86_64-linux",
"aarch64-unknown-linux-gnu" | "aarch64-unknown-linux-musl" => "aarch64-linux",
"x86_64-pc-windows-msvc" | "x86_64-pc-windows-gnu" => "x86_64-win",
"aarch64-pc-windows-msvc" => "aarch64-win",
_ => "unknown",
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn vst3_inner_extension(triple: &str) -> &'static str {
if triple.contains("linux") {
"so"
} else if triple.contains("windows") {
"vst3"
} else {
"so"
}
}
pub(crate) fn stage_vst2(
root: &Path,
p: &PluginDef,
config: &Config,
staging: &Path,
target: Option<&str>,
) -> Result<std::path::PathBuf, crate::CargoTruceError> {
let _ = config; #[cfg(not(target_os = "macos"))]
let dylib = crate::release_lib_for_target(root, &format!("{}_vst2", p.dylib_stem()), target);
#[cfg(target_os = "macos")]
let dylib = {
let _ = target;
crate::release_bundle_bin(root, &p.dylib_stem(), "_vst2")
};
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
#[cfg(target_os = "linux")]
{
let dst = staging.join(format!("{}.so", p.file_stem()));
fs::copy(&dylib, &dst)?;
Ok(dst)
}
#[cfg(target_os = "windows")]
{
let dst = staging.join(format!("{}.dll", p.file_stem()));
fs::copy(&dylib, &dst)?;
Ok(dst)
}
#[cfg(target_os = "macos")]
{
let bundle = staging.join(format!("{}.vst", p.file_stem()));
let macos_dir = bundle.join("Contents/MacOS");
fs::create_dir_all(&macos_dir)?;
let exec_name = p.file_stem();
fs::copy(&dylib, macos_dir.join(&exec_name))?;
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>com.truce.{bundle_id}.vst2</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
);
fs::write(bundle.join("Contents/Info.plist"), &plist)?;
fs::write(bundle.join("Contents/PkgInfo"), "BNDL????")?;
codesign_bundle(
bundle.to_str().unwrap(),
&crate::application_identity(),
false,
)?;
Ok(bundle)
}
}
#[cfg(target_os = "macos")]
pub(crate) fn stage_au2(root: &Path, p: &PluginDef, config: &Config, staging: &Path) -> Res {
let dylib =
truce_build::target_dir(root).join(format!("release/lib{}_au.dylib", p.dylib_stem()));
if !dylib.exists() {
return Err(format!("Missing: {}", dylib.display()).into());
}
let bundle = staging.join(format!("{}.component", p.file_stem()));
let macos_dir = bundle.join("Contents/MacOS");
fs::create_dir_all(&macos_dir)?;
let exec_name = p.file_stem();
fs::copy(&dylib, macos_dir.join(&exec_name))?;
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>{exec_name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.component</string>
<key>CFBundleName</key>
<string>{display_name}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>AudioComponents</key>
<array>
<dict>
<key>type</key>
<string>{au_type}</string>
<key>subtype</key>
<string>{au_subtype}</string>
<key>manufacturer</key>
<string>{au_mfr}</string>
<key>name</key>
<string>{vendor}: {display_name}</string>
<key>description</key>
<string>{display_name}</string>
<key>version</key>
<integer>65536</integer>
<key>factoryFunction</key>
<string>TruceAUFactory</string>
<key>sandboxSafe</key>
<true/>
<key>tags</key>
<array>
<string>{au_tag}</string>
</array>
</dict>
</array>
</dict>
</plist>"#,
display_name = p.name,
bundle_id = p.bundle_id,
vendor_id = config.vendor.id,
vendor = config.vendor.name,
au_type = p.resolved_au_type(),
au_subtype = p.resolved_fourcc(),
au_mfr = config.vendor.au_manufacturer,
au_tag = p.au_tag,
);
fs::write(bundle.join("Contents/Info.plist"), &plist)?;
if let Some(fp) = presets::load_factory_presets(root, p, config)? {
presets::emit_trucepreset_tree(
&fp,
&bundle.join("Contents/Resources/Presets"),
false,
&format!("{}-au-bundle", p.bundle_id),
)?;
}
codesign_bundle(
bundle.to_str().unwrap(),
&crate::application_identity(),
false,
)?;
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn stage_aax(
root: &Path,
p: &PluginDef,
_config: &Config,
staging: &Path,
_universal_mac: bool,
no_pace_sign: bool,
) -> Res {
let bundle_name = format!("{}.aaxplugin", p.file_stem());
let built = truce_build::target_dir(root)
.join("bundles")
.join(&bundle_name);
if !built.exists() {
return Err(format!(
"AAX bundle missing at {}. Call `emit_aax_bundle` from the package driver before staging.",
built.display()
)
.into());
}
let dst = staging.join(&bundle_name);
let _ = fs::remove_dir_all(&dst);
crate::util::copy_dir_recursive(&built, &dst)?;
if !no_pace_sign {
pace_sign_aax_macos(&dst)?;
}
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn stage_au3(root: &Path, p: &PluginDef, _config: &Config, staging: &Path) -> Res {
let app_name = format!("{}.app", p.au3_app_name());
let built_app = truce_build::target_dir(root)
.join("bundles")
.join(&app_name);
if !built_app.exists() {
return Err(format!(
"AU v3 bundle missing at {}. Run `cargo truce build --au3 -p {}` first.",
built_app.display(),
p.bundle_id,
)
.into());
}
let dst = staging.join(&app_name);
if dst.exists() && fs::remove_dir_all(&dst).is_err() {
let status = Command::new("rm")
.args(["-rf", dst.to_str().unwrap()])
.status();
if dst.exists() {
return Err(format!(
"could not remove stale staging dir {} \
(rm exit: {status:?}). \
This is usually root-owned leftovers from an earlier \
`cargo truce install`. Run:\n \
sudo rm -rf {}",
dst.display(),
dst.display(),
)
.into());
}
}
copy_dir_recursive(&built_app, &dst)?;
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn stage_standalone(root: &Path, p: &PluginDef, config: &Config, staging: &Path) -> Res {
use std::os::unix::fs::PermissionsExt;
let bin_stem = crate::read_standalone_bin_name(&p.crate_name)
.unwrap_or_else(|| format!("{}-standalone", p.crate_name));
let built = truce_build::target_dir(root)
.join("release")
.join(&bin_stem);
if !built.exists() {
return Err(format!(
"Standalone binary missing at {}. \
The build step should have produced it - make sure the \
plugin's Cargo.toml declares a [[bin]] target named '{}'.",
built.display(),
bin_stem,
)
.into());
}
let staged_app = staging.join(format!("{}.app", p.file_stem()));
let _ = fs::remove_dir_all(&staged_app);
let macos_dir = staged_app.join("Contents/MacOS");
fs::create_dir_all(&macos_dir)?;
let exe_dst = macos_dir.join(&bin_stem);
fs::copy(&built, &exe_dst)?;
let mut perms = fs::metadata(&exe_dst)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&exe_dst, perms)?;
let icon_present = if let Some(icon_rel) = &p.macos_icon {
let icon_src = crate::project_root().join(icon_rel);
if !icon_src.exists() {
return Err(format!(
"macos_icon for `{}` points to {} but no file is there.",
p.name,
icon_src.display()
)
.into());
}
let resources_dir = staged_app.join("Contents/Resources");
fs::create_dir_all(&resources_dir)?;
fs::copy(&icon_src, resources_dir.join("icon.icns"))?;
true
} else {
false
};
write_standalone_info_plist(&staged_app, p, &bin_stem, &config.vendor, icon_present)?;
crate::commands::install::presets::emit_standalone_factory(root, p, config, &exe_dst)?;
codesign_bundle(
staged_app.to_str().unwrap(),
&crate::application_identity(),
false,
)?;
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn write_standalone_info_plist(
bundle_root: &Path,
plugin: &PluginDef,
bin_stem: &str,
vendor: &crate::config::VendorConfig,
icon_present: bool,
) -> Res {
let mic_usage = format!(
"{} would like to use the microphone for plugin audio input.",
plugin.name
);
let icon_key = if icon_present {
" <key>CFBundleIconFile</key>\n <string>icon</string>\n"
} else {
""
};
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>CFBundleName</key>
<string>{name}</string>
<key>CFBundleDisplayName</key>
<string>{name}</string>
<key>CFBundleIdentifier</key>
<string>{vendor_id}.{bundle_id}.standalone</string>
<key>CFBundleExecutable</key>
<string>{exe}</string>
{icon_key} <key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>{mic_usage}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.music</string>
</dict>
</plist>
"#,
name = plugin.name,
vendor_id = vendor.id,
bundle_id = plugin.bundle_id,
exe = bin_stem,
);
fs::write(bundle_root.join("Contents/Info.plist"), plist)?;
Ok(())
}
#[cfg(target_os = "macos")]
#[cfg(target_os = "macos")]
pub(crate) struct ExtraComponent {
pub suffix: String,
pub label: String,
pub parent: PkgFormat,
}
#[cfg(target_os = "macos")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn generate_distribution_xml(
plugin_name: &str,
vendor_id: &str,
bundle_id: &str,
formats: &[PkgFormat],
extras: &[ExtraComponent],
version: &str,
resources: Option<&MacosPackagingConfig>,
scope: PkgScope,
) -> String {
let mut choices_outline = String::new();
let mut choices = String::new();
let mut pkg_refs = String::new();
for fmt in formats {
let id = fmt.pkg_id_suffix();
let pkg_id = format!("{vendor_id}.{bundle_id}.{id}");
let label = fmt.label();
let desc = fmt.choice_description();
let component_file = format!("{plugin_name}-{label}.pkg");
let enabled_attr = "";
let pkg_ref_auth = match (scope, fmt.is_system_only_on_macos()) {
(PkgScope::User | PkgScope::Ask, true) => " auth=\"Root\"",
(PkgScope::User, false) => " auth=\"None\"",
(PkgScope::Ask | PkgScope::System, _) => "",
};
let mut extra_refs = String::new();
for ec in extras.iter().filter(|e| e.parent == *fmt) {
let ex_id = format!("{vendor_id}.{bundle_id}.{}", ec.suffix);
let _ = writeln!(
extra_refs,
" <pkg-ref id=\"{ex_id}\"{pkg_ref_auth}/>"
);
}
let _ = writeln!(choices_outline, " <line choice=\"{id}\"/>");
let _ = write!(
choices,
r#"
<choice id="{id}" title="{label}" description="{desc}"{enabled_attr}>
<pkg-ref id="{pkg_id}"{pkg_ref_auth}/>
{extra_refs} </choice>
"#
);
let _ = writeln!(
pkg_refs,
" <pkg-ref id=\"{pkg_id}\" version=\"{version}\">{component_file}</pkg-ref>"
);
for ec in extras.iter().filter(|e| e.parent == *fmt) {
let ex_id = format!("{vendor_id}.{bundle_id}.{}", ec.suffix);
let ex_file = format!("{plugin_name}-{}.pkg", ec.label);
let _ = writeln!(
pkg_refs,
" <pkg-ref id=\"{ex_id}\" version=\"{version}\">{ex_file}</pkg-ref>"
);
}
}
let welcome = resources
.and_then(|r| r.welcome_html.as_deref())
.map_or("", |_| " <welcome file=\"welcome.html\"/>\n");
let license = resources
.and_then(|r| r.license_html.as_deref())
.map_or("", |_| " <license file=\"license.html\"/>\n");
let domains = match scope {
PkgScope::User => {
" <domains enable_anywhere=\"false\" enable_currentUserHome=\"true\" \
enable_localSystem=\"false\"/>\n"
}
PkgScope::System => {
" <domains enable_anywhere=\"false\" enable_currentUserHome=\"false\" \
enable_localSystem=\"true\"/>\n"
}
PkgScope::Ask => {
" <domains enable_anywhere=\"false\" enable_currentUserHome=\"true\" \
enable_localSystem=\"true\"/>\n"
}
};
format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
<title>{plugin_name}</title>
{welcome}{license}{domains} <options customize="always" require-scripts="false"/>
<choices-outline>
{choices_outline} </choices-outline>
{choices}
{pkg_refs}</installer-gui-script>
"#
)
}
#[cfg(target_os = "macos")]
const AU3_REGISTER_POSTINSTALL: &str = r#"#!/bin/bash
set -u
APP="$2/{{BUNDLE}}"
APPEX="$APP/Contents/PlugIns/AUExt.appex"
APPEX_ID="{{APPEX_ID}}"
uid=$(stat -f %u /dev/console 2>/dev/null || true)
user=$(stat -f %Su /dev/console 2>/dev/null || true)
if [ -z "${uid:-}" ] || [ "$user" = "root" ] || [ ! -d "$APPEX" ]; then
exit 0
fi
home=$(eval echo "~$user")
asuser() { launchctl asuser "$uid" sudo -u "$user" "$@"; }
LSREG=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
"$LSREG" -f -R "$APP" >/dev/null 2>&1 || true
killall -9 pkd 2>/dev/null || true
killall -9 AudioComponentRegistrar 2>/dev/null || true
rm -rf "$home/Library/Caches/AudioUnitCache" 2>/dev/null || true
sleep 2
i=0
while [ "$i" -lt 8 ]; do
asuser pluginkit -a "$APPEX" >/dev/null 2>&1 || true
if asuser pluginkit -m -i "$APPEX_ID" 2>/dev/null | grep -q "$APPEX_ID"; then
break
fi
sleep 1
i=$((i + 1))
done
exit 0
"#;
#[cfg(target_os = "macos")]
pub(crate) fn write_format_scripts(
staging: &Path,
fmt: &PkgFormat,
bundle_name: &str,
appex_id: Option<&str>,
) -> std::result::Result<PathBuf, crate::CargoTruceError> {
let scripts_dir = staging.join(format!("{}_scripts", fmt.pkg_id_suffix()));
let _ = fs::remove_dir_all(&scripts_dir);
fs::create_dir_all(&scripts_dir)?;
let escaped_bundle = bundle_name.replace('"', "\\\"");
let preinstall = scripts_dir.join("preinstall");
fs::write(
&preinstall,
format!(
"#!/bin/bash\n\
# `cargo truce package` preinstall: remove any prior\n\
# bundle at the destination before shove writes ours.\n\
# `$2` is the resolved install destination (with\n\
# `enable_currentUserHome` redirection applied).\n\
set -u\n\
BUNDLE=\"$2/{escaped_bundle}\"\n\
if [ -e \"$BUNDLE\" ]; then\n \
if rm -rf \"$BUNDLE\" 2>/dev/null; then\n \
echo \"preinstall: removed existing $BUNDLE\"\n \
else\n \
owner=$(stat -f '%Su' \"$BUNDLE\" 2>/dev/null || echo unknown)\n \
echo \"\" >&2\n \
echo \"ERROR: Cannot remove $BUNDLE (owner: $owner).\" >&2\n \
echo \"Either re-run with 'Install for all users of this computer',\" >&2\n \
echo \"or run: sudo rm -rf \\\"$BUNDLE\\\"\" >&2\n \
exit 1\n \
fi\n\
fi\n\
exit 0\n",
),
)?;
Command::new("chmod")
.args(["+x", preinstall.to_str().unwrap()])
.status()?;
if *fmt == PkgFormat::Au2 {
let postinstall = scripts_dir.join("postinstall");
fs::write(
&postinstall,
"#!/bin/bash\n\
killall -9 AudioComponentRegistrar 2>/dev/null || true\n\
rm -rf ~/Library/Caches/AudioUnitCache/ 2>/dev/null || true\n\
rm -f ~/Library/Preferences/com.apple.audio.InfoHelper.plist 2>/dev/null || true\n\
exit 0\n",
)?;
Command::new("chmod")
.args(["+x", postinstall.to_str().unwrap()])
.status()?;
}
if *fmt == PkgFormat::Au3
&& let Some(appex_id) = appex_id
{
let postinstall = scripts_dir.join("postinstall");
let script = AU3_REGISTER_POSTINSTALL
.replace("{{BUNDLE}}", &escaped_bundle)
.replace("{{APPEX_ID}}", appex_id);
fs::write(&postinstall, script)?;
Command::new("chmod")
.args(["+x", postinstall.to_str().unwrap()])
.status()?;
}
Ok(scripts_dir)
}