diff --git a/CMakeLists.txt b/CMakeLists.txt index ceaac9a78..41a84ca95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,6 +249,26 @@ if (ENABLE_QT) if (ENABLE_QT_TRANSLATION) find_package(Qt6 REQUIRED COMPONENTS LinguistTools) endif() + + if (NOT DEFINED QT_TARGET_PATH) + # Determine the location of the compile target's Qt. + get_target_property(qtcore_path Qt6::Core LOCATION_Release) + string(FIND "${qtcore_path}" "/bin/" qtcore_path_bin_pos REVERSE) + string(FIND "${qtcore_path}" "/lib/" qtcore_path_lib_pos REVERSE) + if (qtcore_path_bin_pos GREATER qtcore_path_lib_pos) + string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_bin_pos} QT_TARGET_PATH) + else() + string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_lib_pos} QT_TARGET_PATH) + endif() + endif() + + if (NOT DEFINED QT_HOST_PATH) + # Use the same for host Qt if none is defined. + set(QT_HOST_PATH "${QT_TARGET_PATH}") + endif() + + message(STATUS "Using target Qt at ${QT_TARGET_PATH}") + message(STATUS "Using host Qt at ${QT_HOST_PATH}") endif() # Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic) diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index f5b1c06f4..220d79cda 100644 --- a/CMakeModules/BundleTarget.cmake +++ b/CMakeModules/BundleTarget.cmake @@ -2,37 +2,104 @@ if (BUNDLE_TARGET_EXECUTE) # --- Bundling method logic --- + function(symlink_safe_copy from to) + if (WIN32) + # Use cmake copy for maximum compatibility. + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${from}" "${to}" + RESULT_VARIABLE cp_result) + else() + # Use native copy to turn symlinks into normal files. + execute_process(COMMAND cp -L "${from}" "${to}" + RESULT_VARIABLE cp_result) + endif() + if (NOT cp_result EQUAL "0") + message(FATAL_ERROR "cp \"${from}\" \"${to}\" failed: ${cp_result}") + endif() + endfunction() + function(bundle_qt executable_path) if (WIN32) + # Perform standalone bundling first to copy over all used libraries, as windeployqt does not do this. + bundle_standalone("${executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}") + get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY) - find_program(windeployqt_executable windeployqt6) # Create a qt.conf file pointing to the app directory. # This ensures Qt can find its plugins. - file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nprefix = .") + file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nPrefix = .") + + find_program(windeployqt_executable windeployqt6 PATHS "${QT_HOST_PATH}/bin") + find_program(qtpaths_executable qtpaths6 PATHS "${QT_HOST_PATH}/bin") + + # TODO: Hack around windeployqt's poor cross-compilation support by + # TODO: making a local copy with a prefix pointing to the target Qt. + if (NOT "${QT_HOST_PATH}" STREQUAL "${QT_TARGET_PATH}") + set(windeployqt_dir "${BINARY_PATH}/windeployqt_copy") + file(MAKE_DIRECTORY "${windeployqt_dir}") + symlink_safe_copy("${windeployqt_executable}" "${windeployqt_dir}/windeployqt.exe") + symlink_safe_copy("${qtpaths_executable}" "${windeployqt_dir}/qtpaths.exe") + symlink_safe_copy("${QT_HOST_PATH}/bin/Qt6Core.dll" "${windeployqt_dir}") + + if (EXISTS "${QT_TARGET_PATH}/share") + # Unix-style Qt; we need to wire up the paths manually. + file(WRITE "${windeployqt_dir}/qt.conf" "\ + [Paths]\n + Prefix = ${QT_TARGET_PATH}\n \ + ArchData = ${QT_TARGET_PATH}/share/qt6\n \ + Binaries = ${QT_TARGET_PATH}/bin\n \ + Data = ${QT_TARGET_PATH}/share/qt6\n \ + Documentation = ${QT_TARGET_PATH}/share/qt6/doc\n \ + Headers = ${QT_TARGET_PATH}/include/qt6\n \ + Libraries = ${QT_TARGET_PATH}/lib\n \ + LibraryExecutables = ${QT_TARGET_PATH}/share/qt6/bin\n \ + Plugins = ${QT_TARGET_PATH}/share/qt6/plugins\n \ + QmlImports = ${QT_TARGET_PATH}/share/qt6/qml\n \ + Translations = ${QT_TARGET_PATH}/share/qt6/translations\n \ + ") + else() + # Windows-style Qt; the defaults should suffice. + file(WRITE "${windeployqt_dir}/qt.conf" "[Paths]\nPrefix = ${QT_TARGET_PATH}") + endif() + + set(windeployqt_executable "${windeployqt_dir}/windeployqt.exe") + set(qtpaths_executable "${windeployqt_dir}/qtpaths.exe") + endif() message(STATUS "Executing windeployqt for executable ${executable_path}") execute_process(COMMAND "${windeployqt_executable}" "${executable_path}" + --qtpaths "${qtpaths_executable}" --no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --no-translations - --plugindir "${executable_parent_dir}/plugins") + --plugindir "${executable_parent_dir}/plugins" + RESULT_VARIABLE windeployqt_result) + if (NOT windeployqt_result EQUAL "0") + message(FATAL_ERROR "windeployqt failed: ${windeployqt_result}") + endif() # Remove the FFmpeg multimedia plugin as we don't include FFmpeg. # We want to use the Windows media plugin instead, which is also included. file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll") elseif (APPLE) get_filename_component(executable_name "${executable_path}" NAME_WE) - find_program(MACDEPLOYQT_EXECUTABLE macdeployqt6) + find_program(macdeployqt_executable macdeployqt6 PATHS "${QT_HOST_PATH}/bin") - message(STATUS "Executing macdeployqt for executable ${executable_path}") + message(STATUS "Executing macdeployqt at \"${macdeployqt_executable}\" for executable \"${executable_path}\"") execute_process( - COMMAND "${MACDEPLOYQT_EXECUTABLE}" + COMMAND "${macdeployqt_executable}" "${executable_path}" "-executable=${executable_path}/Contents/MacOS/${executable_name}" - -always-overwrite) + -always-overwrite + RESULT_VARIABLE macdeployqt_result) + if (NOT macdeployqt_result EQUAL "0") + message(FATAL_ERROR "macdeployqt failed: ${macdeployqt_result}") + endif() # Bundling libraries can rewrite path information and break code signatures of system libraries. # Perform an ad-hoc re-signing on the whole app bundle to fix this. - execute_process(COMMAND codesign --deep -fs - "${executable_path}") + execute_process(COMMAND codesign --deep -fs - "${executable_path}" + RESULT_VARIABLE codesign_result) + if (NOT codesign_result EQUAL "0") + message(FATAL_ERROR "codesign failed: ${codesign_result}") + endif() else() message(FATAL_ERROR "Unsupported OS for Qt bundling.") endif() @@ -44,9 +111,9 @@ if (BUNDLE_TARGET_EXECUTE) if (enable_qt) # Find qmake to make sure the plugin uses the right version of Qt. - find_program(QMAKE_EXECUTABLE qmake6) + find_program(qmake_executable qmake6 PATHS "${QT_HOST_PATH}/bin") - set(extra_linuxdeploy_env "QMAKE=${QMAKE_EXECUTABLE}") + set(extra_linuxdeploy_env "QMAKE=${qmake_executable}") set(extra_linuxdeploy_args --plugin qt) endif() @@ -59,7 +126,11 @@ if (BUNDLE_TARGET_EXECUTE) --executable "${executable_path}" --icon-file "${source_path}/dist/citra.svg" --desktop-file "${source_path}/dist/${executable_name}.desktop" - --appdir "${appdir_path}") + --appdir "${appdir_path}" + RESULT_VARIABLE linuxdeploy_appdir_result) + if (NOT linuxdeploy_appdir_result EQUAL "0") + message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}") + endif() if (enable_qt) set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh") @@ -82,7 +153,11 @@ if (BUNDLE_TARGET_EXECUTE) "OUTPUT=${bundle_dir}/${executable_name}.AppImage" "${linuxdeploy_executable}" --output appimage - --appdir "${appdir_path}") + --appdir "${appdir_path}" + RESULT_VARIABLE linuxdeploy_appimage_result) + if (NOT linuxdeploy_appimage_result EQUAL "0") + message(FATAL_ERROR "linuxdeploy failed to create AppImage: ${linuxdeploy_appimage_result}") + endif() endfunction() function(bundle_standalone executable_path original_executable_path bundle_library_paths) @@ -109,16 +184,23 @@ if (BUNDLE_TARGET_EXECUTE) file(MAKE_DIRECTORY ${lib_dir}) foreach (lib_file IN LISTS resolved_deps) message(STATUS "Bundling library ${lib_file}") - # Use native copy to turn symlinks into normal files. - execute_process(COMMAND cp -L "${lib_file}" "${lib_dir}") + symlink_safe_copy("${lib_file}" "${lib_dir}") endforeach() endif() # Add libs directory to executable rpath where applicable. if (APPLE) - execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}") + execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}" + RESULT_VARIABLE install_name_tool_result) + if (NOT install_name_tool_result EQUAL "0") + message(FATAL_ERROR "install_name_tool failed: ${install_name_tool_result}") + endif() elseif (UNIX) - execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}") + execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}" + RESULT_VARIABLE patchelf_result) + if (NOT patchelf_result EQUAL "0") + message(FATAL_ERROR "patchelf failed: ${patchelf_result}") + endif() endif() endfunction() @@ -146,9 +228,7 @@ if (BUNDLE_TARGET_EXECUTE) if (BUNDLE_QT) bundle_qt("${bundled_executable_path}") - endif() - - if (WIN32 OR NOT BUNDLE_QT) + else() bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}") endif() endif() @@ -170,7 +250,11 @@ else() message(STATUS "Extracting ${executable_name}") execute_process( COMMAND "${executable_file}" --appimage-extract - WORKING_DIRECTORY "${base_dir}") + WORKING_DIRECTORY "${base_dir}" + RESULT_VARIABLE extract_result) + if (NOT extract_result EQUAL "0") + message(FATAL_ERROR "AppImage extract failed: ${extract_result}") + endif() else() message(STATUS "Copying ${executable_name}") file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/") @@ -218,7 +302,7 @@ else() endif() # Build a list of library search paths from prefix paths. - foreach(prefix_path IN LISTS CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH) + foreach(prefix_path IN LISTS CMAKE_FIND_ROOT_PATH CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH) if (WIN32) list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/bin") endif() @@ -251,7 +335,8 @@ else() add_custom_command(TARGET ${DEST_TARGET} POST_BUILD COMMAND ${CMAKE_COMMAND} - "-DCMAKE_PREFIX_PATH=\"${CMAKE_PREFIX_PATH}\"" + "-DQT_HOST_PATH=\"${QT_HOST_PATH}\"" + "-DQT_TARGET_PATH=\"${QT_TARGET_PATH}\"" "-DBUNDLE_TARGET_EXECUTE=1" "-DTARGET=${target_name}" "-DSOURCE_PATH=${CMAKE_SOURCE_DIR}" diff --git a/CMakeModules/DownloadExternals.cmake b/CMakeModules/DownloadExternals.cmake index 19b8f477c..dfdc803a4 100644 --- a/CMakeModules/DownloadExternals.cmake +++ b/CMakeModules/DownloadExternals.cmake @@ -1,21 +1,20 @@ set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR}) -# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH. -# Params: -# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool. -function(download_qt target) +# Determines parameters based on the host and target for downloading the right Qt binaries. +function(determine_qt_parameters target host_out type_out arch_out arch_path_out host_type_out host_arch_out host_arch_path_out) if (target MATCHES "tools_.*") - set(DOWNLOAD_QT_TOOL ON) + set(tool ON) else() - set(DOWNLOAD_QT_TOOL OFF) + set(tool OFF) endif() # Determine installation parameters for OS, architecture, and compiler if (WIN32) set(host "windows") set(type "desktop") - if (NOT DOWNLOAD_QT_TOOL) + + if (NOT tool) if (MINGW) set(arch "win64_mingw") set(arch_path "mingw_64") @@ -28,21 +27,35 @@ function(download_qt target) message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.") endif() set(arch "win64_${arch_path}") + + # In case we're cross-compiling, prepare to also fetch the correct host Qt tools. + if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64") + set(host_arch_path "msvc2019_64") + elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64") + # TODO: msvc2019_arm64 doesn't include some of the required tools for some reason, + # TODO: so until it does, just use msvc2019_64 under x86_64 emulation. + # set(host_arch_path "msvc2019_arm64") + set(host_arch_path "msvc2019_64") + endif() + set(host_arch "win64_${host_arch_path}") else() message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.") endif() endif() elseif (APPLE) set(host "mac") - if (IOS AND NOT DOWNLOAD_QT_TOOL) + set(type "desktop") + set(arch "clang_64") + set(arch_path "macos") + + if (IOS AND NOT tool) + set(host_type "${type}") + set(host_arch "${arch}") + set(host_arch_path "${arch_path}") + set(type "ios") set(arch "ios") set(arch_path "ios") - set(host_arch_path "macos") - else() - set(type "desktop") - set(arch "clang_64") - set(arch_path "macos") endif() else() set(host "linux") @@ -51,38 +64,64 @@ function(download_qt target) set(arch_path "linux") endif() - get_external_prefix(qt base_path) - file(MAKE_DIRECTORY "${base_path}") + set(${host_out} "${host}" PARENT_SCOPE) + set(${type_out} "${type}" PARENT_SCOPE) + set(${arch_out} "${arch}" PARENT_SCOPE) + set(${arch_path_out} "${arch_path}" PARENT_SCOPE) + if (DEFINED host_type) + set(${host_type_out} "${host_type}" PARENT_SCOPE) + else() + set(${host_type_out} "${type}" PARENT_SCOPE) + endif() + if (DEFINED host_arch) + set(${host_arch_out} "${host_arch}" PARENT_SCOPE) + else() + set(${host_arch_out} "${arch}" PARENT_SCOPE) + endif() + if (DEFINED host_arch_path) + set(${host_arch_path_out} "${host_arch_path}" PARENT_SCOPE) + else() + set(${host_arch_path_out} "${arch_path}" PARENT_SCOPE) + endif() +endfunction() + +# Download Qt binaries for a specifc configuration. +function(download_qt_configuration prefix_out target host type arch arch_path base_path) + if (target MATCHES "tools_.*") + set(tool ON) + else() + set(tool OFF) + endif() set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini") - if (DOWNLOAD_QT_TOOL) + if (tool) set(prefix "${base_path}/Tools") set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target}) else() set(prefix "${base_path}/${target}/${arch_path}") - if (host_arch_path) - set(host_flag "--autodesktop") - set(host_prefix "${base_path}/${target}/${host_arch_path}") - endif() - set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag} - -m qtmultimedia --archives qttranslations qttools qtsvg qtbase) + set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} + -m qtmultimedia --archives qttranslations qttools qtsvg qtbase) endif() if (NOT EXISTS "${prefix}") - message(STATUS "Downloading binaries for Qt...") + message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}") set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9") if (WIN32) set(aqt_path "${base_path}/aqt.exe") - file(DOWNLOAD - ${AQT_PREBUILD_BASE_URL}/aqt.exe - ${aqt_path} SHOW_PROGRESS) + if (NOT EXISTS "${aqt_path}") + file(DOWNLOAD + ${AQT_PREBUILD_BASE_URL}/aqt.exe + ${aqt_path} SHOW_PROGRESS) + endif() execute_process(COMMAND ${aqt_path} ${install_args} WORKING_DIRECTORY ${base_path}) elseif (APPLE) set(aqt_path "${base_path}/aqt-macos") - file(DOWNLOAD - ${AQT_PREBUILD_BASE_URL}/aqt-macos - ${aqt_path} SHOW_PROGRESS) + if (NOT EXISTS "${aqt_path}") + file(DOWNLOAD + ${AQT_PREBUILD_BASE_URL}/aqt-macos + ${aqt_path} SHOW_PROGRESS) + endif() execute_process(COMMAND chmod +x ${aqt_path}) execute_process(COMMAND ${aqt_path} ${install_args} WORKING_DIRECTORY ${base_path}) @@ -96,18 +135,38 @@ function(download_qt target) execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args} WORKING_DIRECTORY ${base_path}) endif() + + message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}") endif() - message(STATUS "Using downloaded Qt binaries at ${prefix}") + set(${prefix_out} "${prefix}" PARENT_SCOPE) +endfunction() - # Add the Qt prefix path so CMake can locate it. +# This function downloads Qt using aqt. +# The path of the downloaded content will be added to the CMAKE_PREFIX_PATH. +# QT_TARGET_PATH is set to the Qt for the compile target platform. +# QT_HOST_PATH is set to a host-compatible Qt, for running tools. +# Params: +# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool. +function(download_qt target) + determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path) + + get_external_prefix(qt base_path) + file(MAKE_DIRECTORY "${base_path}") + + download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}") + if (DEFINED host_arch_path AND NOT "${host_arch_path}" STREQUAL "${arch_path}") + download_qt_configuration(host_prefix "${target}" "${host}" "${host_type}" "${host_arch}" "${host_arch_path}" "${base_path}") + else() + set(host_prefix "${prefix}") + endif() + + set(QT_TARGET_PATH "${prefix}" CACHE STRING "") + set(QT_HOST_PATH "${host_prefix}" CACHE STRING "") + + # Add the target Qt prefix path so CMake can locate it. list(APPEND CMAKE_PREFIX_PATH "${prefix}") set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) - - if (DEFINED host_prefix) - message(STATUS "Using downloaded host Qt binaries at ${host_prefix}") - set(QT_HOST_PATH "${host_prefix}" CACHE STRING "") - endif() endfunction() function(download_moltenvk) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 878528e68..5f4c23ad3 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -57,6 +57,12 @@ if(USE_SYSTEM_CRYPTOPP) add_library(cryptopp INTERFACE) target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp) else() + if (WIN32 AND NOT MSVC AND "arm64" IN_LIST ARCHITECTURE) + # TODO: CryptoPP ARM64 ASM does not seem to support Windows unless compiled with MSVC. + # TODO: See https://github.com/weidai11/cryptopp/issues/1260 + set(CRYPTOPP_DISABLE_ASM ON CACHE BOOL "") + endif() + set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "") set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "") set(CRYPTOPP_INSTALL OFF CACHE BOOL "") @@ -235,6 +241,18 @@ endif() # DiscordRPC if (USE_DISCORD_PRESENCE) + # rapidjson used by discord-rpc is old and doesn't correctly detect endianness for some platforms. + include(TestBigEndian) + test_big_endian(RAPIDJSON_BIG_ENDIAN) + if(RAPIDJSON_BIG_ENDIAN) + add_compile_definitions(RAPIDJSON_ENDIAN=1) + else() + add_compile_definitions(RAPIDJSON_ENDIAN=0) + endif() + + # Apply a dummy CLANG_FORMAT_SUFFIX to disable discord-rpc's unnecessary automatic clang-format. + set(CLANG_FORMAT_SUFFIX "dummy") + add_subdirectory(discord-rpc EXCLUDE_FROM_ALL) target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) endif() diff --git a/externals/cryptopp-cmake b/externals/cryptopp-cmake index 9327192b0..a99c80c26 160000 --- a/externals/cryptopp-cmake +++ b/externals/cryptopp-cmake @@ -1 +1 @@ -Subproject commit 9327192b0095dc1f420b2082d37bd427b5750d48 +Subproject commit a99c80c26686e44eddf0432140ae397f3efbd0b3