diff options
author | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
---|---|---|
committer | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
commit | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp | |
parent | 537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff) | |
download | winamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz |
Initial community commit
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp')
-rw-r--r-- | Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp | 750 |
1 files changed, 750 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp b/Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp new file mode 100644 index 00000000..0219f915 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/misc/mptWine.cpp @@ -0,0 +1,750 @@ +/* + * mptWine.cpp + * ----------- + * Purpose: Wine stuff. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "mptWine.h" + +#include "mptOS.h" +#include "../common/mptFileIO.h" + +#include <deque> +#include <map> + +#if MPT_OS_WINDOWS +#include <windows.h> +#endif + + +OPENMPT_NAMESPACE_BEGIN + + +#if MPT_OS_WINDOWS + + +namespace mpt +{ +namespace Wine +{ + + + +Context::Context(mpt::OS::Wine::VersionContext versionContext) + : m_VersionContext(versionContext) + , wine_get_dos_file_name(nullptr) + , wine_get_unix_file_name(nullptr) +{ + if(!mpt::OS::Windows::IsWine()) + { + throw mpt::Wine::Exception("Wine not detected."); + } + if(!m_VersionContext.Version().IsValid()) + { + throw mpt::Wine::Exception("Unknown Wine version detected."); + } + m_Kernel32 = std::make_shared<std::optional<mpt::library>>(mpt::library::load({ mpt::library::path_search::system, mpt::library::path_prefix::none, MPT_PATH("kernel32.dll"), mpt::library::path_suffix::none })); + if(!m_Kernel32->has_value()) + { + throw mpt::Wine::Exception("Could not load Wine kernel32.dll."); + } + if(!(*m_Kernel32)->bind(wine_get_unix_file_name, "wine_get_unix_file_name")) + { + throw mpt::Wine::Exception("Could not bind Wine kernel32.dll:wine_get_unix_file_name."); + } + if(!(*m_Kernel32)->bind(wine_get_dos_file_name, "wine_get_dos_file_name")) + { + throw mpt::Wine::Exception("Could not bind Wine kernel32.dll:wine_get_dos_file_name."); + } + { + std::string out; + std::string err; + try + { + if(ExecutePosixShellCommand("uname -m", out, err) != 0) + { + throw mpt::Wine::Exception("Wine 'uname -m' failed."); + } + if(!err.empty()) + { + throw mpt::Wine::Exception("Wine 'uname -m' failed."); + } + out = mpt::trim(out, std::string("\r\n")); + m_Uname_m = out; + } catch(const std::exception &) + { + m_Uname_m = std::string(); + } + } + try + { + m_HOME = GetPosixEnvVar("HOME"); + } catch(const std::exception &) + { + m_HOME = std::string(); + } + try + { + m_XDG_DATA_HOME = GetPosixEnvVar("XDG_DATA_HOME"); + if(m_XDG_DATA_HOME.empty()) + { + m_XDG_DATA_HOME = m_HOME + "/.local/share"; + } + } catch(const std::exception &) + { + m_XDG_DATA_HOME = std::string(); + } + try + { + m_XDG_CACHE_HOME = GetPosixEnvVar("XDG_CACHE_HOME"); + if(m_XDG_CACHE_HOME.empty()) + { + m_XDG_CACHE_HOME = m_HOME + "/.cache"; + } + } catch(const std::exception &) + { + m_XDG_CACHE_HOME = std::string(); + } + try + { + m_XDG_CONFIG_HOME = GetPosixEnvVar("XDG_CONFIG_HOME"); + if(m_XDG_CONFIG_HOME.empty()) + { + m_XDG_CONFIG_HOME = m_HOME + "/.config"; + } + } catch(const std::exception &) + { + m_XDG_CONFIG_HOME = std::string(); + } +} + + +std::string Context::PathToPosix(mpt::PathString windowsPath) +{ + std::string result; + if(windowsPath.empty()) + { + return result; + } + if(windowsPath.Length() >= 32000) + { + throw mpt::Wine::Exception("Path too long."); + } + LPSTR tmp = nullptr; + tmp = wine_get_unix_file_name(windowsPath.ToWide().c_str()); + if(!tmp) + { + throw mpt::Wine::Exception("Wine kernel32.dll:wine_get_unix_file_name failed."); + } + result = tmp; + HeapFree(GetProcessHeap(), 0, tmp); + tmp = nullptr; + return result; +} + +mpt::PathString Context::PathToWindows(std::string hostPath) +{ + mpt::PathString result; + if(hostPath.empty()) + { + return result; + } + if(hostPath.length() >= 32000) + { + throw mpt::Wine::Exception("Path too long."); + } + LPWSTR tmp = nullptr; + tmp = wine_get_dos_file_name(hostPath.c_str()); + if(!tmp) + { + throw mpt::Wine::Exception("Wine kernel32.dll:wine_get_dos_file_name failed."); + } + result = mpt::PathString::FromWide(tmp); + HeapFree(GetProcessHeap(), 0, tmp); + tmp = nullptr; + return result; +} + +std::string Context::PathToPosixCanonical(mpt::PathString windowsPath) +{ + std::string result; + std::string hostPath = PathToPosix(windowsPath); + if(hostPath.empty()) + { + return result; + } + std::string output; + std::string error; + int exitcode = ExecutePosixShellCommand(std::string() + "readlink -f " + EscapePosixShell(hostPath), output, error); + if(!error.empty()) + { + throw mpt::Wine::Exception("Wine readlink failed: " + error); + } + if(exitcode != 0 && exitcode != 1) + { + throw mpt::Wine::Exception("Wine readlink failed."); + } + std::string trimmedOutput = mpt::trim(output, std::string("\r\n")); + result = trimmedOutput; + return result; +} + + +static void ExecutePosixCommandProgressDefault(void * /*userdata*/ ) +{ + ::Sleep(10); + return; +} + +static ExecuteProgressResult ExecutePosixShellScriptProgressDefault(void * /*userdata*/ ) +{ + ::Sleep(10); + return ExecuteProgressContinueWaiting; +} + + +std::string Context::EscapePosixShell(std::string line) +{ + const char escape_chars [] = { '|', '&', ';', '<', '>', '(', ')', '$', '`', '"', '\'', ' ', '\t' }; + const char maybe_escape_chars [] = { '*', '?', '[', '#', '~', '=', '%' }; + line = mpt::replace(line, std::string("\\"), std::string("\\\\")); + for(char c : escape_chars) + { + line = mpt::replace(line, std::string(1, c), std::string("\\") + std::string(1, c)); + } + for(char c : maybe_escape_chars) + { + line = mpt::replace(line, std::string(1, c), std::string("\\") + std::string(1, c)); + } + return line; +} + + +ExecResult Context::ExecutePosixShellScript(std::string script, FlagSet<ExecFlags> flags, std::map<std::string, std::vector<char> > filetree, std::string title, ExecutePosixCommandProgress progress, ExecutePosixShellScriptProgress progressCancel, void *userdata) +{ + // Relevant documentation: + // https://stackoverflow.com/questions/6004070/execute-shell-commands-from-program-running-in-wine + // https://www.winehq.org/pipermail/wine-bugs/2014-January/374918.html + // https://bugs.winehq.org/show_bug.cgi?id=34730 + + if(!progress) progress = &ExecutePosixCommandProgressDefault; + if(!progressCancel) progressCancel = &ExecutePosixShellScriptProgressDefault; + + if(flags[ExecFlagInteractive]) flags.reset(ExecFlagSilent); + if(flags[ExecFlagSplitOutput]) flags.set(ExecFlagSilent); + + std::vector<mpt::PathString> tempfiles; + + progress(userdata); + + mpt::TempDirGuard dirWindowsTemp(mpt::CreateTempFileName()); + if(dirWindowsTemp.GetDirname().empty()) + { + throw mpt::Wine::Exception("Creating temporary directoy failed."); + } + const std::string dirPosix = PathToPosix(dirWindowsTemp.GetDirname()); + if(dirPosix.empty()) + { + throw mpt::Wine::Exception("mpt::Wine::ConvertWindowsPathToHost returned empty path."); + } + const std::string dirPosixEscape = EscapePosixShell(dirPosix); + const mpt::PathString dirWindows = dirWindowsTemp.GetDirname(); + + progress(userdata); + + // write the script to disk + mpt::PathString scriptFilenameWindows = dirWindows + P_("script.sh"); + { + mpt::ofstream tempfile(scriptFilenameWindows, std::ios::binary); + tempfile << script; + tempfile.flush(); + if(!tempfile) + { + throw mpt::Wine::Exception("Error writing script.sh."); + } + } + const std::string scriptFilenamePosix = PathToPosix(scriptFilenameWindows); + if(scriptFilenamePosix.empty()) + { + throw mpt::Wine::Exception("Error converting script.sh path."); + } + const std::string scriptFilenamePosixEscape = EscapePosixShell(scriptFilenamePosix); + + progress(userdata); + + // create a wrapper that will call the script and gather result. + mpt::PathString wrapperstarterFilenameWindows = dirWindows + P_("wrapperstarter.sh"); + { + mpt::ofstream tempfile(wrapperstarterFilenameWindows, std::ios::binary); + std::string wrapperstarterscript; + wrapperstarterscript += std::string() + "#!/usr/bin/env sh" "\n"; + wrapperstarterscript += std::string() + "exec /usr/bin/env sh " + dirPosixEscape + "wrapper.sh" "\n"; + tempfile << wrapperstarterscript; + tempfile.flush(); + if(!tempfile) + { + throw mpt::Wine::Exception("Error writing wrapper.sh."); + } + } + mpt::PathString wrapperFilenameWindows = dirWindows + P_("wrapper.sh"); + std::string cleanupscript; + { + mpt::ofstream tempfile(wrapperFilenameWindows, std::ios::binary); + std::string wrapperscript; + if(!flags[ExecFlagSilent]) + { + wrapperscript += "printf \"\\033]0;" + title + "\\a\"" "\n"; + } + wrapperscript += "chmod u+x " + scriptFilenamePosixEscape + "\n"; + wrapperscript += "cd " + dirPosixEscape + "filetree" "\n"; + if(flags[ExecFlagInteractive]) + { // no stdout/stderr capturing for interactive scripts + wrapperscript += scriptFilenamePosixEscape + "\n"; + wrapperscript += "MPT_RESULT=$?" "\n"; + wrapperscript += "echo ${MPT_RESULT} > " + dirPosixEscape + "exit" "\n"; + } else if(flags[ExecFlagSplitOutput]) + { + wrapperscript += "(" + scriptFilenamePosixEscape + "; echo $? >&4) 4>" + dirPosixEscape + "exit 1>" + dirPosixEscape + "out 2>" + dirPosixEscape + "err" "\n"; + } else + { + wrapperscript += "(" + scriptFilenamePosixEscape + "; echo $? >&4) 2>&1 4>" + dirPosixEscape + "exit | tee " + dirPosixEscape + "out" "\n"; + } + wrapperscript += "echo done > " + dirPosixEscape + "done" "\n"; + cleanupscript += "rm " + dirPosixEscape + "done" "\n"; + cleanupscript += "rm " + dirPosixEscape + "exit" "\n"; + if(flags[ExecFlagInteractive]) + { + // nothing + } else if(flags[ExecFlagSplitOutput]) + { + cleanupscript += "rm " + dirPosixEscape + "out" "\n"; + cleanupscript += "rm " + dirPosixEscape + "err" "\n"; + } else + { + cleanupscript += "rm " + dirPosixEscape + "out" "\n"; + } + cleanupscript += "rm -r " + dirPosixEscape + "filetree" "\n"; + cleanupscript += "rm " + dirPosixEscape + "script.sh" "\n"; + cleanupscript += "rm " + dirPosixEscape + "wrapper.sh" "\n"; + cleanupscript += "rm " + dirPosixEscape + "wrapperstarter.sh" "\n"; + cleanupscript += "rm " + dirPosixEscape + "terminal.sh" "\n"; + if(flags[ExecFlagAsync]) + { + wrapperscript += cleanupscript; + cleanupscript.clear(); + } + tempfile << wrapperscript; + tempfile.flush(); + if(!tempfile) + { + throw mpt::Wine::Exception("Error writing wrapper.sh."); + } + } + + progress(userdata); + + ::CreateDirectory((dirWindows + P_("filetree")).AsNative().c_str(), nullptr); + for(const auto &file : filetree) + { + std::vector<mpt::ustring> path = mpt::String::Split<mpt::ustring>(mpt::ToUnicode(mpt::Charset::UTF8, file.first), U_("/")); + mpt::PathString combinedPath = dirWindows + P_("filetree") + P_("\\"); + if(path.size() > 1) + { + for(std::size_t singlepath = 0; singlepath < path.size() - 1; ++singlepath) + { + if(path[singlepath].empty()) + { + continue; + } + combinedPath += mpt::PathString::FromUnicode(path[singlepath]); + if(!combinedPath.IsDirectory()) + { + if(::CreateDirectory(combinedPath.AsNative().c_str(), nullptr) == 0) + { + throw mpt::Wine::Exception("Error writing filetree."); + } + } + combinedPath += P_("\\"); + } + } + try + { + mpt::LazyFileRef out(dirWindows + P_("filetree") + P_("\\") + mpt::PathString::FromUTF8(mpt::replace(file.first, std::string("/"), std::string("\\")))); + out = file.second; + } catch(std::exception &) + { + throw mpt::Wine::Exception("Error writing filetree."); + } + } + + progress(userdata); + + // create a wrapper that will find a suitable terminal and run the wrapper script in the terminal window. + mpt::PathString terminalWrapperFilenameWindows = dirWindows + P_("terminal.sh"); + { + mpt::ofstream tempfile(terminalWrapperFilenameWindows, std::ios::binary); + // NOTE: + // Modern terminals detach themselves from the invoking shell if another instance is already present. + // This means we cannot rely on terminal invocation being syncronous. + static constexpr const char * terminals[] = + { + "x-terminal-emulator", + "konsole", + "mate-terminal", + "xfce4-terminal", + "gnome-terminal", + "uxterm", + "xterm", + "rxvt", + }; + std::string terminalscript = "\n"; + for(const std::string terminal : terminals) + { + // mate-terminal on Debian 8 cannot execute commands with arguments, + // thus we use a separate script that requires no arguments to execute. + terminalscript += "if command -v " + terminal + " 2>/dev/null 1>/dev/null ; then" "\n"; + terminalscript += " chmod u+x " + dirPosixEscape + "wrapperstarter.sh" "\n"; + terminalscript += " exec `command -v " + terminal + "` -e \"" + dirPosixEscape + "wrapperstarter.sh\"" "\n"; + terminalscript += "fi" "\n"; + } + + tempfile << terminalscript; + tempfile.flush(); + if(!tempfile) + { + return ExecResult::Error(); + } + } + + progress(userdata); + + // build unix command line + std::string unixcommand; + bool createProcessSuccess = false; + + if(!createProcessSuccess) + { + + if(flags[ExecFlagSilent]) + { + unixcommand = "/usr/bin/env sh \"" + dirPosixEscape + "wrapper.sh\""; + } else + { + unixcommand = "/usr/bin/env sh \"" + dirPosixEscape + "terminal.sh\""; + } + + progress(userdata); + + std::wstring unixcommandW = mpt::ToWide(mpt::Charset::UTF8, unixcommand); + std::wstring titleW = mpt::ToWide(mpt::Charset::UTF8, title); + STARTUPINFOW startupInfo = {}; + startupInfo.lpTitle = titleW.data(); + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInformation = {}; + + progress(userdata); + + BOOL success = FALSE; + if(flags[ExecFlagSilent]) + { + success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &startupInfo, &processInformation); + } else + { + success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &startupInfo, &processInformation); + } + + progress(userdata); + + createProcessSuccess = (success != FALSE); + + progress(userdata); + + if(success) + { + + if(!flags[ExecFlagAsync]) + { + // note: execution is not syncronous with all Wine versions, + // we additionally explicitly wait for "done" later + while(WaitForSingleObject(processInformation.hProcess, 0) == WAIT_TIMEOUT) + { // wait + if(progressCancel(userdata) != ExecuteProgressContinueWaiting) + { + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + throw mpt::Wine::Exception("Canceled."); + } + } + } + + progress(userdata); + + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + + } + + } + + progress(userdata); + + // Work around Wine being unable to execute PIE binaries on Debian 9. + // Luckily, /bin/bash is still non-PIE on Debian 9. + + if(!createProcessSuccess) + { + if(flags[ExecFlagSilent]) + { + unixcommand = "/bin/bash \"" + dirPosixEscape + "wrapper.sh\""; + } else + { + unixcommand = "/bin/bash \"" + dirPosixEscape + "terminal.sh\""; + } + + progress(userdata); + + std::wstring unixcommandW = mpt::ToWide(mpt::Charset::UTF8, unixcommand); + std::wstring titleW = mpt::ToWide(mpt::Charset::UTF8, title); + STARTUPINFOW startupInfo = {}; + startupInfo.lpTitle = titleW.data(); + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInformation = {}; + + progress(userdata); + + BOOL success = FALSE; + if(flags[ExecFlagSilent]) + { + success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &startupInfo, &processInformation); + } else + { + success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &startupInfo, &processInformation); + } + + progress(userdata); + + createProcessSuccess = (success != FALSE); + + progress(userdata); + + if(success) + { + if(!flags[ExecFlagAsync]) + { + // note: execution is not syncronous with all Wine versions, + // we additionally explicitly wait for "done" later + while(WaitForSingleObject(processInformation.hProcess, 0) == WAIT_TIMEOUT) + { // wait + if(progressCancel(userdata) != ExecuteProgressContinueWaiting) + { + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + throw mpt::Wine::Exception("Canceled."); + } + } + } + + progress(userdata); + + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + } + + } + + progress(userdata); + + if(!createProcessSuccess) + { + throw mpt::Wine::Exception("CreateProcess failed."); + } + + progress(userdata); + + if(flags[ExecFlagAsync]) + { + ExecResult result; + result.exitcode = 0; + return result; + } + + while(!(dirWindows + P_("done")).IsFile()) + { // wait + if(progressCancel(userdata) != ExecuteProgressContinueWaiting) + { + throw mpt::Wine::Exception("Canceled."); + } + } + + progress(userdata); + + int exitCode = 0; + { + mpt::ifstream exitFile(dirWindows + P_("exit"), std::ios::binary); + if(!exitFile) + { + throw mpt::Wine::Exception("Script .exit file not found."); + } + std::string exitString; + exitFile >> exitString; + if(exitString.empty()) + { + throw mpt::Wine::Exception("Script .exit file empty."); + } + exitCode = ConvertStrTo<int>(exitString); + } + + progress(userdata); + + std::string outputString; + if(!flags[ExecFlagInteractive]) + { + mpt::ifstream outputFile(dirWindows + P_("out"), std::ios::binary); + if(outputFile) + { + outputFile.seekg(0, std::ios::end); + std::streampos outputFileSize = outputFile.tellg(); + outputFile.seekg(0, std::ios::beg); + std::vector<char> outputFileBuf(mpt::saturate_cast<std::size_t>(static_cast<std::streamoff>(outputFileSize))); + outputFile.read(&outputFileBuf[0], outputFileBuf.size()); + outputString = mpt::buffer_cast<std::string>(outputFileBuf); + } + } + + progress(userdata); + + std::string errorString; + if(flags[ExecFlagSplitOutput]) + { + mpt::ifstream errorFile(dirWindows + P_("err"), std::ios::binary); + if(errorFile) + { + errorFile.seekg(0, std::ios::end); + std::streampos errorFileSize = errorFile.tellg(); + errorFile.seekg(0, std::ios::beg); + std::vector<char> errorFileBuf(mpt::saturate_cast<std::size_t>(static_cast<std::streamoff>(errorFileSize))); + errorFile.read(&errorFileBuf[0], errorFileBuf.size()); + errorString = mpt::buffer_cast<std::string>(errorFileBuf); + } + } + + progress(userdata); + + ExecResult result; + result.exitcode = exitCode; + result.output = outputString; + result.error = errorString; + + std::deque<mpt::PathString> paths; + paths.push_back(dirWindows + P_("filetree")); + mpt::PathString basePath = (dirWindows + P_("filetree")).EnsureTrailingSlash(); + while(!paths.empty()) + { + mpt::PathString path = paths.front(); + paths.pop_front(); + path.EnsureTrailingSlash(); + HANDLE hFind = NULL; + WIN32_FIND_DATA wfd = {}; + hFind = FindFirstFile((path + P_("*.*")).AsNative().c_str(), &wfd); + if(hFind != NULL && hFind != INVALID_HANDLE_VALUE) + { + do + { + mpt::PathString filename = mpt::PathString::FromNative(wfd.cFileName); + if(filename != P_(".") && filename != P_("..")) + { + filename = path + filename; + filetree[filename.ToUTF8()] = std::vector<char>(); + if(filename.IsDirectory()) + { + paths.push_back(filename); + } else if(filename.IsFile()) + { + try + { + mpt::LazyFileRef f(filename); + std::vector<char> buf = f; + mpt::PathString treeFilename = mpt::PathString::FromNative(filename.AsNative().substr(basePath.AsNative().length())); + result.filetree[treeFilename.ToUTF8()] = buf; + } catch (std::exception &) + { + // nothing?! + } + } + } + } while(FindNextFile(hFind, &wfd)); + FindClose(hFind); + } + } + + mpt::DeleteWholeDirectoryTree(dirWindows); + + return result; + +} + + +int Context::ExecutePosixShellCommand(std::string command, std::string & output, std::string & error) +{ + std::string script; + script += "#!/usr/bin/env sh" "\n"; + script += "exec " + command + "\n"; + mpt::Wine::ExecResult execResult = ExecutePosixShellScript + ( script + , mpt::Wine::ExecFlagSilent | mpt::Wine::ExecFlagSplitOutput, std::map<std::string, std::vector<char> >() + , std::string() + , nullptr + , nullptr + , nullptr + ); + output = execResult.output; + error = execResult.error; + return execResult.exitcode; +} + + +std::string Context::GetPosixEnvVar(std::string var, std::string def) +{ + // We cannot use std::getenv here because Wine overrides SOME env vars, + // in particular, HOME is unset in the Wine environment. + // Instead, we just spawn a shell that will catch up a sane environment on + // its own. + std::string output; + std::string error; + int exitcode = ExecutePosixShellCommand(std::string() + "echo $" + var, output, error); + if(!error.empty()) + { + throw mpt::Wine::Exception("Wine echo $var failed: " + error); + } + if(exitcode != 0) + { + throw mpt::Wine::Exception("Wine echo $var failed."); + } + std::string result = mpt::trim_right(output, std::string("\r\n")); + if(result.empty()) + { + result = def; + } + return result; +} + + +} // namespace Wine +} // namespace mpt + + +#else // !MPT_OS_WINDOWS + + +MPT_MSVC_WORKAROUND_LNK4221(mptWine) + + +#endif // MPT_OS_WINDOWS + + +OPENMPT_NAMESPACE_END |