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.
This commit is contained in:
Oliver Hamlet
2026-02-03 08:52:23 +00:00
parent 6fb3c8fb6c
commit 81388873da
6 changed files with 133 additions and 15 deletions
+11
View File
@@ -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")
+7
View File
@@ -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:
+19
View File
@@ -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)
@@ -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<GameType> {
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));
+24 -7
View File
@@ -27,6 +27,10 @@
#include <filesystem>
#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