diff --git a/src/common/serialization/common.h b/src/common/serialization/common.h index 5b77082c..3eef07e2 100644 --- a/src/common/serialization/common.h +++ b/src/common/serialization/common.h @@ -41,15 +41,6 @@ static_assert(std::is_same_v); 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 -struct overload : Ts... { - using Ts::operator()...; -}; -template -overload(Ts...) -> overload; - /** * 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 diff --git a/src/common/serialization/vst2.h b/src/common/serialization/vst2.h index 16766edd..eae8f1d2 100644 --- a/src/common/serialization/vst2.h +++ b/src/common/serialization/vst2.h @@ -24,6 +24,7 @@ #include #include +#include "../utils.h" #include "../vst24.h" #include "common.h" diff --git a/src/common/serialization/vst3.h b/src/common/serialization/vst3.h index 72396435..065bcb1b 100644 --- a/src/common/serialization/vst3.h +++ b/src/common/serialization/vst3.h @@ -21,6 +21,7 @@ #include #include "../configuration.h" +#include "../utils.h" #include "common.h" // Event handling for our VST3 plugins works slightly different from how we diff --git a/src/common/utils.h b/src/common/utils.h index a21fb0dd..909411b6 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -21,6 +21,15 @@ #endif #include +// The cannonical overloading template for `std::visitor`, not sure why this +// isn't part of the standard library +template +struct overload : Ts... { + using Ts::operator()...; +}; +template +overload(Ts...) -> overload; + /** * Return the path to the directory for story temporary files. This will be * `$XDG_RUNTIME_DIR` if set, and `/tmp` otherwise. diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h index a252cc4e..6df18c44 100644 --- a/src/plugin/bridges/common.h +++ b/src/plugin/bridges/common.h @@ -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 - 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(std::make_unique( 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( 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() << " "; - } else { - init_msg << find_wineprefix().value_or("").string(); - } + std::visit( + overload{ + [&](const OverridenWinePrefix& prefix) { + init_msg << prefix.value.string() << " "; + }, + [&](const boost::filesystem::path& prefix) { + init_msg << prefix.string(); + }, + [&](const DefaultWinePrefix&) { init_msg << ""; }, + }, + 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; diff --git a/src/plugin/bridges/vst2.cpp b/src/plugin/bridges/vst2.cpp index 27011d29..70e64351 100644 --- a/src/plugin/bridges/vst2.cpp +++ b/src/plugin/bridges/vst2.cpp @@ -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( - 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( + 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 diff --git a/src/plugin/bridges/vst3.cpp b/src/plugin/bridges/vst3.cpp index d1c7b02b..fbc5dc54 100644 --- a/src/plugin/bridges/vst3.cpp +++ b/src/plugin/bridges/vst3.cpp @@ -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( - 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( + 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(Logger::create_from_environment( create_logger_prefix(sockets.base_dir)))) { diff --git a/src/plugin/host-process.cpp b/src/plugin/host-process.cpp index 99620788..a130e7fa 100644 --- a/src/plugin/host-process.cpp +++ b/src/plugin/host-process.cpp @@ -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; } diff --git a/src/plugin/host-process.h b/src/plugin/host-process.h index ca5987c0..5c584a61 100644 --- a/src/plugin/host-process.h +++ b/src/plugin/host-process.h @@ -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; /** diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp index 0693b39c..24dc4b1a 100644 --- a/src/plugin/utils.cpp +++ b/src/plugin/utils.cpp @@ -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 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 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 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& 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 find_wineprefix() { - std::optional 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 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& 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; -} diff --git a/src/plugin/utils.h b/src/plugin/utils.h index c68175be..1ad98d81 100644 --- a/src/plugin/utils.h +++ b/src/plugin/utils.h @@ -16,6 +16,8 @@ #pragma once +#include + #include #include @@ -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 + 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", "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& 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 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 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", "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& 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& 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`.