Files
yabridge/tools/yabridgectl/src/actions.rs
T
Robbert van der Helm bbec4f3163 Update yabridgectl for clap 4.x
This work has been sitting unfinished for ages. The original plan was to
also move it to CLAP's derive macros at the same time, but that makes
some of the path management more difficult.
2023-12-10 17:10:08 +01:00

918 lines
40 KiB
Rust

// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2023 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 clap::ValueEnum;
use colored::Colorize;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::config::{
yabridge_clap_home, yabridge_vst2_home, yabridge_vst3_home, Config, Vst2InstallationLocation,
YabridgeFiles, CLAP_CHAINLOADER_NAME, VST2_CHAINLOADER_NAME, VST3_CHAINLOADER_NAME,
YABRIDGE_HOST_32_EXE_NAME, YABRIDGE_HOST_EXE_NAME,
};
use crate::files::{self, ClapPlugin, NativeFile, Plugin, Vst2Plugin};
use crate::util::{self, get_file_type};
use crate::util::{verify_external_dependencies, verify_path_setup, verify_wine_setup};
use crate::vst3_moduleinfo::ModuleInfo;
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 {
util::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.vst2_location {
Vst2InstallationLocation::Centralized => {
println!("VST2 location: '{}'", yabridge_vst2_home().display());
}
Vst2InstallationLocation::Inline => {
println!("VST2 location: inline next to the Windows plugin file");
}
}
// These are, but just from a UX point of view it might be nice to have as a reminder
println!("VST3 location: '{}'", yabridge_vst3_home().display());
println!("CLAP location: '{}'\n", yabridge_clap_home().display());
let files = config.files();
match &files {
Ok(files) => {
println!(
"{VST2_CHAINLOADER_NAME}: '{}' ({})",
files.vst2_chainloader.display(),
files.vst2_chainloader_arch,
);
println!(
"{VST3_CHAINLOADER_NAME}: {}",
files
.vst3_chainloader
.as_ref()
.map(|(path, arch)| format!("'{}' ({})", path.display(), arch))
.unwrap_or_else(|| "<not found>".red().to_string())
);
println!(
"{CLAP_CHAINLOADER_NAME}: {}\n",
files
.clap_chainloader
.as_ref()
.map(|(path, arch)| format!("'{}' ({})", path.display(), arch))
.unwrap_or_else(|| "<not found>".red().to_string())
);
println!(
"{YABRIDGE_HOST_EXE_NAME}: {}",
files
.yabridge_host_exe
.as_ref()
// We don't care about the actual path, but the file should at least exist
.zip(files.yabridge_host_exe_so.as_ref())
.map(|(path, _)| format!("'{}'", path.display()))
.unwrap_or_else(|| "<not found>".red().to_string())
);
println!(
"{YABRIDGE_HOST_32_EXE_NAME}: {}",
files
.yabridge_host_32_exe
.as_ref()
.zip(files.yabridge_host_32_exe_so.as_ref())
.map(|(path, _)| format!("'{}'", path.display()))
.unwrap_or_else(|| "<not found>".red().to_string())
);
}
Err(err) => {
println!("Could not find yabridge's files: {}", err);
}
}
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(config, files.as_ref().ok())
{
let plugin_type = match plugin {
Plugin::Vst2(Vst2Plugin { architecture, .. }) => {
format!("{}, {}", "VST2".cyan(), architecture)
}
Plugin::Vst3(module) => format!(
"{}, {}, {}",
"VST3".magenta(),
module.type_str(),
module.architecture
),
// CLAP is supposed to be 64-bit-only, but we'll still display the architecture here for
// consistency's sake
Plugin::Clap(ClapPlugin { architecture, .. }) => {
format!("{}, {}", "CLAP".yellow(), architecture)
}
};
// This made more sense when we supported symlinking `libyabridge-*.so`, but we should
// display _something_ to indicate that the plugin is set up correctly
let status_str = match status {
Some(NativeFile::Regular(_)) => "synced".green(),
// This should not occur, but we'll display it just in case it does happen
Some(NativeFile::Symlink(_)) => "symlink".yellow(),
Some(NativeFile::Directory(_)) => "invalid".red(),
None => "not yet synced".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 {
pub path: Option<PathBuf>,
pub path_auto: bool,
pub vst2_location: Option<Vst2Location>,
pub no_verify: Option<bool>,
}
/// The location bridged VST2 plugins are set up in.
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Vst2Location {
/// Sets bridged VST2 plugins up in '~/.vst/yabridge', the default option.
Centralized,
/// Places the yabridge '.so' file alongside the plugin's '.dll' file in the plugin's directory,
/// exists only for legacy purposes.
Inline,
}
/// Change configuration settings. The actual options are defined in the clap [app](clap::App).
pub fn set_settings(config: &mut Config, options: &SetOptions) -> Result<()> {
if let Some(path) = &options.path {
config.yabridge_home = Some(path.clone());
}
if options.path_auto {
config.yabridge_home = None;
}
match options.vst2_location {
Some(Vst2Location::Centralized) => {
config.vst2_location = Vst2InstallationLocation::Centralized
}
Some(Vst2Location::Inline) => config.vst2_location = Vst2InstallationLocation::Inline,
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 vst2_chainloader_hash = util::hash_file(&files.vst2_chainloader)?;
let vst3_chainloader_hash = files
.vst3_chainloader
.as_ref()
.map(|(path, _)| util::hash_file(path))
.transpose()?;
let clap_chainloader_hash = files
.clap_chainloader
.as_ref()
.map(|(path, _)| util::hash_file(path))
.transpose()?;
// Better not add another plugin format!
match (&files.vst3_chainloader, &files.clap_chainloader) {
(Some((vst3_chainloader_path, _)), Some((clap_chainloader_path, _))) => {
println!("Setting up VST2, VST3, and CLAP plugins using:");
println!("- {}", files.vst2_chainloader.display());
println!("- {}", vst3_chainloader_path.display());
println!("- {}\n", clap_chainloader_path.display());
}
(Some((vst3_chainloader_path, _)), None) => {
println!("Setting up VST2 and VST3 plugins using:");
println!("- {}", files.vst2_chainloader.display());
println!("- {}\n", vst3_chainloader_path.display());
}
(None, Some((clap_chainloader_path, _))) => {
println!("Setting up VST2 and CLAP plugins using:");
println!("- {}", files.vst2_chainloader.display());
println!("- {}\n", clap_chainloader_path.display());
}
(None, None) => {
println!("Setting up VST2 plugins using:");
println!("- {}\n", files.vst2_chainloader.display());
}
}
let results = config
.search_directories()
.context("Failure while searching for plugins")?;
// Before doing anything, make sure `~/.{clap,vst,vst3}/yabridge` are not symlinks to one of the
// plugin directories. See
// https://github.com/robbert-vdh/yabridge/issues/185#issuecomment-1166274104.
let bail_if_unsafe_symlink = |plugin_home: &Path, plugin_home_display| {
if let Ok(canonical_plugin_home) = fs::canonicalize(&plugin_home) {
if canonical_plugin_home != plugin_home {
for plugin_dir in &config.plugin_dirs {
if let Ok(canonical_plugin_dir) = fs::canonicalize(&plugin_dir) {
if canonical_plugin_dir.starts_with(&canonical_plugin_home) {
anyhow::bail!(
"'{plugin_home_display}' is a symlink to '{}'. This conflicts \
with '{}' from your plugin directories, so the syncing process \
will now be aborted.",
canonical_plugin_home.display(),
plugin_dir.display(),
);
}
}
}
}
}
Ok(())
};
let vst2_home = yabridge_vst2_home();
let vst3_home = yabridge_vst3_home();
let clap_home = yabridge_clap_home();
bail_if_unsafe_symlink(&vst2_home, "~/.vst/yabridge")?;
bail_if_unsafe_symlink(&vst3_home, "~/.vst3/yabridge")?;
bail_if_unsafe_symlink(&clap_home, "~/.clap/yabridge")?;
// Keep track of some global statistics
// The plugin files we installed. This tracks copies of/symlinks to `libabyrdge-*.so` managed.
// by yabridgectl. This could be optimized a bit so we wouldn't have to track everything, but
// this makes everything much easier since we'll have to deal with things like a plugin
// directory A containing a symlink to plugin directory B, as well as VST3 plugisn that come in
// both x86 and x86_64 flavours.
// Paths added to this and to the `new_plugins` set below should be normalized with
// `utils::normalize_path()` so that the reported numbers are still correct when encountering
// overlapping symlinked paths.
let mut managed_plugins: HashSet<PathBuf> = HashSet::new();
// The plugins we created a new copy of `libyabridge-chainloader-{clap,vst2,vst3}.so` for. We don't
// touch these files if they're already up to date to prevent hosts from unnecessarily
// rescanning the plugins.
let mut new_plugins: HashSet<PathBuf> = HashSet::new();
// 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 files/bundles we found during scanning that don't belong to a known
// plugin. `~/{.clap,.vst,.vst3}/yabridge` will be searched for these files after setting up all
// plugins.
let mut orphan_files: Vec<NativeFile> = 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<PathBuf> = 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_centralized_vst3_files: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
// Similar for CLAP, but since CLAP doesn't use bundles this works the same way as with VST2.
let mut known_centralized_clap_files: HashSet<PathBuf> = HashSet::new();
for (path, search_results) in results {
// 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 {
// 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 {
// 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 =
util::normalize_path(&target_native_plugin_path);
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!(
"{}",
util::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
util::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 = util::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);
}
}
vst2_plugin.path
}
// 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 vst3_chainloader_hash.is_none() {
continue;
}
let target_bundle_home = module.target_bundle_home();
let target_native_module_path = module.target_native_module_path(Some(&files));
let target_windows_module_path = module.target_windows_module_path();
let normalized_native_module_path =
util::normalize_path(&target_native_module_path);
// 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_centralized_vst3_files
.entry(target_bundle_home.clone())
.or_insert_with(HashSet::new);
if managed_vst3_bundle_files.contains(&target_windows_module_path) {
eprintln!(
"{}",
util::wrap(&format!(
"{}: The {} version of '{}' has already been provided by another \
Wine prefix or plugin directory, 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-chainloader-vst3.so` and the Windows VST3 plugin. The path to
// this native module will depend on whether `libyabridge-chainloader-vst3.so`
// is a 32-bit or a 64-bit library file.
util::create_dir_all(target_native_module_path.parent().unwrap())?;
if install_file(
options.force,
InstallationMethod::Copy,
&files.vst3_chainloader.as_ref().unwrap().0,
vst3_chainloader_hash,
&target_native_module_path,
)? {
// We're counting the native `.so` files and not the Windows VST3 plugins
// because even though the 32-bit and 64-bit versions of a plugin are
// technically separate plugins, we can only use one at a time anyways
// because of how these bundles work
new_plugins.insert(normalized_native_module_path.clone());
}
managed_plugins.insert(normalized_native_module_path.clone());
managed_vst3_bundle_files.insert(target_native_module_path);
// 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
util::create_dir_all(target_windows_module_path.parent().unwrap())?;
install_file(
true,
InstallationMethod::Symlink,
&module.original_module_path(),
None,
&target_windows_module_path,
)?;
managed_vst3_bundle_files.insert(target_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() {
let target_resources_dir = module.target_resources_dir();
install_file(
false,
InstallationMethod::Symlink,
&original_resources_dir,
None,
&target_resources_dir,
)?;
managed_vst3_bundle_files.insert(target_resources_dir);
}
// If the plugin has a VST 3.7.10 moduleinfo file, then we'll rewrite the byte
// orders of the class IDs stored within the file and then write it to the
// bridged VST3 bundle.
// https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/VST+Module+Architecture/ModuleInfo-JSON.html
if let Some(original_moduleinfo_path) = module.original_moduleinfo_path() {
let target_moduleinfo_path = module.target_moduleinfo_path();
let result = util::read_to_string(&original_moduleinfo_path)
.and_then(|module_info_json| {
serde_jsonrc::from_str(&module_info_json)
.context("Could not parse JSON file")
})
.and_then(|mut module_info: ModuleInfo| {
module_info.rewrite_uid_byte_orders()?;
Ok(module_info)
})
.and_then(|converted_module_info| {
let converted_json =
serde_jsonrc::to_string_pretty(&converted_module_info)
.context("Could not format JSON file")?;
util::write(&target_moduleinfo_path, converted_json)
});
match result {
Ok(_) => {
managed_vst3_bundle_files.insert(target_moduleinfo_path);
}
Err(error) => {
eprintln!(
"Error converting '{}', skipping...\n{}",
original_moduleinfo_path.display(),
error
);
}
}
}
module.original_path().to_path_buf()
}
Plugin::Clap(clap_plugin) => {
// Only set up CLAP plugins when yabridge has been compiled with CLAP support
if clap_chainloader_hash.is_none() {
continue;
}
let target_native_plugin_path = clap_plugin.native_target();
let target_windows_plugin_path = clap_plugin.windows_target();
let normalized_target_native_plugin_path =
util::normalize_path(&target_native_plugin_path);
let mut is_new =
known_centralized_clap_files.insert(target_native_plugin_path.clone());
is_new |=
known_centralized_clap_files.insert(target_windows_plugin_path.clone());
if !is_new {
eprintln!(
"{}",
util::wrap(&format!(
"{}: '{}' has already been provided by another Wine prefix or \
plugin directory, skipping it\n",
"WARNING".red(),
target_windows_plugin_path.display(),
))
);
continue;
}
// Because CLAP uses the same file extension on all platforms, this needs to
// work slightly different compared to the VST2 bridging. Here we'll create a
// copy of the chainloader as a `foo.clap` file in `~/.clap/yabridge`, and we'll
// then symlink the Windows `.clap` file to `foo.clap-win` next to `foo.clap`.
// That allows yabridge to find the Windows CLAP plugin while also preventing
// DAWs from indexing it themselves.
util::create_dir_all(target_native_plugin_path.parent().unwrap())?;
if install_file(
options.force,
InstallationMethod::Copy,
&files.clap_chainloader.as_ref().unwrap().0,
clap_chainloader_hash,
&target_native_plugin_path,
)? {
new_plugins.insert(normalized_target_native_plugin_path.clone());
}
managed_plugins.insert(normalized_target_native_plugin_path);
// So this ends up symlinking the original Windows `.clap` file to a `.clap-win`
// file in `~/.clap/yabridge`
install_file(
true,
InstallationMethod::Symlink,
&clap_plugin.path,
None,
&target_windows_plugin_path,
)?;
clap_plugin.path
}
};
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!();
}
// 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`, `~/.vst3/yabridge`, and
// `~/.clap/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 and CLAP plugins we can
// simply treat any file in `~/{.clap,.vst}/yabridge` that we did not add to
// `known_centralized_{clap,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(vst2_home)
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_map(|e| {
let path = match e {
Ok(entry) => entry.path().to_owned(),
Err(err) => err.path()?.to_owned(),
};
if !path.is_dir() && matches!(path.extension()?.to_str()?, "dll" | "so") {
Some(path)
} else {
None
}
});
let centralized_clap_files = WalkDir::new(clap_home)
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_map(|e| {
let path = match e {
Ok(entry) => entry.path().to_owned(),
Err(err) => err.path()?.to_owned(),
};
if !path.is_dir() && matches!(path.extension()?.to_str()?, "clap" | "clap-win" | "so") {
Some(path)
} else {
None
}
});
let installed_vst3_bundles = WalkDir::new(vst3_home)
.follow_links(true)
.same_file_system(true)
.into_iter()
.filter_entry(|entry| entry.file_type().is_dir())
.filter_map(|e| {
let path = match e {
Ok(entry) => entry.path().to_owned(),
Err(err) => err.path()?.to_owned(),
};
if path.extension()?.to_str()? == "vst3" {
Some(path)
} else {
None
}
});
orphan_files.extend(centralized_vst2_files.filter_map(|path| {
if known_centralized_vst2_files.contains(&path) {
None
} else {
get_file_type(path)
}
}));
orphan_files.extend(centralized_clap_files.filter_map(|path| {
if known_centralized_clap_files.contains(&path) {
None
} else {
get_file_type(path)
}
}));
for bundle_path in installed_vst3_bundles {
match known_centralized_vst3_files.get(&bundle_path) {
None => orphan_files.push(NativeFile::Directory(bundle_path)),
Some(managed_vst3_bundle_files) => {
// Find orphan files and symlinks within this bundle. We need this to be able to
// switch between 32-bit and 64-bit versions of both yabridge and the Windows plugin
orphan_files.extend(
WalkDir::new(bundle_path)
.follow_links(false)
.into_iter()
.filter_map(|e| {
let path = match e {
Ok(entry) => entry.path().to_owned(),
Err(err) => err.path()?.to_owned(),
};
let managed_file = managed_vst3_bundle_files.contains(&path);
match get_file_type(path).unwrap() {
// Don't remove directories, since we're not tracking the
// directories within the bundle
NativeFile::Directory(_) => None,
unknown_file if !managed_file => Some(unknown_file),
_ => 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
);
}
// 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 {
NativeFile::Regular(path) | NativeFile::Symlink(path) => {
util::remove_file(path)?;
}
NativeFile::Directory(path) => {
util::remove_dir_all(path)?;
}
}
// If the directory `file` was in is now empty, then we'll also recursively prune
// the empty subdirectory
let mut parent_dir = file.path().parent();
while let Some(dir) =
parent_dir.and_then(|dir| fs::remove_dir(dir).ok().map(|_| dir))
{
parent_dir = dir.parent();
}
}
}
println!();
}
// Don't mind the ugly format string, the existence of the symlink-based installation method
// should be hidden as much as possible until it gets removed in yabridge 4.0
println!(
"Finished setting up {} plugins ({} new), skipped {} non-plugin .dll files",
managed_plugins.len(),
new_plugins.len(),
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-chainloader-{clap,vst2,vst3}.so` copies can
// find `yabridge-host.exe` and by extension the plugin libraries. That last part should already
// be the case if we get to this point though.
verify_path_setup()?;
// This check is only performed once per combination of Wine and yabridge versions
verify_wine_setup(config)?;
// Yabridge uses D-Bus notifications to relay important information when something's very wrong,
// so we'll check whether `libdbus-1.so` is available (even though it would be very odd if it
// isn't)
verify_external_dependencies()?;
Ok(())
}
// TODO: Clean this up, in the past this was part of a yabridgectl setting and the enum was simply
// reused here
enum InstallationMethod {
Copy,
Symlink,
}
/// 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() && util::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, _) => (),
}
util::remove_file(&to)?;
};
match method {
InstallationMethod::Copy => {
util::copy_or_reflink(from, to)?;
}
InstallationMethod::Symlink => {
util::symlink(from, to)?;
}
}
Ok(true)
}