From 75fbbd9cbf6d3aa2f130c4fd902cf0cb303c569c Mon Sep 17 00:00:00 2001 From: Joseph Gershgorin Date: Sat, 25 Apr 2026 05:17:10 -0700 Subject: [PATCH] [CI] Add native Windows ARM64 MSYS2, MSVC and Libretro builds --- .ci/windows.sh | 18 +++++++++- .github/workflows/build.yml | 49 +++++++++++++++++++++------- .github/workflows/libretro.yml | 42 ++++++++++++++++++++++++ CMakeModules/BundleTarget.cmake | 8 +++++ CMakeModules/DownloadExternals.cmake | 31 +++++++++++++++--- src/citra_libretro/CMakeLists.txt | 5 ++- src/common/file_util.cpp | 7 ++++ 7 files changed, 142 insertions(+), 18 deletions(-) diff --git a/.ci/windows.sh b/.ci/windows.sh index d94fd6435..0fbee36bb 100644 --- a/.ci/windows.sh +++ b/.ci/windows.sh @@ -6,6 +6,16 @@ if [ "$GITHUB_REF_TYPE" == "tag" ]; then export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON) fi +# Map the workflow ARCH (x64/arm64) onto CMake's ARCHITECTURE variable so +# Qt downloads and arch-dependent paths pick the right target even if the +# compiler-based detection gets confused (e.g. on windows-11-arm runners +# where the MSVC setup may land on an x64 host compiler). +if [ "$ARCH" = "arm64" ]; then + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DARCHITECTURE=arm64) +elif [ "$ARCH" = "x64" ]; then + export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DARCHITECTURE=x86_64) +fi + cmake .. -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ @@ -15,7 +25,13 @@ cmake .. -G Ninja \ "${EXTRA_CMAKE_FLAGS[@]}" ninja ninja bundle -strip -s bundle/*.exe +# MSVC keeps debug info in separate PDB files, so the .exe has nothing +# to strip. The `strip` that ends up in PATH on the runner is also from +# Git-for-Windows' x86_64 mingw and can't read arm64 PE binaries. +case "$TARGET" in + msvc*) ;; + *) strip -s bundle/*.exe ;; +esac ccache -s -v diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0201aa3b0..abf015c9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -143,11 +143,35 @@ jobs: path: artifacts/ windows: - runs-on: windows-latest + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: - target: ["msvc", "msys2"] + include: + - target: msvc + arch: x64 + runner: windows-latest + msvc_arch: x64 + msys2_system: clang64 + vulkan_sdk_url: https://sdk.lunarg.com/sdk/download/1.4.341.1/windows/vulkansdk-windows-X64-1.4.341.1.exe + - target: msys2 + arch: x64 + runner: windows-latest + msvc_arch: x64 + msys2_system: clang64 + vulkan_sdk_url: https://sdk.lunarg.com/sdk/download/1.4.341.1/windows/vulkansdk-windows-X64-1.4.341.1.exe + - target: msvc + arch: arm64 + runner: windows-11-arm + msvc_arch: amd64_arm64 + msys2_system: clangarm64 + vulkan_sdk_url: https://sdk.lunarg.com/sdk/download/1.4.341.1/warm/vulkansdk-windows-ARM64-1.4.341.1.exe + - target: msys2 + arch: arm64 + runner: windows-11-arm + msvc_arch: amd64_arm64 + msys2_system: clangarm64 + vulkan_sdk_url: https://sdk.lunarg.com/sdk/download/1.4.341.1/warm/vulkansdk-windows-ARM64-1.4.341.1.exe defaults: run: shell: ${{ (matrix.target == 'msys2' && 'msys2') || 'bash' }} {0} @@ -156,7 +180,8 @@ jobs: CCACHE_COMPILERCHECK: content CCACHE_SLOPPINESS: time_macros OS: windows - TARGET: ${{ matrix.target }} + TARGET: ${{ matrix.target }}-${{ matrix.arch }} + ARCH: ${{ matrix.arch }} steps: - uses: actions/checkout@v4 with: @@ -165,25 +190,27 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.CCACHE_DIR }} - key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ matrix.arch }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-${{ matrix.target }}- + ${{ runner.os }}-${{ matrix.target }}-${{ matrix.arch }}- - name: Set up MSVC if: ${{ matrix.target == 'msvc' }} uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.msvc_arch }} - name: Install extra tools (MSVC) if: ${{ matrix.target == 'msvc' }} run: choco install ccache ninja ptime wget - name: Install vulkan-sdk (MSVC) if: ${{ matrix.target == 'msvc' }} run: | - wget https://sdk.lunarg.com/sdk/download/1.4.304.1/windows/VulkanSDK-1.4.304.1-Installer.exe -O D:/a/_temp/vulkan.exe - D:/a/_temp/vulkan.exe --accept-licenses --default-answer --confirm-command install + wget ${{ matrix.vulkan_sdk_url }} -O "${{ runner.temp }}/vulkan.exe" + "${{ runner.temp }}/vulkan.exe" --accept-licenses --default-answer --confirm-command install - name: Set up MSYS2 if: ${{ matrix.target == 'msys2' }} uses: msys2/setup-msys2@v2 with: - msystem: clang64 + msystem: ${{ matrix.msys2_system }} update: true install: git make p7zip pacboy: >- @@ -197,8 +224,8 @@ jobs: - name: Install NSIS if: ${{ github.ref_type == 'tag' }} run: | - wget https://download.sourceforge.net/project/nsis/NSIS%203/3.11/nsis-3.11-setup.exe -O D:/a/_temp/nsis-setup.exe - ptime D:/a/_temp/nsis-setup.exe /S + wget https://download.sourceforge.net/project/nsis/NSIS%203/3.11/nsis-3.11-setup.exe -O "${{ runner.temp }}/nsis-setup.exe" + ptime "${{ runner.temp }}/nsis-setup.exe" /S shell: pwsh - name: Disable line ending translation run: git config --global core.autocrlf input @@ -208,7 +235,7 @@ jobs: if: ${{ github.ref_type == 'tag' }} run: | cd src\installer - "C:\Program Files (x86)\NSIS\makensis.exe" /DPRODUCT_VARIANT=${{ matrix.target }} /DPRODUCT_VERSION=${{ github.ref_name }} citra.nsi + "C:\Program Files (x86)\NSIS\makensis.exe" /DPRODUCT_VARIANT=${{ matrix.target }}-${{ matrix.arch }} /DPRODUCT_VERSION=${{ github.ref_name }} citra.nsi mkdir ..\..\artifacts 2> NUL move /y *.exe ..\..\artifacts\ shell: cmd diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml index aa846730d..97852dab1 100644 --- a/.github/workflows/libretro.yml +++ b/.github/workflows/libretro.yml @@ -102,6 +102,48 @@ jobs: with: name: ${{ env.OS }}-${{ env.TARGET }} path: ./*.zip + windows-arm64: + runs-on: windows-11-arm + defaults: + run: + shell: msys2 {0} + env: + OS: windows + TARGET: arm64 + BUILD_DIR: build/windows-arm64 + # LIBRETRO_STATIC + -static fold the clangarm64 runtime (libc++, + # libunwind, libwinpthread) into the core, matching what MXE does + # for the x86_64 libretro build. Without these, the produced DLL + # imports MSYS2 runtime DLLs that aren't ABI-compatible with the + # ones a target machine has alongside RetroArch. + EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja -DLIBRETRO_STATIC=1 -DCMAKE_SHARED_LINKER_FLAGS=-static + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Disable line ending translation + shell: pwsh + run: git config --global core.autocrlf input + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: clangarm64 + update: true + install: git make zip + pacboy: >- + toolchain:p cmake:p ninja:p spirv-tools:p + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc) + - name: Pack + run: ./.ci/libretro-pack.sh + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ./*.zip macos: runs-on: macos-26 strategy: diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index fead41acf..d23b0eeed 100644 --- a/CMakeModules/BundleTarget.cmake +++ b/CMakeModules/BundleTarget.cmake @@ -2,6 +2,14 @@ if (BUNDLE_TARGET_EXECUTE) # --- Bundling method logic --- + # Opt into CMP0207's NEW behavior so file(GET_RUNTIME_DEPENDENCIES) + # normalizes paths before regex matching. The exclude pattern below + # ('.*system32.*') already matches either slash style, so this is + # purely about silencing the dev-mode warning. + if (POLICY CMP0207) + cmake_policy(SET CMP0207 NEW) + endif() + function(symlink_safe_copy from to) if (WIN32) # Use cmake copy for maximum compatibility. diff --git a/CMakeModules/DownloadExternals.cmake b/CMakeModules/DownloadExternals.cmake index a76d95ae1..4cdebc4b8 100644 --- a/CMakeModules/DownloadExternals.cmake +++ b/CMakeModules/DownloadExternals.cmake @@ -2,7 +2,7 @@ set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR}) # 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) +function(determine_qt_parameters target host_out type_out arch_out arch_path_out tools_host_out host_type_out host_arch_out host_arch_path_out) if (target MATCHES "tools_.*") set(tool ON) else() @@ -21,6 +21,10 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out elseif (MSVC) if ("arm64" IN_LIST ARCHITECTURE) set(arch_path "msvc2022_arm64") + # aqt serves Windows ARM64 Qt packages under a separate + # host namespace; using "windows" here makes aqt parse the + # x86_64 package XML and fail to find the arm64 archives. + set(host "windows_arm64") elseif ("x86_64" IN_LIST ARCHITECTURE) set(arch_path "msvc2022_64") else() @@ -39,6 +43,9 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out set(host_arch_path "msvc2022_64") endif() set(host_arch "win64_${host_arch_path}") + # The x86_64 desktop Qt used for host tools lives under the + # regular "windows" aqt host, even when the target is arm64. + set(tools_host "windows") else() message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.") endif() @@ -69,6 +76,11 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out set(${type_out} "${type}" PARENT_SCOPE) set(${arch_out} "${arch}" PARENT_SCOPE) set(${arch_path_out} "${arch_path}" PARENT_SCOPE) + if (DEFINED tools_host) + set(${tools_host_out} "${tools_host}" PARENT_SCOPE) + else() + set(${tools_host_out} "${host}" PARENT_SCOPE) + endif() if (DEFINED host_type) set(${host_type_out} "${host_type}" PARENT_SCOPE) else() @@ -100,8 +112,17 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target}) else() set(prefix "${base_path}/${target}/${arch_path}") - set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} - -m qtmultimedia --archives qttranslations qttools qtsvg qtbase) + # aqt's arm64 Qt package layout differs from x86_64 — the per-module archive + # names (qtbase/qtsvg/…) don't exist in its XML, so passing --archives trips + # the parser. Install the default archive set for arm64 and let --autodesktop + # pull in the matching x86_64 desktop Qt that the arm64 target depends on. + if ("${arch_path}" STREQUAL "msvc2022_arm64") + set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} + --autodesktop -m qtmultimedia) + else() + set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} + -m qtmultimedia --archives qttranslations qttools qtsvg qtbase) + endif() endif() if (NOT EXISTS "${prefix}") @@ -150,14 +171,14 @@ endfunction() # 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) + determine_qt_parameters("${target}" host type arch arch_path tools_host 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}") + download_qt_configuration(host_prefix "${target}" "${tools_host}" "${host_type}" "${host_arch}" "${host_arch_path}" "${base_path}") else() set(host_prefix "${prefix}") endif() diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt index 797deedb7..eb302a868 100644 --- a/src/citra_libretro/CMakeLists.txt +++ b/src/citra_libretro/CMakeLists.txt @@ -96,6 +96,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "tvOS") target_link_libraries(azahar_libretro PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/libretro.osx.def") -else() +elseif (NOT WIN32) + # -Bsymbolic is an ELF-only flag controlling how a shared object resolves + # its own symbols. PE/COFF has no equivalent; GNU ld silently ignored it + # on Windows MinGW, but lld (clangarm64's default linker) rejects it. target_link_libraries(azahar_libretro PRIVATE "-Wl,-Bsymbolic") endif() diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 52f406226..6339634a8 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -38,6 +38,12 @@ #include #include "common/string_util.h" +// Pull in here, before any fstat-redirection macros come into +// scope. MinGW's declares fstat as a real function (not a +// macro); if our `#define fstat _fstat64` were active during its parsing, +// the declaration would be rewritten to a conflicting `_fstat64` overload. +#include + #ifdef _MSC_VER // 64 bit offsets for MSVC #define fseeko _fseeki64 @@ -47,6 +53,7 @@ typedef struct _stat64 file_stat_t; #define fstat _fstat64 #elif defined(HAVE_LIBRETRO) typedef struct _stat64 file_stat_t; +#define fstat _fstat64 #else typedef struct stat file_stat_t; #endif