Skip to main content

nice_plug_xtask/
lib.rs

1use anyhow::Context;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8#[cfg(unix)]
9use std::os::unix::fs::PermissionsExt;
10
11mod symbols;
12mod util;
13
14/// Re-export for the main function.
15pub use anyhow::Result;
16
17fn build_usage_string(command_name: &str) -> String {
18    format!(
19        "Usage:
20  {command_name} bundle <package> [--release]
21  {command_name} bundle -p <package1> -p <package2> ... [--release]
22
23  {command_name} bundle-universal <package> [--release]  (macOS only)
24  {command_name} bundle-universal -p <package1> -p <package2> ... [--release]  (macOS only)
25
26  All other 'cargo build' options are supported, including '--target' and '--profile'."
27    )
28}
29
30/// Any additional configuration that might be useful for creating plugin bundles, stored as
31/// `bundler.toml` alongside the workspace's main `Cargo.toml` file.
32type BundlerConfig = HashMap<String, PackageConfig>;
33
34#[derive(Debug, Clone, Deserialize)]
35struct PackageConfig {
36    name: Option<String>,
37}
38
39/// The target we're generating a plugin for. This can be either the native target or a cross
40/// compilation target, so to reduce redundancy when determining the correct bundle paths we'll use
41/// an enum for this.
42#[derive(Debug, Clone, Copy)]
43pub enum CompilationTarget {
44    Linux(Architecture),
45    MacOS(Architecture),
46    /// A special case for lipo'd `x86_64-apple-darwin` and `aarch64-apple-darwin` builds.
47    MacOSUniversal,
48    Windows(Architecture),
49}
50
51#[derive(Debug, Clone, Copy)]
52pub enum Architecture {
53    X86,
54    X86_64,
55    RISCV64,
56    // There are also a ton of different 32-bit ARM architectures, we'll just pretend they don't
57    // exist for now
58    AArch64,
59}
60
61/// The type of a MacOS bundle.
62#[derive(Debug, Clone, Copy)]
63pub enum BundleType {
64    Plugin,
65    Binary,
66}
67
68/// The main xtask entry point function. See the readme for instructions on how to use this.
69pub fn main() -> Result<()> {
70    let args = std::env::args().skip(1);
71    main_with_args("cargo xtask", args)
72}
73
74/// The main xtask entry point function, but with custom command line arguments. `args` should not
75/// contain the command name, so you should always skip at least one argument from
76/// `std::env::args()` before passing it to this function.
77pub fn main_with_args(command_name: &str, args: impl IntoIterator<Item = String>) -> Result<()> {
78    chdir_workspace_root()?;
79    let cargo_metadata = cargo_metadata::MetadataCommand::new()
80        .manifest_path("./Cargo.toml")
81        .exec()
82        .context("Could not parse `cargo-metadata`")?;
83    let target_dir = cargo_metadata.target_directory.as_std_path();
84
85    let mut args = args.into_iter();
86    let usage_string = build_usage_string(command_name);
87    let command = args
88        .next()
89        .with_context(|| format!("Missing command name\n\n{usage_string}",))?;
90    match command.as_str() {
91        "bundle" => {
92            // For convenience's sake we'll allow building multiple packages with `-p` just like
93            // cargo build, but you can also build a single package without specifying `-p`. Since
94            // multiple packages can be built in parallel if we pass all of these flags to a single
95            // `cargo build` we'll first build all of these packages and only then bundle them.
96            let (packages, other_args) = split_bundle_args(args, &usage_string)?;
97
98            // As explained above, for efficiency's sake this is a two step process
99            build(&packages, &other_args)?;
100
101            bundle(target_dir, &packages[0], &other_args, false)?;
102            for package in packages.into_iter().skip(1) {
103                bundle(target_dir, &package, &other_args, false)?;
104            }
105
106            Ok(())
107        }
108        "bundle-universal" => {
109            // The same as `--bundle`, but builds universal binaries for macOS Cargo will also error
110            // out on duplicate `--target` options, but it seems like a good idea to preemptively
111            // abort the bundling process if that happens
112            let (packages, other_args) = split_bundle_args(args, &usage_string)?;
113
114            for arg in &other_args {
115                if arg == "--target" || arg.starts_with("--target=") {
116                    anyhow::bail!(
117                        "'{command_name} xtask bundle-universal' is incompatible with the '{arg}' \
118                         option."
119                    )
120                }
121            }
122
123            // We can just use the regular build function here. There's sadly no way to build both
124            // targets in parallel, so this will likely take twice as logn as a regular build.
125            // TODO: Explicitly specifying the target even on the native target causes a rebuild in
126            //       the target `target/<target_triple>` directory. This makes bundling much simpler
127            //       because there's no conditional logic required based on the current platform,
128            //       but it does waste some resources and requires a rebuild if the native target
129            //       was already built.
130            let mut x86_64_args = other_args.clone();
131            x86_64_args.push(String::from("--target=x86_64-apple-darwin"));
132            build(&packages, &x86_64_args)?;
133            let mut aarch64_args = other_args.clone();
134            aarch64_args.push(String::from("--target=aarch64-apple-darwin"));
135            build(&packages, &aarch64_args)?;
136
137            // This `true` indicates a universal build. This will cause the two sets of built
138            // binaries to beq lipo'd together into universal binaries before bundling
139            bundle(target_dir, &packages[0], &other_args, true)?;
140            for package in packages.into_iter().skip(1) {
141                bundle(target_dir, &package, &other_args, true)?;
142            }
143
144            Ok(())
145        }
146        // This is only meant to be used by the CI, since using awk for this can be a bit spotty on
147        // macOS
148        "known-packages" => list_known_packages(),
149        _ => anyhow::bail!("Unknown command '{command}'\n\n{usage_string}"),
150    }
151}
152
153/// Change the current directory into the Cargo workspace's root.
154///
155/// This is using a heuristic to find the workspace root. It considers all ancestor directories of
156/// either `CARGO_MANIFEST_DIR` or the current directory, and finds the leftmost one containing a
157/// `Cargo.toml` file.
158pub fn chdir_workspace_root() -> Result<()> {
159    // This is either the directory of the xtask binary when using `nice-plug-xtask` normally, or any
160    // random project when using it through `cargo nice-plug`.
161    let project_dir = std::env::var("CARGO_MANIFEST_DIR")
162        .map(PathBuf::from)
163        .or_else(|_| std::env::current_dir())
164        .context(
165            "'$CARGO_MANIFEST_DIR' was not set and the current working directory could not be \
166             found",
167        )?;
168
169    let workspace_root = project_dir
170        .ancestors()
171        .filter(|dir| dir.join("Cargo.toml").exists())
172        // The ancestors are ordered starting from `project_dir` going up to the filesystem root. So
173        // this is the leftmost matching ancestor.
174        .last()
175        .with_context(|| {
176            format!(
177                "Could not find a 'Cargo.toml' file in '{}' or any of its parent directories",
178                project_dir.display()
179            )
180        })?;
181
182    std::env::set_current_dir(workspace_root)
183        .context("Could not change to workspace root directory")
184}
185
186/// Build one or more packages using the provided `cargo build` arguments. This should be called
187/// before calling [`bundle()`]. This requires the current working directory to have been set to
188/// the workspace's root using [`chdir_workspace_root()`].
189pub fn build(packages: &[String], args: &[String]) -> Result<()> {
190    let package_args = packages.iter().flat_map(|package| ["-p", package]);
191
192    let status = Command::new("cargo")
193        .arg("build")
194        .args(package_args)
195        .args(args)
196        .status()
197        .with_context(|| format!("Could not call cargo to build {}", packages.join(", ")))?;
198    if !status.success() {
199        anyhow::bail!("Could not build {}", packages.join(", "));
200    } else {
201        Ok(())
202    }
203}
204
205/// Bundle a package that was previously built by a call to [`build()`] using the provided `cargo
206/// build` arguments. These two functions are split up because building can be done in parallel by
207/// Cargo itself while bundling is sequential. Options from the `bundler.toml` file in the
208/// workspace's root are respected (see
209/// <https://codeberg.org/BillyDM/nice-plug/src/branch/main/bundler-example.toml>). This requires the
210/// current working directory to have been set to the workspace's root using [`chdir_workspace_root()`].
211///
212/// If the package also exposes a binary target in addition to a library (or just a binary, in case
213/// the binary target has a different name) then this will also be copied into the `bundled`
214/// directory.
215///
216/// Normally this respects the `--target` option for cross compilation. If the `universal` option is
217/// specified instead, then this will assume both `x86_64-apple-darwin` and `aarch64-apple-darwin`
218/// have been built and it will try to lipo those together instead.
219pub fn bundle(target_dir: &Path, package: &str, args: &[String], universal: bool) -> Result<()> {
220    let mut build_type_dir = "debug";
221    let mut cross_compile_target: Option<String> = None;
222    for arg_idx in (0..args.len()).rev() {
223        let arg = &args[arg_idx];
224        match arg.as_str() {
225            "--profile" => {
226                // Since Rust 1.57 you can have custom profiles
227                build_type_dir = args.get(arg_idx + 1).context("Missing profile name")?;
228            }
229            "--release" => build_type_dir = "release",
230            "--target" => {
231                // When cross compiling we should generate the correct bundle type
232                cross_compile_target = Some(
233                    args.get(arg_idx + 1)
234                        .context("Missing cross-compile target")?
235                        .to_owned(),
236                );
237            }
238            arg if arg.starts_with("--profile=") => {
239                build_type_dir = arg
240                    .strip_prefix("--profile=")
241                    .context("Missing profile name")?;
242            }
243            arg if arg.starts_with("--target=") => {
244                cross_compile_target = Some(
245                    arg.strip_prefix("--target=")
246                        .context("Missing cross-compile target")?
247                        .to_owned(),
248                );
249            }
250            _ => (),
251        }
252    }
253
254    // We can bundle both library targets (for plugins) and binary targets (for standalone
255    // applications)
256    if universal {
257        let x86_64_target_base =
258            target_base(target_dir, Some("x86_64-apple-darwin"))?.join(build_type_dir);
259        let x86_64_bin_path = x86_64_target_base.join(binary_basename(
260            package,
261            CompilationTarget::MacOS(Architecture::X86_64),
262        ));
263        let x86_64_lib_path = x86_64_target_base.join(library_basename(
264            package,
265            CompilationTarget::MacOS(Architecture::X86_64),
266        ));
267
268        let aarch64_target_base =
269            target_base(target_dir, Some("aarch64-apple-darwin"))?.join(build_type_dir);
270        let aarch64_bin_path = aarch64_target_base.join(binary_basename(
271            package,
272            CompilationTarget::MacOS(Architecture::AArch64),
273        ));
274        let aarch64_lib_path = aarch64_target_base.join(library_basename(
275            package,
276            CompilationTarget::MacOS(Architecture::AArch64),
277        ));
278
279        let build_bin = x86_64_bin_path.exists() && aarch64_bin_path.exists();
280        let build_lib = x86_64_lib_path.exists() && aarch64_lib_path.exists();
281        if !build_bin && !build_lib {
282            anyhow::bail!("Could not find built libraries for universal build.");
283        }
284
285        eprintln!();
286        if build_bin {
287            bundle_binary(
288                target_dir,
289                package,
290                &[&x86_64_bin_path, &aarch64_bin_path],
291                CompilationTarget::MacOSUniversal,
292            )?;
293        }
294        if build_lib {
295            bundle_plugin(
296                target_dir,
297                package,
298                &[&x86_64_lib_path, &aarch64_lib_path],
299                CompilationTarget::MacOSUniversal,
300            )?;
301        }
302    } else {
303        let compilation_target = compilation_target(cross_compile_target.as_deref())?;
304        let target_base =
305            target_base(target_dir, cross_compile_target.as_deref())?.join(build_type_dir);
306        let bin_path = target_base.join(binary_basename(package, compilation_target));
307        let lib_path = target_base.join(library_basename(package, compilation_target));
308        if !bin_path.exists() && !lib_path.exists() {
309            anyhow::bail!(
310                r#"Could not find a built library at '{}'.
311
312Hint: Maybe you forgot to add:
313
314[lib]
315crate-type = ["cdylib"]
316
317to your Cargo.toml file?"#,
318                lib_path.display()
319            );
320        }
321
322        eprintln!();
323        if bin_path.exists() {
324            bundle_binary(target_dir, package, &[&bin_path], compilation_target)?;
325        }
326        if lib_path.exists() {
327            bundle_plugin(target_dir, package, &[&lib_path], compilation_target)?;
328        }
329    }
330
331    Ok(())
332}
333
334/// Bundle a standalone target. If `bin_path` contains more than one path, then the binaries will be
335/// combined into a single binary using a method that depends on the compilation target. For
336/// universal macOS builds this uses lipo.
337fn bundle_binary(
338    target_dir: &Path,
339    package: &str,
340    bin_paths: &[&Path],
341    compilation_target: CompilationTarget,
342) -> Result<()> {
343    let bundle_home_dir = bundle_home(target_dir);
344    let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) {
345        Some(PackageConfig { name: Some(name) }) => name,
346        _ => package.to_string(),
347    };
348
349    // On MacOS the standalone target needs to be in a bundle
350    let standalone_bundle_binary_name =
351        standalone_bundle_binary_name(&bundle_name, compilation_target);
352    let standalone_binary_path = bundle_home_dir.join(&standalone_bundle_binary_name);
353
354    fs::create_dir_all(standalone_binary_path.parent().unwrap())
355        .context("Could not create standalone bundle directory")?;
356    util::reflink_or_combine(bin_paths, &standalone_binary_path, compilation_target)
357        .context("Could not create standalone bundle")?;
358
359    // FIXME: The reflink crate seems to sometime strip away the executable bit, so we need to help
360    //        it a little here
361    #[cfg(unix)]
362    if let Ok(metadata) = fs::metadata(&standalone_binary_path) {
363        // These are the executable bits
364        let mut permissions = metadata.permissions();
365        permissions.set_mode(permissions.mode() | 0b0001001001);
366
367        fs::set_permissions(&standalone_binary_path, permissions).with_context(|| {
368            format!(
369                "Could not make '{}' executable",
370                standalone_binary_path.display()
371            )
372        })?;
373    }
374
375    let standalone_bundle_home = bundle_home_dir.join(
376        Path::new(&standalone_bundle_binary_name)
377            .components()
378            .next()
379            .expect("Malformed standalone binary path"),
380    );
381    maybe_create_macos_bundle_metadata(
382        package,
383        &bundle_name,
384        &standalone_bundle_home,
385        compilation_target,
386        BundleType::Binary,
387    )?;
388    maybe_codesign(&standalone_bundle_home, compilation_target);
389
390    eprintln!(
391        "Created a standalone bundle at '{}'",
392        standalone_bundle_home.display()
393    );
394
395    Ok(())
396}
397
398/// Bundle all plugin targets for a plugin library. If `lib_path` contains more than one path, then
399/// the libraries will be combined into a single library using a method that depends on the
400/// compilation target. For universal macOS builds this uses lipo.
401fn bundle_plugin(
402    target_dir: &Path,
403    package: &str,
404    lib_paths: &[&Path],
405    compilation_target: CompilationTarget,
406) -> Result<()> {
407    let bundle_home_dir = bundle_home(target_dir);
408    let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) {
409        Some(PackageConfig { name: Some(name) }) => name,
410        _ => package.to_string(),
411    };
412
413    // We'll detect the plugin formats supported by the plugin binary and create bundled accordingly.
414    // If `lib_path` contains paths to multiple plugins that need to be combined into a macOS
415    // universal binary, then we'll assume all of them export the same symbols and only check the
416    // first one.
417    let first_lib_path = lib_paths.first().context("Empty library paths slice")?;
418
419    let bundle_clap = symbols::exported(first_lib_path, "clap_entry")
420        .with_context(|| format!("Could not parse '{}'", first_lib_path.display()))?;
421    // We'll ignore the platform-specific entry points for VST2 plugins since there's no reason to
422    // create a new Rust VST2 plugin that doesn't work in modern DAWs
423    // NOTE: nice-plug does not support VST2, but we'll support bundling VST2 plugins anyways because
424    //       this bundler can also be used standalone.
425    let bundle_vst2 = symbols::exported(first_lib_path, "VSTPluginMain")
426        .with_context(|| format!("Could not parse '{}'", first_lib_path.display()))?;
427    let bundle_vst3 = symbols::exported(first_lib_path, "GetPluginFactory")
428        .with_context(|| format!("Could not parse '{}'", first_lib_path.display()))?;
429    let bundled_plugin = bundle_clap || bundle_vst2 || bundle_vst3;
430
431    if bundle_clap {
432        let clap_bundle_library_name = clap_bundle_library_name(&bundle_name, compilation_target);
433        let clap_lib_path = bundle_home_dir.join(&clap_bundle_library_name);
434
435        fs::create_dir_all(clap_lib_path.parent().unwrap())
436            .context("Could not create CLAP bundle directory")?;
437        util::reflink_or_combine(lib_paths, &clap_lib_path, compilation_target)
438            .context("Could not create CLAP bundle")?;
439
440        // In contrast to VST3, CLAP only uses bundles on macOS, so we'll just take the first
441        // component of the library name instead
442        let clap_bundle_home = bundle_home_dir.join(
443            Path::new(&clap_bundle_library_name)
444                .components()
445                .next()
446                .expect("Malformed CLAP library path"),
447        );
448        maybe_create_macos_bundle_metadata(
449            package,
450            &bundle_name,
451            &clap_bundle_home,
452            compilation_target,
453            BundleType::Plugin,
454        )?;
455        maybe_codesign(&clap_bundle_home, compilation_target);
456
457        eprintln!("Created a CLAP bundle at '{}'", clap_bundle_home.display());
458    }
459    if bundle_vst2 {
460        let vst2_bundle_library_name = vst2_bundle_library_name(&bundle_name, compilation_target);
461        let vst2_lib_path = bundle_home_dir.join(&vst2_bundle_library_name);
462
463        fs::create_dir_all(vst2_lib_path.parent().unwrap())
464            .context("Could not create VST2 bundle directory")?;
465        util::reflink_or_combine(lib_paths, &vst2_lib_path, compilation_target)
466            .context("Could not create VST2 bundle")?;
467
468        // VST2 only uses bundles on macOS, so we'll just take the first component of the library
469        // name instead
470        let vst2_bundle_home = bundle_home_dir.join(
471            Path::new(&vst2_bundle_library_name)
472                .components()
473                .next()
474                .expect("Malformed VST2 library path"),
475        );
476        maybe_create_macos_bundle_metadata(
477            package,
478            &bundle_name,
479            &vst2_bundle_home,
480            compilation_target,
481            BundleType::Plugin,
482        )?;
483        maybe_codesign(&vst2_bundle_home, compilation_target);
484
485        eprintln!("Created a VST2 bundle at '{}'", vst2_bundle_home.display());
486    }
487    if bundle_vst3 {
488        let vst3_lib_path =
489            bundle_home_dir.join(vst3_bundle_library_name(&bundle_name, compilation_target));
490
491        fs::create_dir_all(vst3_lib_path.parent().unwrap())
492            .context("Could not create VST3 bundle directory")?;
493        util::reflink_or_combine(lib_paths, &vst3_lib_path, compilation_target)
494            .context("Could not create VST3 bundle")?;
495
496        let vst3_bundle_home = vst3_lib_path
497            .parent()
498            .unwrap()
499            .parent()
500            .unwrap()
501            .parent()
502            .unwrap();
503        maybe_create_macos_bundle_metadata(
504            package,
505            &bundle_name,
506            vst3_bundle_home,
507            compilation_target,
508            BundleType::Plugin,
509        )?;
510        maybe_codesign(vst3_bundle_home, compilation_target);
511
512        eprintln!("Created a VST3 bundle at '{}'", vst3_bundle_home.display());
513    }
514    if !bundled_plugin {
515        eprintln!("Not creating any plugin bundles because the package does not export any plugins")
516    }
517
518    Ok(())
519}
520
521/// This lists the packages configured in `bundler.toml`. This is only used as part of the CI when
522/// bundling plugins.
523pub fn list_known_packages() -> Result<()> {
524    if let Some(config) = load_bundler_config()? {
525        for package in config.keys() {
526            println!("{package}");
527        }
528    }
529
530    Ok(())
531}
532
533/// Load the `bundler.toml` file, if it exists. If it does exist but it cannot be parsed, then this
534/// will return an error.
535fn load_bundler_config() -> Result<Option<BundlerConfig>> {
536    // We're already in the project root
537    let bundler_config_path = Path::new("bundler.toml");
538    if !bundler_config_path.exists() {
539        return Ok(None);
540    }
541
542    let result = toml::from_str(
543        &fs::read_to_string(bundler_config_path)
544            .with_context(|| format!("Could not read '{}'", bundler_config_path.display()))?,
545    )
546    .with_context(|| format!("Could not parse '{}'", bundler_config_path.display()))?;
547
548    Ok(Some(result))
549}
550
551/// Split the `xtask bundle` arguments into a list of packages and a list of other arguments. The
552/// package vector either contains just the first argument, or if the arguments iterator starts with
553/// one or more occurences of `-p <package>` then this will contain all those packages.
554fn split_bundle_args(
555    args: impl Iterator<Item = String>,
556    usage_string: &str,
557) -> Result<(Vec<String>, Vec<String>)> {
558    let mut args = args.peekable();
559    let mut packages = Vec::new();
560    if args.peek().map(|s| s.as_str()) == Some("-p") {
561        while args.peek().map(|s| s.as_str()) == Some("-p") {
562            packages.push(
563                args.nth(1)
564                    .with_context(|| format!("Missing package name after -p\n\n{usage_string}"))?,
565            );
566        }
567    } else {
568        packages.push(
569            args.next()
570                .with_context(|| format!("Missing package name\n\n{usage_string}"))?,
571        );
572    };
573    let other_args: Vec<_> = args.collect();
574
575    Ok((packages, other_args))
576}
577
578/// The target we're compiling for. This is used to determine the paths and options for creating
579/// plugin bundles.
580fn compilation_target(cross_compile_target: Option<&str>) -> Result<CompilationTarget> {
581    match cross_compile_target {
582        Some("i686-unknown-linux-gnu") => Ok(CompilationTarget::Linux(Architecture::X86)),
583        Some("i686-apple-darwin") => Ok(CompilationTarget::MacOS(Architecture::X86)),
584        Some("i686-pc-windows-gnu") | Some("i686-pc-windows-msvc") => {
585            Ok(CompilationTarget::Windows(Architecture::X86))
586        }
587        Some("x86_64-unknown-linux-gnu") => Ok(CompilationTarget::Linux(Architecture::X86_64)),
588        Some("x86_64-apple-darwin") => Ok(CompilationTarget::MacOS(Architecture::X86_64)),
589        Some("x86_64-pc-windows-gnu") | Some("x86_64-pc-windows-msvc") => {
590            Ok(CompilationTarget::Windows(Architecture::X86_64))
591        }
592        Some("aarch64-unknown-linux-gnu") => Ok(CompilationTarget::Linux(Architecture::AArch64)),
593        Some("aarch64-apple-darwin") => Ok(CompilationTarget::MacOS(Architecture::AArch64)),
594        Some("aarch64-pc-windows-gnu") | Some("aarch64-pc-windows-msvc") => {
595            Ok(CompilationTarget::Windows(Architecture::AArch64))
596        }
597        Some(target) => anyhow::bail!("Unhandled cross-compilation target: {}", target),
598        None => {
599            #[cfg(target_arch = "x86")]
600            let architecture = Architecture::X86;
601            #[cfg(target_arch = "x86_64")]
602            let architecture = Architecture::X86_64;
603            #[cfg(target_arch = "aarch64")]
604            let architecture = Architecture::AArch64;
605            #[cfg(target_arch = "riscv64")]
606            let architecture = Architecture::RISCV64;
607
608            #[cfg(all(target_family = "unix", not(target_os = "macos")))]
609            return Ok(CompilationTarget::Linux(architecture));
610            #[cfg(target_os = "macos")]
611            return Ok(CompilationTarget::MacOS(architecture));
612            #[cfg(target_os = "windows")]
613            return Ok(CompilationTarget::Windows(architecture));
614        }
615    }
616}
617
618/// The directory bundled plugins should be written to.
619fn bundle_home(target_directory: &Path) -> PathBuf {
620    target_directory.join("bundled")
621}
622
623/// The base directory for the compiled binaries. This does not use [`CompilationTarget`] as we need
624/// to be able to differentiate between native and cross-compilation.
625fn target_base(target_directory: &Path, cross_compile_target: Option<&str>) -> Result<PathBuf> {
626    match cross_compile_target {
627        // Unhandled targets will already be handled in `compilation_target`
628        Some(target) => Ok(target_directory.join(target)),
629        None => Ok(target_directory.to_owned()),
630    }
631}
632
633/// The file name of the compiled library for a binary crate.
634fn binary_basename(package: &str, target: CompilationTarget) -> String {
635    // Cargo will replace dashes with underscores
636    let bin_name = package.replace('-', "_");
637
638    match target {
639        CompilationTarget::Linux(_)
640        | CompilationTarget::MacOS(_)
641        | CompilationTarget::MacOSUniversal => bin_name,
642        CompilationTarget::Windows(_) => format!("{bin_name}.exe"),
643    }
644}
645
646/// The file name of the compiled library for a `cdylib` crate.
647fn library_basename(package: &str, target: CompilationTarget) -> String {
648    // Cargo will replace dashes with underscores
649    let lib_name = package.replace('-', "_");
650
651    match target {
652        CompilationTarget::Linux(_) => format!("lib{lib_name}.so"),
653        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal => {
654            format!("lib{lib_name}.dylib")
655        }
656        CompilationTarget::Windows(_) => format!("{lib_name}.dll"),
657    }
658}
659
660/// The filename of the binary target. On macOS this is part of a bundle.
661fn standalone_bundle_binary_name(package: &str, target: CompilationTarget) -> String {
662    match target {
663        CompilationTarget::Linux(_) => package.to_owned(),
664        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal => {
665            format!("{package}.app/Contents/MacOS/{package}")
666        }
667        CompilationTarget::Windows(_) => format!("{package}.exe"),
668    }
669}
670
671/// The filename of the CLAP plugin for Linux and Windows, or the full path to the library file
672/// inside of a CLAP bundle on macOS.
673fn clap_bundle_library_name(package: &str, target: CompilationTarget) -> String {
674    match target {
675        CompilationTarget::Linux(_) | CompilationTarget::Windows(_) => format!("{package}.clap"),
676        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal => {
677            format!("{package}.clap/Contents/MacOS/{package}")
678        }
679    }
680}
681
682/// On Linux and Windows VST2 plugins are regular library files, and on macOS they are put in a
683/// bundle.
684fn vst2_bundle_library_name(package: &str, target: CompilationTarget) -> String {
685    match target {
686        CompilationTarget::Linux(_) => format!("{package}.so"),
687        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal => {
688            format!("{package}.vst/Contents/MacOS/{package}")
689        }
690        CompilationTarget::Windows(_) => format!("{package}.dll"),
691    }
692}
693
694/// The full path to the library file inside of a VST3 bundle, including the leading `.vst3`
695/// directory.
696///
697/// See <https://developer.steinberg.help/display/VST/Plug-in+Format+Structure>.
698fn vst3_bundle_library_name(package: &str, target: CompilationTarget) -> String {
699    match target {
700        CompilationTarget::Linux(Architecture::X86) => {
701            format!("{package}.vst3/Contents/i386-linux/{package}.so")
702        }
703        CompilationTarget::Linux(Architecture::X86_64) => {
704            format!("{package}.vst3/Contents/x86_64-linux/{package}.so")
705        }
706        CompilationTarget::Linux(Architecture::RISCV64) => {
707            format!("{package}.vst3/Contents/riscv64-linux/{package}.so")
708        }
709        CompilationTarget::Linux(Architecture::AArch64) => {
710            format!("{package}.vst3/Contents/aarch64-linux/{package}.so")
711        }
712        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal => {
713            format!("{package}.vst3/Contents/MacOS/{package}")
714        }
715        CompilationTarget::Windows(Architecture::X86) => {
716            format!("{package}.vst3/Contents/x86-win/{package}.vst3")
717        }
718        CompilationTarget::Windows(Architecture::X86_64) => {
719            format!("{package}.vst3/Contents/x86_64-win/{package}.vst3")
720        }
721        CompilationTarget::Windows(Architecture::AArch64) => {
722            format!("{package}.vst3/Contents/arm_64-win/{package}.vst3")
723        }
724        CompilationTarget::Windows(Architecture::RISCV64) => {
725            panic!("riscv64 are not supported by windows currently!")
726        }
727    }
728}
729
730/// If compiling for macOS, create all of the bundl-y stuff Steinberg and Apple require you to have.
731///
732/// This still requires you to move the dylib file to `{bundle_home}/Contents/macOS/{package}`
733/// yourself first.
734pub fn maybe_create_macos_bundle_metadata(
735    package: &str,
736    display_name: &str,
737    bundle_home: &Path,
738    target: CompilationTarget,
739    bundle_type: BundleType,
740) -> Result<()> {
741    if !matches!(
742        target,
743        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal
744    ) {
745        return Ok(());
746    }
747
748    let package_type = match bundle_type {
749        BundleType::Plugin => "BNDL",
750        BundleType::Binary => "APPL",
751    };
752
753    // TODO: May want to add bundler.toml fields for the identifier, version and signature at some
754    //       point.
755    fs::write(
756        bundle_home.join("Contents").join("PkgInfo"),
757        format!("{package_type}????"),
758    )
759    .context("Could not create PkgInfo file")?;
760    fs::write(
761        bundle_home.join("Contents").join("Info.plist"),
762        format!(r#"<?xml version="1.0" encoding="UTF-8"?>
763
764<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
765<plist>
766  <dict>
767    <key>CFBundleExecutable</key>
768    <string>{display_name}</string>
769    <key>CFBundleIconFile</key>
770    <string></string>
771    <key>CFBundleIdentifier</key>
772    <string>com.nice-plug.{package}</string>
773    <key>CFBundleName</key>
774    <string>{display_name}</string>
775    <key>CFBundleDisplayName</key>
776    <string>{display_name}</string>
777    <key>CFBundlePackageType</key>
778    <string>{package_type}</string>
779    <key>CFBundleSignature</key>
780    <string>????</string>
781    <key>CFBundleShortVersionString</key>
782    <string>1.0.0</string>
783    <key>CFBundleVersion</key>
784    <string>1.0.0</string>
785    <key>NSHumanReadableCopyright</key>
786    <string></string>
787    <key>NSHighResolutionCapable</key>
788    <true/>
789  </dict>
790</plist>
791"#),
792    )
793    .context("Could not create Info.plist file")?;
794
795    Ok(())
796}
797
798/// If compiling for macOS, try to self-sign the bundle at the given path. This shouldn't be
799/// necessary, but AArch64 macOS is stricter about these things and sometimes self built plugins may
800/// not load otherwise. Presumably in combination with hardened runtimes.
801///
802/// If the codesigning command could not be run then this merely prints a warning.
803pub fn maybe_codesign(bundle_home: &Path, target: CompilationTarget) {
804    if !matches!(
805        target,
806        CompilationTarget::MacOS(_) | CompilationTarget::MacOSUniversal
807    ) {
808        return;
809    }
810
811    let success = Command::new("codesign")
812        .arg("-f")
813        .arg("-s")
814        .arg("-")
815        .arg(bundle_home)
816        .status()
817        .is_ok();
818    if !success {
819        eprintln!(
820            "WARNING: Could not self-sign '{}', it may fail to run depending on the environment",
821            bundle_home.display()
822        )
823    }
824}