[yabridgectl] Allow setting up VST3 plugins

This is still missing checks for removing leftover files, symlinks for
resources and presets, and a way to differentiate between plugins with
the same name from different prefixes.
This commit is contained in:
Robbert van der Helm
2020-12-24 00:04:05 +01:00
parent bc9801c932
commit 55957ca798
4 changed files with 242 additions and 103 deletions
+141 -55
View File
@@ -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<PathBuf> = Vec::new();
let mut orphan_so_files: Vec<NativeSoFile> = 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<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: Vec<PathBuf> = 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<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 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)
}
+1 -1
View File
@@ -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
+74 -47
View File
@@ -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<NativeSoFile>,
pub so_files: Vec<NativeFile>,
}
/// 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<PathBuf>,
/// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or
/// a regular file.
pub so_files: Vec<NativeSoFile>,
pub so_files: Vec<NativeFile>,
}
/// 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<NativeSoFile>> {
let so_files: HashMap<&Path, &NativeSoFile> = self
pub fn installation_status(&self) -> BTreeMap<PathBuf, Option<NativeFile>> {
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<NativeSoFile>> = self
let mut installation_status: BTreeMap<PathBuf, Option<NativeFile>> = 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<PathBuf> = Vec::new();
let mut vst3_files: Vec<PathBuf> = Vec::new();
let mut so_files: Vec<NativeSoFile> = Vec::new();
let mut so_files: Vec<NativeFile> = 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()));
}
}
_ => (),
+26
View File
@@ -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<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
})
}
/// Wrapper around [`std::fs::create_dir_all()`](std::fs::create_dir_all) with a human readable
/// error message.
pub fn create_dir_all<P: AsRef<Path>>(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<P: AsRef<Path>>(path: P) -> Result<()> {
@@ -66,6 +78,20 @@ pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
})
}
/// Get the type of a file, if it exists.
pub fn get_file_type(path: &Path) -> Option<NativeFile> {
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.
///