#![cfg(target_os = "macos")]
use std::path::{Path, PathBuf};
use std::process::Command;
use super::build::MacArch;
pub(crate) const CLAP_EXPORTS: &[&str] = &["_clap_entry"];
pub(crate) const VST3_EXPORTS: &[&str] = &[
"_GetPluginFactory",
"_BundleEntry",
"_bundleEntry",
"_BundleExit",
"_bundleExit",
];
pub(crate) const VST2_EXPORTS: &[&str] = &["_VSTPluginMain", "_main_macho"];
pub(crate) fn missing_staticlib_error(staticlib_path: &Path) -> String {
format!(
"macOS bundle link needs a Rust staticlib at\n \
{path}\n\
but cargo didn't emit one.\n\
\n\
Starting with truce 0.44.0, macOS bundle formats (CLAP / VST3 / VST2) \
are linked from `lib<stem>.a` via `clang -bundle`. Plugins scaffolded \
before 0.44.0 only declared `[\"cdylib\", \"rlib\"]` and need to add \
`\"staticlib\"` to the `crate-type` array in their plugin crate's \
`Cargo.toml`.\n\
\n\
Exact change:\n\
\n\
# before\n\
[lib]\n\
crate-type = [\"cdylib\", \"rlib\"]\n\
\n\
# after\n\
[lib]\n\
crate-type = [\"cdylib\", \"staticlib\", \"rlib\"]\n\
\n\
Then re-run the failing command.",
path = staticlib_path.display(),
)
}
const MACOS_PLUGIN_FRAMEWORKS: &[&str] = &[
"AppKit",
"Foundation",
"CoreFoundation",
"QuartzCore",
"Metal",
"AudioToolbox",
"AVFAudio",
"CoreAudio",
"CoreMIDI",
"CoreGraphics",
];
pub(crate) fn link_macos_bundle(
staticlibs: &[(MacArch, PathBuf)],
exports: &[&str],
deployment_target: &str,
out_bundle_bin: &Path,
) -> crate::Res {
if staticlibs.is_empty() {
return Err("link_macos_bundle: no input static archives".into());
}
for (_, p) in staticlibs {
if !p.exists() {
return Err(
format!("link_macos_bundle: missing static archive {}", p.display()).into(),
);
}
}
if let Some(parent) = out_bundle_bin.parent() {
std::fs::create_dir_all(parent)?;
}
if staticlibs.len() == 1 {
let (arch, staticlib) = &staticlibs[0];
return clang_bundle_single(*arch, staticlib, exports, deployment_target, out_bundle_bin);
}
let mut per_arch_outputs: Vec<PathBuf> = Vec::with_capacity(staticlibs.len());
for (arch, staticlib) in staticlibs {
let slice_out = out_bundle_bin.with_extension(format!("{}-slice", arch.triple()));
clang_bundle_single(*arch, staticlib, exports, deployment_target, &slice_out)?;
per_arch_outputs.push(slice_out);
}
super::build::lipo_into(&per_arch_outputs, out_bundle_bin)?;
for slice in &per_arch_outputs {
let _ = std::fs::remove_file(slice);
}
Ok(())
}
fn clang_bundle_single(
arch: MacArch,
staticlib: &Path,
exports: &[&str],
deployment_target: &str,
out: &Path,
) -> crate::Res {
let arch_flag = match arch {
MacArch::Arm64 => "arm64",
MacArch::X86_64 => "x86_64",
};
let deduped = dedupe_archive_members(staticlib)?;
let staticlib = deduped.path();
let mut cmd = Command::new("clang");
cmd.args([
"-bundle",
"-arch",
arch_flag,
&format!("-mmacosx-version-min={deployment_target}"),
"-Wl,-undefined,dynamic_lookup",
"-Wl,-all_load",
]);
for framework in MACOS_PLUGIN_FRAMEWORKS {
cmd.args(["-framework", framework]);
}
cmd.arg("-lc++");
for sym in exports {
cmd.arg(format!("-Wl,-exported_symbol,{sym}"));
}
cmd.arg("-Wl,-dead_strip");
cmd.arg(staticlib);
cmd.arg("-o").arg(out);
let output = cmd.output().map_err(|e| -> crate::CargoTruceError {
format!("invoking clang for bundle link: {e}").into()
})?;
if !output.status.success() {
return Err(format!(
"clang -bundle failed for {} ({arch_flag}):\n{}",
staticlib.display(),
String::from_utf8_lossy(&output.stderr),
)
.into());
}
Ok(())
}
struct DedupedArchive {
archive_path: PathBuf,
temp_dir: PathBuf,
}
impl DedupedArchive {
fn path(&self) -> &Path {
&self.archive_path
}
}
impl Drop for DedupedArchive {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.temp_dir);
}
}
fn dedupe_archive_members(staticlib: &Path) -> Result<DedupedArchive, crate::CargoTruceError> {
let parent = staticlib
.parent()
.ok_or_else(|| -> crate::CargoTruceError {
format!(
"staticlib path has no parent directory: {}",
staticlib.display()
)
.into()
})?;
let stem = staticlib
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("staticlib");
let temp_dir = parent.join(format!("{stem}.dedup-{}", std::process::id()));
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir)?;
let extract = Command::new("ar")
.arg("-x")
.arg(staticlib)
.current_dir(&temp_dir)
.output()
.map_err(|e| -> crate::CargoTruceError {
format!("invoking ar -x for archive dedupe: {e}").into()
})?;
if !extract.status.success() {
return Err(format!(
"ar -x failed for {}:\n{}",
staticlib.display(),
String::from_utf8_lossy(&extract.stderr),
)
.into());
}
let mut members: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(&temp_dir)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let path = entry.path();
if path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("__.SYMDEF"))
{
continue;
}
members.push(path);
}
if members.is_empty() {
return Err(format!(
"ar -x produced no object members from {} - archive may be empty or corrupt",
staticlib.display()
)
.into());
}
let archive_path = parent.join(format!("{stem}.dedup-{}.a", std::process::id()));
if archive_path.exists() {
let _ = std::fs::remove_file(&archive_path);
}
let mut compose = Command::new("ar");
compose.arg("-rcs").arg(&archive_path);
for m in &members {
compose.arg(m);
}
let composed = compose.output().map_err(|e| -> crate::CargoTruceError {
format!("invoking ar -rcs for archive dedupe: {e}").into()
})?;
if !composed.status.success() {
return Err(format!(
"ar -rcs failed for {}:\n{}",
archive_path.display(),
String::from_utf8_lossy(&composed.stderr),
)
.into());
}
Ok(DedupedArchive {
archive_path,
temp_dir,
})
}