mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-15 21:15:51 +02:00
6aab25c1de
This was stuck on an old version some of the previous versions did not take the subsequent indent string's width into account when wrapping.
514 lines
22 KiB
Rust
514 lines
22 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/>.
|
|
|
|
//! Helper utilities and wrappers around filesystem functions for use with anyhow.
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use colored::Colorize;
|
|
use is_executable::IsExecutable;
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::hash::Hasher;
|
|
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
|
|
use std::os::unix::fs as unix_fs;
|
|
use std::os::unix::process::CommandExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Stdio};
|
|
|
|
use crate::config::{self, Config, KnownConfig, YABRIDGE_HOST_32_EXE_NAME, YABRIDGE_HOST_EXE_NAME};
|
|
use crate::files::{LibArchitecture, 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
|
|
/// moment without causing issues.
|
|
const YABRIDGE_HOST_EXPECTED_OUTPUT_PREFIX: &str = "Usage: yabridge-";
|
|
|
|
const LIBDBUS_NAME: &str = "libdbus-1.so.3";
|
|
const LIBDBUS_FALLBACK_NAME: &str = "libdbus-1.so";
|
|
|
|
/// Wrapper around [`reflink::reflink_or_copy()`](reflink::reflink_or_copy) with a human readable
|
|
/// error message.
|
|
pub fn copy_or_reflink<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<Option<u64>> {
|
|
reflink::reflink_or_copy(&from, &to).with_context(|| {
|
|
format!(
|
|
"Error reflinking '{}' to '{}'",
|
|
from.as_ref().display(),
|
|
to.as_ref().display()
|
|
)
|
|
})
|
|
}
|
|
|
|
/// 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::read()`](std::fs::read) with a human readable error message.
|
|
pub fn read<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
|
|
fs::read(&path).with_context(|| format!("Could not read file '{}'", path.as_ref().display()))
|
|
}
|
|
|
|
/// Wrapper around [`std::fs::read_to_string()`](std::fs::read_to_string) with a human readable
|
|
/// error message.
|
|
pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
|
fs::read_to_string(&path)
|
|
.with_context(|| format!("Could not read file '{}'", path.as_ref().display()))
|
|
}
|
|
|
|
/// Wrapper around [`std::fs::remove_dir_all()`](std::fs::remove_dir_all) with a human readable
|
|
/// error message.
|
|
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
|
|
fs::remove_dir_all(&path)
|
|
.with_context(|| format!("Could not remove directory '{}'", 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<()> {
|
|
fs::remove_file(&path)
|
|
.with_context(|| format!("Could not remove '{}'", path.as_ref().display()))
|
|
}
|
|
|
|
/// Wrapper around [`std::os::unix::fs::symlink()`](std::os::unix::fs::symlink) with a human
|
|
/// readable error message.
|
|
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
|
unix_fs::symlink(&src, &dst).with_context(|| {
|
|
format!(
|
|
"Error symlinking '{}' to '{}'",
|
|
src.as_ref().display(),
|
|
dst.as_ref().display()
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Wrapper around [`std::fs::write()`](std::fs::write) with a human readable error message.
|
|
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
|
fs::write(&path, contents)
|
|
.with_context(|| format!("Could write to '{}'", path.as_ref().display()))
|
|
}
|
|
|
|
/// Get the architecture of the ELF file at `path`. This detection is a bit naive, but we'd rather
|
|
/// not depend on `libmagic` or `libreadelf` just for this, since encountering a 32-bit yabridge
|
|
/// library is going to be incredibly rare.
|
|
///
|
|
/// This is based on this file header specification:
|
|
/// https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
|
|
pub fn get_elf_architecture(path: &Path) -> Result<LibArchitecture> {
|
|
// We'll assume `path` points to an ELF file and immediately skip to the good stuff
|
|
let mut file = fs::File::open(path)?;
|
|
|
|
// I doubt yabridge will ever run on a Big-Endian machine until audio production on Windows on
|
|
// ARM also becomes big, but we still need to account for this
|
|
let little_endian = {
|
|
let mut endianness_bytes = [0u8; 1];
|
|
file.seek(SeekFrom::Start(0x05))?; // e_ident[EI_DATA], 1 byte
|
|
file.read_exact(&mut endianness_bytes)?;
|
|
endianness_bytes[0] == 1
|
|
};
|
|
|
|
let mut machine_arch_bytes = [0u8; 2];
|
|
file.seek(SeekFrom::Start(0x12))?; // e_machine, 2 bytes
|
|
file.read_exact(&mut machine_arch_bytes)?;
|
|
|
|
let machine_arch = if little_endian {
|
|
u16::from_le_bytes(machine_arch_bytes)
|
|
} else {
|
|
u16::from_be_bytes(machine_arch_bytes)
|
|
};
|
|
match machine_arch {
|
|
0x03 => Ok(LibArchitecture::Lib32), // x86
|
|
0x3E => Ok(LibArchitecture::Lib64), // AMD x86-64
|
|
_ => Err(anyhow!(
|
|
"'{}' is not a recognized ELF machine ISA",
|
|
machine_arch
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Get the type of a file, if it exists.
|
|
pub fn get_file_type(path: PathBuf) -> Option<NativeFile> {
|
|
match path.symlink_metadata() {
|
|
Ok(metadata) if metadata.file_type().is_symlink() => Some(NativeFile::Symlink(path)),
|
|
Ok(metadata) if metadata.file_type().is_dir() => Some(NativeFile::Directory(path)),
|
|
Ok(_) => Some(NativeFile::Regular(path)),
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Get the architecture (either 64-bit or 32-bit) of the default Wine prefix in `~/.wine`. Defaults
|
|
/// to 64-bit if `~/.wine` doesn't exist or if the prefix is invalid.
|
|
pub fn get_default_wine_prefix_arch() -> LibArchitecture {
|
|
let wine_system_reg_path = PathBuf::from(env::var("HOME").expect("$HOME is not set"))
|
|
.join(".wine")
|
|
.join("system.reg");
|
|
|
|
// Fall back to 64-bit if the prefix doesn't exist
|
|
let wine_system_reg = match fs::File::open(wine_system_reg_path) {
|
|
Ok(file) => file,
|
|
_ => return LibArchitecture::Lib64,
|
|
};
|
|
|
|
for line in BufReader::new(wine_system_reg)
|
|
.lines()
|
|
.filter_map(|l| l.ok())
|
|
{
|
|
match line.as_str() {
|
|
"#arch=win32" => return LibArchitecture::Lib32,
|
|
"#arch=win64" => break,
|
|
_ => (),
|
|
};
|
|
}
|
|
|
|
LibArchitecture::Lib64
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// # Note
|
|
///
|
|
/// We convert the hash to an i64 because the TOML library can't deserialize large u64 values since
|
|
/// it uses i64s internally.
|
|
pub fn hash_file(file: &Path) -> Result<i64> {
|
|
let mut hasher = DefaultHasher::new();
|
|
hasher.write(
|
|
&fs::read(file)
|
|
.with_context(|| format!("Could not read contents of '{}'", file.display()))?,
|
|
);
|
|
|
|
Ok(hasher.finish() as i64)
|
|
}
|
|
|
|
/// Resolve symlinks in a path, like the `realpath` coreutil, but don't throw any errors of `path`
|
|
/// does not exist, unlike the `realpath` libc function.
|
|
///
|
|
/// This is used to resolve symlinked directories in the syncing process so the plugin counts are
|
|
/// correct even when one plugin directory contains a symlink to another plugin directory.
|
|
pub fn normalize_path(path: &Path) -> PathBuf {
|
|
for prefix in path.ancestors() {
|
|
// If part of `path`s prefix exists, then we'll try to resolve symlinks there
|
|
if let Ok(normalized_prefix) = fs::canonicalize(&prefix) {
|
|
return normalized_prefix.join(path.strip_prefix(prefix).unwrap());
|
|
}
|
|
}
|
|
|
|
path.to_owned()
|
|
}
|
|
|
|
/// Verify that `yabridge-host.exe` can be found when yabridge is run in a host launched from the
|
|
/// GUI. We do this by launching a login shell, appending `~/.local/share/yabridge` to the login
|
|
/// shell's search path since that's what yabridge also does, and then making the the file can be
|
|
/// found. Returns `true` if it can be found, or if we the login shell is set to an unknown shell.
|
|
/// In the last case we'll just print a warning since we don't know how to invoke the shell as a
|
|
/// login shell. This is needed when using copies to ensure that yabridge can find the host binaries
|
|
/// when the VST host is launched from the desktop enviornment.
|
|
///
|
|
/// This is a bit messy, and with yabridge 2.1 automatically searching in `~/.local/share/yabridge`
|
|
/// it's probably not really needed anymore, but it could still be useful in some edge case
|
|
/// scenarios.
|
|
pub fn verify_path_setup() -> Result<bool> {
|
|
// First we'll check `~/.local/share/yabridge`, since that's a special location where yabridge
|
|
// will always search
|
|
let xdg_data_yabridge = config::yabridge_directories().map(|dirs| dirs.get_data_home());
|
|
let xdg_yabridge_host_exe_path =
|
|
xdg_data_yabridge.map(|data_home| data_home.join(YABRIDGE_HOST_EXE_NAME));
|
|
match xdg_yabridge_host_exe_path {
|
|
Ok(path) if path.is_executable() => return Ok(true),
|
|
// If the file does exist but is not executable, it's almost certainly a mistake. This has
|
|
// happened once or twice before. Some archive extraction programs seem to strip the
|
|
// executable bits.
|
|
Ok(path) if path.exists() => {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"WARNING: '{}' exists but is not executable. Something probably went wrong \
|
|
when extracting yabridge's files.",
|
|
path.display().to_string().bright_white(),
|
|
))
|
|
);
|
|
|
|
return Ok(false);
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
// Then we'll check the login shell, since DAWs launched from the GUI will have the same
|
|
// environment. This check is mostly vestigial since people really shouldn't be installing
|
|
// yabridge to random locations anymore.
|
|
match env::var("SHELL") {
|
|
Ok(shell_path) => {
|
|
// `$SHELL` will often contain a full path, but it doesn't have to
|
|
let shell = Path::new(&shell_path)
|
|
.file_name()
|
|
.and_then(|os_str| os_str.to_str())
|
|
.unwrap_or(shell_path.as_str());
|
|
|
|
// We're using the `-l` flag present in most shells to start a login shell, but some
|
|
// shells don't have this option. According the Bash's man page, another method some
|
|
// shells use to determine that they're being run as a login shell is by checking that
|
|
// `argv[0]` starts with a hyphen, so we'll also do that.
|
|
let mut command = Command::new(&shell_path);
|
|
command.arg0(format!("-{}", &shell_path));
|
|
|
|
let command = match shell {
|
|
// All of these shells support the `-l` flag to start a login shell and have a
|
|
// POSIX-compatible `command` builtin
|
|
"ash" | "bash" | "csh" | "ksh" | "dash" | "fish" | "ion" | "sh" | "tcsh"
|
|
| "zsh" => command
|
|
.arg("-l")
|
|
.arg("-c")
|
|
.arg(format!("command -v {}", YABRIDGE_HOST_EXE_NAME)),
|
|
// These shells either have their own implementation of `which` and don't support
|
|
// `command`, or they don't have a seperate login shell flag
|
|
"elvish" | "oil" => command
|
|
.arg("-c")
|
|
.arg(format!("command -v {}", YABRIDGE_HOST_EXE_NAME)),
|
|
// xonsh's which implementation is broken as of writing this, so I left it out
|
|
"pwsh" => command
|
|
.arg("-l")
|
|
.arg("-c")
|
|
.arg(format!("which {}", YABRIDGE_HOST_EXE_NAME)),
|
|
"nu" => command
|
|
.arg("-c")
|
|
.arg(format!("which {}", YABRIDGE_HOST_EXE_NAME)),
|
|
shell => {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"WARNING: Yabridgectl does not know how to handle your login shell \
|
|
'{}', skipping the PATH environment variable check. Feel free to open a \
|
|
feature request in order to get yabridgectl to support your shell.\n\
|
|
\n\
|
|
https://github.com/robbert-vdh/yabridge/issues",
|
|
shell.bright_white(),
|
|
))
|
|
);
|
|
return Ok(true);
|
|
}
|
|
};
|
|
|
|
// For the login shell we want to a clean environment, but we still have to set `$HOME`
|
|
// or else most shells won't know which profile to load
|
|
command
|
|
.env_clear()
|
|
.env("HOME", env::var("HOME").unwrap_or_default());
|
|
|
|
match command.stdout(Stdio::null()).stderr(Stdio::null()).status() {
|
|
Ok(status) if status.success() => Ok(true),
|
|
Ok(_) => {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"Warning: '{}' is not present in either '{}' your login shell's \
|
|
search path. You may not have extracted yabridge's files to the correct location. \
|
|
See the readme's usage section for more details. Rerun this command to verify \
|
|
that the variable has been set correctly.\n\
|
|
\n\
|
|
https://github.com/robbert-vdh/yabridge#usage",
|
|
"yabridge-host.exe".bright_white(),
|
|
// We could grab the actual `$XDG_DATA_HOME` location here but most
|
|
// people will not have set that environment variable and this looks a
|
|
// bit clearer
|
|
"~/.local/share/yabridge".bright_white(),
|
|
))
|
|
);
|
|
|
|
Ok(false)
|
|
}
|
|
Err(err) => {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"Warning: could not run {} as a login shell, skipping the PATH setup \
|
|
check: {}",
|
|
shell.bright_white(),
|
|
err
|
|
))
|
|
);
|
|
|
|
Ok(true)
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
eprintln!("\nWarning: Could not determine login shell, skipping the PATH setup check");
|
|
|
|
Ok(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify that the installed versions of Wine and yabridge will work together properly. This check
|
|
/// is only performed once per combination of Wine and yabridge, and we'll update the config with
|
|
/// the versions we just tested if the check succeeds. Will return `Err` values if either Wine or
|
|
/// `yabridge-host.exe` can't be run.
|
|
pub fn verify_wine_setup(config: &mut Config) -> Result<()> {
|
|
// These winelib scripts respect `$WINELOADER`, so we'll do the same thing
|
|
let wine_binary = env::var("WINELOADER").unwrap_or_else(|_| String::from("wine"));
|
|
let wine_version_output = Command::new(&wine_binary)
|
|
.arg("--version")
|
|
// NOTE: This mimics the setup used in `PluginInfo::create_host_env()`. The X11 window
|
|
// embedding will break when future Wine versions start defaulting to a native Wayland
|
|
// driver, so we'll prevent that from happening.
|
|
.env_remove("WAYLAND_DISPLAY")
|
|
.output()
|
|
.with_context(|| {
|
|
format!(
|
|
"Could not run '{}', make sure Wine is installed",
|
|
wine_binary
|
|
)
|
|
})?
|
|
.stdout;
|
|
// Strip the trailing newline just to make the config file a bit neater
|
|
let mut wine_version = String::from_utf8(wine_version_output)?;
|
|
wine_version.pop().with_context(|| {
|
|
format!(
|
|
"Running '{} --version' resulted in empty output",
|
|
wine_binary
|
|
)
|
|
})?;
|
|
|
|
let files = config
|
|
.files()
|
|
.context(format!("Could not find '{}'", YABRIDGE_HOST_EXE_NAME))?;
|
|
|
|
// Hash the contents of `yabridge-host.exe.so` since `yabridge-host.exe` is only a Wine
|
|
// generated shell script. If somehow only the 32-bit verison is installed, we'll just hash that
|
|
// one.
|
|
let yabridge_host_hash = hash_file(
|
|
&files
|
|
.yabridge_host_exe_so
|
|
.or(files.yabridge_host_32_exe_so)
|
|
.with_context(|| format!("Could not locate '{}.so'", YABRIDGE_HOST_EXE_NAME))?,
|
|
)?;
|
|
|
|
// Since these checks can take over a second if wineserver isn't already running we'll only
|
|
// perform them when something has changed
|
|
let current_config = KnownConfig {
|
|
wine_version: wine_version.clone(),
|
|
yabridge_host_hash,
|
|
};
|
|
if config.last_known_config.as_ref() == Some(¤t_config) {
|
|
return Ok(());
|
|
}
|
|
|
|
// It could be that the default Wine prefix was created with `WINEARCH=win32` set. In that case
|
|
// we should run the 32-bit `yabridge-host.exe` since the 64-bit verison won't be able to run.
|
|
let host_binary_path = match get_default_wine_prefix_arch() {
|
|
LibArchitecture::Lib32 => files
|
|
.yabridge_host_32_exe
|
|
.with_context(|| format!("Could not find '{}'", YABRIDGE_HOST_32_EXE_NAME)),
|
|
LibArchitecture::Lib64 => files
|
|
.yabridge_host_exe
|
|
.with_context(|| format!("Could not find '{}'", YABRIDGE_HOST_EXE_NAME)),
|
|
}?;
|
|
|
|
let output = Command::new(&host_binary_path)
|
|
.output()
|
|
.with_context(|| format!("Could not run '{}'", host_binary_path.display()))?;
|
|
let stderr = String::from_utf8(output.stderr)?;
|
|
|
|
// There are three scenarios here:
|
|
// - Either everything is fine and we'll see the usage string being printed
|
|
// - Or the used version of Wine is too old and we'll see some line starting with
|
|
// `002b:err:module:__wine_process_init`
|
|
// - Or the used version of Wine is much newer than what was used to compile yabridge with
|
|
//
|
|
// I don't know if it's possible to differentiate between the second and the third case, so
|
|
// we'll always assume it's Wine that's outdated.
|
|
let mut success = false;
|
|
let mut last_error: Option<&str> = None;
|
|
for line in stderr.lines() {
|
|
if line.starts_with(YABRIDGE_HOST_EXPECTED_OUTPUT_PREFIX) {
|
|
success = true;
|
|
break;
|
|
}
|
|
|
|
// Ignore fixme messages here, since those can be produced by wineserver even after the
|
|
// application has errored out
|
|
if line.get(5..10) != Some("fixme") {
|
|
last_error = Some(line);
|
|
}
|
|
}
|
|
|
|
if success {
|
|
config.last_known_config = Some(current_config);
|
|
config.write()?;
|
|
} else {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"Warning: Could not run '{yabridge_host}'. Wine reported the following error:\n\
|
|
\n\
|
|
{error}\n\
|
|
\n\
|
|
Make sure that you have downloaded the correct version of yabridge for your distro.\n\
|
|
This can also happen when using a version of Wine that's not compatible with this \
|
|
version of yabridge, in which case you'll need to upgrade Wine. Your current Wine \
|
|
version is '{wine_version}'. \
|
|
See the link below for instructions on how to upgrade your installation of Wine.\n\
|
|
\n\
|
|
https://github.com/robbert-vdh/yabridge#troubleshooting-common-issues",
|
|
yabridge_host = "yabridge-host.exe".bright_white(),
|
|
error = last_error.unwrap_or("<no_output>").bright_white(),
|
|
wine_version = wine_version
|
|
.strip_prefix("wine-")
|
|
.unwrap_or(&wine_version)
|
|
.bright_white(),
|
|
))
|
|
)
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check whether yabridge's (optional) external dependencies are installed. Right now this only
|
|
/// checks for libdbus, as this is used to relay important information when something's very wrong.
|
|
pub fn verify_external_dependencies() -> Result<()> {
|
|
let libdbus_available = unsafe { libloading::Library::new(&LIBDBUS_NAME) }
|
|
.or_else(|_| unsafe { libloading::Library::new(&LIBDBUS_FALLBACK_NAME) })
|
|
.is_ok();
|
|
if !libdbus_available {
|
|
eprintln!(
|
|
"\n{}",
|
|
wrap(&format!(
|
|
"Warning: Could not find '{}'. This will not prevent yabridge from working, but \
|
|
you will also not receive any notifcations when something is wrong. D-Bus should \
|
|
already be installed on the system, so if you're seeing this warning then \
|
|
something is probably very wrong.",
|
|
LIBDBUS_NAME.bright_white(),
|
|
))
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Wrap a long paragraph of text to terminal width, or 80 characters if the width of the terminal
|
|
/// can't be determined. Everything after the first line gets indented with four spaces.
|
|
pub fn wrap(text: &str) -> String {
|
|
let options = textwrap::Options::with_termwidth().subsequent_indent(" ");
|
|
|
|
textwrap::fill(text, options)
|
|
}
|