diff --git a/CHANGELOG.md b/CHANGELOG.md index 2646d189..e9e09d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### TODOs + +- The readme documentation has not yet been updated to reflect the changes on + the master branch. The main thing that would require documentation changes is + the default VST2 plugin handling. Those are now set up in `~/.vst/yabridge` by + default, with an option to revert back to the old inline behavior. + ### Added - Yabridge 4.0 introduces a completely new way to load plugins that allows @@ -57,8 +64,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). - Removed the `with-static-boost` build option since there's no longer a dependency on Boost.Filesystem. -- Removed the `yabridge-group*` binaries as they are now part of the - `yabridge-host*` binaries. This saves precious megabytes. +- Removed the `yabridge-group` binaries as they are now part of the + `yabridge-host` binaries. This saves precious megabytes. ### Fixed @@ -69,11 +76,27 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### yabridgectl +- VST2 plugins are now set up in `~/.vst/yabridge` by default. This means that + you no longer have to add any directory search locations in your DAW. The + downside is that it's no longer possible for two plugin directories (perhaps + in different Wine prefixes) to provide the same plugin file. Like with + yabridgectl's VST3 handling, the subdirectory structure within the plugin + directory is preserved. You can use `yabridgectl set --vst2-location=inline` + to revert back to the old behavior of setting the plugins up right next to the + VST2 plugin `.dll` files. + + If you were using a `yabridge.toml` configuration file, then you will now need + to place that file in `~/.vst/yabridge` instead. + - As mentioned above, yabridgectl will now use the new chainloading libraries when setting up plugins. This means that once you've ran `yabridgectl sync` once after updating to yabridge 4.0, yabridge can now be updated without needing to rerun `yabridgectl sync`. This is particularly useful when using a distro packaged version of yabridge. + +- The VST3 subdirectory detection is more robust and can now handle arbitrary + directories, not just directories that are called `VST3`. This, of course, + should not be needed. - The previously deprecated symlink installation method has now been removed from yabridgectl, along with the `yabridgectl set --method` option. - `yabridgectl status` now lists the architecture of @@ -87,12 +110,14 @@ Versioning](https://semver.org/spec/v2.0.0.html). - The `yabridge-group` binaries no longer exist as they are now part of the `yabridge-host` binaries. - The `with-bitbridge` build option has been renamed to just `bitbridge`. -- Completely removed the dependency on all Boost libraries. -- Added a dependency on the headers-only +- Both runtime and compile time dependencies on the Boost libraries have been + removed. +- There's a new dependency on the headers-only [`ghc::filesystem`](https://github.com/gulrak/filesystem) library to replace - Boost.Filesystem. + Boost.Filesystem. A Meson wrap is included as a fallback for a distro package. - Added a dependency on the headers-only [Asio](http://think-async.com/Asio/) - library to replace Boost.Asio. + library to replace Boost.Asio. A Meson wrap is included as a fallback for a + distro package. - Fixed a deprecation warning in the Meson build, causing the minimum supported Meson version to be bumped up to **Meson 0.56** from 0.55. diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index 63e34ae4..8dccb42a 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -41,8 +41,8 @@ class HostProcess { /** * Return the full path to the host application in use. The host application - * is chosen depending on the architecture of the plugin's DLL file and on - * the hosting mode. + * is chosen depending on the architecture of the plugin's `.dll` file and + * on the hosting mode. */ virtual ghc::filesystem::path path() = 0; diff --git a/tools/yabridgectl/src/actions.rs b/tools/yabridgectl/src/actions.rs index 9fa2cebf..b09b260d 100644 --- a/tools/yabridgectl/src/actions.rs +++ b/tools/yabridgectl/src/actions.rs @@ -23,7 +23,9 @@ use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir; -use crate::config::{yabridge_vst3_home, Config, Vst2InstallationLocation, YabridgeFiles}; +use crate::config::{ + yabridge_vst2_home, yabridge_vst3_home, Config, Vst2InstallationLocation, YabridgeFiles, +}; use crate::files::{self, NativeFile, Plugin, Vst2Plugin}; use crate::utils::{self, get_file_type}; use crate::utils::{verify_path_setup, verify_wine_setup}; @@ -145,7 +147,7 @@ pub fn show_status(config: &Config) -> Result<()> { println!("\n{}", path.join("").display()); for (plugin_path, (plugin, status)) in - search_results.installation_status(files.as_ref().ok()) + search_results.installation_status(config, files.as_ref().ok()) { let plugin_type = match plugin { Plugin::Vst2(Vst2Plugin { architecture, .. }) => { @@ -263,14 +265,23 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { // `.so` files and unused VST3 modules we found during scanning that didn't have a corresponding // copy or symlink of `libyabridge-chainloader-vst2.so` 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 + let mut known_centralized_vst2_files: HashSet = HashSet::new(); // Since VST3 bundles contain multiple files from multiple sources (native library files from // yabridge, and symlinks to Windows VST3 modules or bundles), cleaning up orphan VST3 files is // a bit more complicated. We want to clean both `.vst3` bundles that weren't used by anything // 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_vst3_files: HashMap> = HashMap::new(); + let mut known_centralized_vst3_files: HashMap> = HashMap::new(); for (path, search_results) in results { - orphan_files.extend(search_results.vst2_orphans().into_iter().cloned()); + // Orphan files in the centralized directories need to be detected separately + orphan_files.extend( + search_results + .vst2_inline_orphans(config) + .into_iter() + .cloned(), + ); skipped_dll_files.extend(search_results.skipped_files); if options.verbose { @@ -282,27 +293,78 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { for plugin in search_results.plugins { // If verbose mode is enabled we'll print the path to the plugin after setting it up let plugin_path: PathBuf = match plugin { - // We'll set up the copies or symlinks for VST2 plugins - Plugin::Vst2(Vst2Plugin { - path: plugin_path, .. - }) => { - let target_path = plugin_path.with_extension("so"); - let normalized_target_path = utils::normalize_path(&target_path); + // VST2 plugins can be set up in either `~/.vst/yabridge` or inline with the + // plugin's `.dll` file + Plugin::Vst2(vst2_plugin) => { + match config.vst2_location { + Vst2InstallationLocation::Centralized => { + let target_native_plugin_path = vst2_plugin.centralized_native_target(); + let target_windows_plugin_path = + vst2_plugin.centralized_windows_target(); + let normalized_target_native_plugin_path = + utils::normalize_path(&target_native_plugin_path); - // Since we skip some files, we'll also keep track of how many new file we've - // actually set up - if install_file( - options.force, - InstallationMethod::Copy, - &files.vst2_chainloader, - Some(vst2_chainloader_hash), - &target_path, - )? { - new_plugins.insert(normalized_target_path.clone()); + let mut is_new = known_centralized_vst2_files + .insert(target_native_plugin_path.clone()); + is_new |= known_centralized_vst2_files + .insert(target_windows_plugin_path.clone()); + if !is_new { + eprintln!( + "{}", + utils::wrap(&format!( + "{}: '{}' has already been provided by another Wine prefix or plugin directory, skipping it\n", + "WARNING".red(), + target_windows_plugin_path.display(), + )) + ); + + continue; + } + + // In the centralized mode we'll create a copy of + // `libyabridge-chainloader-vst2.so` to (a subdirectory of) + // `~/.vst/yabridge`, and then we'll symlink the Windows VST2 plugin + // `.dll` file right next to it + utils::create_dir_all(target_native_plugin_path.parent().unwrap())?; + if install_file( + options.force, + InstallationMethod::Copy, + &files.vst2_chainloader, + Some(vst2_chainloader_hash), + &target_native_plugin_path, + )? { + new_plugins.insert(normalized_target_native_plugin_path.clone()); + } + managed_plugins.insert(normalized_target_native_plugin_path); + + install_file( + true, + InstallationMethod::Symlink, + &vst2_plugin.path, + None, + &target_windows_plugin_path, + )?; + } + Vst2InstallationLocation::Inline => { + let target_path = vst2_plugin.inline_native_target(); + let normalized_target_path = utils::normalize_path(&target_path); + + // Since we skip some files, we'll also keep track of how many new file we've + // actually set up + if install_file( + options.force, + InstallationMethod::Copy, + &files.vst2_chainloader, + Some(vst2_chainloader_hash), + &target_path, + )? { + new_plugins.insert(normalized_target_path.clone()); + } + managed_plugins.insert(normalized_target_path); + } } - managed_plugins.insert(normalized_target_path); - plugin_path.clone() + vst2_plugin.path.clone() } // And then create merged bundles for the VST3 plugins: // https://developer.steinberg.help/display/VST/Plug-in+Format+Structure#PluginFormatStructure-MergedBundle @@ -321,7 +383,7 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { // 32-bit and 64-bit versions of the plugin can live inside of the same bundle), // but it's not possible to use the exact same plugin from multiple Wine // prefixes at the same time so we'll warn when that happens - let managed_vst3_bundle_files = known_vst3_files + let managed_vst3_bundle_files = known_centralized_vst3_files .entry(target_bundle_home.clone()) .or_insert_with(HashSet::new); if managed_vst3_bundle_files.contains(&target_windows_module_path) { @@ -329,7 +391,7 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { "{}", utils::wrap(&format!( "{}: The {} version of '{}' has already been provided by another Wine \ - prefix, skipping '{}'\n", + prefix or plugin directory, skipping '{}'\n", "WARNING".red(), module.architecture, module.target_bundle_home().display(), @@ -420,10 +482,30 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { println!(); } - // We 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 - // kown files within VST3 bundles. + // 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. // TODO: Move this elsewhere + let centralized_vst2_files = WalkDir::new(yabridge_vst2_home()) + .follow_links(true) + .same_file_system(true) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|entry| !entry.file_type().is_dir()) + .filter(|entry| { + matches!( + entry + .path() + .extension() + .and_then(|extension| extension.to_str()), + Some("dll" | "so") + ) + }); let installed_vst3_bundles = WalkDir::new(yabridge_vst3_home()) .follow_links(true) .same_file_system(true) @@ -437,8 +519,16 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { .and_then(|extension| extension.to_str()) == Some("vst3") }); + + orphan_files.extend(centralized_vst2_files.filter_map(|entry| { + if known_centralized_vst2_files.contains(entry.path()) { + None + } else { + get_file_type(entry.path().to_owned()) + } + })); for bundle in installed_vst3_bundles { - match known_vst3_files.get(bundle.path()) { + match known_centralized_vst3_files.get(bundle.path()) { None => orphan_files.push(NativeFile::Directory(bundle.path().to_owned())), Some(managed_vst3_bundle_files) => { // Find orphan files and symlinks within this bundle. We need this to be able to @@ -480,7 +570,10 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { ); } - for file in orphan_files { + // NOTE: This is done in reverse lexicographical order to make sure subdirectories are + // cleaned before their parent directories + orphan_files.sort_by(|a, b| b.path().cmp(a.path())); + for file in orphan_files.into_iter() { println!("- {}", file.path().display()); if options.prune { match &file { diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index dd840bd0..9e91c157 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -49,6 +49,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` 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. +const YABRIDGE_VST2_HOME: &str = ".vst/yabridge"; /// The path relative to `$HOME` that VST3 modules 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. @@ -322,6 +326,13 @@ pub fn yabridgectl_directories() -> Result { BaseDirectories::with_prefix(YABRIDGECTL_PREFIX).context("Error while parsing base directories") } +/// 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. +pub fn yabridge_vst2_home() -> PathBuf { + Path::new(&env::var("HOME").expect("$HOME is not set")).join(YABRIDGE_VST2_HOME) +} + /// Get the path where VST3 modules bridged by yabridgectl should be placed in. This is a /// subdirectory of `~/.vst3` so we can easily clean up leftover files without interfering with /// other native plugins. diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs index ca58a82f..976884e7 100644 --- a/tools/yabridgectl/src/files.rs +++ b/tools/yabridgectl/src/files.rs @@ -26,7 +26,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use walkdir::WalkDir; -use crate::config::{yabridge_vst3_home, YabridgeFiles}; +use crate::config::{yabridge_vst2_home, yabridge_vst3_home, Config, YabridgeFiles}; use crate::utils::get_file_type; /// Stores the results from searching through a directory. We'll search for Windows VST2 plugin @@ -125,6 +125,47 @@ pub enum Vst3ModuleType { Bundle(PathBuf), } +impl Vst2Plugin { + /// Get the absolute path to the `.so` file we should create in `~/.vst/yabridge` for this + /// plugin when using the centralized VST installation location mode. + pub fn centralized_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("so"); + + match &self.subdirectory { + Some(directory) => yabridge_vst2_home().join(directory).join(file_name), + None => yabridge_vst2_home().join(file_name), + } + } + + /// Get the absolute path to the `.dll` file we should symlink to `~/.vst/yabridge` when setting + /// this plugin up with the centralized VST2 installation location setting. + pub fn centralized_windows_target(&self) -> PathBuf { + let file_name = self + .path + .file_name() + .unwrap() + .to_str() + .expect("Plugin name contains invalid UTF-8"); + + match &self.subdirectory { + Some(directory) => yabridge_vst2_home().join(directory).join(file_name), + None => yabridge_vst2_home().join(file_name), + } + } + + /// Get the absolute path to the `.so` file we should create when setting this plugin up with + /// the inline VST2 installation location setting. + pub fn inline_native_target(&self) -> PathBuf { + self.path.with_extension("so") + } +} + impl Vst3Module { /// Get the path to the Windows VST3 plugin. This can be either a file or a directory depending /// on the type of moudle. @@ -195,7 +236,7 @@ impl Vst3Module { /// version of this module. The path here depends on whether we're using a 32-bit or 64-bit /// version of yabridge. If the configuration is not given (for instance, becuase yabridge is /// not set up properly) we'll assume the module should be 64-bit. - pub fn target_native_module_path(&self, config: Option<&YabridgeFiles>) -> PathBuf { + pub fn target_native_module_path(&self, files: Option<&YabridgeFiles>) -> PathBuf { let native_module_name = match &self.module { Vst3ModuleType::Legacy(path) | Vst3ModuleType::Bundle(path) => path .with_extension("so") @@ -210,7 +251,7 @@ impl Vst3Module { path.push("Contents"); #[allow(clippy::wildcard_in_or_patterns)] - match config.and_then(|c| c.vst3_chainloader.as_ref()) { + match files.and_then(|c| c.vst3_chainloader.as_ref()) { Some((_, LibArchitecture::Lib32)) => path.push("i386-linux"), // NOTE: We'll always fall back to this if `libyabridge-chainloader-vst3.so` is not // found, just so we cannot get any errors during `yabridgectl status` even if @@ -280,12 +321,12 @@ impl LibArchitecture { } impl SearchResults { - /// Create a map out of all found plugins based on their file path that contains both a - /// reference to the plugin (so we can print information about it) and the current installation - /// status. The installation status will be `None` if the plugin has not yet been set up. + /// Create a map out of all found Windows plugins and their current installation status, if the + /// plugin has already been set up. pub fn installation_status( &self, - config: Option<&YabridgeFiles>, + config: &Config, + files: Option<&YabridgeFiles>, ) -> BTreeMap)> { let so_files: HashMap<&Path, &NativeFile> = self .so_files @@ -296,12 +337,26 @@ impl SearchResults { self.plugins .iter() .map(|plugin| match plugin { - Plugin::Vst2(Vst2Plugin { path, .. }) => { - // For VST2 plugins we'll just look at the similarly named `.so` file right next - // to the plugin `.dll` file. - match so_files.get(path.with_extension("so").as_path()) { - Some(&file_type) => (path.clone(), (plugin, Some(file_type.clone()))), - None => (path.clone(), (plugin, None)), + Plugin::Vst2(vst2_plugin) => { + // For VST2 plugins depending on the VST2 installation location setting we'll + // either look for a matching file in `~/.vst` or we'll just look at the + // similarly named `.so` file right next to the plugin `.dll` file + match config.vst2_location { + crate::config::Vst2InstallationLocation::Centralized => ( + vst2_plugin.path.clone(), + ( + plugin, + get_file_type(vst2_plugin.centralized_native_target()), + ), + ), + crate::config::Vst2InstallationLocation::Inline => { + match so_files.get(vst2_plugin.inline_native_target().as_path()) { + Some(&file_type) => { + (vst2_plugin.path.clone(), (plugin, Some(file_type.clone()))) + } + None => (vst2_plugin.path.clone(), (plugin, None)), + } + } } } // We have not stored the paths to the corresponding `.so` files yet for VST3 @@ -310,7 +365,7 @@ impl SearchResults { vst3_module.original_path().to_owned(), ( plugin, - get_file_type(vst3_module.target_native_module_path(config)), + get_file_type(vst3_module.target_native_module_path(files)), ), ), }) @@ -318,9 +373,9 @@ impl SearchResults { } /// Find all `.so` files in the search results that do not belong to a VST2 plugin `.dll` file. - /// We cannot yet do the same thing for VST3 plguins because they will all be installed in - /// `~/.vst3`. - pub fn vst2_orphans(&self) -> Vec<&NativeFile> { + /// This depends on the VST2 installation location setting. Centralized VST2 and VST3 orphans + /// should be detected separately. + pub fn vst2_inline_orphans(&self, config: &Config) -> Vec<&NativeFile> { // We need to store these in a map so we can easily entries with corresponding `.dll` files let mut orphans: HashMap<&Path, &NativeFile> = self .so_files @@ -328,9 +383,17 @@ impl SearchResults { .map(|file_type| (file_type.path(), file_type)) .collect(); - for plugin in &self.plugins { - if let Plugin::Vst2(Vst2Plugin { path, .. }) = plugin { - orphans.remove(path.with_extension("so").as_path()); + match config.vst2_location { + // When we set up the plugin in `~/.vst`, any `.so` file in a VST2 plugin search + // directory should be considered an orphan. This can happen when switching between the + // two modes. + crate::config::Vst2InstallationLocation::Centralized => (), + crate::config::Vst2InstallationLocation::Inline => { + for plugin in &self.plugins { + if let Plugin::Vst2(Vst2Plugin { path, .. }) = plugin { + orphans.remove(path.with_extension("so").as_path()); + } + } } }