mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
💥 Major refactor of initialization plumbing
To account for the differences in VST2 plugins and VST3 modules we had to wrap most of our old functions from `src/plugin/utils.h` in a new `PluginInfo` struct that gathers all of this information while taking into account the differences between VST2 and VST3 plugins. With this change things are also a lot more organized. We can just query the plugin information we need rather than having to store things separately or having to recalculate things. This also moved the responsibility of all the weird `WINEPREFIX` behaviour to a single place instead of having it spread around `utils.pp`, the initialisation message, and `host-procoess.cpp`.
This commit is contained in:
@@ -41,15 +41,6 @@ static_assert(std::is_same_v<intptr_t, int64_t>);
|
||||
using native_size_t = uint64_t;
|
||||
using native_intptr_t = int64_t;
|
||||
|
||||
// The cannonical overloading template for `std::visitor`, not sure why this
|
||||
// isn't part of the standard library
|
||||
template <class... Ts>
|
||||
struct overload : Ts... {
|
||||
using Ts::operator()...;
|
||||
};
|
||||
template <class... Ts>
|
||||
overload(Ts...) -> overload<Ts...>;
|
||||
|
||||
/**
|
||||
* An object containing the startup options for hosting a plugin. These options
|
||||
* are passed to `yabridge-host.exe` as command line arguments, and they are
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <bitsery/traits/vector.h>
|
||||
#include <vestige/aeffectx.h>
|
||||
|
||||
#include "../utils.h"
|
||||
#include "../vst24.h"
|
||||
#include "common.h"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <bitsery/ext/std_variant.h>
|
||||
|
||||
#include "../configuration.h"
|
||||
#include "../utils.h"
|
||||
#include "common.h"
|
||||
|
||||
// Event handling for our VST3 plugins works slightly different from how we
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
#endif
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
// The cannonical overloading template for `std::visitor`, not sure why this
|
||||
// isn't part of the standard library
|
||||
template <class... Ts>
|
||||
struct overload : Ts... {
|
||||
using Ts::operator()...;
|
||||
};
|
||||
template <class... Ts>
|
||||
overload(Ts...) -> overload<Ts...>;
|
||||
|
||||
/**
|
||||
* Return the path to the directory for story temporary files. This will be
|
||||
* `$XDG_RUNTIME_DIR` if set, and `/tmp` otherwise.
|
||||
|
||||
+34
-45
@@ -48,23 +48,20 @@ class PluginBridge {
|
||||
* Using a lambda here feels wrong, but I can't think of a better
|
||||
* solution right now.
|
||||
*
|
||||
* @tparam F A `TSockets(boost::asio::io_context&)` function to create the
|
||||
* `TSockets` instance.
|
||||
* @tparam F A `TSockets(boost::asio::io_context&, const PluginInfo&)`
|
||||
* function to create the `TSockets` instance.
|
||||
*
|
||||
* @throw std::runtime_error Thrown when the Wine plugin host could not be
|
||||
* found, or if it could not locate and load a VST3 module.
|
||||
*/
|
||||
template <typename F>
|
||||
PluginBridge(PluginType plugin_type,
|
||||
const boost::filesystem::path& plugin_path,
|
||||
F create_socket_instance)
|
||||
: plugin_type(plugin_type),
|
||||
plugin_path(plugin_path),
|
||||
PluginBridge(PluginType plugin_type, F create_socket_instance)
|
||||
: info(plugin_type),
|
||||
io_context(),
|
||||
sockets(create_socket_instance(io_context)),
|
||||
sockets(create_socket_instance(io_context, info)),
|
||||
// This is still correct for VST3 plugins because we can configure an
|
||||
// entire directory (the module's bundle) at once
|
||||
config(load_config_for(get_this_file_location())),
|
||||
config(load_config_for(info.native_library_path)),
|
||||
generic_logger(Logger::create_from_environment(
|
||||
create_logger_prefix(sockets.base_dir))),
|
||||
plugin_host(
|
||||
@@ -72,9 +69,10 @@ class PluginBridge {
|
||||
? std::unique_ptr<HostProcess>(std::make_unique<GroupHost>(
|
||||
io_context,
|
||||
generic_logger,
|
||||
info,
|
||||
HostRequest{
|
||||
.plugin_type = plugin_type,
|
||||
.plugin_path = plugin_path.string(),
|
||||
.plugin_path = info.windows_plugin_path.string(),
|
||||
.endpoint_base_dir = sockets.base_dir.string()},
|
||||
sockets,
|
||||
*config.group))
|
||||
@@ -82,8 +80,10 @@ class PluginBridge {
|
||||
std::make_unique<IndividualHost>(
|
||||
io_context,
|
||||
generic_logger,
|
||||
info,
|
||||
HostRequest{.plugin_type = plugin_type,
|
||||
.plugin_path = plugin_path.string(),
|
||||
.plugin_path =
|
||||
info.windows_plugin_path.string(),
|
||||
.endpoint_base_dir =
|
||||
sockets.base_dir.string()}))),
|
||||
has_realtime_priority(set_realtime_priority()),
|
||||
@@ -102,9 +102,9 @@ class PluginBridge {
|
||||
<< std::endl;
|
||||
init_msg << "host: '" << plugin_host->path().string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "plugin: '" << plugin_path.string() << "'"
|
||||
<< std::endl;
|
||||
init_msg << "plugin type: '" << plugin_type_to_string(plugin_type)
|
||||
init_msg << "plugin: '" << info.windows_plugin_path.string()
|
||||
<< "'" << std::endl;
|
||||
init_msg << "plugin type: '" << plugin_type_to_string(info.plugin_type)
|
||||
<< "'" << std::endl;
|
||||
init_msg << "realtime: '" << (has_realtime_priority ? "yes" : "no")
|
||||
<< "'" << std::endl;
|
||||
@@ -112,14 +112,17 @@ class PluginBridge {
|
||||
<< std::endl;
|
||||
init_msg << "wine prefix: '";
|
||||
|
||||
// If the Wine prefix is manually overridden, then this should be made
|
||||
// clear. This follows the behaviour of `set_wineprefix()`.
|
||||
boost::process::environment env = boost::this_process::environment();
|
||||
if (!env["WINEPREFIX"].empty()) {
|
||||
init_msg << env["WINEPREFIX"].to_string() << " <overridden>";
|
||||
} else {
|
||||
init_msg << find_wineprefix().value_or("<default>").string();
|
||||
}
|
||||
std::visit(
|
||||
overload{
|
||||
[&](const OverridenWinePrefix& prefix) {
|
||||
init_msg << prefix.value.string() << " <overridden>";
|
||||
},
|
||||
[&](const boost::filesystem::path& prefix) {
|
||||
init_msg << prefix.string();
|
||||
},
|
||||
[&](const DefaultWinePrefix&) { init_msg << "<default>"; },
|
||||
},
|
||||
info.wine_prefix);
|
||||
init_msg << "'" << std::endl;
|
||||
|
||||
init_msg << "wine version: '" << get_wine_version() << "'" << std::endl;
|
||||
@@ -139,10 +142,13 @@ class PluginBridge {
|
||||
} else {
|
||||
init_msg << "individually";
|
||||
}
|
||||
if (plugin_host->architecture() == LibArchitecture::dll_32) {
|
||||
init_msg << ", 32-bit";
|
||||
} else {
|
||||
init_msg << ", 64-bit";
|
||||
switch (info.plugin_arch) {
|
||||
case LibArchitecture::dll_32:
|
||||
init_msg << ", 32-bit";
|
||||
break;
|
||||
case LibArchitecture::dll_64:
|
||||
init_msg << ", 64-bit";
|
||||
break;
|
||||
}
|
||||
init_msg << "'" << std::endl;
|
||||
|
||||
@@ -239,26 +245,9 @@ class PluginBridge {
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the plugin we're dealing with. Passed to the host process and
|
||||
* printed in the initialisation message.
|
||||
* Information about the plugin we're bridging.
|
||||
*/
|
||||
const PluginType plugin_type;
|
||||
|
||||
/**
|
||||
* The path to the plugin (`.dll` or module) being loaded in the Wine plugin
|
||||
* host.
|
||||
*
|
||||
* Forst VST2 plugins this will be a `.dll` file. For VST3 plugins this is
|
||||
* normally a directory called `MyPlugin.vst3` that contains
|
||||
* `MyPlugin.vst3/Contents/x86-win/MyPlugin.vst3`, but there's also an older
|
||||
* deprecated (but still ubiquitous) format where the top level
|
||||
* `MyPlugin.vst3` is not a directory but a .dll file. This points to either
|
||||
* of those things, and then `VST3::Hosting::Win32Module::create()` will be
|
||||
* able to load it.
|
||||
*
|
||||
* https://developer.steinberg.help/pages/viewpage.action?pageId=9798275
|
||||
*/
|
||||
const boost::filesystem::path plugin_path;
|
||||
const PluginInfo info;
|
||||
|
||||
boost::asio::io_context io_context;
|
||||
|
||||
|
||||
+10
-11
@@ -40,17 +40,16 @@ Vst2PluginBridge& get_bridge_instance(const AEffect& plugin) {
|
||||
}
|
||||
|
||||
Vst2PluginBridge::Vst2PluginBridge(audioMasterCallback host_callback)
|
||||
: PluginBridge(PluginType::vst2,
|
||||
find_vst_plugin(),
|
||||
[](boost::asio::io_context& io_context) {
|
||||
return Vst2Sockets<std::jthread>(
|
||||
io_context,
|
||||
generate_endpoint_base(find_vst_plugin()
|
||||
.filename()
|
||||
.replace_extension("")
|
||||
.string()),
|
||||
true);
|
||||
}),
|
||||
: PluginBridge(
|
||||
PluginType::vst2,
|
||||
[](boost::asio::io_context& io_context, const PluginInfo& info) {
|
||||
return Vst2Sockets<std::jthread>(
|
||||
io_context,
|
||||
generate_endpoint_base(info.native_library_path.filename()
|
||||
.replace_extension("")
|
||||
.string()),
|
||||
true);
|
||||
}),
|
||||
// All the fields should be zero initialized because
|
||||
// `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin
|
||||
// bridge will crash otherwise
|
||||
|
||||
+10
-12
@@ -17,18 +17,16 @@
|
||||
#include "vst3.h"
|
||||
|
||||
Vst3PluginBridge::Vst3PluginBridge()
|
||||
: PluginBridge(PluginType::vst3,
|
||||
// TODO: This is incorrect for VST3 modules
|
||||
find_vst_plugin(),
|
||||
[](boost::asio::io_context& io_context) {
|
||||
return Vst3Sockets<std::jthread>(
|
||||
io_context,
|
||||
generate_endpoint_base(find_vst_plugin()
|
||||
.filename()
|
||||
.replace_extension("")
|
||||
.string()),
|
||||
true);
|
||||
}),
|
||||
: PluginBridge(
|
||||
PluginType::vst3,
|
||||
[](boost::asio::io_context& io_context, const PluginInfo& info) {
|
||||
return Vst3Sockets<std::jthread>(
|
||||
io_context,
|
||||
generate_endpoint_base(info.native_library_path.filename()
|
||||
.replace_extension("")
|
||||
.string()),
|
||||
true);
|
||||
}),
|
||||
// TODO: This is UB, use composition with `generic_logger` instead
|
||||
logger(static_cast<Vst3Logger&&>(Logger::create_from_environment(
|
||||
create_logger_prefix(sockets.base_dir)))) {
|
||||
|
||||
+20
-38
@@ -86,20 +86,22 @@ void HostProcess::async_log_pipe_lines(patched_async_pipe& pipe,
|
||||
|
||||
IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const HostRequest& plugin_info)
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request)
|
||||
: HostProcess(io_context, logger),
|
||||
// FIXME: This will require changing for VST3 bundles
|
||||
plugin_arch(find_dll_architecture(plugin_info.plugin_path)),
|
||||
host_path(find_vst_host(plugin_arch, false)),
|
||||
plugin_info(plugin_info),
|
||||
host_path(find_vst_host(plugin_info.native_library_path,
|
||||
plugin_info.plugin_arch,
|
||||
false)),
|
||||
host(launch_host(host_path,
|
||||
plugin_type_to_string(plugin_info.plugin_type),
|
||||
plugin_type_to_string(host_request.plugin_type),
|
||||
#ifdef WITH_WINEDBG
|
||||
plugin_info.plugin_path.filename(),
|
||||
host_request.plugin_path.filename(),
|
||||
#else
|
||||
plugin_info.plugin_path,
|
||||
host_request.plugin_path,
|
||||
#endif
|
||||
plugin_info.endpoint_base_dir,
|
||||
bp::env = set_wineprefix(),
|
||||
host_request.endpoint_base_dir,
|
||||
bp::env = plugin_info.create_host_env(),
|
||||
bp::std_out = stdout_pipe,
|
||||
bp::std_err = stderr_pipe
|
||||
#ifdef WITH_WINEDBG
|
||||
@@ -115,10 +117,6 @@ IndividualHost::IndividualHost(boost::asio::io_context& io_context,
|
||||
#endif
|
||||
}
|
||||
|
||||
LibArchitecture IndividualHost::architecture() {
|
||||
return plugin_arch;
|
||||
}
|
||||
|
||||
fs::path IndividualHost::path() {
|
||||
return host_path;
|
||||
}
|
||||
@@ -134,13 +132,15 @@ void IndividualHost::terminate() {
|
||||
|
||||
GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
Sockets& sockets,
|
||||
std::string group_name)
|
||||
: HostProcess(io_context, logger),
|
||||
// FIXME: This will require changing for VST3 bundles
|
||||
plugin_arch(find_dll_architecture(host_request.plugin_path)),
|
||||
host_path(find_vst_host(plugin_arch, true)),
|
||||
plugin_info(plugin_info),
|
||||
host_path(find_vst_host(plugin_info.native_library_path,
|
||||
plugin_info.plugin_arch,
|
||||
true)),
|
||||
sockets(sockets) {
|
||||
#ifdef WITH_WINEDBG
|
||||
if (plugin_path.string().find(' ') != std::string::npos) {
|
||||
@@ -156,25 +156,10 @@ GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
// other processes will exit. When a plugin's host process has exited, it
|
||||
// will try to connect to the socket once more in the case that another
|
||||
// process is now listening on it.
|
||||
const bp::environment host_env = set_wineprefix();
|
||||
fs::path wine_prefix;
|
||||
if (auto wine_prefix_envvar = host_env.find("WINEPREFIX");
|
||||
wine_prefix_envvar != host_env.end()) {
|
||||
// This is a bit ugly, but Boost.Process's environment does not have a
|
||||
// graceful way to check for empty environment variables in const
|
||||
// qualified environments
|
||||
wine_prefix = wine_prefix_envvar->to_string();
|
||||
} else {
|
||||
// Fall back to `~/.wine` if this has not been set or detected. This
|
||||
// would happen if the plugin's .dll file is not inside of a Wine
|
||||
// prefix. If this happens, then the Wine instance will be launched in
|
||||
// the default Wine prefix, so we should reflect that here.
|
||||
wine_prefix = fs::path(host_env.at("HOME").to_string()) / ".wine";
|
||||
}
|
||||
|
||||
const fs::path endpoint_base_dir = sockets.base_dir;
|
||||
const fs::path group_socket_path =
|
||||
generate_group_endpoint(group_name, wine_prefix, plugin_arch);
|
||||
generate_group_endpoint(group_name, plugin_info.normalize_wine_prefix(),
|
||||
plugin_info.plugin_arch);
|
||||
const auto connect = [&io_context, host_request, endpoint_base_dir,
|
||||
group_socket_path]() {
|
||||
boost::asio::local::stream_protocol::socket group_socket(io_context);
|
||||
@@ -194,7 +179,8 @@ GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
// because it should run independently of this yabridge instance as
|
||||
// it will likely outlive it.
|
||||
bp::child group_host =
|
||||
launch_host(host_path, group_socket_path, bp::env = host_env,
|
||||
launch_host(host_path, group_socket_path,
|
||||
bp::env = plugin_info.create_host_env(),
|
||||
bp::std_out = stdout_pipe, bp::std_err = stderr_pipe);
|
||||
group_host.detach();
|
||||
|
||||
@@ -231,10 +217,6 @@ GroupHost::GroupHost(boost::asio::io_context& io_context,
|
||||
}
|
||||
}
|
||||
|
||||
LibArchitecture GroupHost::architecture() {
|
||||
return plugin_arch;
|
||||
}
|
||||
|
||||
fs::path GroupHost::path() {
|
||||
return host_path;
|
||||
}
|
||||
|
||||
+10
-12
@@ -41,12 +41,6 @@ class HostProcess {
|
||||
public:
|
||||
virtual ~HostProcess(){};
|
||||
|
||||
/**
|
||||
* Return the architecture of the plugin we are loading, i.e. whether it is
|
||||
* 32-bit or 64-bit.
|
||||
*/
|
||||
virtual LibArchitecture architecture() = 0;
|
||||
|
||||
/**
|
||||
* Return the full path to the host application in use. The host application
|
||||
* is chosen depending on the architecture of the plugin's DLL file and on
|
||||
@@ -120,7 +114,9 @@ class IndividualHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param plugin_info The information about the plugin we should launch a
|
||||
* @param plugin_info Information about the plugin we're going to use. Used
|
||||
* to retrieve the Wine prefix and the plugin's architecture.
|
||||
* @param host_request The information about the plugin we should launch a
|
||||
* host process for. The values in the struct will be used as command line
|
||||
* arguments.
|
||||
*
|
||||
@@ -129,15 +125,15 @@ class IndividualHost : public HostProcess {
|
||||
*/
|
||||
IndividualHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const HostRequest& plugin_info);
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request);
|
||||
|
||||
LibArchitecture architecture() override;
|
||||
boost::filesystem::path path() override;
|
||||
bool running() override;
|
||||
void terminate() override;
|
||||
|
||||
private:
|
||||
LibArchitecture plugin_arch;
|
||||
const PluginInfo& plugin_info;
|
||||
boost::filesystem::path host_path;
|
||||
boost::process::child host;
|
||||
};
|
||||
@@ -163,6 +159,8 @@ class GroupHost : public HostProcess {
|
||||
* handled on.
|
||||
* @param logger The `Logger` instance the redirected STDIO streams will be
|
||||
* written to.
|
||||
* @param plugin_info Information about the plugin we're going to use. Used
|
||||
* to retrieve the Wine prefix and the plugin's architecture.
|
||||
* @param host_request The information about the plugin we should launch a
|
||||
* host process for. This object will be sent to the group host process.
|
||||
* @param sockets The socket endpoints that will be used for communication
|
||||
@@ -172,17 +170,17 @@ class GroupHost : public HostProcess {
|
||||
*/
|
||||
GroupHost(boost::asio::io_context& io_context,
|
||||
Logger& logger,
|
||||
const PluginInfo& plugin_info,
|
||||
const HostRequest& host_request,
|
||||
Sockets& socket_endpoint,
|
||||
std::string group_name);
|
||||
|
||||
LibArchitecture architecture() override;
|
||||
boost::filesystem::path path() override;
|
||||
bool running() override;
|
||||
void terminate() override;
|
||||
|
||||
private:
|
||||
LibArchitecture plugin_arch;
|
||||
const PluginInfo& plugin_info;
|
||||
boost::filesystem::path host_path;
|
||||
|
||||
/**
|
||||
|
||||
+230
-86
@@ -32,6 +32,230 @@
|
||||
namespace bp = boost::process;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// These functions are used to populate the fields in `PluginInfo`. See the
|
||||
// docstrings for the corresponding fields for more information on what we're
|
||||
// actually doing here.
|
||||
fs::path find_plugin_library(const fs::path& this_plugin_path,
|
||||
PluginType plugin_type);
|
||||
fs::path normalize_plugin_path(const fs::path& windows_library_path,
|
||||
PluginType plugin_type);
|
||||
std::variant<OverridenWinePrefix, fs::path, DefaultWinePrefix> find_wine_prefix(
|
||||
fs::path windows_plugin_path);
|
||||
|
||||
PluginInfo::PluginInfo(PluginType plugin_type)
|
||||
: plugin_type(plugin_type),
|
||||
native_library_path(get_this_file_location()),
|
||||
// As explained in the docstring, this is the actual Windows library. For
|
||||
// VST3 plugins that come in a module we should be loading that module
|
||||
// instead of the `.vst3` file within in, which is where
|
||||
// `windows_plugin_path` comes in.
|
||||
windows_library_path(
|
||||
find_plugin_library(native_library_path, plugin_type)),
|
||||
plugin_arch(find_dll_architecture(windows_library_path)),
|
||||
windows_plugin_path(
|
||||
normalize_plugin_path(windows_library_path, plugin_type)),
|
||||
wine_prefix(find_wine_prefix(windows_plugin_path)) {}
|
||||
|
||||
bp::environment PluginInfo::create_host_env() const {
|
||||
bp::environment env = boost::this_process::environment();
|
||||
|
||||
// Only set the prefix when could auto detect it and it's not being
|
||||
// overridden (this entire `std::visit` instead of `std::has_alternative` is
|
||||
// just for clarity's sake)
|
||||
std::visit(overload{
|
||||
[](const OverridenWinePrefix&) {},
|
||||
[&](const boost::filesystem::path& prefix) {
|
||||
env["WINEPREFIX"] = prefix.string();
|
||||
},
|
||||
[](const DefaultWinePrefix&) {},
|
||||
},
|
||||
wine_prefix);
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
boost::filesystem::path PluginInfo::normalize_wine_prefix() const {
|
||||
return std::visit(
|
||||
overload{
|
||||
[](const OverridenWinePrefix& prefix) { return prefix.value; },
|
||||
[](const boost::filesystem::path& prefix) { return prefix; },
|
||||
[](const DefaultWinePrefix&) {
|
||||
const bp::environment env = boost::this_process::environment();
|
||||
return fs::path(env.at("HOME").to_string()) / ".wine";
|
||||
},
|
||||
},
|
||||
wine_prefix);
|
||||
}
|
||||
|
||||
fs::path find_plugin_library(const fs::path& this_plugin_path,
|
||||
PluginType plugin_type) {
|
||||
switch (plugin_type) {
|
||||
case PluginType::vst2: {
|
||||
fs::path plugin_path(this_plugin_path);
|
||||
plugin_path.replace_extension(".dll");
|
||||
if (fs::exists(plugin_path)) {
|
||||
// Also resolve symlinks here, to support symlinked .dll files
|
||||
return fs::canonical(plugin_path);
|
||||
}
|
||||
|
||||
// In case this files does not exist and our `.so` file is a
|
||||
// symlink, we'll also repeat this check after resolving that
|
||||
// symlink to support links to copies of `libyabridge-vst2.so` as
|
||||
// described in issue #3
|
||||
fs::path alternative_plugin_path = fs::canonical(this_plugin_path);
|
||||
alternative_plugin_path.replace_extension(".dll");
|
||||
if (fs::exists(alternative_plugin_path)) {
|
||||
return fs::canonical(alternative_plugin_path);
|
||||
}
|
||||
|
||||
// This function is used in the constructor's initializer list so we
|
||||
// have to throw when the path could not be found
|
||||
throw std::runtime_error("'" + plugin_path.string() +
|
||||
"' does not exist, make sure to rename "
|
||||
"'libyabridge-vst2.so' to match a "
|
||||
"VST plugin .dll file.");
|
||||
} break;
|
||||
case PluginType::vst3: {
|
||||
// A VST3 plugin in Linux always has to be inside of a bundle (=
|
||||
// directory) named `X.vst3` that contains a static object
|
||||
// `X.vst3/Contents/x86_64-linux/X.so`. On Linux `X.so` is not
|
||||
// allowed to be standalone, so for yabridge this should also always
|
||||
// be installed this way.
|
||||
// https://developer.steinberg.help/pages/viewpage.action?pageId=9798275
|
||||
const fs::path bundle_home =
|
||||
this_plugin_path.parent_path().parent_path().parent_path();
|
||||
const fs::path win_module_name =
|
||||
this_plugin_path.filename().replace_extension(".vst3");
|
||||
|
||||
// Quick check in case the plugin was set up without yabridgectl,
|
||||
// since the format is very specific and any deviations from that
|
||||
// will be incorrect.
|
||||
if (bundle_home.extension() != ".vst3") {
|
||||
throw std::runtime_error(
|
||||
"'" + this_plugin_path.string() +
|
||||
"' is not inside of a VST3 bundle. Use yabridgectl to "
|
||||
"set up yabridge for VST3 plugins or check the readme "
|
||||
"for the correct format.");
|
||||
}
|
||||
|
||||
// Finding the Windows plugin consists of two steps because
|
||||
// Steinberg changed the format around:
|
||||
// - First we'll find the plugin in the VST3 bundle created by
|
||||
// yabridgectl in `~/.vst3`. The plugin can be either 32-bit or
|
||||
// 64-bit.
|
||||
// TODO: Right now we can't select between the 64-bit and the
|
||||
// 32-bit version and we'll just pick whichever one is
|
||||
// available
|
||||
// - After that we'll resolve the symlink to the module in the Wine
|
||||
// prefix, and then we'll have to figure out if this module is an
|
||||
// old style standalone module (< 3.6.10) or if it's inside of
|
||||
// a bundle (>= 3.6.10)
|
||||
fs::path candidate_path =
|
||||
bundle_home / "Contents" / "x86_64-win" / win_module_name;
|
||||
if (!fs::exists(candidate_path)) {
|
||||
// Try the 32-bit version no 64-bit version exists (although, is
|
||||
// there a single VST3 plugin where this is the case?)
|
||||
fs::path candidate_path =
|
||||
bundle_home / "Contents" / "x86-win" / win_module_name;
|
||||
}
|
||||
|
||||
// After this we'll have to use `normalize_plugin_path()` to get the
|
||||
// actual module entry point in case the plugin is using a VST
|
||||
// 3.6.10 style bundle
|
||||
if (fs::exists(candidate_path)) {
|
||||
return fs::canonical(candidate_path);
|
||||
}
|
||||
|
||||
throw std::runtime_error(
|
||||
"'" + bundle_home.string() +
|
||||
"' does not contain a Windows VST3 module. Use yabridgectl to "
|
||||
"set up yabridge for VST3 plugins or check the readme "
|
||||
"for the correct format.");
|
||||
} break;
|
||||
default:
|
||||
throw std::runtime_error("How did you manage to get this?");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path normalize_plugin_path(const fs::path& windows_library_path,
|
||||
PluginType plugin_type) {
|
||||
switch (plugin_type) {
|
||||
case PluginType::vst2:
|
||||
return windows_library_path;
|
||||
break;
|
||||
case PluginType::vst3: {
|
||||
// Now we'll have to figure out if this is a new-style bundle or
|
||||
// an old standalone module
|
||||
const fs::path win_module_name =
|
||||
windows_library_path.filename().replace_extension(".vst3");
|
||||
const fs::path windows_bundle_home =
|
||||
windows_library_path.parent_path().parent_path().parent_path();
|
||||
if (equals_case_insensitive(windows_bundle_home.filename().string(),
|
||||
win_module_name.string())) {
|
||||
return windows_bundle_home;
|
||||
} else {
|
||||
return windows_library_path;
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
throw std::runtime_error("How did you manage to get this?");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::variant<OverridenWinePrefix, fs::path, DefaultWinePrefix> find_wine_prefix(
|
||||
fs::path windows_plugin_path) {
|
||||
bp::environment env = boost::this_process::environment();
|
||||
if (!env["WINEPREFIX"].empty()) {
|
||||
return OverridenWinePrefix{env["WINEPREFIX"].to_string()};
|
||||
}
|
||||
|
||||
std::optional<fs::path> dosdevices_dir = find_dominating_file(
|
||||
"dosdevices", windows_plugin_path, fs::is_directory);
|
||||
if (!dosdevices_dir) {
|
||||
return DefaultWinePrefix{};
|
||||
}
|
||||
|
||||
return dosdevices_dir->parent_path();
|
||||
}
|
||||
|
||||
fs::path get_this_file_location() {
|
||||
// HACK: Not sure why, but `boost::dll::this_line_location()` returns a path
|
||||
// starting with a double slash on some systems. I've seen this happen
|
||||
// on both Ubuntu 18.04 and 20.04, but not on Arch based distros.
|
||||
// Under Linux a path starting with two slashes is treated the same as
|
||||
// a path starting with only a single slash, but Wine will refuse to
|
||||
// load any files when the path starts with two slashes. The easiest
|
||||
// way to work around this if this happens is to just add another
|
||||
// leading slash and then normalize the path, since three or more
|
||||
// slashes will be coerced into a single slash.
|
||||
fs::path this_file = boost::dll::this_line_location();
|
||||
if (this_file.string().starts_with("//")) {
|
||||
this_file = ("/" / this_file).lexically_normal();
|
||||
}
|
||||
|
||||
return this_file;
|
||||
}
|
||||
|
||||
bool equals_case_insensitive(const std::string& a, const std::string& b) {
|
||||
return std::equal(a.begin(), a.end(), b.begin(),
|
||||
[](const char& a_char, const char& b_char) {
|
||||
return std::tolower(a_char) == std::tolower(b_char);
|
||||
});
|
||||
}
|
||||
|
||||
std::string join_quoted_strings(std::vector<std::string>& strings) {
|
||||
bool is_first = true;
|
||||
std::ostringstream joined_strings{};
|
||||
for (const auto& option : strings) {
|
||||
joined_strings << (is_first ? "'" : ", '") << option << "'";
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
return joined_strings.str();
|
||||
}
|
||||
|
||||
std::string create_logger_prefix(const fs::path& endpoint_base_dir) {
|
||||
// Use the name of the base directory used for our sockets as the logger
|
||||
// prefix, but strip the `yabridge-` part since that's redundant
|
||||
@@ -44,17 +268,9 @@ std::string create_logger_prefix(const fs::path& endpoint_base_dir) {
|
||||
return "[" + endpoint_name + "] ";
|
||||
}
|
||||
|
||||
std::optional<fs::path> find_wineprefix() {
|
||||
std::optional<fs::path> dosdevices_dir =
|
||||
find_dominating_file("dosdevices", find_vst_plugin(), fs::is_directory);
|
||||
if (!dosdevices_dir) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return dosdevices_dir->parent_path();
|
||||
}
|
||||
|
||||
fs::path find_vst_host(LibArchitecture plugin_arch, bool use_plugin_groups) {
|
||||
fs::path find_vst_host(const boost::filesystem::path& this_plugin_path,
|
||||
LibArchitecture plugin_arch,
|
||||
bool use_plugin_groups) {
|
||||
auto host_name = use_plugin_groups ? yabridge_group_host_name
|
||||
: yabridge_individual_host_name;
|
||||
if (plugin_arch == LibArchitecture::dll_32) {
|
||||
@@ -62,8 +278,10 @@ fs::path find_vst_host(LibArchitecture plugin_arch, bool use_plugin_groups) {
|
||||
: yabridge_individual_host_name_32bit;
|
||||
}
|
||||
|
||||
// If our `.so` file is a symlink, then search for the host in the directory
|
||||
// of the file that symlink points to
|
||||
fs::path host_path =
|
||||
fs::canonical(get_this_file_location()).remove_filename() / host_name;
|
||||
fs::canonical(this_plugin_path).remove_filename() / host_name;
|
||||
if (fs::exists(host_path)) {
|
||||
return host_path;
|
||||
}
|
||||
@@ -80,35 +298,6 @@ fs::path find_vst_host(LibArchitecture plugin_arch, bool use_plugin_groups) {
|
||||
return vst_host_path;
|
||||
}
|
||||
|
||||
fs::path find_vst_plugin() {
|
||||
// TODO: This has to be able to differentiate between VST2 plugins and VST3
|
||||
// modules
|
||||
const fs::path this_plugin_path = get_this_file_location();
|
||||
|
||||
fs::path plugin_path(this_plugin_path);
|
||||
plugin_path.replace_extension(".dll");
|
||||
if (fs::exists(plugin_path)) {
|
||||
// Also resolve symlinks here, to support symlinked .dll files
|
||||
return fs::canonical(plugin_path);
|
||||
}
|
||||
|
||||
// In case this files does not exist and our `.so` file is a symlink, we'll
|
||||
// also repeat this check after resolving that symlink to support links to
|
||||
// copies of `libyabridge-vst2.so` as described in issue #3
|
||||
fs::path alternative_plugin_path = fs::canonical(this_plugin_path);
|
||||
alternative_plugin_path.replace_extension(".dll");
|
||||
if (fs::exists(alternative_plugin_path)) {
|
||||
return fs::canonical(alternative_plugin_path);
|
||||
}
|
||||
|
||||
// This function is used in the constructor's initializer list so we have to
|
||||
// throw when the path could not be found
|
||||
throw std::runtime_error("'" + plugin_path.string() +
|
||||
"' does not exist, make sure to rename "
|
||||
"'libyabridge-vst2.so' to match a "
|
||||
"VST plugin .dll file.");
|
||||
}
|
||||
|
||||
boost::filesystem::path generate_group_endpoint(
|
||||
const std::string& group_name,
|
||||
const boost::filesystem::path& wine_prefix,
|
||||
@@ -145,24 +334,6 @@ std::vector<boost::filesystem::path> get_augmented_search_path() {
|
||||
return search_path;
|
||||
}
|
||||
|
||||
fs::path get_this_file_location() {
|
||||
// HACK: Not sure why, but `boost::dll::this_line_location()` returns a path
|
||||
// starting with a double slash on some systems. I've seen this happen
|
||||
// on both Ubuntu 18.04 and 20.04, but not on Arch based distros.
|
||||
// Under Linux a path starting with two slashes is treated the same as
|
||||
// a path starting with only a single slash, but Wine will refuse to
|
||||
// load any files when the path starts with two slashes. The easiest
|
||||
// way to work around this if this happens is to just add another
|
||||
// leading slash and then normalize the path, since three or more
|
||||
// slashes will be coerced into a single slash.
|
||||
fs::path this_file = boost::dll::this_line_location();
|
||||
if (this_file.string().starts_with("//")) {
|
||||
this_file = ("/" / this_file).lexically_normal();
|
||||
}
|
||||
|
||||
return this_file;
|
||||
}
|
||||
|
||||
std::string get_wine_version() {
|
||||
// The '*.exe' scripts generated by winegcc allow you to override the binary
|
||||
// used to run Wine, so will will respect this as well
|
||||
@@ -196,17 +367,6 @@ std::string get_wine_version() {
|
||||
return version_string;
|
||||
}
|
||||
|
||||
std::string join_quoted_strings(std::vector<std::string>& strings) {
|
||||
bool is_first = true;
|
||||
std::ostringstream joined_strings{};
|
||||
for (const auto& option : strings) {
|
||||
joined_strings << (is_first ? "'" : ", '") << option << "'";
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
return joined_strings.str();
|
||||
}
|
||||
|
||||
Configuration load_config_for(const fs::path& yabridge_path) {
|
||||
// First find the closest `yabridge.tmol` file for the plugin, falling back
|
||||
// to default configuration settings if it doesn't exist
|
||||
@@ -218,19 +378,3 @@ Configuration load_config_for(const fs::path& yabridge_path) {
|
||||
|
||||
return Configuration(*config_file, yabridge_path);
|
||||
}
|
||||
|
||||
bp::environment set_wineprefix() {
|
||||
bp::environment env = boost::this_process::environment();
|
||||
|
||||
// Allow the wine prefix to be overridden manually
|
||||
if (!env["WINEPREFIX"].empty()) {
|
||||
return env;
|
||||
}
|
||||
|
||||
const auto wineprefix_path = find_wineprefix();
|
||||
if (wineprefix_path) {
|
||||
env["WINEPREFIX"] = wineprefix_path->string();
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
+139
-48
@@ -16,6 +16,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include <boost/process/async_pipe.hpp>
|
||||
#include <boost/process/environment.hpp>
|
||||
|
||||
@@ -39,6 +41,134 @@ class patched_async_pipe : public boost::process::async_pipe {
|
||||
typedef typename handle_type::executor_type executor_type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marker struct for when we use the default Wine prefix.
|
||||
*/
|
||||
struct DefaultWinePrefix {};
|
||||
|
||||
/**
|
||||
* Marker struct for when the Wine prefix is overriden using the `WINEPREFIX`
|
||||
* environment variable.
|
||||
*/
|
||||
struct OverridenWinePrefix {
|
||||
boost::filesystem::path value;
|
||||
};
|
||||
|
||||
/**
|
||||
* This will locate the plugin we're going to host based on the location of the
|
||||
* `.so` that we're currently operating from and provides information and
|
||||
* utility functions based on that.
|
||||
*/
|
||||
struct PluginInfo {
|
||||
public:
|
||||
/**
|
||||
* Locate the Windows plugin based on the location of this copy of
|
||||
* `libyabridge-{vst2,vst3}.so` file and the type of the plugin we're going
|
||||
* to load. For VST2 plugins this is a file with the same name but with a
|
||||
* `.dll` file extension instead of `.so`. In case this file does not exist
|
||||
* and the `.so` file is a symlink, we'll also repeat this check for the
|
||||
* file it links to. This is to support the workflow described in issue #3
|
||||
* where you use symlinks to copies of `libyabridge-vst2.so`.
|
||||
*
|
||||
* For VST3 plugins there is a strict format as defined by Steinberg, and
|
||||
* we'll have yabridgectl create a 'merged bundle' that also contains the
|
||||
* Windows VST3 plugin.
|
||||
*
|
||||
* TODO: At the moment we can't choose to use the 32-bit VST3 if a 64-bit
|
||||
* plugin exists. Potential solutions are to add a config option to
|
||||
* use the 32-bit version, or we can add a filename suffix to all
|
||||
* 32-bit versions so they can live alongside each other.
|
||||
*
|
||||
* @param plugin_type The type of the plugin we're going to load. The
|
||||
* detection works slightly differently depending on the plugin type.
|
||||
*
|
||||
* @throw std::runtime_error If we cannot find a corresponding Windows
|
||||
* plugin. The error message contains a human readable description of what
|
||||
* went wrong.
|
||||
*/
|
||||
PluginInfo(PluginType plugin_type);
|
||||
|
||||
/**
|
||||
* Create the environment for the plugin host based on `wine_prefix`. If
|
||||
* `WINEPREFIX` was already set then nothing will be changed. Otherwise
|
||||
* we'll set `WINEPREFIX` to the detected Wine prefix, or it will be left
|
||||
* unset if we could not detect a prefix.
|
||||
*/
|
||||
boost::process::environment create_host_env() const;
|
||||
|
||||
/**
|
||||
* Return the path to the actual Wine prefix in use, taking into account
|
||||
* `WINEPREFIX` overrides and the default `~/.wine` fallback.
|
||||
*/
|
||||
boost::filesystem::path normalize_wine_prefix() const;
|
||||
|
||||
const PluginType plugin_type;
|
||||
|
||||
/**
|
||||
* The path to our `.so` file. For VST3 plugins this is *not* the VST3
|
||||
* module (since that has to be bundle on Linux) but rather the .so file
|
||||
* contained in that bundle.
|
||||
*/
|
||||
const boost::filesystem::path native_library_path;
|
||||
|
||||
private:
|
||||
/**
|
||||
* The path to the Windows library (`.dll` or `.vst3`, not to be confused
|
||||
* with a `.vst3` bundle) that we're targeting. This should **not** be
|
||||
* passed to the plugin host and `windows_plugin_path` should be used
|
||||
* instead. We store this intermediate value so we can determine the
|
||||
* plugin's architecture.
|
||||
*/
|
||||
const boost::filesystem::path windows_library_path;
|
||||
|
||||
public:
|
||||
const LibArchitecture plugin_arch;
|
||||
|
||||
/**
|
||||
* The path to the plugin (`.dll` or module) we're going to in the Wine
|
||||
* plugin host.
|
||||
*
|
||||
* For VST2 plugins this will be a `.dll` file. For VST3 plugins this is
|
||||
* normally a directory called `MyPlugin.vst3` that contains
|
||||
* `MyPlugin.vst3/Contents/x86-win/MyPlugin.vst3`, but there's also an older
|
||||
* deprecated (but still ubiquitous) format where the top level
|
||||
* `MyPlugin.vst3` is not a directory but a .dll file. This points to either
|
||||
* of those things, and then `VST3::Hosting::Win32Module::create()` will be
|
||||
* able to load it.
|
||||
*
|
||||
* https://developer.steinberg.help/pages/viewpage.action?pageId=9798275
|
||||
*/
|
||||
const boost::filesystem::path windows_plugin_path;
|
||||
|
||||
/**
|
||||
* The Wine prefix to use for hosting `windows_plugin_path`. If the
|
||||
* `WINEPREFIX` environment variable is set, then that will be used as an
|
||||
* override. Otherwise, we'll try to find the Wine prefix
|
||||
* `windows_plugin_path` is located in. The detection works by looking for a
|
||||
* directory containing a directory called `dosdevices`. If the plugin is
|
||||
* not inside of a Wine prefix, this will be left empty, and the default
|
||||
* prefix will be used instead.
|
||||
*/
|
||||
const std::
|
||||
variant<OverridenWinePrefix, boost::filesystem::path, DefaultWinePrefix>
|
||||
wine_prefix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns equality for two strings when ignoring casing. Used for comparing
|
||||
* filenames inside of Wine prefixes since Windows/Wine does case folding for
|
||||
* filenames.
|
||||
*/
|
||||
bool equals_case_insensitive(const std::string& a, const std::string& b);
|
||||
|
||||
/**
|
||||
* Join a vector of strings with commas while wrapping the strings in quotes.
|
||||
* For example, `join_quoted_strings(std::vector<string>{"string", "another
|
||||
* string", "also a string"})` outputs `"'string', 'another string', 'also a
|
||||
* string'"`. This is used to format the initialisation message.
|
||||
*/
|
||||
std::string join_quoted_strings(std::vector<std::string>& strings);
|
||||
|
||||
/**
|
||||
* Create a logger prefix based on the endpoint base directory used for the
|
||||
* sockets for easy identification. This will result in a prefix of the form
|
||||
@@ -63,6 +193,8 @@ std::string create_logger_prefix(
|
||||
* 2. In the regular search path, augmented with `~/.local/share/yabridge` to
|
||||
* ease the setup process.
|
||||
*
|
||||
* @param this_plugin_path The path to the `.so` file this code is being run
|
||||
* from.
|
||||
* @param plugin_arch The architecture of the plugin, either 64-bit or 32-bit.
|
||||
* Used to determine which host application to use, if available.
|
||||
* @param use_plugin_groups Whether the plugin is using plugin groups and we
|
||||
@@ -70,35 +202,13 @@ std::string create_logger_prefix(
|
||||
*
|
||||
* @return The a path to the VST host, if found.
|
||||
* @throw std::runtime_error If the Wine VST host could not be found.
|
||||
*/
|
||||
boost::filesystem::path find_vst_host(LibArchitecture plugin_arch,
|
||||
bool use_plugin_groups);
|
||||
|
||||
/**
|
||||
* Find the VST plugin .dll file that corresponds to this copy of
|
||||
* `libyabridge-vst2.so`. This should be the same as the name of this file but
|
||||
* with a `.dll` file extension instead of `.so`. In case this file does not
|
||||
* exist and the `.so` file is a symlink, we'll also repeat this check for the
|
||||
* file it links to. This is to support the workflow described in issue #3 where
|
||||
* you use symlinks to copies of `libyabridge-vst2.so`.
|
||||
*
|
||||
* TODO: This should probably be renamed to `find_vst2_plugin()` so we can have
|
||||
* a separate `find_vst3_plugin()`
|
||||
*
|
||||
* @return The a path to the accompanying VST plugin .dll file.
|
||||
* @throw std::runtime_error If no matching .dll file could be found.
|
||||
* TODO: Perhaps also move this somewhere else
|
||||
*/
|
||||
boost::filesystem::path find_vst_plugin();
|
||||
|
||||
/**
|
||||
* Locate the Wine prefix this file is located in, if it is inside of a wine
|
||||
* prefix. This is done by locating the first parent directory that contains a
|
||||
* directory named `dosdevices`.
|
||||
*
|
||||
* @return Either the path to the Wine prefix (containing the `drive_c?`
|
||||
* directory), or `std::nullopt` if it is not inside of a wine prefix.
|
||||
*/
|
||||
std::optional<boost::filesystem::path> find_wineprefix();
|
||||
boost::filesystem::path find_vst_host(
|
||||
const boost::filesystem::path& this_plugin_path,
|
||||
LibArchitecture plugin_arch,
|
||||
bool use_plugin_groups);
|
||||
|
||||
/**
|
||||
* Generate the group socket endpoint name used based on the name of the group,
|
||||
@@ -111,16 +221,12 @@ std::optional<boost::filesystem::path> find_wineprefix();
|
||||
*
|
||||
* @param group_name The name of the plugin group.
|
||||
* @param wine_prefix The name of the Wine prefix in use. This should be
|
||||
* obtained by first calling `set_wineprefix()` to allow the user to override
|
||||
* this, and then falling back to `$HOME/.wine` if the environment variable is
|
||||
* unset. Otherwise plugins run from outwide of a Wine prefix will not be
|
||||
* groupable with those run from within `~/.wine` even though they both run
|
||||
* under the same prefix.
|
||||
* obtained from `PluginInfo::normalize_wine_prefix()`.
|
||||
* @param architecture The architecture the plugin is using, since 64-bit
|
||||
* processes can't host 32-bit plugins and the other way around.
|
||||
*
|
||||
* @return A socket endpoint path that corresponds to the format described
|
||||
* above.
|
||||
* above.
|
||||
*/
|
||||
boost::filesystem::path generate_group_endpoint(
|
||||
const std::string& group_name,
|
||||
@@ -153,14 +259,6 @@ boost::filesystem::path get_this_file_location();
|
||||
*/
|
||||
std::string get_wine_version();
|
||||
|
||||
/**
|
||||
* Join a vector of strings with commas while wrapping the strings in quotes.
|
||||
* For example, `join_quoted_strings(std::vector<string>{"string", "another
|
||||
* string", "also a string"})` outputs `"'string', 'another string', 'also a
|
||||
* string'"`. This is used to format the initialisation message.
|
||||
*/
|
||||
std::string join_quoted_strings(std::vector<std::string>& strings);
|
||||
|
||||
/**
|
||||
* Load the configuration that belongs to a copy of or symlink to
|
||||
* `libyabridge-{vst2,vst3}.so`. If no configuration file could be found then
|
||||
@@ -184,13 +282,6 @@ std::string join_quoted_strings(std::vector<std::string>& strings);
|
||||
*/
|
||||
Configuration load_config_for(const boost::filesystem::path& yabridge_path);
|
||||
|
||||
/**
|
||||
* Locate the Wine prefix and set the `WINEPREFIX` environment variable if
|
||||
* found. This way it's also possible to run .dll files outside of a Wine prefix
|
||||
* using the user's default prefix.
|
||||
*/
|
||||
boost::process::environment set_wineprefix();
|
||||
|
||||
/**
|
||||
* Starting from the starting file or directory, go up in the directory
|
||||
* hierarchy until we find a file named `filename`.
|
||||
|
||||
Reference in New Issue
Block a user