diff --git a/tools/yabridgectl/src/actions.rs b/tools/yabridgectl/src/actions.rs new file mode 100644 index 00000000..4aeeb965 --- /dev/null +++ b/tools/yabridgectl/src/actions.rs @@ -0,0 +1,280 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 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 clap::ArgMatches; +use colored::Colorize; +use std::fs; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::process::exit; + +use crate::config::{Config, InstallationMethod}; +use crate::files; +use crate::files::FoundFile; + +/// 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) { + config.plugin_dirs.insert(path); + config.write().unwrap_or_else(|err| { + eprintln!("Error while writing config file: {}", err); + exit(1); + }); +} + +/// Remove a direcotry to the plugin locations. The path is assumed to be part of +/// `config.plugin_dirs`, otherwise this si silently ignored. +pub fn remove_directory(config: &mut Config, path: &Path) { + // 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().unwrap_or_else(|err| { + eprintln!("Error while writing config file: {}", err); + exit(1); + }); + + // Ask the user to remove any leftover files to prevent possible future problems and out of date + // copies + let orphan_files = files::index_so_files(path); + 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 { + fs::remove_file(file.path()).unwrap_or_else(|err| { + eprintln!("Could not remove '{}': {}", file.path().display(), err); + exit(1); + }); + } + + println!("\nRemoved {} files.", orphan_files.len()); + } + _ => {} + } + } +} + +/// List the plugin locations. +pub fn list_directories(config: &Config) { + for directory in &config.plugin_dirs { + println!("{}", directory.display()); + } +} + +/// Print the current configuration and the installation status for all found plugins. +pub fn show_status(config: &Config) { + let results = match config.index_directories() { + Ok(results) => results, + Err(err) => { + eprintln!("Error while searching for plugins: {}", err); + exit(1); + } + }; + + println!( + "yabridge path: {}", + config + .yabridge_home + .as_ref() + .map(|path| format!("'{}'", path.display())) + .unwrap_or_else(|| String::from("")) + ); + println!( + "libyabridge.so: {}", + config + .libyabridge() + .map(|path| format!("'{}'", path.display())) + .unwrap_or_else(|_| format!("{}", "".red())) + ); + 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(FoundFile::Regular(_)) => "copy".green(), + Some(FoundFile::Symlink(_)) => "symlink".green(), + None => "not installed".red(), + }; + + println!(" {} :: {}", plugin.display(), status_str); + } + } +} + +/// Change configuration settings. The actual options are defined in the clap [app](clap::App). +pub fn set_settings(config: &mut Config, options: &ArgMatches) { + match options.value_of("method") { + Some("copy") => config.method = InstallationMethod::Copy, + Some("symlink") => config.method = InstallationMethod::Symlink, + Some(s) => unimplemented!("Unexpected installation method '{}'", s), + None => (), + } + + match options.value_of_t("path") { + Ok(path) => config.yabridge_home = Some(path), + Err(clap::Error { + kind: clap::ErrorKind::ArgumentNotFound, + .. + }) => (), + // I don't think we can get any parsing errors here since we already validated that the + // argument has to be a valid path, but you never know + Err(err) => err.exit(), + } + + config.write().unwrap_or_else(|err| { + eprintln!("Error while writing config file: {}", err); + exit(1); + }); +} + +/// 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: &Config, prune: bool, verbose: bool) { + let libyabridge_path = match config.libyabridge() { + Ok(path) => { + println!("Using '{}'\n", path.display()); + path + } + Err(err) => { + // The error messages here are already formatted + eprintln!("{}", err); + exit(1); + } + }; + + let results = match config.index_directories() { + Ok(results) => results, + Err(err) => { + eprintln!("Error while searching for plugins: {}", err); + exit(1); + } + }; + + // Keep track of some global statistics + let mut num_installed = 0; + let mut skipped_dll_files: Vec = Vec::new(); + let mut orphan_so_files: 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()); + skipped_dll_files.extend(search_results.skipped_files); + + if verbose { + println!("{}:", path.display()); + } + for plugin in search_results.vst2_files { + // If the target file already exists, we'll remove it first to prevent issues with + // mixing symlinks and regular files + let target_path = plugin.with_extension("so"); + if target_path.exists() { + fs::remove_file(&target_path).unwrap_or_else(|err| { + eprintln!("Could not remove '{}': {}", target_path.display(), err); + exit(1); + }); + } + + match config.method { + InstallationMethod::Copy => { + fs::copy(&libyabridge_path, &target_path).unwrap_or_else(|err| { + eprintln!( + "Error copying '{}' to '{}': {}", + libyabridge_path.display(), + target_path.display(), + err + ); + exit(1); + }); + } + InstallationMethod::Symlink => { + symlink(&libyabridge_path, &target_path).unwrap_or_else(|err| { + eprintln!( + "Error symlinking '{}' to '{}': {}", + libyabridge_path.display(), + target_path.display(), + err + ); + exit(1); + }); + } + } + + if verbose { + println!(" {}", plugin.display()); + } + } + if verbose { + println!(); + } + } + + // We'll print the skipped files all at once to prevetn clutter + let num_skipped_files = skipped_dll_files.len(); + if verbose && !skipped_dll_files.is_empty() { + println!("Skipped files:"); + for path in skipped_dll_files { + println!("- {}", path.display()); + } + println!(); + } + + // Always warn about leftover files sicne those might cause warnings or errors when a VST host + // tries to load them + if !orphan_so_files.is_empty() { + if prune { + println!("Removing {} leftover '.so' files:", orphan_so_files.len()); + } else { + println!( + "Found {} leftover '.so' files, rerun with the '--prune' option to remove them:", + orphan_so_files.len() + ); + } + + for file in orphan_so_files { + let path = file.path(); + + println!("- {}", path.display()); + if prune { + fs::remove_file(path).unwrap_or_else(|err| { + eprintln!("Error while trying to remove '{}': {}", path.display(), err); + exit(1); + }); + } + } + + println!(); + } + + println!( + "Finished setting up {} plugins using {}, skipped {} non-plugin '.dll' files.", + num_installed, + config.method.plural(), + num_skipped_files + ) +} diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index d09b0318..1b378ebb 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Utilities for managing yabrigectl's configuration. + use rayon::prelude::*; use serde_derive::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs index d4a50a0c..4a23b60b 100644 --- a/tools/yabridgectl/src/main.rs +++ b/tools/yabridgectl/src/main.rs @@ -14,16 +14,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use clap::{app_from_crate, App, AppSettings, Arg, ArgMatches}; +use clap::{app_from_crate, App, AppSettings, Arg}; use colored::Colorize; -use std::fs; -use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; -use std::process::exit; -use crate::config::{Config, InstallationMethod}; -use crate::files::FoundFile; +use crate::config::Config; +mod actions; mod config; mod files; @@ -122,14 +119,16 @@ fn main() { .get_matches(); match matches.subcommand() { - ("add", Some(options)) => add_directory(&mut config, options.value_of_t_or_exit("path")), - ("rm", Some(options)) => { - remove_directory(&mut config, &options.value_of_t_or_exit::("path")) + ("add", Some(options)) => { + actions::add_directory(&mut config, options.value_of_t_or_exit("path")) } - ("list", _) => list_directories(&config), - ("status", _) => show_status(&config), - ("set", Some(options)) => set_settings(&mut config, options), - ("sync", Some(options)) => do_sync( + ("rm", Some(options)) => { + actions::remove_directory(&mut config, &options.value_of_t_or_exit::("path")) + } + ("list", _) => actions::list_directories(&config), + ("status", _) => actions::show_status(&config), + ("set", Some(options)) => actions::set_settings(&mut config, options), + ("sync", Some(options)) => actions::do_sync( &config, options.is_present("prune"), options.is_present("verbose"), @@ -138,258 +137,6 @@ fn main() { } } -/// Add a direcotry to the plugin locations. Duplicates get ignord because we're using ordered sets. -fn add_directory(config: &mut Config, path: PathBuf) { - config.plugin_dirs.insert(path); - config.write().unwrap_or_else(|err| { - eprintln!("Error while writing config file: {}", err); - exit(1); - }); -} - -/// Remove a direcotry to the plugin locations. The path is assumed to be part of -/// `config.plugin_dirs`, otherwise this si silently ignored. -fn remove_directory(config: &mut Config, path: &Path) { - // 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().unwrap_or_else(|err| { - eprintln!("Error while writing config file: {}", err); - exit(1); - }); - - // Ask the user to remove any leftover files to prevent possible future problems and out of date - // copies - let orphan_files = files::index_so_files(path); - 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 { - fs::remove_file(file.path()).unwrap_or_else(|err| { - eprintln!("Could not remove '{}': {}", file.path().display(), err); - exit(1); - }); - } - - println!("\nRemoved {} files.", orphan_files.len()); - } - _ => {} - } - } -} - -/// List the plugin locations. -fn list_directories(config: &Config) { - for directory in &config.plugin_dirs { - println!("{}", directory.display()); - } -} - -/// Print the current configuration and the installation status for all found plugins. -fn show_status(config: &Config) { - let results = match config.index_directories() { - Ok(results) => results, - Err(err) => { - eprintln!("Error while searching for plugins: {}", err); - exit(1); - } - }; - - println!( - "yabridge path: {}", - config - .yabridge_home - .as_ref() - .map(|path| format!("'{}'", path.display())) - .unwrap_or_else(|| String::from("")) - ); - println!( - "libyabridge.so: {}", - config - .libyabridge() - .map(|path| format!("'{}'", path.display())) - .unwrap_or_else(|_| format!("{}", "".red())) - ); - 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(FoundFile::Regular(_)) => "copy".green(), - Some(FoundFile::Symlink(_)) => "symlink".green(), - None => "not installed".red(), - }; - - println!(" {} :: {}", plugin.display(), status_str); - } - } -} - -/// Change configuration settings. The actual options are defined in the clap [app](clap::App). -fn set_settings(config: &mut Config, options: &ArgMatches) { - match options.value_of("method") { - Some("copy") => config.method = InstallationMethod::Copy, - Some("symlink") => config.method = InstallationMethod::Symlink, - Some(s) => unimplemented!("Unexpected installation method '{}'", s), - None => (), - } - - match options.value_of_t("path") { - Ok(path) => config.yabridge_home = Some(path), - Err(clap::Error { - kind: clap::ErrorKind::ArgumentNotFound, - .. - }) => (), - // I don't think we can get any parsing errors here since we already validated that the - // argument has to be a valid path, but you never know - Err(err) => err.exit(), - } - - config.write().unwrap_or_else(|err| { - eprintln!("Error while writing config file: {}", err); - exit(1); - }); -} - -/// Set up yabridge for all Windows VST2 plugins in the plugin directories. Will also remove orphan -/// `.so` files if the prune option is set. -fn do_sync(config: &Config, prune: bool, verbose: bool) { - let libyabridge_path = match config.libyabridge() { - Ok(path) => { - println!("Using '{}'\n", path.display()); - path - } - Err(err) => { - // The error messages here are already formatted - eprintln!("{}", err); - exit(1); - } - }; - - let results = match config.index_directories() { - Ok(results) => results, - Err(err) => { - eprintln!("Error while searching for plugins: {}", err); - exit(1); - } - }; - - // Keep track of some global statistics - let mut num_installed = 0; - let mut skipped_dll_files: Vec = Vec::new(); - let mut orphan_so_files: 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()); - skipped_dll_files.extend(search_results.skipped_files); - - if verbose { - println!("{}:", path.display()); - } - for plugin in search_results.vst2_files { - // If the target file already exists, we'll remove it first to prevent issues with - // mixing symlinks and regular files - let target_path = plugin.with_extension("so"); - if target_path.exists() { - fs::remove_file(&target_path).unwrap_or_else(|err| { - eprintln!("Could not remove '{}': {}", target_path.display(), err); - exit(1); - }); - } - - match config.method { - InstallationMethod::Copy => { - fs::copy(&libyabridge_path, &target_path).unwrap_or_else(|err| { - eprintln!( - "Error copying '{}' to '{}': {}", - libyabridge_path.display(), - target_path.display(), - err - ); - exit(1); - }); - } - InstallationMethod::Symlink => { - symlink(&libyabridge_path, &target_path).unwrap_or_else(|err| { - eprintln!( - "Error symlinking '{}' to '{}': {}", - libyabridge_path.display(), - target_path.display(), - err - ); - exit(1); - }); - } - } - - if verbose { - println!(" {}", plugin.display()); - } - } - if verbose { - println!(); - } - } - - // We'll print the skipped files all at once to prevetn clutter - let num_skipped_files = skipped_dll_files.len(); - if verbose && !skipped_dll_files.is_empty() { - println!("Skipped files:"); - for path in skipped_dll_files { - println!("- {}", path.display()); - } - println!(); - } - - // Always warn about leftover files sicne those might cause warnings or errors when a VST host - // tries to load them - if !orphan_so_files.is_empty() { - if prune { - println!("Removing {} leftover '.so' files:", orphan_so_files.len()); - } else { - println!( - "Found {} leftover '.so' files, rerun with the '--prune' option to remove them:", - orphan_so_files.len() - ); - } - - for file in orphan_so_files { - let path = file.path(); - - println!("- {}", path.display()); - if prune { - fs::remove_file(path).unwrap_or_else(|err| { - eprintln!("Error while trying to remove '{}': {}", path.display(), err); - exit(1); - }); - } - } - - println!(); - } - - println!( - "Finished setting up {} plugins using {}, skipped {} non-plugin '.dll' files.", - num_installed, - config.method.plural(), - num_skipped_files - ) -} - /// Verify that a path exists, used for validating arguments. fn validate_path(path: &str) -> Result<(), String> { let path = Path::new(path);