diff --git a/tools/yabridgectl/src/actions.rs b/tools/yabridgectl/src/actions.rs index 5b5a4a47..215a74cb 100644 --- a/tools/yabridgectl/src/actions.rs +++ b/tools/yabridgectl/src/actions.rs @@ -23,7 +23,7 @@ use std::path::{Path, PathBuf}; use crate::config::{Config, InstallationMethod, YabridgeFiles}; use crate::files; -use crate::files::NativeSoFile; +use crate::files::NativeFile; use crate::utils; use crate::utils::{verify_path_setup, verify_wine_setup}; @@ -34,10 +34,9 @@ pub fn add_directory(config: &mut Config, path: PathBuf) -> Result<()> { } /// Remove a direcotry to the plugin locations. The path is assumed to be part of -/// `config.plugin_dirs`, otherwise this si silently ignored. +/// `config.plugin_dirs`, otherwise this is silently ignored. pub fn remove_directory(config: &mut Config, path: &Path) -> Result<()> { // We've already verified that this path is in `config.plugin_dirs` - // XXS: Would it be a good idea to warn about leftover .so files? config.plugin_dirs.remove(path); config.write()?; @@ -121,8 +120,9 @@ pub fn show_status(config: &Config) -> Result<()> { for (plugin, status) in search_results.installation_status() { let status_str = match status { - Some(NativeSoFile::Regular(_)) => "copy".green(), - Some(NativeSoFile::Symlink(_)) => "symlink".green(), + Some(NativeFile::Regular(_)) => "copy".green(), + Some(NativeFile::Symlink(_)) => "symlink".green(), + Some(NativeFile::Directory(_)) => "invalid".red(), None => "not installed".red(), }; @@ -168,20 +168,47 @@ pub struct SyncOptions { pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { let files: YabridgeFiles = config.files()?; let libyabridge_vst2_hash = utils::hash_file(&files.libyabridge_vst2)?; - println!("Using '{}'\n", files.libyabridge_vst2.display()); + let libyabridge_vst3_hash = match &files.libyabridge_vst3 { + Some(path) => Some(utils::hash_file(path)?), + None => None, + }; + + if let Some(libyabridge_vst3_path) = &files.libyabridge_vst3 { + println!("Setting up VST2 and VST3 plugins using:"); + println!("- {}", files.libyabridge_vst2.display()); + println!("- {}\n", libyabridge_vst3_path.display()); + } else { + println!("Setting up VST2 plugins using:"); + println!("- {}\n", files.libyabridge_vst2.display()); + } let results = config .search_directories() .context("Failure while searching for plugins")?; // Keep track of some global statistics + // The number of plugins we set up yabridge for let mut num_installed = 0; + // The number of plugins we create a (new) copy of `libyabridge-{vst2,vst3}.so` for let mut num_new = 0; + // The files we skipped during the scan because they turned out to not be plugins let mut skipped_dll_files: Vec = Vec::new(); - let mut orphan_so_files: Vec = Vec::new(); + // `.so` files we found during scanning that didn't have a corresponding copy or symlink of + // `libyabridge-vst2.so` + let mut orphan_vst2_so_files: Vec = Vec::new(); + // All the VST3 modules we have set up yabridge for. We need this to detect leftover VST3 + // modules in `~/.vst3/yabridge`. + let mut yabridge_vst3_bundles: Vec = Vec::new(); for (path, search_results) in results { num_installed += search_results.vst2_files.len(); - orphan_so_files.extend(search_results.orphans().into_iter().cloned()); + num_installed += search_results.vst3_modules.len(); + orphan_vst2_so_files.extend(search_results.vst2_orphans().into_iter().cloned()); + yabridge_vst3_bundles.extend( + search_results + .vst3_modules + .iter() + .map(|module| module.yabridge_bundle_home()), + ); skipped_dll_files.extend(search_results.skipped_files); if options.verbose { @@ -190,56 +217,58 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { for plugin in search_results.vst2_files { let target_path = plugin.with_extension("so"); - // We'll only recreate existing files when updating yabridge, when switching between the - // symlink and copy installation methods, or when the `force` option is set. If the - // target file already exists and does not require updating, we'll just skip the file - // since some DAWs will otherwise unnecessarily reindex the file. - // We check `std::fs::symlink_metadata` instead of `Path::exists()` because the latter - // reports false for broken symlinks. - if let Ok(metadata) = fs::symlink_metadata(&target_path) { - match (options.force, &config.method) { - (false, InstallationMethod::Copy) => { - // If the target file is already a real file (not a symlink) and its hash is - // the same as the `libyabridge-vst2.so` file we're trying to copy there, - // then we don't have to do anything - if metadata.file_type().is_file() - && utils::hash_file(&target_path)? == libyabridge_vst2_hash - { - continue; - } - } - (false, InstallationMethod::Symlink) => { - // If the target file is already a symlink to `libyabridge-vst2.so`, then we - // can skip this file - if metadata.file_type().is_symlink() - && target_path.read_link()? == files.libyabridge_vst2 - { - continue; - } - } - // With the force option we always want to recreate existing .so files - (true, _) => (), - } - - utils::remove_file(&target_path)?; - }; - // Since we skip some files, we'll also keep track of how many new file we've actually // set up - num_new += 1; - match config.method { - InstallationMethod::Copy => { - utils::copy(&files.libyabridge_vst2, &target_path)?; - } - InstallationMethod::Symlink => { - utils::symlink(&files.libyabridge_vst2, &target_path)?; - } + if install_file( + options.force, + config.method, + &files.libyabridge_vst2, + libyabridge_vst2_hash, + &target_path, + )? { + num_new += 1; } if options.verbose { println!(" {}", plugin.display()); } } + if let Some(libyabridge_vst3_hash) = libyabridge_vst3_hash { + for module in search_results.vst3_modules { + let native_module_path = module.yabridge_native_module_path(); + + // For VST3 plugins we'll first have to create the bundle structure + utils::create_dir_all(native_module_path.parent().unwrap())?; + + // We'll then symlink the Windows VST3 module to that bundle to create a merged + // bundle: https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles + let windows_module_path = module.yabridge_windows_module_path(); + utils::create_dir_all(windows_module_path.parent().unwrap())?; + install_file( + true, + InstallationMethod::Symlink, + &module.original_module_path(), + 0, + &windows_module_path, + )?; + + // TODO: Symlink resources and presets + + if install_file( + options.force, + config.method, + files.libyabridge_vst3.as_ref().unwrap(), + libyabridge_vst3_hash, + &native_module_path, + )? { + num_new += 1; + } + + if options.verbose { + println!(" {}", module.original_path().display()); + } + } + } if options.verbose { println!(); } @@ -255,19 +284,24 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { println!(); } - // Always warn about leftover files sicne those might cause warnings or errors when a VST host + // TODO: Remove leftover files for VST3 plugins + + // Always warn about leftover files since those might cause warnings or errors when a VST host // tries to load them - if !orphan_so_files.is_empty() { + if !orphan_vst2_so_files.is_empty() { if options.prune { - println!("Removing {} leftover .so files:", orphan_so_files.len()); + println!( + "Removing {} leftover .so files:", + orphan_vst2_so_files.len() + ); } else { println!( "Found {} leftover .so files, rerun with the '--prune' option to remove them:", - orphan_so_files.len() + orphan_vst2_so_files.len() ); } - for file in orphan_so_files { + for file in orphan_vst2_so_files { let path = file.path(); println!("- {}", path.display()); @@ -291,6 +325,8 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { return Ok(()); } + // The path setup is to make sure that the `libyabridge-{vst2,vst3}.so` copies can find + // `yabridge-host.exe` if config.method == InstallationMethod::Copy { verify_path_setup(config)?; } @@ -300,3 +336,53 @@ pub fn do_sync(config: &mut Config, options: &SyncOptions) -> Result<()> { Ok(()) } + +/// Create a copy or symlink of `from` to `to`. Depending on `force`, we might not actually create a +/// new copy or symlink if `to` matches `from_hash`. +fn install_file( + force: bool, + method: InstallationMethod, + from: &Path, + from_hash: i64, + to: &Path, +) -> Result { + // We'll only recreate existing files when updating yabridge, when switching between the symlink + // and copy installation methods, or when the `force` option is set. If the target file already + // exists and does not require updating, we'll just skip the file since some DAWs will otherwise + // unnecessarily reindex the file. We check `std::fs::symlink_metadata` instead of + // `Path::exists()` because the latter reports false for broken symlinks. + if let Ok(metadata) = fs::symlink_metadata(&to) { + match (force, &method) { + (false, InstallationMethod::Copy) => { + // If the target file is already a real file (not a symlink) and its hash is + // the same as the `libyabridge-vst2.so` file we're trying to copy there, + // then we don't have to do anything + if metadata.file_type().is_file() && utils::hash_file(to)? == from_hash { + return Ok(false); + } + } + (false, InstallationMethod::Symlink) => { + // If the target file is already a symlink to `libyabridge-vst2.so`, then we + // can skip this file + if metadata.file_type().is_symlink() && to.read_link()? == from { + return Ok(false); + } + } + // With the force option we always want to recreate existing .so files + (true, _) => (), + } + + utils::remove_file(&to)?; + }; + + match method { + InstallationMethod::Copy => { + utils::copy(from, to)?; + } + InstallationMethod::Symlink => { + utils::symlink(from, to)?; + } + } + + Ok(true) +} diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index 9bca24f3..0669e109 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -73,7 +73,7 @@ pub struct Config { } /// Specifies how yabridge will be set up for the found plugins. -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum InstallationMethod { /// Create a copy of `libyabridge-{vst2,vst3}.so` for every Windows VST2 plugin `.dll` file or diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs index cad40cce..9eb81ebd 100644 --- a/tools/yabridgectl/src/files.rs +++ b/tools/yabridgectl/src/files.rs @@ -26,6 +26,7 @@ use std::process::Command; use walkdir::WalkDir; use crate::config::yabridge_vst3_home; +use crate::utils::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. @@ -44,7 +45,7 @@ pub struct SearchResults { /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. - pub so_files: Vec, + pub so_files: Vec, } /// The results of the first step of the search process. We'll first index all possibly relevant @@ -58,22 +59,24 @@ pub struct SearchIndex { pub vst3_files: Vec, /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. - pub so_files: Vec, + pub so_files: Vec, } -/// Native `.so` files we found during a search. +/// Native `.so` files and VST3 bundle directories we found during a search. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NativeSoFile { +pub enum NativeFile { Symlink(PathBuf), Regular(PathBuf), + Directory(PathBuf), } -impl NativeSoFile { +impl NativeFile { /// Return the path of a found `.so` file. pub fn path(&self) -> &Path { match &self { - NativeSoFile::Symlink(path) => path, - NativeSoFile::Regular(path) => path, + NativeFile::Symlink(path) | NativeFile::Regular(path) | NativeFile::Directory(path) => { + path + } } } } @@ -101,19 +104,16 @@ impl Vst3Module { } } - /// Get the path to the `libyabridge.so` file in `~/.vst3` corresponding to the bridged version - /// of this module. - pub fn libyabridge_location(&self) -> PathBuf { - let mut path = yabridge_vst3_home(); - path.push(self.module_name()); - path.push("Contents"); - path.push("x86_64-linux"); - path.push(self.native_module_name()); - path + /// Get the path to the Windows VST3 plugin. This can be either a file or a directory depending + /// on the type of moudle. + pub fn original_path(&self) -> &Path { + match &self { + Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path, + } } /// Get the name of the module as a string. Should be in the format `Plugin Name.vst3`. - pub fn module_name(&self) -> &str { + pub fn original_module_name(&self) -> &str { match &self { Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path .file_name() @@ -123,9 +123,32 @@ impl Vst3Module { } } - /// `module_name()` but with a `.so` file extension isntead of `.vst3`. - pub fn native_module_name(&self) -> String { + /// Get the path to the actual `.vst3` module file. + pub fn original_module_path(&self) -> PathBuf { match &self { + Vst3Module::Legacy(path, _) => path.to_owned(), + Vst3Module::Bundle(bundle_home, architecture) => { + let mut path = bundle_home.join("Contents"); + path.push(architecture.vst_arch()); + path.push(self.original_module_name()); + + path + } + } + } + + /// Get the path to the bundle in `~/.vst3` corresponding to the bridged version of this module. + /// + /// FIXME: How do we solve naming clashes from the same VST3 plugin being installed to multiple + /// Wine prefixes? + pub fn yabridge_bundle_home(&self) -> PathBuf { + yabridge_vst3_home().join(self.original_module_name()) + } + + /// Get the path to the `libyabridge.so` file in `~/.vst3` corresponding to the bridged version + /// of this module. + pub fn yabridge_native_module_path(&self) -> PathBuf { + let native_module_name = match &self { Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path .with_extension("so") .file_name() @@ -133,15 +156,23 @@ impl Vst3Module { .to_str() .expect("VST3 module name contains invalid UTF-8") .to_owned(), - } + }; + + let mut path = self.yabridge_bundle_home(); + path.push("Contents"); + path.push("x86_64-linux"); + path.push(native_module_name); + path } - /// Get the path to the module. This can be either a file or a directory depending on the type - /// of moudle. - pub fn path(&self) -> &Path { - match &self { - Vst3Module::Legacy(path, _) | Vst3Module::Bundle(path, _) => path, - } + /// Get the path to where we'll symlink `original_module_path`. This is part of the merged VST3 + /// bundle in `~/.vst3/yabridge`. + pub fn yabridge_windows_module_path(&self) -> PathBuf { + let mut path = self.yabridge_bundle_home(); + path.push("Contents"); + path.push(self.architecture().vst_arch()); + path.push(self.original_module_name()); + path } } @@ -167,21 +198,21 @@ impl SearchResults { /// For every found VST2 plugin and VST3 module, find the associated copy or symlink of /// `libyabridge-{vst2,vst3}.so`. The returned hashmap will contain a `None` value for plugins /// that have not yet been set up. - pub fn installation_status(&self) -> BTreeMap<&Path, Option> { - let so_files: HashMap<&Path, &NativeSoFile> = self + pub fn installation_status(&self) -> BTreeMap> { + let so_files: HashMap<&Path, &NativeFile> = self .so_files .iter() .map(|file| (file.path(), file)) .collect(); // Do this for the VST2 plugins - let mut installation_status: BTreeMap<&Path, Option> = self + let mut installation_status: BTreeMap> = self .vst2_files .iter() .map( |path| match so_files.get(path.with_extension("so").as_path()) { - Some(&file_type) => (path.as_path(), Some(file_type.clone())), - None => (path.as_path(), None), + Some(&file_type) => (path.clone(), Some(file_type.clone())), + None => (path.clone(), None), }, ) .collect(); @@ -189,29 +220,25 @@ impl SearchResults { // And for VST3 modules. We have not stored the paths to the corresponding `.so` files yet // because they are not in any of the directories we're indexing. installation_status.extend(self.vst3_modules.iter().map(|module| { - let install_path = module.libyabridge_location(); - match install_path.metadata() { - Ok(metadata) if metadata.file_type().is_symlink() => { - (module.path(), Some(NativeSoFile::Symlink(install_path))) - } - Ok(_) => (module.path(), Some(NativeSoFile::Regular(install_path))), - Err(_) => (module.path(), None), - } + let module_path = module.yabridge_native_module_path(); + let install_type = get_file_type(&module_path); + (module_path, install_type) })); installation_status } /// Find all `.so` files in the search results that do not belong to a VST2 plugin `.dll` file. - /// - /// TODO: Also do something similar for VST3 plugins - pub fn orphans(&self) -> Vec<&NativeSoFile> { + /// 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> { // We need to store these in a map so we can easily entries with corresponding `.dll` files - let mut orphans: HashMap<&Path, &NativeSoFile> = self + let mut orphans: HashMap<&Path, &NativeFile> = self .so_files .iter() - .map(|file| (file.path(), file)) + .map(|file_type| (file_type.path(), file_type)) .collect(); + for vst2_path in &self.vst2_files { orphans.remove(vst2_path.with_extension("so").as_path()); } @@ -225,7 +252,7 @@ impl SearchResults { pub fn index(directory: &Path) -> SearchIndex { let mut dll_files: Vec = Vec::new(); let mut vst3_files: Vec = Vec::new(); - let mut so_files: Vec = Vec::new(); + let mut so_files: Vec = Vec::new(); // XXX: We're silently skipping directories and files we don't have permission to read. This // sounds like the expected behavior, but I"m not entirely sure. for (file_idx, entry) in WalkDir::new(directory) @@ -251,9 +278,9 @@ pub fn index(directory: &Path) -> SearchIndex { Some("vst3") => vst3_files.push(entry.into_path()), Some("so") => { if entry.path_is_symlink() { - so_files.push(NativeSoFile::Symlink(entry.into_path())); + so_files.push(NativeFile::Symlink(entry.into_path())); } else { - so_files.push(NativeSoFile::Regular(entry.into_path())); + so_files.push(NativeFile::Regular(entry.into_path())); } } _ => (), diff --git a/tools/yabridgectl/src/utils.rs b/tools/yabridgectl/src/utils.rs index f73c6294..ff2ede7c 100644 --- a/tools/yabridgectl/src/utils.rs +++ b/tools/yabridgectl/src/utils.rs @@ -30,6 +30,7 @@ use std::process::{Command, Stdio}; use textwrap::Wrapper; use crate::config::{self, Config, KnownConfig, YABRIDGE_HOST_EXE_NAME}; +use crate::files::NativeFile; /// (Part of) the expected output when running `yabridge-host.exe`. Used to verify that everything's /// working correctly. We'll only match this prefix so we can modify the exact output at a later @@ -47,6 +48,17 @@ pub fn copy, Q: AsRef>(from: P, to: Q) -> Result { }) } +/// Wrapper around [`std::fs::create_dir_all()`](std::fs::create_dir_all) with a human readable +/// error message. +pub fn create_dir_all>(path: P) -> Result<()> { + fs::create_dir_all(&path).with_context(|| { + format!( + "Error creating directories for '{}'", + path.as_ref().display(), + ) + }) +} + /// Wrapper around [`std::fs::remove_file()`](std::fs::remove_file) with a human readable error /// message. pub fn remove_file>(path: P) -> Result<()> { @@ -66,6 +78,20 @@ pub fn symlink, Q: AsRef>(src: P, dst: Q) -> Result<()> { }) } +/// Get the type of a file, if it exists. +pub fn get_file_type(path: &Path) -> Option { + match path.metadata() { + Ok(metadata) if metadata.file_type().is_symlink() => { + Some(NativeFile::Symlink(path.to_owned())) + } + Ok(metadata) if metadata.file_type().is_dir() => { + Some(NativeFile::Directory(path.to_owned())) + } + Ok(_) => Some(NativeFile::Regular(path.to_owned())), + Err(_) => None, + } +} + /// Hash the conetnts of a file as an `i64` using Rust's built in hasher. Collisions are not a big /// issue in our situation so we can get away with this. ///