Files
ARMSX2/.github/workflows/build-all.yml
T
J1coding 1b737e25f0 iOS: give CI builds the RetroAchievements client identity
Nightly and MoonStore builds identify themselves to RetroAchievements as
stock PCSX2, so the server allows softcore only. Hardcore has worked on
locally built IPAs and nowhere else.

Nothing was broken. pcsx2/Host.cpp reads the client version from
ra_ua_secret.h behind __has_include and falls back to a stock agent when
the macro is absent. That header is gitignored, so it has never existed on
a runner.

CI now writes it from the IOS_RA_UA_VERSION repository secret, the way the
Android job already writes its keystores from NIGHTLY_RELEASE_KS_B64. A
missing secret warns and continues, because fork pull requests never
receive secrets and a nightly that fails to publish is worse than one
without hardcore. A malformed version fails the build instead:
RetroAchievements refuses a version it cannot order, and a refused agent is
indistinguishable from an unknown one on the client.

Only the nightly and pushes to master embed it. Pull request artifacts keep
the stock agent, so a test build is not one more public copy of an identity
that needs a release to revoke.

A second step reads the finished binary and fails unless the exact version
is in it, trailing space included, since a stale 1.2.3 is a prefix of a
current 1.2.345. Both iOS jobs are continue-on-error, so that failure does
not stop the workflow, but it does skip the upload that follows it, and the
publish step treats a missing IPA as absent rather than fatal. A softcore
build is not shipped.

None of this makes the version private. The compiler bakes the finished
agent into the binary as a plain literal, so anyone holding a build can
read it out. What the gitignored header prevents is a fork inheriting a
live identity straight from source.
2026-08-22 16:26:22 +02:00

271 lines
11 KiB
YAML

