[yabridgectl] Add support for CLAP plugins

CLAP support in yabridge itself has barely started at this point, but
having yabridgectl up and running in advance seems useful.
This commit is contained in:
Robbert van der Helm
2022-08-29 17:43:19 +02:00
parent bfe3cab8d2
commit 7810c9d631
6 changed files with 371 additions and 102 deletions
+4
View File
@@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### yabridgectl
- Added support for setting up CLAP plugins.
### Packaging notes ### Packaging notes
- The VST3 dependency is now at tag `v3.7.5_build_44-patched-2`. The only - The VST3 dependency is now at tag `v3.7.5_build_44-patched-2`. The only
+12 -13
View File
@@ -18,12 +18,11 @@ mentioned here are only useful during development.
### Yabridge path ### Yabridge path
Yabridgectl will need to know where it can find `libyabridge-vst2.so` and Yabridgectl will need to know where it can find yabridge's files. By default it
`libyabridge-vst3.so`. By default it will search for it in both will search for it in both `~/.local/share/yabridge` (the recommended
`~/.local/share/yabridge` (the recommended installation directory when using the installation directory when using the prebuilt binaries), in `/usr/lib` and in
prebuilt binaries), in `/usr/lib` and in `/usr/local/lib`. You can use the `/usr/local/lib`. You can use the command below to override this behaviour and
command below to override this behaviour and to use a custom installation to use a custom installation directory instead.
directory instead.
```shell ```shell
yabridgectl set --path=<path/to/directory/containing/yabridge/files> yabridgectl set --path=<path/to/directory/containing/yabridge/files>
@@ -58,13 +57,13 @@ yabridgectl blacklist
### Installing and updating ### Installing and updating
Lastly you can tell yabridgectl to set up or update yabridge for all of your 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 VST2, VST3 and CLAP plugins at the same time using the commands below.
will warn you if it finds unrelated `.so` files that may have been left after Yabridgectl will warn you if it finds unrelated `.so` files that may have been
uninstalling a plugin, or if it finds any unknown VST3 plugins in left after uninstalling a plugin, or if it finds any unknown plugins in
`~/.vst3/yabridge`. You can rerun the sync command with the `--prune` option to `~/{.clap,.vst,.vst3}/yabridge`. You can rerun the sync command with the
delete those files. If you are using the default copy-based installation method, `--prune` option to delete those files. After setting up the plugin yabridgectl
it will also verify that your search `PATH` has been set up correctly so you can performs some post-installation setup checks to make sure yabridge is going to
get up and running faster. run correctly.
```shell ```shell
# Set up or update yabridge for all plugins found under the plugin locations # Set up or update yabridge for all plugins found under the plugin locations
+179 -64
View File
@@ -24,9 +24,11 @@ use std::path::{Path, PathBuf};
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::config::{ 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::{self, get_file_type};
use crate::util::{verify_external_dependencies, verify_path_setup, verify_wine_setup}; use crate::util::{verify_external_dependencies, verify_path_setup, verify_wine_setup};
use crate::vst3_moduleinfo::ModuleInfo; 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"); 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!("VST3 location: '{}'\n", yabridge_vst3_home().display());
println!("CLAP location: '{}'\n", yabridge_clap_home().display());
let files = config.files(); let files = config.files();
match &files { match &files {
Ok(files) => { Ok(files) => {
println!( println!(
"libyabridge-chainloader-vst2.so: '{}' ({})", "{VST2_CHAINLOADER_NAME}: '{}' ({})",
files.vst2_chainloader.display(), files.vst2_chainloader.display(),
files.vst2_chainloader_arch, files.vst2_chainloader_arch,
); );
println!( println!(
"libyabridge-chainloader-vst3.so: {}\n", "{VST3_CHAINLOADER_NAME}: {}\n",
files files
.vst3_chainloader .vst3_chainloader
.as_ref() .as_ref()
@@ -129,7 +132,15 @@ pub fn show_status(config: &Config) -> Result<()> {
.unwrap_or_else(|| "<not found>".red().to_string()) .unwrap_or_else(|| "<not found>".red().to_string())
); );
println!( println!(
"yabridge-host.exe: {}", "{CLAP_CHAINLOADER_NAME}: {}\n",
files
.clap_chainloader
.as_ref()
.map(|(path, arch)| format!("'{}' ({})", path.display(), arch))
.unwrap_or_else(|| "<not found>".red().to_string())
);
println!(
"{YABRIDGE_HOST_EXE_NAME}: {}",
files files
.yabridge_host_exe .yabridge_host_exe
.as_ref() .as_ref()
@@ -139,7 +150,7 @@ pub fn show_status(config: &Config) -> Result<()> {
.unwrap_or_else(|| "<not found>".red().to_string()) .unwrap_or_else(|| "<not found>".red().to_string())
); );
println!( println!(
"yabridge-host-32.exe: {}", "{YABRIDGE_HOST_32_EXE_NAME}: {}",
files files
.yabridge_host_32_exe .yabridge_host_32_exe
.as_ref() .as_ref()
@@ -171,6 +182,11 @@ pub fn show_status(config: &Config) -> Result<()> {
module.type_str(), module.type_str(),
module.architecture 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 // 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<()> { pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> {
let files: YabridgeFiles = config.files()?; let files: YabridgeFiles = config.files()?;
let vst2_chainloader_hash = util::hash_file(&files.vst2_chainloader)?; let vst2_chainloader_hash = util::hash_file(&files.vst2_chainloader)?;
let vst3_chainloader_hash = match &files.vst3_chainloader { let vst3_chainloader_hash = files
Some((path, _)) => Some(util::hash_file(path)?), .vst3_chainloader
None => None, .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 { // Better not add another plugin format!
println!("Setting up VST2 and VST3 plugins using:"); match (&files.vst3_chainloader, &files.clap_chainloader) {
println!("- {}", files.vst2_chainloader.display()); (Some((vst3_chainloader_path, _)), Some((clap_chainloader_path, _))) => {
println!("- {}\n", vst3_chainloader_path.display()); println!("Setting up VST2, VST3, and CLAP plugins using:");
} else { println!("- {}", files.vst2_chainloader.display());
println!("Setting up VST2 plugins using:"); println!("- {}", vst3_chainloader_path.display());
println!("- {}\n", files.vst2_chainloader.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 let results = config
.search_directories() .search_directories()
.context("Failure while searching for plugins")?; .context("Failure while searching for plugins")?;
// Before doing anything, make sure `~/.vst/yabridge` and `~/.vst3/yabridge` are not symlinks to // Before doing anything, make sure `~/.{clap,vst,vst3}/yabridge` are not symlinks to one of the
// one of the plugin directories. See // plugin directories. See
// https://github.com/robbert-vdh/yabridge/issues/185#issuecomment-1166274104. // 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 vst2_home = yabridge_vst2_home();
let vst3_home = yabridge_vst3_home(); let vst3_home = yabridge_vst3_home();
if let Ok(canonical_vst2_home) = fs::canonicalize(&vst2_home) { let clap_home = yabridge_clap_home();
if canonical_vst2_home != vst2_home { bail_if_unsafe_symlink(&vst2_home, "~/.vst/yabridge")?;
for plugin_dir in &config.plugin_dirs { bail_if_unsafe_symlink(&vst3_home, "~/.vst3/yabridge")?;
if let Ok(canonical_plugin_dir) = fs::canonicalize(&plugin_dir) { bail_if_unsafe_symlink(&clap_home, "~/.clap/yabridge")?;
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(),
);
}
}
}
}
}
// Keep track of some global statistics // Keep track of some global statistics
// The plugin files we installed. This tracks copies of/symlinks to `libabyrdge-*.so` managed. // 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<PathBuf> = HashSet::new(); let mut new_plugins: HashSet<PathBuf> = HashSet::new();
// The files we skipped during the scan because they turned out to not be plugins // The files we skipped during the scan because they turned out to not be plugins
let mut skipped_dll_files: Vec<PathBuf> = Vec::new(); let mut skipped_dll_files: Vec<PathBuf> = Vec::new();
// `.so` files and unused VST3 modules we found during scanning that didn't have a corresponding // `.so` files and unused files/bundles we found during scanning that don't belong to a known
// copy or symlink of `libyabridge-chainloader-vst2.so` // plugin. `~/{.clap,.vst,.vst3}/yabridge` will be searched for these files after setting up all
// plugins.
let mut orphan_files: Vec<NativeFile> = Vec::new(); let mut orphan_files: Vec<NativeFile> = Vec::new();
// When using the centralized VST2 installation location in `~/.vst/yabridge` we'll want to // 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 // 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 // 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. // can then at the end remove all unkonwn bundles, and all unkonwn files within a bundle.
let mut known_centralized_vst3_files: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new(); let mut known_centralized_vst3_files: HashMap<PathBuf, HashSet<PathBuf>> = 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<PathBuf> = HashSet::new();
for (path, search_results) in results { for (path, search_results) in results {
// Orphan files in the centralized directories need to be detected separately // Orphan files in the centralized directories need to be detected separately
orphan_files.extend( 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: // And then create merged bundles for the VST3 plugins:
// https://developer.steinberg.help/display/VST/Plug-in+Format+Structure#PluginFormatStructure-MergedBundle // 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() 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 { 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 // 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 // do something similar for orphan files in `~/.vst/yabridge`, `~/.vst3/yabridge`, and
// plugins we'll want to remove both unmanaged VST3 bundles in `~/.vst3/yabridge` as well as // `~/.clap/yabridge`. For VST3 plugins we'll want to remove both unmanaged VST3 bundles in
// unmanged files within managed bundles. That's why we'll immediately filter out known files // `~/.vst3/yabridge` as well as unmanged files within managed bundles. That's why we'll
// within VST3 bundles. For VST2 plugins we can simply treat any file in `~/.vst/yabridge` that // immediately filter out known files within VST3 bundles. For VST2 and CLAP plugins we can
// we did not add to `known_centralized_vst2_files` as an orphan. We'll want to do this // simply treat any file in `~/{.clap,.vst}/yabridge` that we did not add to
// regardless of the VST2 installation location setting so switching between the two modes and // `known_centralized_{clap,vst2}_files` as an orphan. We'll want to do this regardless of the
// then pruning works as expected. // VST2 installation location setting so switching between the two modes and then pruning works
// as expected.
// TODO: Move this elsewhere // TODO: Move this elsewhere
let centralized_vst2_files = WalkDir::new(vst2_home) let centralized_vst2_files = WalkDir::new(vst2_home)
.follow_links(true) .follow_links(true)
@@ -594,6 +686,22 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> {
None 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) let installed_vst3_bundles = WalkDir::new(vst3_home)
.follow_links(true) .follow_links(true)
.same_file_system(true) .same_file_system(true)
@@ -619,6 +727,13 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> {
get_file_type(path) 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 { for bundle_path in installed_vst3_bundles {
match known_centralized_vst3_files.get(&bundle_path) { match known_centralized_vst3_files.get(&bundle_path) {
None => orphan_files.push(NativeFile::Directory(bundle_path)), None => orphan_files.push(NativeFile::Directory(bundle_path)),
+44 -5
View File
@@ -35,6 +35,8 @@ pub const CONFIG_FILE_NAME: &str = "config.toml";
/// `$XDG_DATA_HOME`. /// `$XDG_DATA_HOME`.
const YABRIDGECTL_PREFIX: &str = "yabridgectl"; 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. /// The name of yabridge's VST2 chainloading library yabridgectl will create copies of.
pub const VST2_CHAINLOADER_NAME: &str = "libyabridge-chainloader-vst2.so"; pub const VST2_CHAINLOADER_NAME: &str = "libyabridge-chainloader-vst2.so";
/// The name of yabridge's VST3 chainloading library yabridgectl will create copies of. /// 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`. /// `$XDG_CONFIG_HOME` and `$XDG_DATA_HOME`.
const YABRIDGE_PREFIX: &str = "yabridge"; 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 /// 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 /// VST2 installation location setting. By putting this in a subdirectory we can easily clean up any
/// orphan files without interfering with other native plugins. /// 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 /// 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. /// the expected locations for yabridge to be installed in.
pub yabridge_home: Option<PathBuf>, pub yabridge_home: Option<PathBuf>,
/// Directories to search for Windows VST plugins. These directories can contain both VST2 /// Directories to search for Windows VST plugins. These directories can contain VST2 plugin
/// plugin `.dll` files and VST3 modules (which should be located in `<prefix>/drive_c/Program /// `.dll` files, VST3 modules (which should be located in `<prefix>/drive_c/Program
/// Files/Common/VST3`). We're using an ordered set here out of convenience so we can't get /// Files/Common/VST3`), and CLAP plugins (which should similarly be installed to
/// duplicates and the config file is always sorted. /// `<prefix>/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<PathBuf>, pub plugin_dirs: BTreeSet<PathBuf>,
/// Where VST2 plugins are setup. This can be either in `~/.vst/yabridge` or inline with the /// Where VST2 plugins are setup. This can be either in `~/.vst/yabridge` or inline with the
/// plugin's .dll` files.` /// 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 /// 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. /// set up the merged VST3 bundles.
pub vst3_chainloader: Option<(PathBuf, LibArchitecture)>, 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 /// 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`. /// not have to be relative to `yabridge_home`.
pub yabridge_host_exe: Option<PathBuf>, pub yabridge_host_exe: Option<PathBuf>,
@@ -275,6 +287,22 @@ impl Config {
_ => None, _ => 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 // `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()`) // `~/.local/share/yabridge` (which was appended to the `$PATH` at the start of `main()`)
let yabridge_host_exe = which(YABRIDGE_HOST_EXE_NAME).ok(); let yabridge_host_exe = which(YABRIDGE_HOST_EXE_NAME).ok();
@@ -290,6 +318,7 @@ impl Config {
vst2_chainloader, vst2_chainloader,
vst2_chainloader_arch, vst2_chainloader_arch,
vst3_chainloader, vst3_chainloader,
clap_chainloader,
yabridge_host_exe, yabridge_host_exe,
yabridge_host_exe_so, yabridge_host_exe_so,
yabridge_host_32_exe, 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<BTreeMap<&Path, SearchResults>> { pub fn search_directories(&self) -> Result<BTreeMap<&Path, SearchResults>> {
let blacklist: HashSet<&Path> = self.blacklist.iter().map(|p| p.as_path()).collect(); let blacklist: HashSet<&Path> = self.blacklist.iter().map(|p| p.as_path()).collect();
@@ -325,6 +354,9 @@ pub fn yabridgectl_directories() -> Result<BaseDirectories> {
BaseDirectories::with_prefix(YABRIDGECTL_PREFIX).context("Error while parsing base directories") 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 /// 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 /// installation location setting. This is a subdirectory of `~/.vst` so we can easily clean up
/// leftover files without interfering with other native plugins. /// leftover files without interfering with other native plugins.
@@ -338,3 +370,10 @@ pub fn yabridge_vst2_home() -> PathBuf {
pub fn yabridge_vst3_home() -> PathBuf { pub fn yabridge_vst3_home() -> PathBuf {
Path::new(&env::var("HOME").expect("$HOME is not set")).join(YABRIDGE_VST3_HOME) 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)
}
+129 -17
View File
@@ -23,18 +23,22 @@ use std::fmt::Display;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use walkdir::WalkDir; 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::symbols::parse_pe32_binary;
use crate::util::get_file_type; use crate::util::get_file_type;
/// Stores the results from searching through a directory. We'll search for Windows VST2 plugin /// 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. /// `.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 /// 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 /// plugins and to be able to prune orphan files. Since yabridgectl 4.0 now sets up plugins in the
/// `~/.vst3`, these orphan files are only relevant for VST2 plugins. /// 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)] #[derive(Debug)]
pub struct SearchResults { 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<Plugin>, pub plugins: Vec<Plugin>,
/// `.dll` files skipped over during the search. Used for printing statistics and shown when /// `.dll` files skipped over during the search. Used for printing statistics and shown when
/// running `yabridgectl sync --verbose`. /// running `yabridgectl sync --verbose`.
@@ -49,12 +53,14 @@ pub struct SearchResults {
/// files in a directory before filtering them down to a `SearchResults` object. /// files in a directory before filtering them down to a `SearchResults` object.
#[derive(Debug)] #[derive(Debug)]
pub struct SearchIndex { 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<PathBuf>)>, pub dll_files: Vec<(PathBuf, Option<PathBuf>)>,
/// Any `.vst3` file or directory, along with its relative path in the search directory. This /// 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 /// can be either a legacy `.vst3` DLL module or a VST 3.6.10 module (or some kind of random
/// other file, of course). /// other file, of course).
pub vst3_files: Vec<(PathBuf, Option<PathBuf>)>, pub vst3_files: Vec<(PathBuf, Option<PathBuf>)>,
/// Any `.clap` file, along with its relative path in the search directory.
pub clap_files: Vec<(PathBuf, Option<PathBuf>)>,
/// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or
/// a regular file. /// a regular file.
pub so_files: Vec<NativeFile>, pub so_files: Vec<NativeFile>,
@@ -84,6 +90,7 @@ impl NativeFile {
pub enum Plugin { pub enum Plugin {
Vst2(Vst2Plugin), Vst2(Vst2Plugin),
Vst3(Vst3Module), Vst3(Vst3Module),
Clap(ClapPlugin),
} }
/// VST2 plugins we found during a search along with their architecture. /// VST2 plugins we found during a search along with their architecture.
@@ -110,6 +117,20 @@ pub struct Vst3Module {
pub subdirectory: Option<PathBuf>, pub subdirectory: Option<PathBuf>,
} }
/// 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<PathBuf>,
}
/// The type of the VST3 module. VST 3.6.10 style bundles require slightly different handling /// The type of the VST3 module. VST 3.6.10 style bundles require slightly different handling
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Vst3ModuleType { 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 /// 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. /// we can create a merged bundle for VST3 plugins.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
@@ -393,6 +451,10 @@ impl SearchResults {
get_file_type(vst3_module.target_native_module_path(files)), 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() .collect()
} }
@@ -426,18 +488,20 @@ impl SearchResults {
} }
} }
/// Find all `.dll`, `.vst3` and `.so` files under a directory. These results can be filtered down /// Find all `.dll`, `.vst3`, `.clap`, and `.so` files under a directory. These results can be
/// to actual VST2 plugins and VST3 modules using `search()`. Any path found in the blacklist will /// filtered down to actual VST2 plugins, VST3 modules, and CLAP plugins using `search()`. Any path
/// be pruned immediately, so this can be used to both not index individual files and to skip an /// found in the blacklist will be pruned immediately, so this can be used to both not index
/// entire directory. /// individual files and to skip an entire directory.
/// ///
/// For VST3 plugin _bundles_ the subdirectory also contains the `foo.vst3/Contents/x86_64-win` /// 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. /// suffix. This needs to be stripped out to get the bundle root.
pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex { pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex {
// These are pairs of `(absolute_path, subdirectory)`. The subdirectory is used for setting up // 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<PathBuf>)> = Vec::new(); let mut dll_files: Vec<(PathBuf, Option<PathBuf>)> = Vec::new();
let mut vst3_files: Vec<(PathBuf, Option<PathBuf>)> = Vec::new(); let mut vst3_files: Vec<(PathBuf, Option<PathBuf>)> = Vec::new();
let mut clap_files: Vec<(PathBuf, Option<PathBuf>)> = Vec::new();
let mut so_files: Vec<NativeFile> = Vec::new(); let mut so_files: Vec<NativeFile> = Vec::new();
for (file_idx, path) in WalkDir::new(directory) for (file_idx, path) in WalkDir::new(directory)
.follow_links(true) .follow_links(true)
@@ -494,6 +558,13 @@ pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex {
.map(|p| p.to_owned()); .map(|p| p.to_owned());
vst3_files.push((path, subdirectory)); 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") => { Some("so") => {
if path.is_symlink() { if path.is_symlink() {
so_files.push(NativeFile::Symlink(path)); so_files.push(NativeFile::Symlink(path));
@@ -508,6 +579,7 @@ pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex {
SearchIndex { SearchIndex {
dll_files, dll_files,
vst3_files, vst3_files,
clap_files,
so_files, so_files,
} }
} }
@@ -518,6 +590,8 @@ impl SearchIndex {
pub fn search(self) -> Result<SearchResults> { pub fn search(self) -> Result<SearchResults> {
const VST2_ENTRY_POINTS: [&str; 2] = ["VSTPluginMain", "main"]; const VST2_ENTRY_POINTS: [&str; 2] = ["VSTPluginMain", "main"];
const VST3_ENTRY_POINTS: [&str; 1] = ["GetPluginFactory"]; 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 // 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 // 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 // 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 // junk files from zip files containing Windows plugins created on macOS to their plugin
// their plugin directories (how does such a thing even happen in the first place?) // directories (how does such a thing even happen in the first place?)
.filter_map(|result: Result<Result<Vst2Plugin, PathBuf>>| match result { .filter_map(|result: Result<Result<Vst2Plugin, PathBuf>>| match result {
Ok(result) => Some(result), Ok(result) => Some(result),
Err(err) => { Err(err) => {
@@ -639,9 +713,7 @@ impl SearchIndex {
Ok(Err(module_path)) Ok(Err(module_path))
} }
}) })
// Make parsing failures non-fatal. People somehow extract these `__MACOSX` and other // See above
// 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?)
.filter_map(|result: Result<Result<Vst3Module, PathBuf>>| match result { .filter_map(|result: Result<Result<Vst3Module, PathBuf>>| match result {
Ok(result) => Some(result), Ok(result) => Some(result),
Err(err) => { Err(err) => {
@@ -651,22 +723,62 @@ impl SearchIndex {
}) })
.collect(); .collect();
// Same for CLAP plugins
let is_clap_plugin: Vec<Result<ClapPlugin, PathBuf>> = 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<Result<ClapPlugin, PathBuf>>| match result {
Ok(result) => Some(result),
Err(err) => {
eprintln!("WARNING: Skipping file during scan: {err:#}\n");
None
}
})
.collect();
let mut plugins: Vec<Plugin> = Vec::new(); let mut plugins: Vec<Plugin> = Vec::new();
let mut skipped_files: Vec<PathBuf> = Vec::new(); let mut skipped_files: Vec<PathBuf> = Vec::new();
for dandidate in is_vst2_plugin { for dandidate in is_vst2_plugin {
match dandidate { match dandidate {
Ok(plugin) => plugins.push(Plugin::Vst2(plugin)), Ok(plugin) => plugins.push(Plugin::Vst2(plugin)),
Err(path) => skipped_files.push(path), Err(path) => skipped_files.push(path),
} }
} }
for candidate in is_vst3_module { for candidate in is_vst3_module {
match candidate { match candidate {
Ok(module) => plugins.push(Plugin::Vst3(module)), Ok(module) => plugins.push(Plugin::Vst3(module)),
Err(path) => skipped_files.push(path), 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 { Ok(SearchResults {
plugins, plugins,
+3 -3
View File
@@ -63,7 +63,7 @@ fn main() -> Result<()> {
.display_order(1) .display_order(1)
.arg( .arg(
Arg::new("path") 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) .validator(validate_directory)
.takes_value(true) .takes_value(true)
.required(true), .required(true),
@@ -134,8 +134,8 @@ fn main() -> Result<()> {
) )
.long_help( .long_help(
"Path to the directory containing \ "Path to the directory containing \
'libyabridge-chainloader-{clap,vst2,vst3}.so'. If this is not set, then \ 'libyabridge-chainloader-{clap,vst2,vst3}.so'. If this is not set, \
yabridgectl will look in both '/usr/lib' and \ then yabridgectl will look in both '/usr/lib' and \
'~/.local/share/yabridge' by default.", '~/.local/share/yabridge' by default.",
) )
.validator(validate_path) .validator(validate_path)