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);