Files
yabridge/tools/yabridgectl/src/actions.rs
T
Robbert van der Helm b04c0b2c65 Update links to the VST3 documentation
Those were broken after Steinberg restructured the docs with the VST
3.7.2 SDK release.
2021-04-18 14:24:03 +02:00

503 lines
19 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
//! Handlers for the subcommands, just to keep `main.rs` clean.
use anyhow::{Context, Result};
use colored::Colorize;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::config::{yabridge_vst3_home, Config, InstallationMethod, YabridgeFiles};
use crate::files::{self, LibArchitecture, NativeFile, Plugin, Vst2Plugin};
use crate::utils;
use crate::utils::{verify_path_setup, verify_wine_setup};
pub mod blacklist;
/// 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, &HashSet::new()).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::<String, &str>(
"\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("<auto>"))
);
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(|| "<not found>".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 {
// Always print these paths with trailing slashes for consistency's sake because paths can
// be added both with and without a trailing slash
println!("\n{}", path.join("").display());
for (plugin_path, (plugin, status)) in search_results.installation_status() {
let plugin_type = match plugin {
Plugin::Vst2(Vst2Plugin { architecture, .. }) => {
format!("{}, {}", "VST2".cyan(), architecture)
}
Plugin::Vst3(module) => format!(
"{}, {}, {}",
"VST3".magenta(),
module.type_str(),
module.architecture
),
};
let status_str = match status {
Some(NativeFile::Regular(_)) => "copy".green(),
Some(NativeFile::Symlink(_)) => "symlink".green(),
Some(NativeFile::Directory(_)) => "invalid".red(),
None => "not yet installed".into(),
};
println!(
" {} :: {}, {}",
plugin_path
.strip_prefix(path)
.unwrap_or(&plugin_path)
.display(),
plugin_type,
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<PathBuf>,
pub path_auto: bool,
pub no_verify: Option<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;
}
if let Some(no_verify) = options.no_verify {
config.no_verify = no_verify;
}
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<PathBuf> = 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<NativeFile> = 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<PathBuf, BTreeSet<LibArchitecture>> = BTreeMap::new();
for (path, search_results) in results {
num_installed += search_results.plugins.len();
orphan_files.extend(search_results.vst2_orphans().into_iter().cloned());
skipped_dll_files.extend(search_results.skipped_files);
if options.verbose {
// Always print these paths with trailing slashes for consistency's sake because paths
// can be added both with and without a trailing slash
println!("{}", path.join("").display());
}
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");
// 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;
}
plugin_path.clone()
}
// And then create merged bundles for the VST3 plugins:
// https://developer.steinberg.help/display/VST/Plug-in+Format+Structure#PluginFormatStructure-MergedBundle
Plugin::Vst3(module) => {
// Only set up VST3 plugins when yabridge has been compiled with VST3 support
if libyabridge_vst3_hash.is_none() {
continue;
}
// 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(),
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://developer.steinberg.help/display/VST/Plug-in+Format+Structure#PluginFormatStructure-MergedBundle
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://developer.steinberg.help/display/VST/Preset+Locations
if let Some(original_resources_dir) = module.original_resources_dir() {
install_file(
false,
InstallationMethod::Symlink,
&original_resources_dir,
None,
&module.target_resources_dir(),
)?;
}
module.original_path().to_path_buf()
}
};
if options.verbose {
println!(
" {}",
plugin_path
.strip_prefix(path)
.unwrap_or(&plugin_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!();
}
// TODO: Move this elsewhere
// TODO: This can leave behind empty directories if we remove a subdirectory
orphan_files.extend(
WalkDir::new(yabridge_vst3_home())
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_entry(|entry| entry.file_type().is_dir())
.filter_map(|e| e.ok())
.filter(|entry| {
// 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.
let extension = entry
.path()
.extension()
.and_then(|extension| extension.to_str());
extension == Some("vst3") && !yabridge_vst3_bundles.contains_key(entry.path())
})
.map(|entry| NativeFile::Directory(entry.path().to_owned())),
);
// 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
);
// Skipping the post-installation seting checks can be done only for this invocation of
// `yabridgectl sync`, or it can be skipped permanently through a config file option
if options.no_verify || config.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<i64>,
to: &Path,
) -> Result<bool> {
// 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)
}