CI: dual-core (4k + 16k) + PGO nightly, push trigger, plain changelog

- tools/ci-nightly-dualcore.sh builds the core at both host page sizes and
  merges both .so into one APK so 16k-page devices load their native core
- nightly builds with PGO=optimize (committed pgo/armsx2.profdata)
- trigger the nightly on push to master (was schedule + dispatch only)
- plain-language changelog in the release body from filtered commit subjects
This commit is contained in:
jpolo1224
2026-07-13 00:42:11 -04:00
parent 6cfd8ee00b
commit e2889838ab
4 changed files with 218 additions and 10 deletions
+88 -10
View File
@@ -14,6 +14,11 @@
name: Nightly
on:
push:
branches: [master] # build a nightly on every push to master
paths-ignore:
- '**/*.md' # skip doc-only pushes
- 'docs/**'
schedule:
- cron: '0 8 * * *' # 08:00 UTC daily
workflow_dispatch:
@@ -35,6 +40,7 @@ jobs:
proceed: ${{ steps.check.outputs.proceed }}
date: ${{ steps.check.outputs.date }}
sha: ${{ steps.check.outputs.sha }}
notes: ${{ steps.notes.outputs.body }}
steps:
- uses: actions/checkout@v7
with:
@@ -57,6 +63,58 @@ jobs:
echo "proceed=$PROCEED" >> "$GITHUB_OUTPUT"
echo "proceed=$PROCEED date=$DATE sha=$SHA"
# Plain-language changelog for the release body: one bullet per commit
# since the previous nightly tag, with noise filtered and dev "[scope]"
# tags stripped. No hashes/emojis/dashes. Built here because gate already
# has full history (+ tags) checked out; the publish job (which has no
# checkout) consumes it via needs.gate.outputs.notes.
- id: notes
run: |
DATE="${{ steps.check.outputs.date }}"
REPO="${{ github.repository }}"
# most recent nightly-* tag that isn't today's (empty on the first nightly)
PREV="$(git tag -l 'nightly-*' --sort=-creatordate | grep -vx "nightly-${DATE}" | head -1 || true)"
if [ -n "$PREV" ] && git rev-parse -q --verify "refs/tags/${PREV}" >/dev/null; then
RANGE="${PREV}..HEAD"
elif git rev-parse -q --verify "HEAD~20" >/dev/null; then
RANGE="HEAD~20..HEAD" # first nightly: fall back to the last 20 commits
else
RANGE="HEAD"
fi
# Turn commit subjects into plain user-facing bullets:
# - drop internal commits (merge/revert/ci/chore/docs/build/style/test/
# refactor, translation syncs, version/cmake bumps)
# - strip conventional "feat/fix/perf:" prefixes and leading "[scope]" tags
# - convert em/en dashes (—/) to commas so no AI-style dashes survive
# || true keeps head's SIGPIPE from tripping pipefail; empty output is fine.
git log "$RANGE" --no-merges --pretty=format:'%s' \
| grep -ivE '^(merge |revert )' \
| grep -ivE '^(ci|chore|docs|build|style|test|refactor)(\(|:)' \
| grep -ivE 'update translations|transifex|bump version|^version |^cmake ' \
| sed -E 's/^(feat|fix|perf)(\([^)]*\))?!?: *//' \
| sed 's/ *— */, /g; s/ * */, /g' \
| sed 's/^\[[^]]*\] *//' \
| sed 's/^/- /' \
| head -100 > changes.txt || true
{
echo "Automated nightly build for community testing (unsigned)."
echo
echo "Platforms: macOS arm64, Linux arm64 (4K and 16K pages), Windows arm64, Android (sideloadable APK). iOS is not included yet."
echo
echo "## What's new"
if [ -s changes.txt ]; then cat changes.txt; else echo "- No notable changes since the last nightly."; fi
if [ -n "$PREV" ]; then
echo
echo "Full changelog: https://github.com/${REPO}/compare/${PREV}...${{ github.sha }}"
fi
} > notes.md
{
echo 'body<<ARMSX2_NOTES_EOF'
cat notes.md
echo 'ARMSX2_NOTES_EOF'
} >> "$GITHUB_OUTPUT"
echo "----- generated release notes -----"; cat notes.md
# ---- PC arm64 Qt GUI (blocking for the release) ------------------------
pc-macos-arm64:
name: PC · macOS arm64 (Qt)
@@ -131,16 +189,38 @@ jobs:
- name: Fetch shaderc third-party deps
run: python3 app/src/main/cpp/3rdparty/shaderc/utils/git-sync-deps
# Release build; when armsx2_keystore.properties is absent (it is, in CI) the
# release APK is debug-key-signed -> installable/sideloadable, not Play-grade.
- name: Build release APK
run: ./gradlew :app:assembleRelease --stacktrace
# Dual-core (4k + 16k page size) + PGO nightly. A stock `assembleRelease`
# ships ONLY the 4k core and cannot load its native core on 16k-page
# devices (Android 15+ handhelds). This compiles the core at both page
# sizes and merges both .so's into one APK, with PGO=optimize so the
# nightly matches release performance. See tools/ci-nightly-dualcore.sh.
- name: Build dual-core (4k + 16k) + PGO APK
env:
FLAVOR: Github # sideload build (com.armsx2); use Play for the SAF-only build
# Optional stable signing: add repo secrets NIGHTLY_KEYSTORE_B64 (base64 of a
# .jks) + NIGHTLY_KS_PASS / NIGHTLY_KEY_ALIAS / NIGHTLY_KEY_PASS so every
# nightly is signed with one key and installs as an update. Without them a
# throwaway debug key is generated (users reinstall between nightlies).
NIGHTLY_KS_PASS: ${{ secrets.NIGHTLY_KS_PASS }}
NIGHTLY_KEY_ALIAS: ${{ secrets.NIGHTLY_KEY_ALIAS }}
NIGHTLY_KEY_PASS: ${{ secrets.NIGHTLY_KEY_PASS }}
run: |
export VC="${{ github.run_number }}"
export VN="$(grep 'versionName' app/build.gradle.kts | sed -n 's/.*?:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
export VN="${VN:-nightly}"
if [ -n "${{ secrets.NIGHTLY_KEYSTORE_B64 }}" ]; then
echo "${{ secrets.NIGHTLY_KEYSTORE_B64 }}" | base64 -d > "$RUNNER_TEMP/nightly.jks"
export NIGHTLY_KEYSTORE="$RUNNER_TEMP/nightly.jks"
fi
bash tools/ci-nightly-dualcore.sh
- name: Upload APK
uses: actions/upload-artifact@v7
with:
name: armsx2-android-arm64
path: platforms/android/app/build/outputs/apk/**/*.apk
# ONLY the merged dual-core APK — not the intermediate single-core bases —
# so the publish job attaches exactly one APK to the release.
path: platforms/android/app/build/outputs/apk/nightly/*.apk
if-no-files-found: warn
# ---- publish: gather artifacts -> dated pre-release --------------------
@@ -185,11 +265,9 @@ jobs:
prerelease: true
target_commitish: ${{ github.sha }}
fail_on_unmatched_files: false
body: |
Automated **nightly** build from `${{ github.sha }}` — unsigned, for community testing.
**Platforms:** macOS arm64 · Linux arm64 (4K + 16K pages) · Windows arm64 · Android (sideloadable APK).
_iOS is not yet included — the device build is still being brought up._
# Eden-style release notes (static intro + per-commit changelog + Full
# Changelog link) assembled by the gate job, which has git history.
body: ${{ needs.gate.outputs.notes }}
files: out/*
- name: Discord announce
+3
View File
@@ -55,3 +55,6 @@ app/src/main/cpp/3rdparty/shaderc/third_party/googletest/
app/src/main/cpp/3rdparty/shaderc/third_party/re2/
app/src/main/cpp/3rdparty/shaderc/third_party/spirv-headers/
app/src/main/cpp/3rdparty/shaderc/third_party/spirv-tools/
# CI PGO profile for the dual-core nightly (build-release recipe uses this)
!pgo/armsx2.profdata
Binary file not shown.
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# ============================================================================
# ci-nightly-dualcore.sh — GitHub Actions dual-core (4k + 16k) + PGO nightly.
# ----------------------------------------------------------------------------
# A stock `./gradlew :app:assembleRelease` produces ONE core at the default
# host page size (emucore_4k @ 0x1000). That APK cannot load its native core on
# 16k-page devices (Android 15+ handhelds), so half the fleet is broken.
#
# PCSX2 fastmem bakes the host page size in at compile time, so a universal APK
# needs the core compiled TWICE (0x1000 and 0x4000) and both .so's merged into
# one APK — exactly what tools/build-release-apk.sh does for hand-built
# releases. This is the CI-portable (Linux, no macOS assumptions) version of
# that recipe, plus PGO=optimize so the nightly matches release performance.
#
# Runs from platforms/android (the gradle root); the nightly workflow sets
# working-directory accordingly.
#
# Env:
# VC, VN versionCode / versionName (required)
# FLAVOR Github (sideload, com.armsx2) | Play (default Github)
# PROF merged .profdata (default pgo/armsx2.profdata; if the
# file is absent the build falls back to PGO=none rather
# than failing, so a missing profile never breaks CI)
# OUT output apk path
# NIGHTLY_KEYSTORE / NIGHTLY_KS_PASS / NIGHTLY_KEY_ALIAS / NIGHTLY_KEY_PASS
# stable signing key (recommended: add as a repo secret
# so nightly-over-nightly updates install; otherwise a
# throwaway debug key is generated and users must
# reinstall between nightlies).
# ============================================================================
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # platforms/android
GRADLE="$ROOT_DIR/gradlew"
VC="${VC:?set VC=<versionCode>}"
VN="${VN:?set VN=<versionName>}"
FLAVOR="${FLAVOR:-Github}" # Github | Play
flavor_lc="$(printf '%s' "$FLAVOR" | tr '[:upper:]' '[:lower:]')"
PROF="${PROF:-$ROOT_DIR/pgo/armsx2.profdata}"
OUT="${OUT:-$ROOT_DIR/app/build/outputs/apk/nightly/ARMSX2-nightly-${VN}-vc${VC}.apk}"
# --- resolve Android SDK build-tools (Linux CI exports ANDROID_HOME/SDK_ROOT) ---
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
[[ -n "$SDK" && -d "$SDK" ]] || { echo "FATAL: ANDROID_HOME/ANDROID_SDK_ROOT not set" >&2; exit 1; }
BT="$(find "$SDK/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)"
ZIPALIGN="$BT/zipalign"; APKSIGNER="$BT/apksigner"; AAPT="$BT/aapt2"
for t in "$ZIPALIGN" "$APKSIGNER"; do
[[ -x "$t" ]] || { echo "FATAL: missing build-tool $t" >&2; exit 1; }
done
# --- PGO: optimize when the profile exists, else fall back so CI never fails ---
if [[ -f "$PROF" ]]; then
PGO_ARGS=(-Parmsx2.pgo=optimize -Parmsx2.pgoProfile="$PROF")
echo "PGO: optimize using $PROF ($(( $(wc -c < "$PROF") / 1024 / 1024 )) MB)"
else
PGO_ARGS=(-Parmsx2.pgo=none)
echo "WARNING: PGO profile not found at $PROF — building PGO=none (slower core)." >&2
fi
WORK="$ROOT_DIR/app/build/nightly-dualcore"; rm -rf "$WORK"
mkdir -p "$WORK/lib-stage/lib/arm64-v8a" "$(dirname "$OUT")"
BUILT="$ROOT_DIR/app/build/outputs/apk/${flavor_lc}/release/app-${flavor_lc}-release.apk"
fsize() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1"; } # Linux || macOS
build_core() { # pagesize libname
local ps="$1" ln="$2"
echo "=== core $ln (page=$ps, flavor=$FLAVOR) ==="
rm -f "$BUILT"
"$GRADLE" -p "$ROOT_DIR" ":app:assemble${FLAVOR}Release" \
-Parmsx2.hostPageSize="$ps" \
-Parmsx2.nativeLibName="$ln" \
"${PGO_ARGS[@]}" \
-Parmsx2.versionCode="$VC" \
-Parmsx2.versionName="$VN" \
--stacktrace
[[ -f "$BUILT" ]] || { echo "FATAL: gradle produced no APK for $ln ($BUILT)" >&2; exit 1; }
unzip -l "$BUILT" "lib/arm64-v8a/lib${ln}.so" >/dev/null \
|| { echo "FATAL: lib${ln}.so missing from $BUILT" >&2; exit 1; }
cp -f "$BUILT" "$WORK/base-${ln}.apk"
unzip -p "$BUILT" "lib/arm64-v8a/lib${ln}.so" > "$WORK/lib-stage/lib/arm64-v8a/lib${ln}.so"
}
build_core 0x1000 emucore_4k
build_core 0x4000 emucore_16k
for so in emucore_4k emucore_16k; do
sz="$(fsize "$WORK/lib-stage/lib/arm64-v8a/lib${so}.so")"
echo " lib${so}.so = $(( sz / 1024 / 1024 )) MB"
[[ "$sz" -gt 10000000 ]] || { echo "FATAL: lib${so}.so too small ($sz bytes)" >&2; exit 1; }
done
# --- merge: 4k APK as base, drop old signatures + both cores, re-add STORED ---
UNS="$WORK/universal-unsigned.apk"; ALN="$WORK/universal-aligned.apk"
cp -f "$WORK/base-emucore_4k.apk" "$UNS"
zip -qd "$UNS" "META-INF/*" >/dev/null 2>&1 || true
zip -qd "$UNS" "lib/arm64-v8a/libemucore_4k.so" "lib/arm64-v8a/libemucore_16k.so" >/dev/null 2>&1 || true
( cd "$WORK/lib-stage" && zip -qr -0 "$UNS" lib )
"$ZIPALIGN" -f -P 16 4 "$UNS" "$ALN"
# --- sign: stable key if provided, else a throwaway debug key (warn) ----------
KS="${NIGHTLY_KEYSTORE:-$HOME/.android/debug.keystore}"
KS_PASS="${NIGHTLY_KS_PASS:-android}"
KS_ALIAS="${NIGHTLY_KEY_ALIAS:-androiddebugkey}"
KEY_PASS="${NIGHTLY_KEY_PASS:-android}"
if [[ ! -f "$KS" ]]; then
echo "WARNING: no keystore at $KS — generating a throwaway debug key." >&2
echo " Set NIGHTLY_KEYSTORE (repo secret) for stable nightly-over-nightly updates." >&2
mkdir -p "$(dirname "$KS")"
keytool -genkeypair -keystore "$KS" -storepass "$KS_PASS" -keypass "$KEY_PASS" \
-alias "$KS_ALIAS" -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US" >/dev/null 2>&1
fi
rm -f "$OUT"
"$APKSIGNER" sign --ks "$KS" --ks-key-alias "$KS_ALIAS" \
--ks-pass "pass:$KS_PASS" --key-pass "pass:$KEY_PASS" \
--out "$OUT" "$ALN"
echo; echo "================= VERIFY ================="
echo "-- both cores present --"
unzip -l "$OUT" | grep -E "libemucore_(4k|16k)\.so" || { echo "FATAL: cores missing" >&2; exit 1; }
echo "-- 16k alignment --"; "$ZIPALIGN" -c -P 16 4 "$OUT" && echo " align OK"
echo "-- signature --"; "$APKSIGNER" verify "$OUT" && echo " sig OK"
[[ -x "$AAPT" ]] && "$AAPT" dump badging "$OUT" 2>/dev/null | grep -E "package: name|versionCode|versionName" | head -2
echo; echo "OUTPUT: $OUT"
echo "NIGHTLY-DUALCORE-DONE"