Files
ARMSX3/android/build-play-aab.sh
T
jpolo1224 eaba425972 Build: stamp the real commit into the version string
Builds were reporting whichever commit cmake last configured against, not the
one being built. rpcs3/git-version.h is generated at CONFIGURE time, and
build-variants.sh deliberately skips reconfiguring an already-correct build
dir because re-running cmake regenerates LLVM's headers and costs a full
rebuild. So the stamp froze, and every build after that lied about itself.

The gap was wide. HEAD is 20120-da26a455; the header on disk still said
19985-91952ae4, a commit from before 0.9.2.

This is not cosmetic. A tester running 0.9.3 reported that older commit in
their log, which sent an investigation hunting for a regression among upstream
ISO changes their build did not contain, and very nearly had a fix built and
sent for a version they were not running. A build that misreports itself makes
every report from it ambiguous.

stamp-git-version.sh writes the header from HEAD and is called by both build
entry points before anything compiles. It only writes when the contents differ,
so an unchanged HEAD does not force a rebuild, and ninja rebuilds just the
translation units that include it.
2026-08-20 12:43:45 -04:00

181 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build the Google Play bundle, and refuse to produce one that Play would reject.
#
# The legacy variant is what goes to Play: armv8.1-a and minSdk 30 is the widest floor ARMSX3
# has, and Play serves one bundle to every device, so the lowest floor reaches the most people.
# targetSdk is 37 for every variant, so the API-level requirement is met regardless.
#
# The checks below are the point of this script. A flavor split is only as good as the thing
# that notices when it stops working, and every one of these has a specific failure behind it:
#
# REQUEST_INSTALL_PACKAGES a self-updating app is a hard policy violation, and it is the
# declared permission that gets rejected, not the code path
# MANAGE_EXTERNAL_STORAGE all-files access needs a declared exemption the app does not need
# RECORD_AUDIO would put "Microphone" on the listing for a capability not shipped
# libarmsx3_lsfg.so frame generation is not distributed through Play
# updateprovider the FileProvider that hands the downloaded APK to the installer
#
# Fails closed: if a check cannot run, that is a failure too.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UI="$HERE/armsx3-ui"
: "${OUT_DIR:=$HOME/Downloads}"
: "${ANDROID_HOME:=$HOME/Library/Android/sdk}"
MIN_SDK="${PLAY_MIN_SDK:-30}"
# Gradle needs a JDK and the shell this is run from may not have one on PATH. Android Studio
# ships one; fall back to it rather than failing several steps later with "Unable to locate a
# Java Runtime", which does not point at the cause.
if [ -z "${JAVA_HOME:-}" ]; then
for candidate in \
"/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
"$(/usr/libexec/java_home 2>/dev/null || true)"
do
[ -x "$candidate/bin/java" ] && { export JAVA_HOME="$candidate"; break; }
done
fi
[ -n "${JAVA_HOME:-}" ] || { echo "FAIL: no JDK found; set JAVA_HOME" >&2; exit 1; }
export PATH="$JAVA_HOME/bin:$PATH"
# Refuse to build at all without an upload key. A debug-signed bundle is rejected by Play, and
# finding that out at upload time after a fifteen-minute build is a poor way to learn it.
if [ ! -f "$UI/keystore.properties" ] || grep -q "REPLACE_ME\|REPLACE_WITH_ABSOLUTE_PATH" "$UI/keystore.properties"; then
cat >&2 <<'MSG'
FAIL: upload key not configured (file missing, or placeholders not filled in).
Create android/armsx3-ui/keystore.properties with:
storeFile=/absolute/path/to/upload.jks
storePassword=...
keyAlias=upload
keyPassword=...
and generate the key itself with:
keytool -genkeypair -v -keystore upload.jks -alias upload \
-keyalg RSA -keysize 4096 -validity 10000
Keep upload.jks and its passwords safe and backed up: Play ties the listing to this key
and losing it means losing the ability to update the app. Both the keystore and the
properties file are gitignored.
MSG
exit 1
fi
# armsx3.noMinify because AGP 9.2.1 cannot bundle with R8 on: R8 writes mapping.prt, a
# compressed per-class archive, and packageBundle demands a plain mapping.txt. ARMSX2 ships
# its Play build with minify off too, so this is the existing precedent rather than a new
# compromise.
# Stage the LEGACY core, and do not trust whatever happens to be in jniLibs.
#
# build-variants.sh writes each variant's core to the same path in turn, so the file left there
# is simply whichever variant ran last. A bundle built on top of that would ship an armv8.2 core
# with minSdk 30 -- installable on devices that cannot execute it, and failing at dlopen with
# nothing to explain why. Play serves one bundle to every device, so the ISA floor has to be the
# lowest one ARMSX3 supports.
CORE_SRC="$(cd "$HERE/.." && pwd)/build-legacy/android/libarmsx3-core.so"
JNI="$UI/app/src/main/jniLibs/arm64-v8a"
if [ ! -f "$CORE_SRC" ]; then
echo "FAIL: legacy core not built. Run: ninja -C build-legacy android/libarmsx3-core.so" >&2
exit 1
fi
NDK_DIR="$(ls -d "$ANDROID_HOME/ndk/"*/ 2>/dev/null | sort -V | tail -1)"
STRIP="${NDK_DIR}toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-strip"
[ -x "$STRIP" ] || { echo "FAIL: llvm-strip not found under $ANDROID_HOME/ndk" >&2; exit 1; }
# Same reason as build-variants.sh: cmake only regenerates this at configure time.
bash "$HERE/stamp-git-version.sh"
echo "==> Staging the legacy core"
mkdir -p "$JNI"
"$STRIP" --strip-unneeded -o "$JNI/libarmsx3-core.so" "$CORE_SRC"
echo "==> Building Play bundle (minSdk $MIN_SDK, minify off)"
( cd "$UI" && ./gradlew --quiet :app:bundlePlayRelease "-Parmsx3.minSdk=$MIN_SDK" -Parmsx3.noMinify )
AAB="$UI/app/build/outputs/bundle/playRelease/app-play-release.aab"
[ -f "$AAB" ] || { echo "FAIL: no bundle produced at $AAB" >&2; exit 1; }
echo "==> Verifying the bundle"
# An AAB is a ZIP and its entries are COMPRESSED, so grepping the archive itself finds
# nothing and reports a clean bundle no matter what is in it. That is not a theoretical
# risk: the first version of this script did exactly that and passed every check while
# also failing to find the applicationId it was supposed to find, which is what gave it
# away. Extract first, then inspect the manifest and the file list.
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
unzip -q -o "$AAB" -d "$WORK"
MANIFEST="$WORK/base/manifest/AndroidManifest.xml"
[ -f "$MANIFEST" ] || { echo "FAIL: no manifest in the bundle" >&2; exit 1; }
fail=0
# The manifest is protobuf-encoded, so permission names appear as plain strings inside it
# but the file contains NUL bytes -- LC_ALL=C and -a keep grep from calling it binary and
# silently saying nothing, which reads exactly like a pass.
check_absent_manifest() {
local needle="$1" why="$2"
if LC_ALL=C grep -aqF "$needle" "$MANIFEST"; then
echo "FAIL: '$needle' is declared in the bundle -- $why" >&2
fail=1
else
echo " ok: $needle absent from the manifest"
fi
}
check_absent_manifest "REQUEST_INSTALL_PACKAGES" "Play forbids self-updating apps"
check_absent_manifest "MANAGE_EXTERNAL_STORAGE" "all-files access needs a declared exemption"
check_absent_manifest "RECORD_AUDIO" "would add Microphone to the listing"
check_absent_manifest "updateprovider" "the updater FileProvider must not ship"
# Native libraries are entries in the archive, so check the listing rather than the bytes.
#
# Captured ONCE into a variable rather than piped into each grep. Under `set -o pipefail`,
# `unzip -l | grep -q x` fails whenever x IS found: grep exits at the first match, closes the
# pipe, unzip takes SIGPIPE, and the pipeline reports failure. That inverted every positive
# check -- it reported the core library missing from a bundle that plainly contained it, while
# the absence checks passed for the wrong reason, because grep read to the end and found
# nothing.
LISTING="$(unzip -l "$AAB")"
# Matched with `case`, not with a pipe into grep. Under `set -o pipefail` any `... | grep -q x`
# FAILS when x is found: grep exits at the first match, closes the pipe, and whatever is feeding
# it takes SIGPIPE. That inverted every positive check -- the core library was reported missing
# from a bundle that plainly contained it, while the absence checks passed for the wrong reason,
# because grep read to the end and found nothing. Piping printf instead of unzip moved the
# broken pipe rather than removing it; case has no subprocess to signal.
if [[ "$LISTING" == *libarmsx3_lsfg.so* ]]; then
echo "FAIL: libarmsx3_lsfg.so is in the bundle -- frame generation is not shipped through Play" >&2
fail=1
else
echo " ok: libarmsx3_lsfg.so absent"
fi
# And the things that MUST be there.
if LC_ALL=C grep -aqF "com.armsx3.play" "$MANIFEST"; then
echo " ok: applicationId is com.armsx3.play"
else
echo "FAIL: applicationId com.armsx3.play not in the manifest -- wrong flavor built?" >&2
fail=1
fi
if [[ "$LISTING" == *libarmsx3-core.so* ]]; then
echo " ok: core library present"
else
echo "FAIL: libarmsx3-core.so missing from the bundle" >&2
fail=1
fi
[ "$fail" -eq 0 ] || { echo "==> REFUSING to ship this bundle" >&2; exit 1; }
OUT="$OUT_DIR/ARMSX3-$(sed -n 's/.*versionName = "\([^"]*\)".*/\1/p' "$UI/app/build.gradle.kts" | head -1)-play.aab"
cp "$AAB" "$OUT"
echo "==> $OUT"