# Unified "build everything on every commit" pipeline for the single-core monorepo.
#
# One push to the default branch fans out to every supported target and uploads
# an individual binary artifact per platform:
# * PC arm64 Qt GUI -> macOS, Linux, Windows (reuses the reusable workflows)
# * Android APK -> platforms/android (Gradle + NDK, thin CMake on root core)
# * iOS .app -> platforms/ios (Xcode/CMake on root core)
#
# Windows-on-arm64 builds natively on a Windows 11 arm64 runner (see
# windows_build_qt.yml): a single arm64 deps stage, then the arm64 GUI.
#
# NOTE: The Android and iOS jobs build against the freshly-collapsed single core
# and have not yet had a green run; they are the mechanism that will surface the
# remaining reconciliation work. Keep them non-blocking until they first pass.
#
# INVARIANT (do not weaken): the PC arm64 jobs (pc-macos-arm64 / pc-linux-arm64 /
# pc-windows-arm64) are BLOCKING and must stay so. They are the guardrail that a
# mobile-motivated change has not silently altered the shared canonical core.
# Anything mobile-specific belongs behind a platform guard (#if __ANDROID__ /
# ARMSX2_IOS) or in a forked, CMake-selected translation unit (see the aR5900*
# EE fork in pcsx2/CMakeLists.txt) -- never grafted unguarded into shared files.
name: Build All Platforms
on:
push:
branches:
- master
# Doc-only pushes shouldn't burn ~2h of arm64 CI.
paths-ignore:
- '**/*.md'
- 'LICENSE*'
- '.gitignore'
pull_request:
paths-ignore:
- '**/*.md'
- 'LICENSE*'
- '.gitignore'
workflow_dispatch:
concurrency:
group: build-all-${{ github.ref }}
cancel-in-progress: true
jobs:
# ---- PC arm64 Qt GUI (mac / linux) -------------------------------------
pc-macos-arm64:
name: PC · macOS arm64 (Qt)
uses: ./.github/workflows/macos_build.yml
with:
jobName: "macOS arm64"
artifactPrefixName: "armsx2-macos-arm64"
sign_and_notarize: true
secrets: inherit
pc-linux-arm64:
name: PC · Linux arm64 (Qt)
strategy:
fail-fast: false
matrix:
include:
# The GitHub aarch64 runners use 4K pages, so an unqualified build only
# runs on 4K-page kernels. 16K-page kernels (e.g. Asahi Linux on Apple
# Silicon) need a separate binary, so build both variants explicitly
# instead of quietly inheriting the runner's page size.
- pageVariant: "4k"
pageSize: "4096"
cacheLineSize: "64"
- pageVariant: "16k"
pageSize: "16384"
cacheLineSize: "128"
uses: ./.github/workflows/linux_build_qt.yml
with:
jobName: "Linux arm64 (${{ matrix.pageVariant }} pages)"
artifactPrefixName: "armsx2-linux-arm64-${{ matrix.pageVariant }}-appimage"
compiler: clang
cmakeflags: "-DHOST_PAGE_SIZE=${{ matrix.pageSize }} -DHOST_CACHE_LINE_SIZE=${{ matrix.cacheLineSize }}"
buildAppImage: true
# ---- libretro core (linux arm64) ---------------------------------------
pc-linux-arm64-libretro:
name: PC · Linux arm64 (libretro core)
uses: ./.github/workflows/linux_build_libretro.yml
with:
jobName: "libretro Core Build (arm64)"
artifactPrefixName: "armsx2-linux-arm64-libretro"
fetchTags: true
secrets: inherit
# ---- SDL/kmsdrm handheld frontend (linux arm64) ------------------------
pc-linux-arm64-sdl:
name: PC · Linux arm64 (SDL kmsdrm)
uses: ./.github/workflows/linux_build_sdl.yml
with:
jobName: "SDL Handheld Build (arm64)"
artifactPrefixName: "armsx2-linux-arm64-sdl"
fetchTags: true
secrets: inherit
# ---- PC arm64 Qt GUI (windows) -----------------------------------------
pc-windows-arm64:
name: PC · Windows arm64 (Qt)
uses: ./.github/workflows/windows_build_qt.yml
with:
jobName: "Windows arm64"
artifactPrefixName: "armsx2-windows-arm64"
# ---- Android APK -------------------------------------------------------
android:
name: Android APK
runs-on: ubuntu-24.04
# Non-blocking until the collapsed-core Android build first goes green.
continue-on-error: true
defaults:
run:
working-directory: platforms/android
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Android SDK
uses: android-actions/setup-android@v3
# NDK matches the version AGP resolves to (build.gradle.kts pins none, so it
# uses AGP's default 28.2.13676358); cmake 3.31.6 matches the pin in
# build.gradle.kts. 3.22.1 (AGP's default) is the shaderc deps' exact
# cmake_minimum_required floor and fails to configure spirv-tools.
- name: Install NDK + CMake
run: yes | sdkmanager "ndk;28.2.13676358" "cmake;3.31.6" >/dev/null || true
# shaderc's SPIRV-Tools/glslang/etc. are fetched on demand (not vendored,
# not submodules), so `submodules: recursive` above does not provide them.
- name: Fetch shaderc third-party deps
run: python3 app/src/main/cpp/3rdparty/shaderc/utils/git-sync-deps
# librashader is built by cargo for the Android target; the runner's
# rustup only ships the host std, so the cargo step dies with
# "error[E0463]: can't find crate for `core`" without this.
- name: Install Rust Android target
run: rustup target add aarch64-linux-android
- name: Build release APK
run: ./gradlew :app:assembleRelease --stacktrace
- name: Upload APK
uses: actions/upload-artifact@v7
with:
name: armsx2-android-arm64
path: platforms/android/app/build/outputs/apk/**/*.apk
if-no-files-found: warn
# ---- iOS .app ----------------------------------------------------------
ios:
name: iOS (arm64, unsigned device IPA)
runs-on: macos-26
# Non-blocking until the collapsed-core iOS build first goes green.
continue-on-error: true
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode.app
# RetroAchievements grants hardcore on the client name and version in the HTTP user
# agent, and pcsx2/Host.cpp reads that version from a gitignored header. Only builds
# that reach players get it: a pull request artifact would be one more public copy of
# an identity that cannot be revoked without a release. Those keep the stock PCSX2
# agent and stay softcore, which is what a test build should be.
- name: Embed the RetroAchievements client identity
if: >-
github.repository == 'ARMSX2/ARMSX2' &&
github.event_name == 'push' && github.ref == 'refs/heads/master'
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: bash .github/workflows/scripts/common/write-ra-ua-secret.sh
- name: Configure (CMake, Xcode generator, real device SDK, no signing)
working-directory: platforms/ios/app/src/main/cpp
run: >
cmake -G Xcode -B build
-DCMAKE_SYSTEM_NAME=iOS
-DCMAKE_OSX_ARCHITECTURES=arm64
-DARMSX2_REAL_DEVICE=ON
- name: Build (iphoneos SDK, unsigned)
working-directory: platforms/ios/app/src/main/cpp
run: >
cmake --build build --config Release
-- -sdk iphoneos CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
- name: Package unsigned .ipa
working-directory: platforms/ios/app/src/main/cpp
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-8)
IPA_NAME="ARMSX2-iOS-${SHORT_SHA}.ipa"
APP_PATH=$(find build -name "ARMSX2iOS.app" -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "ERROR: ARMSX2iOS.app not found in build output"
exit 1
fi
echo "Found app at: $APP_PATH"
echo "Packaging as: $IPA_NAME"
STAGING=$(mktemp -d)
mkdir -p "$STAGING/Payload"
cp -R "$APP_PATH" "$STAGING/Payload/"
cd "$STAGING"
zip -r "$GITHUB_WORKSPACE/$IPA_NAME" Payload -x "*.DS_Store"
cd "$GITHUB_WORKSPACE"
echo "IPA_NAME=$IPA_NAME" >> $GITHUB_ENV
ls -lh "$IPA_NAME"
# Nothing on the client reports a rejected agent, so a master build that lost the
# identity looks healthy until a player loses an unlock. Fail here instead. Carries
# the same guard as the write step, or every pull request would fail this check.
- name: Verify the client identity reached the binary
if: >-
github.repository == 'ARMSX2/ARMSX2' &&
github.event_name == 'push' && github.ref == 'refs/heads/master'
working-directory: platforms/ios/app/src/main/cpp
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: |
APP_PATH=$(find build -name "ARMSX2iOS.app" -type d | head -1)
bash "$GITHUB_WORKSPACE/.github/workflows/scripts/common/verify-ra-ua-agent.sh" \
"$APP_PATH/ARMSX2iOS"
- name: Upload .ipa
uses: actions/upload-artifact@v4
with:
name: ${{ env.IPA_NAME }}
path: ${{ github.workspace }}/${{ env.IPA_NAME }}
if-no-files-found: error
# ---- Alert on a broken master build ------------------------------------
# Fires only when a BLOCKING guardrail job (the PC arm64 builds) fails on a
# push to master -- never on PRs, and never on the non-blocking mobile jobs
# (they are continue-on-error, so they can't trip `failure()`). GitHub also
# emails the pusher natively; this adds a Discord ping to the shared channel.
notify-failure:
name: Notify on master build failure
needs: [pc-macos-arm64, pc-linux-arm64, pc-windows-arm64]
if: ${{ failure() && github.event_name == 'push' }}
runs-on: ubuntu-latest
# Job-level so the step's `if: env.HOOK != ''` guard can see it.
env:
HOOK: ${{ secrets.DISCORD_BUILD_WEBHOOK }}
steps:
- name: Discord failure ping
if: ${{ env.HOOK != '' }} # no-op on forks that lack the secret
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
jq -n \
--arg url "$RUN_URL" \
--arg desc "Commit \`${{ github.sha }}\` by ${{ github.actor }} failed a required build." \
'{username:"ARMSX2 CI", embeds:[{title:"❌ master build broke", url:$url, color:15158332, description:$desc}]}' \
| curl -fsSL -H 'Content-Type: application/json' -d @- "$HOOK"