Use anyhow for formatting and handling errors

This works better than doing it manually, and it significantly reduces
the amount of boilerplate needed.
This commit is contained in:
Robbert van der Helm
2020-07-16 14:31:00 +02:00
parent f48b5e66ca
commit 1ee2fe4746
5 changed files with 76 additions and 112 deletions
+7
View File
@@ -9,6 +9,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.6" version = "0.3.6"
@@ -665,6 +671,7 @@ name = "yabridgectl"
version = "1.2.1" version = "1.2.1"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow",
"clap", "clap",
"colored", "colored",
"lazy_static", "lazy_static",
+1
View File
@@ -10,6 +10,7 @@ repository = "https://github.com/robbert-vdh/yabridge"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[dependencies] [dependencies]
anyhow = "1.0.31"
aho-corasick = "0.7.13" aho-corasick = "0.7.13"
colored = "2.0.0" colored = "2.0.0"
clap = { version = "3.0.0-beta.1", features = ["wrap_help"] } clap = { version = "3.0.0-beta.1", features = ["wrap_help"] }
+45 -73
View File
@@ -16,36 +16,30 @@
//! Handlers for the subcommands, just to keep `main.rs` clean. //! Handlers for the subcommands, just to keep `main.rs` clean.
use anyhow::{Context, Result};
use clap::ArgMatches; use clap::ArgMatches;
use colored::Colorize; use colored::Colorize;
use std::fs; use std::fs;
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::exit;
use crate::config::{Config, InstallationMethod}; use crate::config::{Config, InstallationMethod};
use crate::files; use crate::files;
use crate::files::FoundFile; use crate::files::FoundFile;
/// Add a direcotry to the plugin locations. Duplicates get ignord because we're using ordered sets. /// 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) { pub fn add_directory(config: &mut Config, path: PathBuf) -> Result<()> {
config.plugin_dirs.insert(path); config.plugin_dirs.insert(path);
config.write().unwrap_or_else(|err| { config.write()
eprintln!("Error while writing config file: {}", err);
exit(1);
});
} }
/// Remove a direcotry to the plugin locations. The path is assumed to be part of /// 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 si silently ignored.
pub fn remove_directory(config: &mut Config, path: &Path) { pub fn remove_directory(config: &mut Config, path: &Path) -> Result<()> {
// We've already verified that this path is in `config.plugin_dirs` // 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? // XXS: Would it be a good idea to warn about leftover .so files?
config.plugin_dirs.remove(path); config.plugin_dirs.remove(path);
config.write().unwrap_or_else(|err| { config.write()?;
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 // Ask the user to remove any leftover files to prevent possible future problems and out of date
// copies // copies
@@ -66,10 +60,8 @@ pub fn remove_directory(config: &mut Config, path: &Path) {
) { ) {
Ok(Some(answer)) if answer == "YES" => { Ok(Some(answer)) if answer == "YES" => {
for file in &orphan_files { for file in &orphan_files {
fs::remove_file(file.path()).unwrap_or_else(|err| { fs::remove_file(file.path())
eprintln!("Could not remove '{}': {}", file.path().display(), err); .with_context(|| format!("Could not remove '{}'", file.path().display()))?;
exit(1);
});
} }
println!("\nRemoved {} files.", orphan_files.len()); println!("\nRemoved {} files.", orphan_files.len());
@@ -77,24 +69,24 @@ pub fn remove_directory(config: &mut Config, path: &Path) {
_ => {} _ => {}
} }
} }
Ok(())
} }
/// List the plugin locations. /// List the plugin locations.
pub fn list_directories(config: &Config) { pub fn list_directories(config: &Config) -> Result<()> {
for directory in &config.plugin_dirs { for directory in &config.plugin_dirs {
println!("{}", directory.display()); println!("{}", directory.display());
} }
Ok(())
} }
/// Print the current configuration and the installation status for all found plugins. /// Print the current configuration and the installation status for all found plugins.
pub fn show_status(config: &Config) { pub fn show_status(config: &Config) -> Result<()> {
let results = match config.index_directories() { let results = config
Ok(results) => results, .index_directories()
Err(err) => { .context("Failure while searching for plugins")?;
eprintln!("Error while searching for plugins: {}", err);
exit(1);
}
};
println!( println!(
"yabridge path: {}", "yabridge path: {}",
@@ -126,10 +118,12 @@ pub fn show_status(config: &Config) {
println!(" {} :: {}", plugin.display(), status_str); println!(" {} :: {}", plugin.display(), status_str);
} }
} }
Ok(())
} }
/// Change configuration settings. The actual options are defined in the clap [app](clap::App). /// Change configuration settings. The actual options are defined in the clap [app](clap::App).
pub fn set_settings(config: &mut Config, options: &ArgMatches) { pub fn set_settings(config: &mut Config, options: &ArgMatches) -> Result<()> {
match options.value_of("method") { match options.value_of("method") {
Some("copy") => config.method = InstallationMethod::Copy, Some("copy") => config.method = InstallationMethod::Copy,
Some("symlink") => config.method = InstallationMethod::Symlink, Some("symlink") => config.method = InstallationMethod::Symlink,
@@ -148,34 +142,18 @@ pub fn set_settings(config: &mut Config, options: &ArgMatches) {
Err(err) => err.exit(), Err(err) => err.exit(),
} }
config.write().unwrap_or_else(|err| { config.write()
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 /// Set up yabridge for all Windows VST2 plugins in the plugin directories. Will also remove orphan
/// `.so` files if the prune option is set. /// `.so` files if the prune option is set.
pub fn do_sync(config: &Config, prune: bool, verbose: bool) { pub fn do_sync(config: &Config, prune: bool, verbose: bool) -> Result<()> {
let libyabridge_path = match config.libyabridge() { let libyabridge_path = config.libyabridge()?;
Ok(path) => { println!("Using '{}'\n", libyabridge_path.display());
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() { let results = config
Ok(results) => results, .index_directories()
Err(err) => { .context("Failure while searching for plugins")?;
eprintln!("Error while searching for plugins: {}", err);
exit(1);
}
};
// Keep track of some global statistics // Keep track of some global statistics
let mut num_installed = 0; let mut num_installed = 0;
@@ -194,34 +172,28 @@ pub fn do_sync(config: &Config, prune: bool, verbose: bool) {
// mixing symlinks and regular files // mixing symlinks and regular files
let target_path = plugin.with_extension("so"); let target_path = plugin.with_extension("so");
if target_path.exists() { if target_path.exists() {
fs::remove_file(&target_path).unwrap_or_else(|err| { fs::remove_file(&target_path)
eprintln!("Could not remove '{}': {}", target_path.display(), err); .with_context(|| format!("Could not remove '{}'", target_path.display()))?;
exit(1);
});
} }
match config.method { match config.method {
InstallationMethod::Copy => { InstallationMethod::Copy => {
fs::copy(&libyabridge_path, &target_path).unwrap_or_else(|err| { fs::copy(&libyabridge_path, &target_path).with_context(|| {
eprintln!( format!(
"Error copying '{}' to '{}': {}", "Error copying '{}' to '{}'",
libyabridge_path.display(), libyabridge_path.display(),
target_path.display(), target_path.display()
err )
); })?;
exit(1);
});
} }
InstallationMethod::Symlink => { InstallationMethod::Symlink => {
symlink(&libyabridge_path, &target_path).unwrap_or_else(|err| { symlink(&libyabridge_path, &target_path).with_context(|| {
eprintln!( format!(
"Error symlinking '{}' to '{}': {}", "Error symlinking '{}' to '{}'",
libyabridge_path.display(), libyabridge_path.display(),
target_path.display(), target_path.display()
err )
); })?;
exit(1);
});
} }
} }
@@ -261,10 +233,8 @@ pub fn do_sync(config: &Config, prune: bool, verbose: bool) {
println!("- {}", path.display()); println!("- {}", path.display());
if prune { if prune {
fs::remove_file(path).unwrap_or_else(|err| { fs::remove_file(path)
eprintln!("Error while trying to remove '{}': {}", path.display(), err); .with_context(|| format!("Could not remove '{}'", path.display()))?;
exit(1);
});
} }
} }
@@ -276,5 +246,7 @@ pub fn do_sync(config: &Config, prune: bool, verbose: bool) {
num_installed, num_installed,
config.method.plural(), config.method.plural(),
num_skipped_files num_skipped_files
) );
Ok(())
} }
+20 -31
View File
@@ -16,6 +16,7 @@
//! Utilities for managing yabrigectl's configuration. //! Utilities for managing yabrigectl's configuration.
use anyhow::{anyhow, Context, Result};
use rayon::prelude::*; use rayon::prelude::*;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
@@ -90,19 +91,15 @@ impl Display for InstallationMethod {
impl Config { impl Config {
/// Try to read the config file, creating a new default file if necessary. This will fail if the /// Try to read the config file, creating a new default file if necessary. This will fail if the
/// file could not be created or if it could not be parsed. /// file could not be created or if it could not be parsed.
pub fn read() -> Result<Config, String> { pub fn read() -> Result<Config> {
match yabridgectl_directories()?.find_config_file(CONFIG_FILE_NAME) { match yabridgectl_directories()?.find_config_file(CONFIG_FILE_NAME) {
Some(path) => { Some(path) => {
let toml_str = fs::read_to_string(&path).map_err(|err| { let toml_str = fs::read_to_string(&path).with_context(|| {
format!( format!("Could not read config file at '{}'", path.display())
"Could not read config file at '{}': {}",
path.display(),
err
)
})?; })?;
Ok(toml::from_str(&toml_str) toml::from_str(&toml_str)
.map_err(|err| format!("Could not parse TOML: {}", err))?) .with_context(|| format!("Failed to parse '{}'", path.display()))
} }
None => { None => {
let defaults = Config { let defaults = Config {
@@ -121,34 +118,28 @@ impl Config {
} }
/// Write the config to disk, creating the file if it does not yet exist. /// Write the config to disk, creating the file if it does not yet exist.
pub fn write(&self) -> Result<(), String> { pub fn write(&self) -> Result<()> {
let toml_str = toml::to_string_pretty(&self) let toml_str = toml::to_string_pretty(&self).context("Could not format TOML")?;
.map_err(|err| format!("Could not format TOML: {}", err))?; let config_path = yabridgectl_directories()?
let config_file = yabridgectl_directories()?
.place_config_file(CONFIG_FILE_NAME) .place_config_file(CONFIG_FILE_NAME)
.map_err(|err| format!("Could not write config file: {}", err))?; .context("Could not create config file")?;
fs::write(&config_file, toml_str).map_err(|err| { fs::write(&config_path, toml_str)
format!( .with_context(|| format!("Failed to write config file to '{}'", config_path.display()))
"Could not write config file to '{}': {}",
config_file.display(),
err
)
})
} }
/// Return the path to `libyabridge.so`, or a descriptive error if it can't be found. If /// Return the path to `libyabridge.so`, or a descriptive error if it can't be found. If
/// `yabridge_home` is `None`, then we'll search in both `/usr/lib` and /// `yabridge_home` is `None`, then we'll search in both `/usr/lib` and
/// `$XDG_DATA_HOME/yabridge`. /// `$XDG_DATA_HOME/yabridge`.
pub fn libyabridge(&self) -> Result<PathBuf, String> { pub fn libyabridge(&self) -> Result<PathBuf> {
match &self.yabridge_home { match &self.yabridge_home {
Some(directory) => { Some(directory) => {
let candidate = directory.join(LIBYABRIDGE_NAME); let candidate = directory.join(LIBYABRIDGE_NAME);
if candidate.exists() { if candidate.exists() {
Ok(candidate) Ok(candidate)
} else { } else {
Err(format!( Err(anyhow!(
"Could not find '{}' in '{}'.", "Could not find '{}' in '{}'",
LIBYABRIDGE_NAME, LIBYABRIDGE_NAME,
directory.display() directory.display()
)) ))
@@ -165,7 +156,7 @@ impl Config {
} }
} }
Err(format!( Err(anyhow!(
"Could not find '{}' in either '{}' or '{}'. You can tell yabridgectl where \ "Could not find '{}' in either '{}' or '{}'. You can tell yabridgectl where \
to search for it using 'yabridgectl set --path=<path>'.", to search for it using 'yabridgectl set --path=<path>'.",
LIBYABRIDGE_NAME, LIBYABRIDGE_NAME,
@@ -189,14 +180,12 @@ impl Config {
/// Fetch the XDG base directories for yabridge's own files, converting any error messages if this /// Fetch the XDG base directories for yabridge's own files, converting any error messages if this
/// somehow fails into a printable string to reduce boiler plate. This is only used when searching /// somehow fails into a printable string to reduce boiler plate. This is only used when searching
/// for `libyabridge.so` when no explicit search path has been set. /// for `libyabridge.so` when no explicit search path has been set.
fn yabridge_directories() -> Result<BaseDirectories, String> { fn yabridge_directories() -> Result<BaseDirectories> {
BaseDirectories::with_prefix(YABRIDGE_PREFIX) BaseDirectories::with_prefix(YABRIDGE_PREFIX).context("Error while parsing base directories")
.map_err(|err| format!("Error while parsing base directories: {}", err))
} }
/// Fetch the XDG base directories used for yabridgectl, converting any error messages if this /// Fetch the XDG base directories used for yabridgectl, converting any error messages if this
/// somehow fails into a printable string to reduce boiler plate. /// somehow fails into a printable string to reduce boiler plate.
fn yabridgectl_directories() -> Result<BaseDirectories, String> { fn yabridgectl_directories() -> Result<BaseDirectories> {
BaseDirectories::with_prefix(YABRIDGECTL_PREFIX) BaseDirectories::with_prefix(YABRIDGECTL_PREFIX).context("Error while parsing base directories")
.map_err(|err| format!("Error while parsing base directories: {}", err))
} }
+3 -8
View File
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use anyhow::Result;
use clap::{app_from_crate, App, AppSettings, Arg}; use clap::{app_from_crate, App, AppSettings, Arg};
use colored::Colorize; use colored::Colorize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -28,14 +29,8 @@ mod files;
// TODO: When creating copies, check whether `yabridge-host.exe` is in the PATH for the login shell // TODO: When creating copies, check whether `yabridge-host.exe` is in the PATH for the login shell
// TODO: Reward parts of the readme // TODO: Reward parts of the readme
fn main() { fn main() -> Result<()> {
let mut config = match Config::read() { let mut config = Config::read()?;
Ok(config) => config,
Err(err) => {
eprintln!("Error while reading config:\n\n{}", err);
std::process::exit(1);
}
};
// Used for validation in `yabridgectl rm <path>` // Used for validation in `yabridgectl rm <path>`
let plugin_directories: Vec<&str> = config let plugin_directories: Vec<&str> = config