mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-06-10 06:12:14 +02:00
a7381f008b
Rust 1.58 will be released tomorrow, so now that Rust 2021 has been out for two releases bumping this should be fine.
377 lines
15 KiB
Rust
377 lines
15 KiB
Rust
// yabridge: a Wine VST bridge
|
|
// Copyright (C) 2020-2022 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/>.
|
|
|
|
use anyhow::Result;
|
|
use clap::{app_from_crate, App, AppSettings, Arg};
|
|
use colored::Colorize;
|
|
use std::collections::HashSet;
|
|
use std::env;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use crate::config::Config;
|
|
|
|
mod actions;
|
|
mod config;
|
|
mod files;
|
|
mod utils;
|
|
|
|
fn main() -> Result<()> {
|
|
// We'll modify our `PATH` environment variable so it matches up with
|
|
// `get_augmented_search_path()` from `src/plugin/utils.h` for easier setup
|
|
let yabridge_home = config::yabridge_directories()?.get_data_home();
|
|
env::set_var(
|
|
"PATH",
|
|
match env::var("PATH") {
|
|
Ok(path) => format!("{}:{}", path, yabridge_home.display()),
|
|
_ => format!("{}", yabridge_home.display()),
|
|
},
|
|
);
|
|
|
|
let mut config = Config::read()?;
|
|
|
|
// Used for validation in `yabridgectl rm <path>`
|
|
let plugin_directories: HashSet<&Path> = config
|
|
.plugin_dirs
|
|
.iter()
|
|
.map(|path| path.as_path())
|
|
.collect();
|
|
// Used for validation in `yabridgectl blacklist rm <path>`
|
|
let blacklist_entries: HashSet<&Path> =
|
|
config.blacklist.iter().map(|path| path.as_path()).collect();
|
|
|
|
let matches = app_from_crate!()
|
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
|
.subcommand(
|
|
App::new("add")
|
|
.about("Add a plugin install location")
|
|
.display_order(1)
|
|
.arg(
|
|
Arg::new("path")
|
|
.help("Path to a directory containing Windows VST2 or VST3 plugins")
|
|
.validator(validate_directory)
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("rm")
|
|
.about("Remove a plugin install location")
|
|
.display_order(2)
|
|
.arg(
|
|
Arg::new("path")
|
|
.help("Path to a previously added directory")
|
|
.validator(|path| match_in_path_list(Path::new(path), &plugin_directories))
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("list")
|
|
.about("List the plugin install locations")
|
|
.display_order(3),
|
|
)
|
|
.subcommand(
|
|
App::new("status")
|
|
.about("Show the installation status for all plugins")
|
|
.display_order(4),
|
|
)
|
|
.subcommand(
|
|
App::new("sync")
|
|
.about("Set up or update yabridge for all plugins")
|
|
.display_order(100)
|
|
.arg(
|
|
Arg::new("force")
|
|
.short('f')
|
|
.long("force")
|
|
.help("Always update files, even not necessary"),
|
|
)
|
|
.arg(
|
|
Arg::new("no-verify")
|
|
.short('n')
|
|
.long("no-verify")
|
|
.help("Skip post-installation setup checks"),
|
|
)
|
|
.arg(
|
|
Arg::new("prune")
|
|
.short('p')
|
|
.long("prune")
|
|
.help("Remove unrelated or leftover .so files"),
|
|
)
|
|
.arg(
|
|
Arg::new("verbose")
|
|
.short('v')
|
|
.long("verbose")
|
|
.help("Print information about plugins being set up or skipped"),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("set")
|
|
.about("Change the yabridge path (advanced)")
|
|
.display_order(200)
|
|
.setting(AppSettings::ArgRequiredElseHelp)
|
|
.arg(
|
|
Arg::new("method")
|
|
.long("method")
|
|
.help("The installation method to use (deprecated)")
|
|
.long_help(format!(
|
|
"This feature has been deprecated in yabridgectl 3.8.0 and should \
|
|
not be used anymore. \
|
|
\n\n\
|
|
The installation method to use. \
|
|
'{}' works in every situation but it requires you to modify your PATH \
|
|
environment variable so yabridge is able to find 'yabridge-host.exe'. \
|
|
'yabridgectl sync' whenever you update yabridge. You'll also have to \
|
|
rerun 'yabridgectl sync' whenever you update yabridge. \
|
|
'{}' only works for hosts that support individually sandboxed plugins \
|
|
such as Bitwig Studio, but it does not require setting environment \
|
|
variables or to manual updates.",
|
|
"copy".bright_white(),
|
|
"symlink".bright_white()
|
|
).as_ref())
|
|
.setting(clap::ArgSettings::NextLineHelp)
|
|
.possible_values(["copy", "symlink"])
|
|
.takes_value(true),
|
|
)
|
|
.arg(
|
|
Arg::new("path")
|
|
.long("path")
|
|
.help("Path to the directory containing 'libyabridge-{vst2,vst3}.so'")
|
|
.long_help(
|
|
"Path to the directory containing 'libyabridge-{vst2,vst3}.so'. If this \
|
|
is not set, then yabridgectl will look in both '/usr/lib' and \
|
|
'~/.local/share/yabridge' by default.",
|
|
)
|
|
.validator(validate_path)
|
|
.takes_value(true).conflicts_with("path_auto"),
|
|
)
|
|
.arg(
|
|
Arg::new("path_auto")
|
|
.long("path-auto")
|
|
.help("Automatically locate yabridge's files")
|
|
.long_help(
|
|
"Automatically locate yabridge's files. This can be used after manually \
|
|
setting a path with the '--path' option to revert back to the default \
|
|
auto detection behaviour.",
|
|
),
|
|
).arg(
|
|
Arg::new("no_verify")
|
|
.long("no-verify")
|
|
.help("Always skip post-installation setup checks")
|
|
.long_help(
|
|
"Always skip post-installation setup checks. This can be set temporarily \
|
|
by passing the '--no-verify' option to 'yabridgectl sync'.",
|
|
)
|
|
.possible_values(["true", "false"])
|
|
.takes_value(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("blacklist")
|
|
.about("Manage the indexing blacklist (advanced)")
|
|
.display_order(201)
|
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
|
.long_about(
|
|
"Manage the indexing blacklist (advanced)\n\
|
|
\n\
|
|
This lets you skip over individual files and entire directories in the \
|
|
indexing process. You most likely won't have to use this feature.",
|
|
)
|
|
.subcommand(
|
|
App::new("add")
|
|
.about("Add a path to the blacklist")
|
|
.display_order(1)
|
|
.arg(
|
|
Arg::new("path")
|
|
.help("Path to a file or a directory")
|
|
.validator(validate_path)
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("rm")
|
|
.about("Remove a path from the blacklist")
|
|
.display_order(2)
|
|
.arg(
|
|
Arg::new("path")
|
|
.help("Path to a previously added file or directory")
|
|
.validator(|path| match_in_path_list(Path::new(path), &blacklist_entries))
|
|
.validator(validate_path)
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
App::new("list")
|
|
.about("List the blacklisted paths")
|
|
.display_order(3),
|
|
)
|
|
.subcommand(
|
|
App::new("clear")
|
|
.about("Clear the entire blacklist")
|
|
.display_order(4),
|
|
),
|
|
)
|
|
.get_matches();
|
|
|
|
// We're calling canonicalize when adding and setting paths since relative paths would cause
|
|
// some weird behaviour. There's no built-in way to make relative paths absoltue without
|
|
// resolving symlinks, but I don't think this will cause any issues.
|
|
//
|
|
// https://github.com/rust-lang/rust/issues/59117
|
|
match matches.subcommand() {
|
|
Some(("add", options)) => actions::add_directory(
|
|
&mut config,
|
|
options
|
|
.value_of_t_or_exit::<PathBuf>("path")
|
|
.canonicalize()?,
|
|
),
|
|
Some(("rm", options)) => {
|
|
// Clap sadly doesn't have custom parsers/transforms, so we need to rerun the validator
|
|
// to get the result
|
|
let path = match_in_path_list(
|
|
&options.value_of_t_or_exit::<PathBuf>("path"),
|
|
&plugin_directories,
|
|
)
|
|
.unwrap()
|
|
.to_owned();
|
|
|
|
actions::remove_directory(&mut config, &path)
|
|
}
|
|
Some(("list", _)) => actions::list_directories(&config),
|
|
Some(("status", _)) => actions::show_status(&config),
|
|
Some(("sync", options)) => actions::do_sync(
|
|
&mut config,
|
|
&actions::SyncOptions {
|
|
force: options.is_present("force"),
|
|
no_verify: options.is_present("no-verify"),
|
|
prune: options.is_present("prune"),
|
|
verbose: options.is_present("verbose"),
|
|
},
|
|
),
|
|
Some(("set", options)) => actions::set_settings(
|
|
&mut config,
|
|
&actions::SetOptions {
|
|
method: options.value_of("method"),
|
|
// We've already verified that the path is valid, so we should only be getting
|
|
// errors for missing arguments
|
|
path: options
|
|
.value_of_t::<PathBuf>("path")
|
|
.ok()
|
|
.and_then(|path| path.canonicalize().ok()),
|
|
path_auto: options.is_present("path_auto"),
|
|
no_verify: options.value_of("no_verify").map(|value| value == "true"),
|
|
},
|
|
),
|
|
Some(("blacklist", blacklist)) => match blacklist.subcommand() {
|
|
Some(("add", options)) => actions::blacklist::add_path(
|
|
&mut config,
|
|
options
|
|
.value_of_t_or_exit::<PathBuf>("path")
|
|
.canonicalize()?,
|
|
),
|
|
Some(("rm", options)) => {
|
|
let path = match_in_path_list(
|
|
&options.value_of_t_or_exit::<PathBuf>("path"),
|
|
&blacklist_entries,
|
|
)
|
|
.unwrap()
|
|
.to_owned();
|
|
|
|
actions::blacklist::remove_path(&mut config, &path)
|
|
}
|
|
Some(("list", _)) => actions::blacklist::list_paths(&config),
|
|
Some(("clear", _)) => actions::blacklist::clear(&mut config),
|
|
_ => unreachable!(),
|
|
},
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Verify that a path exists and that is is either a directory or a symlink to a directory.
|
|
fn validate_directory(path: &str) -> Result<(), String> {
|
|
validate_path(path)?;
|
|
|
|
let path = Path::new(path);
|
|
if path.is_dir() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("'{}' is not a directory", path.display()))
|
|
}
|
|
}
|
|
|
|
/// Verify that a path exists, used for validating arguments.
|
|
fn validate_path(path: &str) -> Result<(), String> {
|
|
let path = Path::new(path);
|
|
|
|
if path.exists() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"File or directory '{}' could not be found",
|
|
path.display()
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Find `path` in `candidates` and return it as an absolute path. If the path is relative, we will
|
|
/// try to resolve as much of it as possible (in case the referred to file doesn't exist anymore).
|
|
/// We don't iteratively try to resolve symlinks until a candidate matches a path in `candidates`,
|
|
/// but this can match a relative path to a symlink that's in the paths list.
|
|
fn match_in_path_list<'a>(path: &Path, candidates: &'a HashSet<&Path>) -> Result<&'a Path, String> {
|
|
let absolute_path = if path.is_absolute() {
|
|
path.to_owned()
|
|
} else {
|
|
// This absolute absolute_path is also needed for the `utils::normalize_path()` below
|
|
std::env::current_dir()
|
|
.expect("Couldn't get current directory")
|
|
.join(path)
|
|
};
|
|
if let Some(path) = candidates.get(absolute_path.as_path()) {
|
|
return Ok(path);
|
|
}
|
|
|
|
// This will include a trailing slash if `path` was `.`. All paths entered through yabridgectl
|
|
// will be cannonicalized and won't contain a trailing slash, but we'll try both variants
|
|
// anyways just in case someone edited the config file.
|
|
let normalized_path = utils::normalize_path(absolute_path.as_path());
|
|
|
|
// Is there a nicer way to strip trailing slashes with the standard library?
|
|
let normalized_path_str = normalized_path
|
|
.to_str()
|
|
.expect("Input path contains invalid characters");
|
|
let normalized_path_without_slash = if normalized_path_str.ends_with('/') {
|
|
Path::new(normalized_path_str.trim_end_matches('/'))
|
|
} else {
|
|
normalized_path.as_path()
|
|
};
|
|
// This ia bit of a hack, but it works
|
|
let normalized_path_with_slash = normalized_path.join("");
|
|
|
|
if let Some(path) = candidates
|
|
.get(normalized_path_without_slash)
|
|
.or_else(|| candidates.get(normalized_path_with_slash.as_path()))
|
|
{
|
|
return Ok(path);
|
|
}
|
|
|
|
Err(format!(
|
|
"'{}' is not a known path.\n\n\tPossible options are: {}",
|
|
path.display(),
|
|
format!("{:?}", candidates).green()
|
|
))
|
|
}
|