prokopyl / clack

Safe, low-level wrapper to create CLAP audio plugins and hosts in Rust
Apache License 2.0
90 stars 11 forks source link

Create bundles on macOS #18

Open BenLeadbetter opened 8 months ago

BenLeadbetter commented 8 months ago

I noticed that on macOS the example plugins are just raw binary files. Most tooling expects a "bundle" for a clap plugins with a Info.plist, etc. I'm only able to load the examples in REAPER, or validate with the clap-validator after I manually wrap the dynamic libraries into a basic bundle.

Should we consider integrating with tools like cargo-bundle to have this stuff happen automatically?

prokopyl commented 8 months ago

I think managing the generation of the output bundles is outside of the scope for Clack (which is only a safe wrapper around the CLAP APIs), and is something better suited to be implemented by higher-level plugin frameworks that already handle cross-API/cross-platform bundle generation.

That being said, I think it would be beneficial to use something like cargo-bundle for the examples instead of integrating it into Clack directly. That way people would have an easier time to get the examples up and running in actual hosts.

However, I don't have a macOS machine available to test things, and to be fair I have absolutely no knowledge of how things are handled on macOS at all, so I'd definitely welcome help and contributions on that front. :slightly_smiling_face:

CrushedPixel commented 6 days ago

Here's some simple Rust code that wraps a single-entry .dylib as a working .clap bundle on macOS. I integrated/call this using the xtask pattern.

Make sure to update the CFBundleIdentifier as appropriate.

#[cfg(target_os = "macos")]
fn package_as_clap_macos(plugin_name: &str) -> Result<(), DynError> {
    println!("Creating bundle for {}", plugin_name);
    let bundle_name = format!("{}.clap", plugin_name);
    let bundle_root = project_root().join("target").join(&bundle_name);
    let contents_dir = bundle_root.join("Contents");
    let macos_dir = contents_dir.join("MacOS");

    // Create directory structure
    fs::create_dir_all(&macos_dir)?;

    // Define dylib name
    let dylib_name = format!("lib{}.dylib", plugin_name);

    // Copy dylib
    let src_dylib = project_root()
        .join("target")
        .join("release")
        .join(&dylib_name);
    let dst_dylib = macos_dir.join(&dylib_name);
    fs::copy(&src_dylib, &dst_dylib)?;
    println!("Copied dylib from {:?} to {:?}", src_dylib, dst_dylib);

    // Create Info.plist
    let info_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>{}</string>
    <key>CFBundleExecutable</key>
    <string>{}</string>
    <key>CFBundleIdentifier</key>
    <string>com.yourcompany.{}</string>
</dict>
</plist>"#,
        plugin_name,
        dylib_name,
        plugin_name.to_lowercase()
    );
    let info_plist_path = contents_dir.join("Info.plist");
    fs::write(&info_plist_path, info_plist)?;
    println!("Created Info.plist at {:?}", info_plist_path);

    println!("Bundle created at {:?}", bundle_root);
    Ok(())
}