// yabridge: a Wine VST bridge // Copyright (C) 2020-2021 Robbert van der Helm // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Handlers for the subcommands, just to keep `main.rs` clean. use anyhow::{Context, Result}; use colored::Colorize; use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; use crate::config::{yabridge_vst3_home, Config, InstallationMethod, YabridgeFiles}; use crate::files::{self, LibArchitecture, NativeFile}; use crate::utils; use crate::utils::{verify_path_setup, verify_wine_setup}; /// Add a direcotry to the plugin locations. Duplicates get ignord because we're using ordered sets. pub fn add_directory(config: &mut Config, path: PathBuf) -> Result<()> { config.plugin_dirs.insert(path); config.write() } /// Remove a direcotry to the plugin locations. The path is assumed to be part of /// `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` config.plugin_dirs.remove(path); config.write()?; // Ask the user to remove any leftover files to prevent possible future problems and out of date // copies let orphan_files = files::index(path).so_files; if !orphan_files.is_empty() { println!( "Warning: Found {} leftover .so files still in this directory:", orphan_files.len() ); for file in &orphan_files { println!("- {}", file.path().display()); } match promptly::prompt_opt::( "\nWould you like to remove these files? Entering anything other than YES will leave \ these files intact", ) { Ok(Some(answer)) if answer == "YES" => { for file in &orphan_files { utils::remove_file(file.path())?; } println!("\nRemoved {} files", orphan_files.len()); } _ => {} } } Ok(()) } /// List the plugin locations. pub fn list_directories(config: &Config) -> Result<()> { for directory in &config.plugin_dirs { println!("{}", directory.display()); } Ok(()) } /// Print the current configuration and the installation status for all found plugins. pub fn show_status(config: &Config) -> Result<()> { let results = config .search_directories() .context("Failure while searching for plugins")?; println!( "yabridge path: {}", config .yabridge_home .as_ref() .map(|path| format!("'{}'", path.display())) .unwrap_or_else(|| String::from("")) ); match config.files() { Ok(files) => { println!( "libyabridge-vst2.so: '{}'", files.libyabridge_vst2.display() ); println!( "libyabridge-vst3.so: {}\n", files .libyabridge_vst3 .map(|path| format!("'{}'", path.display())) .unwrap_or_else(|| "".red().to_string()) ); } Err(err) => { println!("Could not find yabridge's files files: {}\n", err); } } println!("installation method: {}", config.method); for (path, search_results) in results { println!("\n{}:", path.display()); for (plugin, status) in search_results.installation_status() { let status_str = match status { Some(NativeFile::Regular(_)) => "copy".green(), Some(NativeFile::Symlink(_)) => "symlink".green(), Some(NativeFile::Directory(_)) => "invalid".red(), None => "not installed".red(), }; println!(" {} :: {}", plugin.display(), status_str); } } Ok(()) } /// Options passed to `yabridgectl set`, see `main()` for the definitions of these options. pub struct SetOptions<'a> { pub method: Option<&'a str>, pub path: Option, pub path_auto: bool, } /// Change configuration settings. The actual options are defined in the clap [app](clap::App). pub fn set_settings(config: &mut Config, options: &SetOptions) -> Result<()> { match options.method { Some("copy") => config.method = InstallationMethod::Copy, Some("symlink") => config.method = InstallationMethod::Symlink, Some(s) => unimplemented!("Unexpected installation method '{}'", s), None => (), } if let Some(path) = &options.path { config.yabridge_home = Some(path.clone()); } if options.path_auto { config.yabridge_home = None; } config.write() } /// Options passed to `yabridgectl sync`, see `main()` for the definitions of these options. pub struct SyncOptions { pub force: bool, pub no_verify: bool, pub prune: bool, pub verbose: bool, } /// Set up yabridge for all Windows VST2 plugins in the plugin directories. Will also remove orphan /// `.so` files if the prune option is set. 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)?; 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(); // `.so` files and unused VST3 modules we found during scanning that didn't have a corresponding // copy or symlink of `libyabridge-vst2.so` let mut orphan_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: BTreeMap> = BTreeMap::new(); for (path, search_results) in results { num_installed += search_results.vst2_files.len(); num_installed += search_results.vst3_modules.len(); orphan_files.extend(search_results.vst2_orphans().into_iter().cloned()); skipped_dll_files.extend(search_results.skipped_files); if options.verbose { println!("{}:", path.display()); } // We'll set up the copies or symlinks for VST2 plugins for plugin in search_results.vst2_files { let target_path = plugin.with_extension("so"); // 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, config.method, &files.libyabridge_vst2, Some(libyabridge_vst2_hash), &target_path, )? { num_new += 1; } if options.verbose { println!(" {}", plugin.display()); } } // And then create merged bundles for the VST3 plugins: // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#mergedbundles if let Some(libyabridge_vst3_hash) = libyabridge_vst3_hash { for module in search_results.vst3_modules { // Check if we already set up the same architecture version of the plugin (since // 32-bit and 64-bit versions of the plugin cna live inside of the same bundle), and // show a warning if we come across any duplicates. let already_installed_architectures = yabridge_vst3_bundles .entry(module.target_bundle_home()) .or_insert_with(BTreeSet::new); if !already_installed_architectures.insert(module.architecture()) { eprintln!( "{}", utils::wrap(&format!( "{}: The {} version of '{}' has already been provided by another Wine \ prefix, skipping '{}'\n", "WARNING".red(), module.architecture(), module.target_bundle_home().display(), module.original_module_path().display(), )) ); continue; } // We're building a merged VST3 bundle containing both a copy or symlink to // `libyabridge-vst3.so` and the Windows VST3 plugin let native_module_path = module.target_native_module_path(); utils::create_dir_all(native_module_path.parent().unwrap())?; if install_file( options.force, config.method, files.libyabridge_vst3.as_ref().unwrap(), Some(libyabridge_vst3_hash), &native_module_path, )? { num_new += 1; } // 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.target_windows_module_path(); utils::create_dir_all(windows_module_path.parent().unwrap())?; install_file( true, InstallationMethod::Symlink, &module.original_module_path(), None, &windows_module_path, )?; // If `module` is a bundle, then it may contain a `Resources` directory with // screenshots and documentation // TODO: Also symlink presets, but this is a bit more involved. See // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#win7preset if let Some(original_resources_dir) = module.original_resources_dir() { install_file( false, InstallationMethod::Symlink, &original_resources_dir, None, &module.target_resources_dir(), )?; } if options.verbose { println!(" {}", module.original_path().display()); } } } if options.verbose { println!(); } } // We'll print the skipped files all at once to prevetn clutter let num_skipped_files = skipped_dll_files.len(); if options.verbose && !skipped_dll_files.is_empty() { println!("Skipped files:"); for path in skipped_dll_files { println!("- {}", path.display()); } println!(); } if let Ok(dirs) = fs::read_dir(yabridge_vst3_home()) { orphan_files.extend( dirs.filter_map(|entry| entry.ok()) .map(|entry| entry.path()) .filter_map(|path| { // Add all directories in `~/.vst3/yabridge` to `orphan_files` if they are not a // VST3 module we just created. We'll ignore symlinks and regular files since // those are always user created. match ( yabridge_vst3_bundles.contains_key(&path), utils::get_file_type(path), ) { (false, result @ Some(NativeFile::Directory(_))) => result, _ => None, } }), ); } // Always warn about leftover files since those might cause warnings or errors when a VST host // tries to load them if !orphan_files.is_empty() { let leftover_files_str = if orphan_files.len() == 1 { format!("{} leftover file", orphan_files.len()) } else { format!("{} leftover files", orphan_files.len()) }; if options.prune { println!("Removing {}:", leftover_files_str); } else { println!( "Found {}, rerun with the '--prune' option to remove them:", leftover_files_str ); } for file in orphan_files { println!("- {}", file.path().display()); if options.prune { match file { NativeFile::Regular(path) | NativeFile::Symlink(path) => { utils::remove_file(path)?; } NativeFile::Directory(path) => { utils::remove_dir_all(path)?; } } } } println!(); } println!( "Finished setting up {} plugins using {} ({} new), skipped {} non-plugin .dll files", num_installed, config.method.plural_name(), num_new, num_skipped_files ); if options.no_verify { 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)?; } // This check is only performed once per combination of Wine and yabridge versions verify_wine_setup(config)?; 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: Option, 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 that of the `from` file we're trying to copy there, then we don't have to // do anything if let Some(hash) = from_hash { if metadata.file_type().is_file() && utils::hash_file(to)? == hash { return Ok(false); } } } (false, InstallationMethod::Symlink) => { // If the target file is already a symlink to `from`, 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) }