From 81388873daef43b87c5422172c0a9910e2aec88a Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sat, 31 Jan 2026 16:55:06 +0000 Subject: [PATCH] Add bits for cross-compiling to Windows using MinGW Some tests have been updated because UTF-8 is used as the native path encoding with MinGW/Wine, unlike MSVC/Windows. Some of the tests fail: - 4 Rust tests fail because long paths are not enabled and so the paths used when creating symlinks and junction paths are too long. I've tested them with x86_64-pc-windows-gnu and x86_64-pc-windows-gnullvm, and both see the same behaviour. The tests pass when the MinGW-built executable is run on Windows, so this is a Wine limitation. - 12 C++ tests fail because directory symlink creation is not implemented. They fail whether the MinGW-built executable is run in Wine or on Windows, so this is a MinGW limitation. - 1 C++ filesystem test fails because long paths are not enabled. The failing tests are skipped at runtime when built with MinGW, aside from the one test for long paths being enabled, which expects them to be disabled when built with MinGW. If long paths are enabled, e.g. by running wine reg add HKLM\\System\\CurrentControlSet\\Control\\Filesystem /v LongPathsEnabled /t REG_DWORD /d 1 /f then many more tests fail because the C++ tests create long paths when that Registry value is set, but it doesn't seem to actually enable long path support in Wine, so various filesystem operations fail. --- cpp/CMakeLists.txt | 11 ++++ cpp/README.md | 7 ++ cpp/cmake/toolchain-mingw64.cmake | 19 ++++++ .../api/interface/create_game_handle_test.h | 64 ++++++++++++++++--- cpp/src/tests/api/interface/main.cpp | 31 +++++++-- src/game.rs | 16 +++++ 6 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 cpp/cmake/toolchain-mingw64.cmake diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 24b6f231..ef67c272 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -29,7 +29,18 @@ else() set(LIBLOOT_CPP_FILENAME "liblibloot_cpp.a") endif() +if(CMAKE_CROSSCOMPILING AND NOT DEFINED RUST_TARGET) + if(WIN32 AND MINGW AND CMAKE_SIZEOF_VOID_P EQUAL 8) + set(RUST_TARGET "x86_64-pc-windows-gnu") + elseif(WIN32 AND MINGW AND CMAKE_SIZEOF_VOID_P EQUAL 4) + set(RUST_TARGET "i686-pc-windows-gnu") + else() + message(FATAL_ERROR "Cross-compiling without RUST_TARGET set, and an appropriate value could not be set automatically!") + endif() +endif() + if(DEFINED RUST_TARGET) + message(STATUS "Building for Rust target ${RUST_TARGET}") set(TARGET_PATH "${PROJECT_SOURCE_DIR}/../target/${RUST_TARGET}") else() set(TARGET_PATH "${PROJECT_SOURCE_DIR}/../target") diff --git a/cpp/README.md b/cpp/README.md index 34781e6d..697fb481 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -25,6 +25,13 @@ cmake -B build . -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build --parallel ``` +To cross-compile on Linux for Windows: + +``` +cmake -B build . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw64.cmake +cmake --build build --parallel +``` + To build a debug build, pass `Debug` instead of `RelWithDebInfo`. The following CMake variables can be used to configure the build: diff --git a/cpp/cmake/toolchain-mingw64.cmake b/cpp/cmake/toolchain-mingw64.cmake new file mode 100644 index 00000000..8bd755cc --- /dev/null +++ b/cpp/cmake/toolchain-mingw64.cmake @@ -0,0 +1,19 @@ +# toolchain-mingw64.cmake +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +# specify the cross compiler +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +# where is the target environment +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) + +# search for programs in the build host directories +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# for libraries and headers in the target directories +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_CROSSCOMPILING_EMULATOR wine) diff --git a/cpp/src/tests/api/interface/create_game_handle_test.h b/cpp/src/tests/api/interface/create_game_handle_test.h index dfc8814d..6b647fdb 100644 --- a/cpp/src/tests/api/interface/create_game_handle_test.h +++ b/cpp/src/tests/api/interface/create_game_handle_test.h @@ -32,6 +32,53 @@ along with LOOT. If not, see namespace loot { namespace test { +inline std::string replace(std::string_view str, + std::string_view from, + std::string_view to) { + std::string out; + out.reserve(str.size()); + + size_t startPos = 0; + auto findPos = str.find(from, startPos); + while (findPos != std::string_view::npos) { + out.append(str.substr(startPos, findPos - startPos)); + out.append(to); + + startPos = findPos + from.size(); + findPos = str.find(from, startPos); + } + + out.append(str.substr(startPos)); + + return out; +} + +// This is limited to supporting only the UTF-8 strings that are expected to be +// passed in. +inline std::string utf8ToWindows1252(std::string_view str) { + return replace(str, "\xC3\xA9", "\xE9"); +} + +#ifdef _WIN32 +inline void makeJunctionLink(const std::filesystem::path &linkPath, + const std::filesystem::path &targetPath) { +#ifdef __MINGW64__ + const auto linkPathString = + utf8ToWindows1252(std::filesystem::absolute(linkPath).string()); + const auto targetPathString = + utf8ToWindows1252(std::filesystem::absolute(targetPath).string()); +#else + const auto linkPathString = std::filesystem::absolute(linkPath).string(); + const auto targetPathString = std::filesystem::absolute(targetPath).string(); +#endif + + const auto command = + "mklink /J \"" + linkPathString + "\" \"" + targetPathString + "\""; + + system(command.c_str()); +} +#endif + class CreateGameHandleTest : public CommonGameTestFixture, public testing::WithParamInterface { protected: @@ -99,6 +146,13 @@ TEST_P(CreateGameHandleTest, shouldReturnOkIfPassedAnEmptyLocalPathString) { #endif TEST_P(CreateGameHandleTest, shouldReturnOkIfPassedGameAndLocalPathSymlinks) { +#ifdef __MINGW64__ + GTEST_SKIP() + << "This test fails when built with MinGW and run in Wine 11.0 or on " + "Windows, due to a C++ exception with description \"filesystem error: " + "cannot create directory symlink: Function not implemented\""; +#endif + const auto gamePathSymlink = std::filesystem::u8path(gamePath.u8string() + ".symlink"); const auto localPathSymlink = @@ -125,14 +179,8 @@ TEST_P(CreateGameHandleTest, const auto localPathJunctionLink = std::filesystem::u8path(localPath.u8string() + ".junction"); - system(("mklink /J \"" + - std::filesystem::absolute(gamePathJunctionLink).string() + "\" \"" + - std::filesystem::absolute(dataPath).parent_path().string() + "\"") - .c_str()); - system(("mklink /J \"" + - std::filesystem::absolute(localPathJunctionLink).string() + "\" \"" + - std::filesystem::absolute(localPath).string() + "\"") - .c_str()); + makeJunctionLink(gamePathJunctionLink, dataPath.parent_path()); + makeJunctionLink(localPathJunctionLink, localPath); EXPECT_NO_THROW(handle_ = CreateGameHandle( GetParam(), gamePathJunctionLink, localPathJunctionLink)); diff --git a/cpp/src/tests/api/interface/main.cpp b/cpp/src/tests/api/interface/main.cpp index 1df259b0..8036ba39 100644 --- a/cpp/src/tests/api/interface/main.cpp +++ b/cpp/src/tests/api/interface/main.cpp @@ -27,6 +27,10 @@ #include #include "loot/api.h" +#include "tests/api/interface/create_game_handle_test.h" +#include "tests/api/interface/database_interface_test.h" +#include "tests/api/interface/game_interface_test.h" +#include "tests/api/interface/is_compatible_test.h" #include "tests/api/interface/metadata/file_test.h" #include "tests/api/interface/metadata/group_test.h" #include "tests/api/interface/metadata/location_test.h" @@ -35,10 +39,6 @@ #include "tests/api/interface/metadata/plugin_cleaning_data_test.h" #include "tests/api/interface/metadata/plugin_metadata_test.h" #include "tests/api/interface/metadata/tag_test.h" -#include "tests/api/interface/create_game_handle_test.h" -#include "tests/api/interface/database_interface_test.h" -#include "tests/api/interface/game_interface_test.h" -#include "tests/api/interface/is_compatible_test.h" #include "tests/api/interface/plugin_interface_test.h" int main(int argc, char **argv) { @@ -57,9 +57,15 @@ TEST(Filesystem, std::filesystem::path path(utf8); +#ifdef __MINGW64__ + EXPECT_EQ(utf8, path.string()); + EXPECT_EQ(utf8, path.u8string()); + EXPECT_EQ(utf16, path.u16string()); +#else EXPECT_EQ(utf8, path.string()); EXPECT_NE(utf8, path.u8string()); EXPECT_NE(utf16, path.u16string()); +#endif } TEST( @@ -73,7 +79,12 @@ TEST( std::filesystem::path path(utf8, std::locale::classic()); +#ifdef __MINGW64__ + // The string is double-encoded as UTF-8. + EXPECT_EQ(u8"Andr\xC3\x83\xC2\xA9_settings.toml", path.string()); +#else EXPECT_EQ(utf8, path.string()); +#endif EXPECT_NE(utf8, path.u8string()); EXPECT_NE(utf16, path.u16string()); @@ -103,7 +114,7 @@ TEST(Filesystem, u8pathConvertsCharacterEncodingFromUtf8ToNative) { std::filesystem::path path = std::filesystem::u8path(utf8); -#ifdef _WIN32 +#if defined(_WIN32) && !defined(__MINGW64__) EXPECT_NE(utf8, path.string()); #else EXPECT_EQ(utf8, path.string()); @@ -114,8 +125,14 @@ TEST(Filesystem, u8pathConvertsCharacterEncodingFromUtf8ToNative) { } #ifdef _WIN32 -TEST(WindowsRegistry, hasLongPathsEnabled) { - EXPECT_TRUE(loot::test::windowsHasLongPathsEnabled()); +TEST(WindowsRegistry, hasLongPathsEnabled) { +#ifdef __MINGW64__ + // MinGW does not support long paths, most tests will fail if they're + // enabled. + EXPECT_FALSE(loot::test::windowsHasLongPathsEnabled()); +#else + EXPECT_TRUE(loot::test::windowsHasLongPathsEnabled()); +#endif } #endif diff --git a/src/game.rs b/src/game.rs index 1c3c1ae0..eda8ecb0 100644 --- a/src/game.rs +++ b/src/game.rs @@ -970,6 +970,10 @@ mod tests { } #[test] + #[cfg_attr( + all(windows, target_env = "gnu"), + ignore = "Cannot resolve the symlink's long path when run in Wine" + )] fn should_succeed_if_given_a_symlink_path() { let fixture = Fixture::without_long_paths(GameType::Morrowind); @@ -985,6 +989,10 @@ mod tests { #[cfg(windows)] #[test] + #[cfg_attr( + all(windows, target_env = "gnu"), + ignore = "Creating the junction link causes a page fault exception when run in Wine" + )] fn should_succeed_if_given_a_junction_link_path() { let fixture = Fixture::new(GameType::Morrowind); @@ -1054,6 +1062,10 @@ mod tests { } #[test] + #[cfg_attr( + all(windows, target_env = "gnu"), + ignore = "Cannot resolve the symlink's long path when run in Wine" + )] fn should_succeed_if_given_symlink_paths() { let fixture = Fixture::without_long_paths(GameType::Oblivion); @@ -1080,6 +1092,10 @@ mod tests { #[cfg(windows)] #[test] + #[cfg_attr( + all(windows, target_env = "gnu"), + ignore = "Creating the junction link causes a page fault exception when run in Wine" + )] fn should_succeed_if_given_junction_link_paths() { let fixture = Fixture::new(GameType::Oblivion);