Move the xtask binary to a library
So you can use it in your own projects with minimal copying and pasting.
This commit is contained in:
parent
5b369f6ab5
commit
8e0597b780
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -563,6 +563,17 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nih_plug_xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"goblin",
|
||||||
|
"reflink",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.22.3"
|
version = "0.22.3"
|
||||||
|
@ -1153,9 +1164,5 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"nih_plug_xtask",
|
||||||
"goblin",
|
|
||||||
"reflink",
|
|
||||||
"serde",
|
|
||||||
"toml",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,6 +13,7 @@ repository = "https://github.com/robbert-vdh/nih-plugs"
|
||||||
members = [
|
members = [
|
||||||
"nih_plug_derive",
|
"nih_plug_derive",
|
||||||
"nih_plug_egui",
|
"nih_plug_egui",
|
||||||
|
"nih_plug_xtask",
|
||||||
"xtask",
|
"xtask",
|
||||||
|
|
||||||
"plugins/examples/gain",
|
"plugins/examples/gain",
|
||||||
|
|
15
nih_plug_xtask/Cargo.toml
Normal file
15
nih_plug_xtask/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "nih_plug_xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Robbert van der Helm <mail@robbertvanderhelm.nl>"]
|
||||||
|
description = "NIH-plug's cargo xtask command, as a library"
|
||||||
|
license = "ISC"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
goblin = "0.5"
|
||||||
|
# Version 0.1.3 from crates.io assumes a 64-bit toolchain
|
||||||
|
reflink = { git = "https://github.com/nicokoch/reflink", rev = "e8d93b465f5d9ad340cd052b64bbc77b8ee107e2" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.5"
|
45
nih_plug_xtask/README.md
Normal file
45
nih_plug_xtask/README.md
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# NIH-plug: bundler and other utilities
|
||||||
|
|
||||||
|
This is NIH-plug's `cargo xtask` command, but as a library. This way you can use
|
||||||
|
it in your own projects without forking this repo or copying the binary into
|
||||||
|
your own repo. This is necessary until Cargo supports [running binaries from
|
||||||
|
dependencies directly](https://github.com/rust-lang/rfcs/pull/3168).
|
||||||
|
|
||||||
|
To use this, add an `xtask` binary to your project using `cargo new --bin xtask`
|
||||||
|
and add it to the Cargo workspace in your repository's main `Cargo.toml` file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["xtask"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add `nih_plug_xtask` to your new xtask package's dependencies:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# xtask/Cargo.toml
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Call `nih_plug_xtask`'s main function own xtask binary:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# xtask/src/main.rs
|
||||||
|
|
||||||
|
fn main() -> nih_plug_xtask::Result<()> {
|
||||||
|
nih_plug_xtask::main()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally create a `.cargo/config` file in your repository and add a Cargo
|
||||||
|
alias so you can invoke the binary with `cargo xtask`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# .cargo/config
|
||||||
|
|
||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --release --"
|
||||||
|
```
|
362
nih_plug_xtask/src/lib.rs
Normal file
362
nih_plug_xtask/src/lib.rs
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
mod symbols;
|
||||||
|
|
||||||
|
/// Re-export for the main function.
|
||||||
|
pub use anyhow::Result;
|
||||||
|
|
||||||
|
const USAGE_STRING: &str = "Usage:
|
||||||
|
cargo xtask bundle <package> [--release] [--target <triple>]
|
||||||
|
cargo xtask bundle -p <package1> -p <package2> ... [--release] [--target <triple>]";
|
||||||
|
|
||||||
|
/// The base birectory for the bundler's output.
|
||||||
|
const BUNDLE_HOME: &str = "target/bundled";
|
||||||
|
|
||||||
|
/// Any additional configuration that might be useful for creating plugin bundles, stored as
|
||||||
|
/// `bundler.toml` alongside the workspace's main `Cargo.toml` file.
|
||||||
|
type BundlerConfig = HashMap<String, PackageConfig>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct PackageConfig {
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The target we're generating a plugin for. This can be either the native target or a cross
|
||||||
|
/// compilation target, so to reduce redundancy when determining the correct bundle paths we'll use
|
||||||
|
/// an enum for this.
|
||||||
|
///
|
||||||
|
/// TODO: Right now we don't consider ARM targets at all
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CompilationTarget {
|
||||||
|
Linux64,
|
||||||
|
Linux32,
|
||||||
|
Mac64,
|
||||||
|
Windows64,
|
||||||
|
Windows32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main xtask entry point function. See the readme for instructions on how to use this.
|
||||||
|
pub fn main() -> Result<()> {
|
||||||
|
chdir_workspace_root()?;
|
||||||
|
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let command = args
|
||||||
|
.next()
|
||||||
|
.context(format!("Missing command name\n\n{USAGE_STRING}"))?;
|
||||||
|
match command.as_str() {
|
||||||
|
"bundle" => {
|
||||||
|
// For convenience's sake we'll allow building multiple packages with -p just like carg
|
||||||
|
// obuild, but you can also build a single package without specifying -p
|
||||||
|
let mut args = args.peekable();
|
||||||
|
let mut packages = Vec::new();
|
||||||
|
if args.peek().map(|s| s.as_str()) == Some("-p") {
|
||||||
|
while args.peek().map(|s| s.as_str()) == Some("-p") {
|
||||||
|
packages.push(
|
||||||
|
args.nth(1)
|
||||||
|
.context(format!("Missing package name after -p\n\n{USAGE_STRING}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
packages.push(
|
||||||
|
args.next()
|
||||||
|
.context(format!("Missing package name\n\n{USAGE_STRING}"))?,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let other_args: Vec<_> = args.collect();
|
||||||
|
|
||||||
|
bundle(&packages[0], &other_args)?;
|
||||||
|
for package in packages.into_iter().skip(1) {
|
||||||
|
eprintln!();
|
||||||
|
bundle(&package, &other_args)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// This is only meant to be used by the CI, since using awk for this can be a bit spotty on
|
||||||
|
// macOS
|
||||||
|
"known-packages" => list_known_packages(),
|
||||||
|
_ => bail!("Unknown command '{command}'\n\n{USAGE_STRING}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the current directory into the Cargo workspace's root.
|
||||||
|
pub fn chdir_workspace_root() -> Result<()> {
|
||||||
|
let project_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.context("Could not find project root")?;
|
||||||
|
std::env::set_current_dir(project_root).context("Could not change to project root directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle a package using the provided `cargo build` arguments. Options from the `bundler.toml`
|
||||||
|
/// file in the workspace's root are respected (see
|
||||||
|
/// <https://github.com/robbert-vdh/nih-plug/blob/master/bundler.toml>). This requires the current
|
||||||
|
/// working directory to have been set to the workspace's root using [chdir_workspace_root].
|
||||||
|
pub fn bundle(package: &str, args: &[String]) -> Result<()> {
|
||||||
|
let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) {
|
||||||
|
Some(PackageConfig { name: Some(name) }) => name,
|
||||||
|
_ => package.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_release_build = false;
|
||||||
|
let mut cross_compile_target: Option<String> = None;
|
||||||
|
for arg_idx in (0..args.len()).rev() {
|
||||||
|
let arg = &args[arg_idx];
|
||||||
|
match arg.as_str() {
|
||||||
|
"--release" => is_release_build = true,
|
||||||
|
"--target" => {
|
||||||
|
// When cross compiling we should generate the correct bundle type
|
||||||
|
cross_compile_target = Some(
|
||||||
|
args.get(arg_idx + 1)
|
||||||
|
.context("Missing cross-compile target")?
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
arg if arg.starts_with("--target=") => {
|
||||||
|
cross_compile_target = Some(
|
||||||
|
arg.strip_prefix("--target=")
|
||||||
|
.context("Missing cross-compile target")?
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = Command::new("cargo")
|
||||||
|
.arg("build")
|
||||||
|
.arg("-p")
|
||||||
|
.arg(package)
|
||||||
|
.args(args)
|
||||||
|
.status()
|
||||||
|
.context(format!("Could not call cargo to build {package}"))?;
|
||||||
|
if !status.success() {
|
||||||
|
bail!("Could not build {}", package);
|
||||||
|
}
|
||||||
|
|
||||||
|
let compilation_target = compilation_target(cross_compile_target.as_deref())?;
|
||||||
|
let lib_path = Path::new(target_base(cross_compile_target.as_deref())?)
|
||||||
|
.join(if is_release_build { "release" } else { "debug" })
|
||||||
|
.join(library_basename(package, compilation_target));
|
||||||
|
if !lib_path.exists() {
|
||||||
|
bail!("Could not find built library at '{}'", lib_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll detect the pugin formats supported by the plugin binary and create bundled accordingly
|
||||||
|
// TODO: Support VST2 at some point
|
||||||
|
let bundle_clap = symbols::exported(&lib_path, "clap_entry")
|
||||||
|
.with_context(|| format!("Could not parse '{}'", lib_path.display()))?;
|
||||||
|
let bundle_vst3 = symbols::exported(&lib_path, "GetPluginFactory")
|
||||||
|
.with_context(|| format!("Could not parse '{}'", lib_path.display()))?;
|
||||||
|
let bundled_plugin = bundle_clap || bundle_vst3;
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
if bundle_clap {
|
||||||
|
let clap_bundle_library_name = clap_bundle_library_name(&bundle_name, compilation_target);
|
||||||
|
let clap_lib_path = Path::new(BUNDLE_HOME).join(&clap_bundle_library_name);
|
||||||
|
|
||||||
|
fs::create_dir_all(clap_lib_path.parent().unwrap())
|
||||||
|
.context("Could not create CLAP bundle directory")?;
|
||||||
|
reflink::reflink_or_copy(&lib_path, &clap_lib_path)
|
||||||
|
.context("Could not copy library to CLAP bundle")?;
|
||||||
|
|
||||||
|
// In contrast to VST3, CLAP only uses bundles on macOS, so we'll just take the first
|
||||||
|
// component of the library name instead
|
||||||
|
let clap_bundle_home = Path::new(BUNDLE_HOME).join(
|
||||||
|
Path::new(&clap_bundle_library_name)
|
||||||
|
.components()
|
||||||
|
.next()
|
||||||
|
.expect("Malformed CLAP library path"),
|
||||||
|
);
|
||||||
|
maybe_create_macos_bundle_metadata(package, &clap_bundle_home, compilation_target)?;
|
||||||
|
|
||||||
|
eprintln!("Created a CLAP bundle at '{}'", clap_bundle_home.display());
|
||||||
|
}
|
||||||
|
if bundle_vst3 {
|
||||||
|
let vst3_lib_path =
|
||||||
|
Path::new(BUNDLE_HOME).join(vst3_bundle_library_name(&bundle_name, compilation_target));
|
||||||
|
|
||||||
|
fs::create_dir_all(vst3_lib_path.parent().unwrap())
|
||||||
|
.context("Could not create VST3 bundle directory")?;
|
||||||
|
reflink::reflink_or_copy(&lib_path, &vst3_lib_path)
|
||||||
|
.context("Could not copy library to VST3 bundle")?;
|
||||||
|
|
||||||
|
let vst3_bundle_home = vst3_lib_path
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap();
|
||||||
|
maybe_create_macos_bundle_metadata(package, vst3_bundle_home, compilation_target)?;
|
||||||
|
|
||||||
|
eprintln!("Created a VST3 bundle at '{}'", vst3_bundle_home.display());
|
||||||
|
}
|
||||||
|
if !bundled_plugin {
|
||||||
|
eprintln!("Not creating any plugin bundles because the package does not export any plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This lists the packages configured in `bundler.toml`. This is only used as part of the CI when
|
||||||
|
/// bundling plugins.
|
||||||
|
pub fn list_known_packages() -> Result<()> {
|
||||||
|
if let Some(config) = load_bundler_config()? {
|
||||||
|
for package in config.keys() {
|
||||||
|
println!("{package}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the `bundler.toml` file, if it exists. If it does exist but it cannot be parsed, then this
|
||||||
|
/// will return an error.
|
||||||
|
fn load_bundler_config() -> Result<Option<BundlerConfig>> {
|
||||||
|
// We're already in the project root
|
||||||
|
let bundler_config_path = Path::new("bundler.toml");
|
||||||
|
if !bundler_config_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = toml::from_str(
|
||||||
|
&fs::read_to_string(&bundler_config_path)
|
||||||
|
.with_context(|| format!("Could not read '{}'", bundler_config_path.display()))?,
|
||||||
|
)
|
||||||
|
.with_context(|| format!("Could not parse '{}'", bundler_config_path.display()))?;
|
||||||
|
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The target we're compiling for. This is used to determine the paths and options for creating
|
||||||
|
/// plugin bundles.
|
||||||
|
fn compilation_target(cross_compile_target: Option<&str>) -> Result<CompilationTarget> {
|
||||||
|
match cross_compile_target {
|
||||||
|
Some("x86_64-unknown-linux-gnu") => Ok(CompilationTarget::Linux64),
|
||||||
|
Some("x86_64-apple-darwin") => Ok(CompilationTarget::Mac64),
|
||||||
|
Some("x86_64-pc-windows-gnu") => Ok(CompilationTarget::Windows64),
|
||||||
|
Some(target) => bail!("Unhandled cross-compilation target: {}", target),
|
||||||
|
None => {
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
return Ok(CompilationTarget::Linux64);
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86"))]
|
||||||
|
return Ok(CompilationTarget::Linux32);
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||||
|
return Ok(CompilationTarget::Mac64);
|
||||||
|
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||||
|
return Ok(CompilationTarget::Windows64);
|
||||||
|
#[cfg(all(target_os = "windows", target_arch = "x86"))]
|
||||||
|
return Ok(CompilationTarget::Windows32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The base directory for the compiled binaries. This does not use [CompilationTarget] as we need
|
||||||
|
/// to be able to differentiate between native and cross-compilation.
|
||||||
|
fn target_base(cross_compile_target: Option<&str>) -> Result<&'static str> {
|
||||||
|
match cross_compile_target {
|
||||||
|
Some("x86_64-unknown-linux-gnu") => Ok("target/x86_64-unknown-linux-gnu"),
|
||||||
|
Some("x86_64-pc-windows-gnu") => Ok("target/x86_64-pc-windows-gnu"),
|
||||||
|
Some("x86_64-apple-darwin") => Ok("target/x86_64-apple-darwin"),
|
||||||
|
Some(target) => bail!("Unhandled cross-compilation target: {}", target),
|
||||||
|
None => Ok("target"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file name of the compiled library for a `cdylib` crate.
|
||||||
|
fn library_basename(package: &str, target: CompilationTarget) -> String {
|
||||||
|
match target {
|
||||||
|
CompilationTarget::Linux64 | CompilationTarget::Linux32 => format!("lib{package}.so"),
|
||||||
|
CompilationTarget::Mac64 => format!("lib{package}.dylib"),
|
||||||
|
CompilationTarget::Windows64 | CompilationTarget::Windows32 => format!("{package}.dll"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The filename of the CLAP plugin for Linux and Windows, or the full path to the library file
|
||||||
|
/// inside of a CLAP bundle on macOS
|
||||||
|
fn clap_bundle_library_name(package: &str, target: CompilationTarget) -> String {
|
||||||
|
match target {
|
||||||
|
CompilationTarget::Linux64
|
||||||
|
| CompilationTarget::Linux32
|
||||||
|
| CompilationTarget::Windows64
|
||||||
|
| CompilationTarget::Windows32 => format!("{package}.clap"),
|
||||||
|
CompilationTarget::Mac64 => format!("{package}.clap/Contents/MacOS/{package}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full path to the library file inside of a VST3 bundle, including the leading `.vst3`
|
||||||
|
/// directory.
|
||||||
|
///
|
||||||
|
/// See <https://developer.steinberg.help/display/VST/Plug-in+Format+Structure>.
|
||||||
|
fn vst3_bundle_library_name(package: &str, target: CompilationTarget) -> String {
|
||||||
|
match target {
|
||||||
|
CompilationTarget::Linux64 => format!("{package}.vst3/Contents/x86_64-linux/{package}.so"),
|
||||||
|
CompilationTarget::Linux32 => format!("{package}.vst3/Contents/i386-linux/{package}.so"),
|
||||||
|
CompilationTarget::Mac64 => format!("{package}.vst3/Contents/MacOS/{package}"),
|
||||||
|
CompilationTarget::Windows64 => {
|
||||||
|
format!("{package}.vst3/Contents/x86_64-win/{package}.vst3")
|
||||||
|
}
|
||||||
|
CompilationTarget::Windows32 => format!("{package}.vst3/Contents/x86-win/{package}.vst3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If compiling for macOS, create all of the bundl-y stuff Steinberg and Apple require you to have.
|
||||||
|
///
|
||||||
|
/// This still requires you to move the dylib file to `{bundle_home}/Contents/macOS/{package}`
|
||||||
|
/// yourself first.
|
||||||
|
pub fn maybe_create_macos_bundle_metadata(
|
||||||
|
package: &str,
|
||||||
|
bundle_home: &Path,
|
||||||
|
target: CompilationTarget,
|
||||||
|
) -> Result<()> {
|
||||||
|
if target != CompilationTarget::Mac64 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: May want to add bundler.toml fields for the identifier, version and signature at some
|
||||||
|
// point.
|
||||||
|
fs::write(bundle_home.join("Contents").join("PkgInfo"), "BNDL????")
|
||||||
|
.context("Could not create PkgInfo file")?;
|
||||||
|
fs::write(
|
||||||
|
bundle_home.join("Contents").join("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>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{package}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.nih-plug.{package}</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{package}</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>{package}</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string></string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
"#),
|
||||||
|
)
|
||||||
|
.context("Could not create Info.plist file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -6,9 +6,4 @@ authors = ["Robbert van der Helm <mail@robbertvanderhelm.nl>"]
|
||||||
license = "ISC"
|
license = "ISC"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
nih_plug_xtask = { path = "../nih_plug_xtask" }
|
||||||
goblin = "0.5"
|
|
||||||
# Version 0.1.3 from crates.io assumes a 64-bit toolchain
|
|
||||||
reflink = { git = "https://github.com/nicokoch/reflink", rev = "e8d93b465f5d9ad340cd052b64bbc77b8ee107e2" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = "0.5"
|
|
||||||
|
|
|
@ -1,348 +1,3 @@
|
||||||
use anyhow::{bail, Context, Result};
|
fn main() -> nih_plug_xtask::Result<()> {
|
||||||
use serde::Deserialize;
|
nih_plug_xtask::main()
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
mod symbols;
|
|
||||||
|
|
||||||
const USAGE_STRING: &str = "Usage:
|
|
||||||
cargo xtask bundle <package> [--release] [--target <triple>]
|
|
||||||
cargo xtask bundle -p <package1> -p <package2> ... [--release] [--target <triple>]";
|
|
||||||
|
|
||||||
/// The base birectory for the bundler's output.
|
|
||||||
const BUNDLE_HOME: &str = "target/bundled";
|
|
||||||
|
|
||||||
/// Any additional configuration that might be useful for creating plugin bundles, stored as
|
|
||||||
/// `bundler.toml` alongside the workspace's main `Cargo.toml` file.
|
|
||||||
type BundlerConfig = HashMap<String, PackageConfig>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
struct PackageConfig {
|
|
||||||
name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The target we're generating a plugin for. This can be either the native target or a cross
|
|
||||||
/// compilation target, so to reduce redundancy when determining the correct bundle paths we'll use
|
|
||||||
/// an enum for this.
|
|
||||||
///
|
|
||||||
/// TODO: Right now we don't consider ARM targets at all
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum CompilationTarget {
|
|
||||||
Linux64,
|
|
||||||
Linux32,
|
|
||||||
Mac64,
|
|
||||||
Windows64,
|
|
||||||
Windows32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let project_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.parent()
|
|
||||||
.context("Could not find project root")?;
|
|
||||||
std::env::set_current_dir(project_root)
|
|
||||||
.context("Could not change to project root directory")?;
|
|
||||||
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let command = args
|
|
||||||
.next()
|
|
||||||
.context(format!("Missing command name\n\n{USAGE_STRING}"))?;
|
|
||||||
match command.as_str() {
|
|
||||||
"bundle" => {
|
|
||||||
// For convenience's sake we'll allow building multiple packages with -p just like carg
|
|
||||||
// obuild, but you can also build a single package without specifying -p
|
|
||||||
let mut args = args.peekable();
|
|
||||||
let mut packages = Vec::new();
|
|
||||||
if args.peek().map(|s| s.as_str()) == Some("-p") {
|
|
||||||
while args.peek().map(|s| s.as_str()) == Some("-p") {
|
|
||||||
packages.push(
|
|
||||||
args.nth(1)
|
|
||||||
.context(format!("Missing package name after -p\n\n{USAGE_STRING}"))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
packages.push(
|
|
||||||
args.next()
|
|
||||||
.context(format!("Missing package name\n\n{USAGE_STRING}"))?,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
let other_args: Vec<_> = args.collect();
|
|
||||||
|
|
||||||
bundle(&packages[0], &other_args)?;
|
|
||||||
for package in packages.into_iter().skip(1) {
|
|
||||||
eprintln!();
|
|
||||||
bundle(&package, &other_args)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
// This is only meant to be used by the CI, since using awk for this can be a bit spotty on
|
|
||||||
// macOS
|
|
||||||
"known-packages" => list_known_packages(),
|
|
||||||
_ => bail!("Unknown command '{command}'\n\n{USAGE_STRING}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The macOS version has not been tested
|
|
||||||
fn bundle(package: &str, args: &[String]) -> Result<()> {
|
|
||||||
let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) {
|
|
||||||
Some(PackageConfig { name: Some(name) }) => name,
|
|
||||||
_ => package.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut is_release_build = false;
|
|
||||||
let mut cross_compile_target: Option<String> = None;
|
|
||||||
for arg_idx in (0..args.len()).rev() {
|
|
||||||
let arg = &args[arg_idx];
|
|
||||||
match arg.as_str() {
|
|
||||||
"--release" => is_release_build = true,
|
|
||||||
"--target" => {
|
|
||||||
// When cross compiling we should generate the correct bundle type
|
|
||||||
cross_compile_target = Some(
|
|
||||||
args.get(arg_idx + 1)
|
|
||||||
.context("Missing cross-compile target")?
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
arg if arg.starts_with("--target=") => {
|
|
||||||
cross_compile_target = Some(
|
|
||||||
arg.strip_prefix("--target=")
|
|
||||||
.context("Missing cross-compile target")?
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = Command::new("cargo")
|
|
||||||
.arg("build")
|
|
||||||
.arg("-p")
|
|
||||||
.arg(package)
|
|
||||||
.args(args)
|
|
||||||
.status()
|
|
||||||
.context(format!("Could not call cargo to build {package}"))?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("Could not build {}", package);
|
|
||||||
}
|
|
||||||
|
|
||||||
let compilation_target = compilation_target(cross_compile_target.as_deref())?;
|
|
||||||
let lib_path = Path::new(target_base(cross_compile_target.as_deref())?)
|
|
||||||
.join(if is_release_build { "release" } else { "debug" })
|
|
||||||
.join(library_basename(package, compilation_target));
|
|
||||||
if !lib_path.exists() {
|
|
||||||
bail!("Could not find built library at '{}'", lib_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll detect the pugin formats supported by the plugin binary and create bundled accordingly
|
|
||||||
// TODO: Support VST2 at some point
|
|
||||||
let bundle_clap = symbols::exported(&lib_path, "clap_entry")
|
|
||||||
.with_context(|| format!("Could not parse '{}'", lib_path.display()))?;
|
|
||||||
let bundle_vst3 = symbols::exported(&lib_path, "GetPluginFactory")
|
|
||||||
.with_context(|| format!("Could not parse '{}'", lib_path.display()))?;
|
|
||||||
let bundled_plugin = bundle_clap || bundle_vst3;
|
|
||||||
|
|
||||||
eprintln!();
|
|
||||||
if bundle_clap {
|
|
||||||
let clap_bundle_library_name = clap_bundle_library_name(&bundle_name, compilation_target);
|
|
||||||
let clap_lib_path = Path::new(BUNDLE_HOME).join(&clap_bundle_library_name);
|
|
||||||
|
|
||||||
fs::create_dir_all(clap_lib_path.parent().unwrap())
|
|
||||||
.context("Could not create CLAP bundle directory")?;
|
|
||||||
reflink::reflink_or_copy(&lib_path, &clap_lib_path)
|
|
||||||
.context("Could not copy library to CLAP bundle")?;
|
|
||||||
|
|
||||||
// In contrast to VST3, CLAP only uses bundles on macOS, so we'll just take the first
|
|
||||||
// component of the library name instead
|
|
||||||
let clap_bundle_home = Path::new(BUNDLE_HOME).join(
|
|
||||||
Path::new(&clap_bundle_library_name)
|
|
||||||
.components()
|
|
||||||
.next()
|
|
||||||
.expect("Malformed CLAP library path"),
|
|
||||||
);
|
|
||||||
maybe_create_macos_bundle(package, &clap_bundle_home, compilation_target)?;
|
|
||||||
|
|
||||||
eprintln!("Created a CLAP bundle at '{}'", clap_bundle_home.display());
|
|
||||||
}
|
|
||||||
if bundle_vst3 {
|
|
||||||
let vst3_lib_path =
|
|
||||||
Path::new(BUNDLE_HOME).join(vst3_bundle_library_name(&bundle_name, compilation_target));
|
|
||||||
|
|
||||||
fs::create_dir_all(vst3_lib_path.parent().unwrap())
|
|
||||||
.context("Could not create VST3 bundle directory")?;
|
|
||||||
reflink::reflink_or_copy(&lib_path, &vst3_lib_path)
|
|
||||||
.context("Could not copy library to VST3 bundle")?;
|
|
||||||
|
|
||||||
let vst3_bundle_home = vst3_lib_path
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.parent()
|
|
||||||
.unwrap();
|
|
||||||
maybe_create_macos_bundle(package, vst3_bundle_home, compilation_target)?;
|
|
||||||
|
|
||||||
eprintln!("Created a VST3 bundle at '{}'", vst3_bundle_home.display());
|
|
||||||
}
|
|
||||||
if !bundled_plugin {
|
|
||||||
eprintln!("Not creating any plugin bundles because the package does not export any plugins")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This lists the packages configured in `bundler.toml`. This is only used as part of the CI when
|
|
||||||
/// bundling plugins.
|
|
||||||
fn list_known_packages() -> Result<()> {
|
|
||||||
if let Some(config) = load_bundler_config()? {
|
|
||||||
for package in config.keys() {
|
|
||||||
println!("{package}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the `bundler.toml` file, if it exists. If it does exist but it cannot be parsed, then this
|
|
||||||
/// will return an error.
|
|
||||||
fn load_bundler_config() -> Result<Option<BundlerConfig>> {
|
|
||||||
// We're already in the project root
|
|
||||||
let bundler_config_path = Path::new("bundler.toml");
|
|
||||||
if !bundler_config_path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = toml::from_str(
|
|
||||||
&fs::read_to_string(&bundler_config_path)
|
|
||||||
.with_context(|| format!("Could not read '{}'", bundler_config_path.display()))?,
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Could not parse '{}'", bundler_config_path.display()))?;
|
|
||||||
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The target we're compiling for. This is used to determine the paths and options for creating
|
|
||||||
/// plugin bundles.
|
|
||||||
fn compilation_target(cross_compile_target: Option<&str>) -> Result<CompilationTarget> {
|
|
||||||
match cross_compile_target {
|
|
||||||
Some("x86_64-unknown-linux-gnu") => Ok(CompilationTarget::Linux64),
|
|
||||||
Some("x86_64-apple-darwin") => Ok(CompilationTarget::Mac64),
|
|
||||||
Some("x86_64-pc-windows-gnu") => Ok(CompilationTarget::Windows64),
|
|
||||||
Some(target) => bail!("Unhandled cross-compilation target: {}", target),
|
|
||||||
None => {
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
||||||
return Ok(CompilationTarget::Linux64);
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86"))]
|
|
||||||
return Ok(CompilationTarget::Linux32);
|
|
||||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
||||||
return Ok(CompilationTarget::Mac64);
|
|
||||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
|
||||||
return Ok(CompilationTarget::Windows64);
|
|
||||||
#[cfg(all(target_os = "windows", target_arch = "x86"))]
|
|
||||||
return Ok(CompilationTarget::Windows32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The base directory for the compiled binaries. This does not use [CompilationTarget] as we need
|
|
||||||
/// to be able to differentiate between native and cross-compilation.
|
|
||||||
fn target_base(cross_compile_target: Option<&str>) -> Result<&'static str> {
|
|
||||||
match cross_compile_target {
|
|
||||||
Some("x86_64-unknown-linux-gnu") => Ok("target/x86_64-unknown-linux-gnu"),
|
|
||||||
Some("x86_64-pc-windows-gnu") => Ok("target/x86_64-pc-windows-gnu"),
|
|
||||||
Some("x86_64-apple-darwin") => Ok("target/x86_64-apple-darwin"),
|
|
||||||
Some(target) => bail!("Unhandled cross-compilation target: {}", target),
|
|
||||||
None => Ok("target"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The file name of the compiled library for a `cdylib` crate.
|
|
||||||
fn library_basename(package: &str, target: CompilationTarget) -> String {
|
|
||||||
match target {
|
|
||||||
CompilationTarget::Linux64 | CompilationTarget::Linux32 => format!("lib{package}.so"),
|
|
||||||
CompilationTarget::Mac64 => format!("lib{package}.dylib"),
|
|
||||||
CompilationTarget::Windows64 | CompilationTarget::Windows32 => format!("{package}.dll"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The filename of the CLAP plugin for Linux and Windows, or the full path to the library file
|
|
||||||
/// inside of a CLAP bundle on macOS
|
|
||||||
fn clap_bundle_library_name(package: &str, target: CompilationTarget) -> String {
|
|
||||||
match target {
|
|
||||||
CompilationTarget::Linux64
|
|
||||||
| CompilationTarget::Linux32
|
|
||||||
| CompilationTarget::Windows64
|
|
||||||
| CompilationTarget::Windows32 => format!("{package}.clap"),
|
|
||||||
CompilationTarget::Mac64 => format!("{package}.clap/Contents/MacOS/{package}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The full path to the library file inside of a VST3 bundle, including the leading `.vst3`
|
|
||||||
/// directory.
|
|
||||||
///
|
|
||||||
/// See <https://developer.steinberg.help/display/VST/Plug-in+Format+Structure>.
|
|
||||||
fn vst3_bundle_library_name(package: &str, target: CompilationTarget) -> String {
|
|
||||||
match target {
|
|
||||||
CompilationTarget::Linux64 => format!("{package}.vst3/Contents/x86_64-linux/{package}.so"),
|
|
||||||
CompilationTarget::Linux32 => format!("{package}.vst3/Contents/i386-linux/{package}.so"),
|
|
||||||
CompilationTarget::Mac64 => format!("{package}.vst3/Contents/MacOS/{package}"),
|
|
||||||
CompilationTarget::Windows64 => {
|
|
||||||
format!("{package}.vst3/Contents/x86_64-win/{package}.vst3")
|
|
||||||
}
|
|
||||||
CompilationTarget::Windows32 => format!("{package}.vst3/Contents/x86-win/{package}.vst3"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If compiling for macOS, create all of the bundl-y stuff Steinberg and Apple require you to have.
|
|
||||||
fn maybe_create_macos_bundle(
|
|
||||||
package: &str,
|
|
||||||
bundle_home: &Path,
|
|
||||||
target: CompilationTarget,
|
|
||||||
) -> Result<()> {
|
|
||||||
if target != CompilationTarget::Mac64 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: May want to add bundler.toml fields for the identifier, version and signature at some
|
|
||||||
// point.
|
|
||||||
fs::write(bundle_home.join("Contents").join("PkgInfo"), "BNDL????")
|
|
||||||
.context("Could not create PkgInfo file")?;
|
|
||||||
fs::write(
|
|
||||||
bundle_home.join("Contents").join("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>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>{package}</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>com.nih-plug.{package}</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>{package}</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>{package}</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>BNDL</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1.0.0</string>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string></string>
|
|
||||||
<key>NSHighResolutionCapable</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
"#),
|
|
||||||
)
|
|
||||||
.context("Could not create Info.plist file")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue