diff --git a/CHANGELOG.md b/CHANGELOG.md index ff8222c4..7cbc45ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### yabridgectl + +- Added support for setting up CLAP plugins. + ### Packaging notes - The VST3 dependency is now at tag `v3.7.5_build_44-patched-2`. The only diff --git a/tools/yabridgectl/README.md b/tools/yabridgectl/README.md index fd5ea459..6797c5d8 100644 --- a/tools/yabridgectl/README.md +++ b/tools/yabridgectl/README.md @@ -18,12 +18,11 @@ mentioned here are only useful during development. ### Yabridge path -Yabridgectl will need to know where it can find `libyabridge-vst2.so` and -`libyabridge-vst3.so`. By default it will search for it in both -`~/.local/share/yabridge` (the recommended installation directory when using the -prebuilt binaries), in `/usr/lib` and in `/usr/local/lib`. You can use the -command below to override this behaviour and to use a custom installation -directory instead. +Yabridgectl will need to know where it can find yabridge's files. By default it +will search for it in both `~/.local/share/yabridge` (the recommended +installation directory when using the prebuilt binaries), in `/usr/lib` and in +`/usr/local/lib`. You can use the command below to override this behaviour and +to use a custom installation directory instead. ```shell yabridgectl set --path= @@ -58,13 +57,13 @@ yabridgectl blacklist ### Installing and updating Lastly you can tell yabridgectl to set up or update yabridge for all of your -VST2 and VST3 plugins at the same time using the commands below. Yabridgectl -will warn you if it finds unrelated `.so` files that may have been left after -uninstalling a plugin, or if it finds any unknown VST3 plugins in -`~/.vst3/yabridge`. You can rerun the sync command with the `--prune` option to -delete those files. If you are using the default copy-based installation method, -it will also verify that your search `PATH` has been set up correctly so you can -get up and running faster. +VST2, VST3 and CLAP plugins at the same time using the commands below. +Yabridgectl will warn you if it finds unrelated `.so` files that may have been +left after uninstalling a plugin, or if it finds any unknown plugins in +`~/{.clap,.vst,.vst3}/yabridge`. You can rerun the sync command with the +`--prune` option to delete those files. After setting up the plugin yabridgectl +performs some post-installation setup checks to make sure yabridge is going to +run correctly. ```shell # Set up or update yabridge for all plugins found under the plugin locations diff --git a/tools/yabridgectl/src/actions.rs b/tools/yabridgectl/src/actions.rs index 39d36a6e..2db2ac47 100644 --- a/tools/yabridgectl/src/actions.rs +++ b/tools/yabridgectl/src/actions.rs @@ -24,9 +24,11 @@ use std::path::{Path, PathBuf}; use walkdir::WalkDir; use crate::config::{ - yabridge_vst2_home, yabridge_vst3_home, Config, Vst2InstallationLocation, YabridgeFiles, + yabridge_clap_home, yabridge_vst2_home, yabridge_vst3_home, Config, Vst2InstallationLocation, + YabridgeFiles, CLAP_CHAINLOADER_NAME, VST2_CHAINLOADER_NAME, VST3_CHAINLOADER_NAME, + YABRIDGE_HOST_32_EXE_NAME, YABRIDGE_HOST_EXE_NAME, }; -use crate::files::{self, NativeFile, Plugin, Vst2Plugin}; +use crate::files::{self, ClapPlugin, NativeFile, Plugin, Vst2Plugin}; use crate::util::{self, get_file_type}; use crate::util::{verify_external_dependencies, verify_path_setup, verify_wine_setup}; use crate::vst3_moduleinfo::ModuleInfo; @@ -109,19 +111,20 @@ pub fn show_status(config: &Config) -> Result<()> { println!("VST2 location: inline next to the Windows plugin file"); } } - // This is fixed, but just from a UX point of view it might be nice to have as a reminder + // These are, but just from a UX point of view it might be nice to have as a reminder println!("VST3 location: '{}'\n", yabridge_vst3_home().display()); + println!("CLAP location: '{}'\n", yabridge_clap_home().display()); let files = config.files(); match &files { Ok(files) => { println!( - "libyabridge-chainloader-vst2.so: '{}' ({})", + "{VST2_CHAINLOADER_NAME}: '{}' ({})", files.vst2_chainloader.display(), files.vst2_chainloader_arch, ); println!( - "libyabridge-chainloader-vst3.so: {}\n", + "{VST3_CHAINLOADER_NAME}: {}\n", files .vst3_chainloader .as_ref() @@ -129,7 +132,15 @@ pub fn show_status(config: &Config) -> Result<()> { .unwrap_or_else(|| "".red().to_string()) ); println!( - "yabridge-host.exe: {}", + "{CLAP_CHAINLOADER_NAME}: {}\n", + files + .clap_chainloader + .as_ref() + .map(|(path, arch)| format!("'{}' ({})", path.display(), arch)) + .unwrap_or_else(|| "".red().to_string()) + ); + println!( + "{YABRIDGE_HOST_EXE_NAME}: {}", files .yabridge_host_exe .as_ref() @@ -139,7 +150,7 @@ pub fn show_status(config: &Config) -> Result<()> { .unwrap_or_else(|| "".red().to_string()) ); println!( - "yabridge-host-32.exe: {}", + "{YABRIDGE_HOST_32_EXE_NAME}: {}", files .yabridge_host_32_exe .as_ref() @@ -171,6 +182,11 @@ pub fn show_status(config: &Config) -> Result<()> { module.type_str(), module.architecture ), + // CLAP is supposed to be 64-bit-only, but we'll still display the architecture here for + // consistency's sake + Plugin::Clap(ClapPlugin { architecture, .. }) => { + format!("{}, {}", "CLAP".yellow(), architecture) + } }; // This made more sense when we supported symlinking `libyabridge-*.so`, but we should @@ -243,63 +259,76 @@ pub struct SyncOptions { pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { let files: YabridgeFiles = config.files()?; let vst2_chainloader_hash = util::hash_file(&files.vst2_chainloader)?; - let vst3_chainloader_hash = match &files.vst3_chainloader { - Some((path, _)) => Some(util::hash_file(path)?), - None => None, - }; + let vst3_chainloader_hash = files + .vst3_chainloader + .as_ref() + .map(|(path, _)| util::hash_file(path)) + .transpose()?; + let clap_chainloader_hash = files + .clap_chainloader + .as_ref() + .map(|(path, _)| util::hash_file(path)) + .transpose()?; - if let Some((vst3_chainloader_path, _)) = &files.vst3_chainloader { - println!("Setting up VST2 and VST3 plugins using:"); - println!("- {}", files.vst2_chainloader.display()); - println!("- {}\n", vst3_chainloader_path.display()); - } else { - println!("Setting up VST2 plugins using:"); - println!("- {}\n", files.vst2_chainloader.display()); + // Better not add another plugin format! + match (&files.vst3_chainloader, &files.clap_chainloader) { + (Some((vst3_chainloader_path, _)), Some((clap_chainloader_path, _))) => { + println!("Setting up VST2, VST3, and CLAP plugins using:"); + println!("- {}", files.vst2_chainloader.display()); + println!("- {}", vst3_chainloader_path.display()); + println!("- {}\n", clap_chainloader_path.display()); + } + (Some((vst3_chainloader_path, _)), None) => { + println!("Setting up VST2 and VST3 plugins using:"); + println!("- {}", files.vst2_chainloader.display()); + println!("- {}\n", vst3_chainloader_path.display()); + } + (None, Some((clap_chainloader_path, _))) => { + println!("Setting up VST2 and CLAP plugins using:"); + println!("- {}", files.vst2_chainloader.display()); + println!("- {}\n", clap_chainloader_path.display()); + } + (None, None) => { + println!("Setting up VST2 plugins using:"); + println!("- {}\n", files.vst2_chainloader.display()); + } } let results = config .search_directories() .context("Failure while searching for plugins")?; - // Before doing anything, make sure `~/.vst/yabridge` and `~/.vst3/yabridge` are not symlinks to - // one of the plugin directories. See + // Before doing anything, make sure `~/.{clap,vst,vst3}/yabridge` are not symlinks to one of the + // plugin directories. See // https://github.com/robbert-vdh/yabridge/issues/185#issuecomment-1166274104. + let bail_if_unsafe_symlink = |plugin_home: &Path, plugin_home_display| { + if let Ok(canonical_plugin_home) = fs::canonicalize(&plugin_home) { + if canonical_plugin_home != plugin_home { + for plugin_dir in &config.plugin_dirs { + if let Ok(canonical_plugin_dir) = fs::canonicalize(&plugin_dir) { + if canonical_plugin_dir.starts_with(&canonical_plugin_home) { + anyhow::bail!( + "'{plugin_home_display}' is a symlink to '{}'. This conflicts \ + with '{}' from your plugin directories, so the syncing process \ + will now be aborted.", + canonical_plugin_home.display(), + plugin_dir.display(), + ); + } + } + } + } + } + + Ok(()) + }; + let vst2_home = yabridge_vst2_home(); let vst3_home = yabridge_vst3_home(); - if let Ok(canonical_vst2_home) = fs::canonicalize(&vst2_home) { - if canonical_vst2_home != vst2_home { - for plugin_dir in &config.plugin_dirs { - if let Ok(canonical_plugin_dir) = fs::canonicalize(&plugin_dir) { - if canonical_plugin_dir.starts_with(&canonical_vst2_home) { - anyhow::bail!( - "'~/.vst/yabridge' is a symlink to '{}'. This conflicts with '{}' \ - from your plugin directories, so the syncing process will now be \ - aborted.", - canonical_vst2_home.display(), - plugin_dir.display(), - ); - } - } - } - } - } - if let Ok(canonical_vst3_home) = fs::canonicalize(&vst3_home) { - if canonical_vst3_home != vst3_home { - for plugin_dir in &config.plugin_dirs { - if let Ok(canonical_plugin_dir) = fs::canonicalize(&plugin_dir) { - if canonical_plugin_dir.starts_with(&canonical_vst3_home) { - anyhow::bail!( - "'~/.vst3/yabridge' is a symlink to '{}'. This conflicts with '{}' \ - from your plugin directories, so the syncing process will now be \ - aborted.", - canonical_vst3_home.display(), - plugin_dir.display(), - ); - } - } - } - } - } + let clap_home = yabridge_clap_home(); + bail_if_unsafe_symlink(&vst2_home, "~/.vst/yabridge")?; + bail_if_unsafe_symlink(&vst3_home, "~/.vst3/yabridge")?; + bail_if_unsafe_symlink(&clap_home, "~/.clap/yabridge")?; // Keep track of some global statistics // The plugin files we installed. This tracks copies of/symlinks to `libabyrdge-*.so` managed. @@ -317,8 +346,9 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { let mut new_plugins: HashSet = HashSet::new(); // The files we skipped during the scan because they turned out to not be plugins let mut skipped_dll_files: Vec = Vec::new(); - // `.so` files and unused VST3 modules we found during scanning that didn't have a corresponding - // copy or symlink of `libyabridge-chainloader-vst2.so` + // `.so` files and unused files/bundles we found during scanning that don't belong to a known + // plugin. `~/{.clap,.vst,.vst3}/yabridge` will be searched for these files after setting up all + // plugins. let mut orphan_files: Vec = Vec::new(); // When using the centralized VST2 installation location in `~/.vst/yabridge` we'll want to // track all unmanaged files in that directory and add them to the orphans list @@ -329,6 +359,8 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { // during the syncing process, so we'll keep track of which VST3 files we touched per-bundle. We // can then at the end remove all unkonwn bundles, and all unkonwn files within a bundle. let mut known_centralized_vst3_files: HashMap> = HashMap::new(); + // Similar for CLAP, but since CLAP doesn't use bundles this works the same way as with VST2. + let mut known_centralized_clap_files: HashSet = HashSet::new(); for (path, search_results) in results { // Orphan files in the centralized directories need to be detected separately orphan_files.extend( @@ -420,7 +452,7 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { } } - vst2_plugin.path.clone() + vst2_plugin.path } // And then create merged bundles for the VST3 plugins: // https://developer.steinberg.help/display/VST/Plug-in+Format+Structure#PluginFormatStructure-MergedBundle @@ -541,6 +573,65 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { module.original_path().to_path_buf() } + Plugin::Clap(clap_plugin) => { + // Only set up CLAP plugins when yabridge has been compiled with CLAP support + if clap_chainloader_hash.is_none() { + continue; + } + + let target_native_plugin_path = clap_plugin.native_target(); + let target_windows_plugin_path = clap_plugin.windows_target(); + let normalized_target_native_plugin_path = + util::normalize_path(&target_native_plugin_path); + + let mut is_new = + known_centralized_clap_files.insert(target_native_plugin_path.clone()); + is_new |= + known_centralized_clap_files.insert(target_windows_plugin_path.clone()); + if !is_new { + eprintln!( + "{}", + util::wrap(&format!( + "{}: '{}' has already been provided by another Wine prefix or \ + plugin directory, skipping it\n", + "WARNING".red(), + target_windows_plugin_path.display(), + )) + ); + + continue; + } + + // Because CLAP uses the same file extension on all platforms, this needs to + // work slightly different compared to the VST2 bridging. Here we'll create a + // copy of the chainloader as a `foo.clap` file in `~/.clap/yabridge`, and we'll + // then symlink the Windows `.clap` file to `foo.clap-win` next to `foo.clap`. + // That allows yabridge to find the Windows CLAP plugin while also preventing + // DAWs from indexing it themselves. + util::create_dir_all(target_native_plugin_path.parent().unwrap())?; + if install_file( + options.force, + InstallationMethod::Copy, + &files.clap_chainloader.as_ref().unwrap().0, + clap_chainloader_hash, + &target_native_plugin_path, + )? { + new_plugins.insert(normalized_target_native_plugin_path.clone()); + } + managed_plugins.insert(normalized_target_native_plugin_path); + + // So this ends up symlinking the original Windows `.clap` file to a `.clap-win` + // file in `~/.clap/yabridge` + install_file( + true, + InstallationMethod::Symlink, + &clap_plugin.path, + None, + &target_windows_plugin_path, + )?; + + clap_plugin.path + } }; if options.verbose { @@ -570,13 +661,14 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { } // We've already kept track of orphan `.dll` files in the plugin directories, but now we need to - // do something similar for orphan files in `~/.vst/yabridge` and `~/.vst3/yabridge`. For VST3 - // plugins we'll want to remove both unmanaged VST3 bundles in `~/.vst3/yabridge` as well as - // unmanged files within managed bundles. That's why we'll immediately filter out known files - // within VST3 bundles. For VST2 plugins we can simply treat any file in `~/.vst/yabridge` that - // we did not add to `known_centralized_vst2_files` as an orphan. We'll want to do this - // regardless of the VST2 installation location setting so switching between the two modes and - // then pruning works as expected. + // do something similar for orphan files in `~/.vst/yabridge`, `~/.vst3/yabridge`, and + // `~/.clap/yabridge`. For VST3 plugins we'll want to remove both unmanaged VST3 bundles in + // `~/.vst3/yabridge` as well as unmanged files within managed bundles. That's why we'll + // immediately filter out known files within VST3 bundles. For VST2 and CLAP plugins we can + // simply treat any file in `~/{.clap,.vst}/yabridge` that we did not add to + // `known_centralized_{clap,vst2}_files` as an orphan. We'll want to do this regardless of the + // VST2 installation location setting so switching between the two modes and then pruning works + // as expected. // TODO: Move this elsewhere let centralized_vst2_files = WalkDir::new(vst2_home) .follow_links(true) @@ -594,6 +686,22 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { None } }); + let centralized_clap_files = WalkDir::new(clap_home) + .follow_links(true) + .same_file_system(true) + .into_iter() + .filter_map(|e| { + let path = match e { + Ok(entry) => entry.path().to_owned(), + Err(err) => err.path()?.to_owned(), + }; + + if !path.is_dir() && matches!(path.extension()?.to_str()?, "clap" | "clap-win" | "so") { + Some(path) + } else { + None + } + }); let installed_vst3_bundles = WalkDir::new(vst3_home) .follow_links(true) .same_file_system(true) @@ -619,6 +727,13 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { get_file_type(path) } })); + orphan_files.extend(centralized_clap_files.filter_map(|path| { + if known_centralized_clap_files.contains(&path) { + None + } else { + get_file_type(path) + } + })); for bundle_path in installed_vst3_bundles { match known_centralized_vst3_files.get(&bundle_path) { None => orphan_files.push(NativeFile::Directory(bundle_path)), diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index 7a0cb67c..e125cbba 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -35,6 +35,8 @@ pub const CONFIG_FILE_NAME: &str = "config.toml"; /// `$XDG_DATA_HOME`. const YABRIDGECTL_PREFIX: &str = "yabridgectl"; +/// The name of yabridge's CLAP chainloading library yabridgectl will create copies of. +pub const CLAP_CHAINLOADER_NAME: &str = "libyabridge-chainloader-clap.so"; /// The name of yabridge's VST2 chainloading library yabridgectl will create copies of. pub const VST2_CHAINLOADER_NAME: &str = "libyabridge-chainloader-vst2.so"; /// The name of yabridge's VST3 chainloading library yabridgectl will create copies of. @@ -49,6 +51,10 @@ pub const YABRIDGE_HOST_32_EXE_NAME: &str = "yabridge-host-32.exe"; /// `$XDG_CONFIG_HOME` and `$XDG_DATA_HOME`. const YABRIDGE_PREFIX: &str = "yabridge"; +/// The path relative to `$HOME` that CLAP plugins bridged by yabridgectl life in. By putting this +/// in a subdirectory we can easily clean up any orphan files without interfering with other native +/// plugins. +const YABRIDGE_CLAP_HOME: &str = ".clap/yabridge"; /// The path relative to `$HOME` we will set up bridged VST2 plugins in when using the centralized /// VST2 installation location setting. By putting this in a subdirectory we can easily clean up any /// orphan files without interfering with other native plugins. @@ -67,10 +73,11 @@ pub struct Config { /// set, then yabridgectl will look in `/usr/lib` and `$XDG_DATA_HOME/yabridge` since those are /// the expected locations for yabridge to be installed in. pub yabridge_home: Option, - /// Directories to search for Windows VST plugins. These directories can contain both VST2 - /// plugin `.dll` files and VST3 modules (which should be located in `/drive_c/Program - /// Files/Common/VST3`). We're using an ordered set here out of convenience so we can't get - /// duplicates and the config file is always sorted. + /// Directories to search for Windows VST plugins. These directories can contain VST2 plugin + /// `.dll` files, VST3 modules (which should be located in `/drive_c/Program + /// Files/Common/VST3`), and CLAP plugins (which should similarly be installed to + /// `/drive_c/Program Files/Common/CLAP`). We're using an ordered set here out of + /// convenience so we can't get duplicates and the config file is always sorted. pub plugin_dirs: BTreeSet, /// Where VST2 plugins are setup. This can be either in `~/.vst/yabridge` or inline with the /// plugin's .dll` files.` @@ -138,6 +145,11 @@ pub struct YabridgeFiles { /// with VST3 support. We need to know if it's a 32-bit or a 64-bit library so we can properly /// set up the merged VST3 bundles. pub vst3_chainloader: Option<(PathBuf, LibArchitecture)>, + /// The path to `libyabridge-chainloader-clap.so` we should use. The architecture is only used + /// for display purposes in `yabridgectl status`. Because CLAP is supposed to be 64-bit-only on + /// AMD64 systems we can also just leave this out, but it looks more consisent this way. + /// Yabridge can be configurued without CLAP support, so this is optional. + pub clap_chainloader: Option<(PathBuf, LibArchitecture)>, /// The path to `yabridge-host.exe`. This is the path yabridge will actually use, and it does /// not have to be relative to `yabridge_home`. pub yabridge_host_exe: Option, @@ -275,6 +287,22 @@ impl Config { _ => None, }; + // And the same thing for `libyabridge-chainloader-clap.so`. + let clap_chainloader = match vst2_chainloader.with_file_name(CLAP_CHAINLOADER_NAME) { + path if path.exists() => { + // The architecture is only used for display purposes + let arch = util::get_elf_architecture(&path).with_context(|| { + format!( + "Could not determine ELF architecture for '{}'", + path.display() + ) + })?; + + Some((path, arch)) + } + _ => None, + }; + // `yabridge-host.exe` should either be in the search path, or it should be in // `~/.local/share/yabridge` (which was appended to the `$PATH` at the start of `main()`) let yabridge_host_exe = which(YABRIDGE_HOST_EXE_NAME).ok(); @@ -290,6 +318,7 @@ impl Config { vst2_chainloader, vst2_chainloader_arch, vst3_chainloader, + clap_chainloader, yabridge_host_exe, yabridge_host_exe_so, yabridge_host_32_exe, @@ -297,7 +326,7 @@ impl Config { }) } - /// Search for VST2 and VST3 plugins in all of the registered plugins directories. + /// Search for VST2, VST3, and CLAP plugins in all of the registered plugins directories. pub fn search_directories(&self) -> Result> { let blacklist: HashSet<&Path> = self.blacklist.iter().map(|p| p.as_path()).collect(); @@ -325,6 +354,9 @@ pub fn yabridgectl_directories() -> Result { BaseDirectories::with_prefix(YABRIDGECTL_PREFIX).context("Error while parsing base directories") } +// TODO: Use `lazy_static` for these things. `$HOME` can technically change at runtime but +// realistically it won't. + /// Get the path where bridged VST2 plugin files should be placed when using the centralized /// installation location setting. This is a subdirectory of `~/.vst` so we can easily clean up /// leftover files without interfering with other native plugins. @@ -338,3 +370,10 @@ pub fn yabridge_vst2_home() -> PathBuf { pub fn yabridge_vst3_home() -> PathBuf { Path::new(&env::var("HOME").expect("$HOME is not set")).join(YABRIDGE_VST3_HOME) } + +/// Get the path where CLAP modules bridged by yabridgectl should be placed in. This is a +/// subdirectory of `~/.clap` so we can easily clean up leftover files without interfering with +/// other native plugins. +pub fn yabridge_clap_home() -> PathBuf { + Path::new(&env::var("HOME").expect("$HOME is not set")).join(YABRIDGE_CLAP_HOME) +} diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs index a41ae4f1..b4ec3cdc 100644 --- a/tools/yabridgectl/src/files.rs +++ b/tools/yabridgectl/src/files.rs @@ -23,18 +23,22 @@ use std::fmt::Display; use std::path::{Path, PathBuf}; use walkdir::WalkDir; -use crate::config::{yabridge_vst2_home, yabridge_vst3_home, Config, YabridgeFiles}; +use crate::config::{ + yabridge_clap_home, yabridge_vst2_home, yabridge_vst3_home, Config, YabridgeFiles, +}; use crate::symbols::parse_pe32_binary; use crate::util::get_file_type; /// Stores the results from searching through a directory. We'll search for Windows VST2 plugin /// `.dll` files, Windows VST3 plugin modules, and native Linux `.so` files inside of a directory. /// These `.so` files are kept track of so we can report the current installation status of VST2 -/// plugins and to be able to prune orphan files. Since VST3 plugins have to be installed in -/// `~/.vst3`, these orphan files are only relevant for VST2 plugins. +/// plugins and to be able to prune orphan files. Since yabridgectl 4.0 now sets up plugins in the +/// user's home directory, these inline orphans are mostly useful for cleaning up old installations +/// and when the user has explicitly enabled the inline installation location for VST2 plugins. #[derive(Debug)] pub struct SearchResults { - /// The plugins found during the search. This contains both VST2 plugins and VST3 modules. + /// The plugins found during the search. This contains VST2 plugins, VST3 modules, and CLAP + /// plugins. pub plugins: Vec, /// `.dll` files skipped over during the search. Used for printing statistics and shown when /// running `yabridgectl sync --verbose`. @@ -49,12 +53,14 @@ pub struct SearchResults { /// files in a directory before filtering them down to a `SearchResults` object. #[derive(Debug)] pub struct SearchIndex { - /// Any `.dll` file, along with its relative path inq the search directory. + /// Any `.dll` file, along with its relative path in the search directory. pub dll_files: Vec<(PathBuf, Option)>, /// Any `.vst3` file or directory, along with its relative path in the search directory. This /// can be either a legacy `.vst3` DLL module or a VST 3.6.10 module (or some kind of random /// other file, of course). pub vst3_files: Vec<(PathBuf, Option)>, + /// Any `.clap` file, along with its relative path in the search directory. + pub clap_files: Vec<(PathBuf, Option)>, /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. pub so_files: Vec, @@ -84,6 +90,7 @@ impl NativeFile { pub enum Plugin { Vst2(Vst2Plugin), Vst3(Vst3Module), + Clap(ClapPlugin), } /// VST2 plugins we found during a search along with their architecture. @@ -110,6 +117,20 @@ pub struct Vst3Module { pub subdirectory: Option, } +/// CLAP plugins we found during a search along with their architecture. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClapPlugin { + /// The absolute path to the Windows CLAP plugin's `.clap` file. + pub path: PathBuf, + /// The architecture of the CLAP plugin. This is supposed to be only the native architecture (no + /// official x86 support), but we'll keep track of it anyways for consistency with other + /// formats. + pub architecture: LibArchitecture, + /// The subdirectory within the plugins directory the orignal plugin was in. If this could not + /// be detected then this will be `None`. + pub subdirectory: Option, +} + /// The type of the VST3 module. VST 3.6.10 style bundles require slightly different handling #[derive(Debug, Clone, PartialEq, Eq)] pub enum Vst3ModuleType { @@ -317,6 +338,43 @@ impl Vst3Module { } } +impl ClapPlugin { + /// Get the absolute path to the `.clap` file we should create in `~/.clap/yabridge` for this + /// plugin. + pub fn native_target(&self) -> PathBuf { + let file_name = self + .path + .file_name() + .unwrap() + .to_str() + .expect("Plugin name contains invalid UTF-8"); + let file_name = Path::new(file_name).with_extension("clap"); + + match &self.subdirectory { + Some(directory) => yabridge_clap_home().join(directory).join(file_name), + None => yabridge_clap_home().join(file_name), + } + } + + /// Get the absolute path to the `.clap-win` file in `~/.clap/yabrdge` the Windows `.clap` + /// plugin should be symlinked to. This uses a different file extension so we can use the same + /// setup as for VST2 plugins without confusing DAWs. + pub fn windows_target(&self) -> PathBuf { + let file_name = self + .path + .file_name() + .unwrap() + .to_str() + .expect("Plugin name contains invalid UTF-8"); + let file_name = Path::new(file_name).with_extension("clap-win"); + + match &self.subdirectory { + Some(directory) => yabridge_clap_home().join(directory).join(file_name), + None => yabridge_clap_home().join(file_name), + } + } +} + /// The architecture of a library file (either `.dll` or `.so` depending on the context). Needed so /// we can create a merged bundle for VST3 plugins. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] @@ -393,6 +451,10 @@ impl SearchResults { get_file_type(vst3_module.target_native_module_path(files)), ), ), + Plugin::Clap(clap_plugin) => ( + clap_plugin.path.clone(), + (plugin, get_file_type(clap_plugin.native_target())), + ), }) .collect() } @@ -426,18 +488,20 @@ impl SearchResults { } } -/// Find all `.dll`, `.vst3` and `.so` files under a directory. These results can be filtered down -/// to actual VST2 plugins and VST3 modules using `search()`. Any path found in the blacklist will -/// be pruned immediately, so this can be used to both not index individual files and to skip an -/// entire directory. +/// Find all `.dll`, `.vst3`, `.clap`, and `.so` files under a directory. These results can be +/// filtered down to actual VST2 plugins, VST3 modules, and CLAP plugins using `search()`. Any path +/// found in the blacklist will be pruned immediately, so this can be used to both not index +/// individual files and to skip an entire directory. /// /// For VST3 plugin _bundles_ the subdirectory also contains the `foo.vst3/Contents/x86_64-win` /// suffix. This needs to be stripped out to get the bundle root. pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex { // These are pairs of `(absolute_path, subdirectory)`. The subdirectory is used for setting up - // VST3 plugins and for setting up VST2 plugins in the centralized installation location mode. + // VST3 and CLAP plugins and for setting up VST2 plugins in the centralized installation + // location mode. let mut dll_files: Vec<(PathBuf, Option)> = Vec::new(); let mut vst3_files: Vec<(PathBuf, Option)> = Vec::new(); + let mut clap_files: Vec<(PathBuf, Option)> = Vec::new(); let mut so_files: Vec = Vec::new(); for (file_idx, path) in WalkDir::new(directory) .follow_links(true) @@ -494,6 +558,13 @@ pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex { .map(|p| p.to_owned()); vst3_files.push((path, subdirectory)); } + Some("clap") => { + let subdirectory = path + .parent() + .and_then(|p| p.strip_prefix(directory).ok()) + .map(|p| p.to_owned()); + clap_files.push((path, subdirectory)); + } Some("so") => { if path.is_symlink() { so_files.push(NativeFile::Symlink(path)); @@ -508,6 +579,7 @@ pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex { SearchIndex { dll_files, vst3_files, + clap_files, so_files, } } @@ -518,6 +590,8 @@ impl SearchIndex { pub fn search(self) -> Result { const VST2_ENTRY_POINTS: [&str; 2] = ["VSTPluginMain", "main"]; const VST3_ENTRY_POINTS: [&str; 1] = ["GetPluginFactory"]; + // This is a constant with external linkage, not a function + const CLAP_ENTRY_POINTS: [&str; 1] = ["clap_entry"]; // We'll have to figure out which `.dll` files are VST2 plugins and which should be skipped // by checking whether the file contains one of the VST2 entry point functions. This vector @@ -548,8 +622,8 @@ impl SearchIndex { } }) // Make parsing failures non-fatal. People somehow extract these `__MACOSX` and other - // junk files from zip files containing Windows VST2/VST3 plugins created on macOS to - // their plugin directories (how does such a thing even happen in the first place?) + // junk files from zip files containing Windows plugins created on macOS to their plugin + // directories (how does such a thing even happen in the first place?) .filter_map(|result: Result>| match result { Ok(result) => Some(result), Err(err) => { @@ -639,9 +713,7 @@ impl SearchIndex { Ok(Err(module_path)) } }) - // Make parsing failures non-fatal. People somehow extract these `__MACOSX` and other - // junk files from zip files containing Windows VST2/VST3 plugins created on macOS to - // their plugin directories (how does such a thing even happen in the first place?) + // See above .filter_map(|result: Result>| match result { Ok(result) => Some(result), Err(err) => { @@ -651,22 +723,62 @@ impl SearchIndex { }) .collect(); + // Same for CLAP plugins + let is_clap_plugin: Vec> = self + .clap_files + .into_par_iter() + .map(|(path, subdirectory)| { + let info = parse_pe32_binary(&path)?; + let architecture = if info.is_64_bit { + LibArchitecture::Lib64 + } else { + LibArchitecture::Lib32 + }; + + if info + .exports + .into_iter() + .any(|symbol| CLAP_ENTRY_POINTS.contains(&symbol.as_str())) + { + Ok(Ok(ClapPlugin { + path, + architecture, + subdirectory, + })) + } else { + Ok(Err(path)) + } + }) + // See above + .filter_map(|result: Result>| match result { + Ok(result) => Some(result), + Err(err) => { + eprintln!("WARNING: Skipping file during scan: {err:#}\n"); + None + } + }) + .collect(); + let mut plugins: Vec = Vec::new(); let mut skipped_files: Vec = Vec::new(); - for dandidate in is_vst2_plugin { match dandidate { Ok(plugin) => plugins.push(Plugin::Vst2(plugin)), Err(path) => skipped_files.push(path), } } - for candidate in is_vst3_module { match candidate { Ok(module) => plugins.push(Plugin::Vst3(module)), Err(path) => skipped_files.push(path), } } + for candidate in is_clap_plugin { + match candidate { + Ok(module) => plugins.push(Plugin::Clap(module)), + Err(path) => skipped_files.push(path), + } + } Ok(SearchResults { plugins, diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs index dd5f18c1..e5618575 100644 --- a/tools/yabridgectl/src/main.rs +++ b/tools/yabridgectl/src/main.rs @@ -63,7 +63,7 @@ fn main() -> Result<()> { .display_order(1) .arg( Arg::new("path") - .help("Path to a directory containing Windows VST2 or VST3 plugins") + .help("Path to a directory containing Windows VST2, VST3, or CLAP plugins") .validator(validate_directory) .takes_value(true) .required(true), @@ -134,8 +134,8 @@ fn main() -> Result<()> { ) .long_help( "Path to the directory containing \ - 'libyabridge-chainloader-{clap,vst2,vst3}.so'. If this is not set, then \ - yabridgectl will look in both '/usr/lib' and \ + 'libyabridge-chainloader-{clap,vst2,vst3}.so'. If this is not set, \ + then yabridgectl will look in both '/usr/lib' and \ '~/.local/share/yabridge' by default.", ) .validator(validate_path)