build(android): add a side-by-side release build for validating R8
R8 has broken release APKs here before by stripping the JNI-loaded player and security classes, and the only way to reproduce that was to build with the real signing key and clobber the install you actually use. `./scripts/build-and-deploy.sh release --device --debug` now builds a fully minified release APK — exactly what ships — into the .debug applicationId slot, signed with the local debug keystore: release com.dtourolle.jellytau 0.5.5 release --debug com.dtourolle.jellytau.debug 0.5.5-debug-release debug com.dtourolle.jellytau.debug 0.5.5-debug It shares the applicationId *and* the signature with the plain debug build, so the two replace each other cleanly rather than colliding, and the versionName suffix says which is currently installed. No real key is needed, so the side-by-side path deliberately skips write-keystore-properties.sh. The flag reaches Gradle as JT_SIDE_BY_SIDE=1. CI never sets it, and the release manifest merges byte-identical without it — verified both ways through processUniversalReleaseMainManifest. deploy-android.sh and build-and-deploy.sh learned the flag too, since the APK path is unchanged but the package to launch is not.
This commit is contained in:
@@ -34,9 +34,12 @@ bun run android:logs # logcat
|
|||||||
The **debug** build type carries `applicationIdSuffix ".debug"`, so
|
The **debug** build type carries `applicationIdSuffix ".debug"`, so
|
||||||
`com.dtourolle.jellytau.debug` ("JellyTau Debug") installs *alongside* a release
|
`com.dtourolle.jellytau.debug` ("JellyTau Debug") installs *alongside* a release
|
||||||
build with its own data dir — never uninstall the release app to test a debug
|
build with its own data dir — never uninstall the release app to test a debug
|
||||||
one. Only the applicationId is suffixed; Kotlin classes stay in the `namespace`
|
one. `./scripts/build-and-deploy.sh release --device --debug` puts an
|
||||||
package `com.dtourolle.jellytau`, so JNI lookups and R8 keep rules are
|
R8-minified *release* build in that same slot, signed with the local debug
|
||||||
unaffected. See [README_ANDROID_BUILD.md](src-tauri/android/README_ANDROID_BUILD.md).
|
keystore, for validating minification without the real key. Only the
|
||||||
|
applicationId is suffixed; Kotlin classes stay in the `namespace` package
|
||||||
|
`com.dtourolle.jellytau`, so JNI lookups and R8 keep rules are unaffected. See
|
||||||
|
[README_ANDROID_BUILD.md](src-tauri/android/README_ANDROID_BUILD.md).
|
||||||
|
|
||||||
CI runs on **Gitea Actions** (`.gitea/workflows/`), not GitHub. Use the `gh` CLI
|
CI runs on **Gitea Actions** (`.gitea/workflows/`), not GitHub. Use the `gh` CLI
|
||||||
only against the mirror if one exists; the canonical remote is
|
only against the mirror if one exists; the canonical remote is
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ echo ""
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Deploy APK — extract build type (default debug), ignoring flags like --clean.
|
# Deploy APK — forward the build type and the side-by-side flag (which decides
|
||||||
BUILD_TYPE="debug"
|
# which package to launch), ignoring build-only flags like --clean and --device.
|
||||||
|
DEPLOY_ARGS=("debug")
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
debug|release) BUILD_TYPE="$arg" ;;
|
debug|release) DEPLOY_ARGS[0]="$arg" ;;
|
||||||
|
--debug|--side-by-side) DEPLOY_ARGS+=("--side-by-side") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
./scripts/deploy-android.sh "$BUILD_TYPE"
|
./scripts/deploy-android.sh "${DEPLOY_ARGS[@]}"
|
||||||
|
|||||||
@@ -23,9 +23,18 @@ echo ""
|
|||||||
# which is what a distributable universal APK needs — but for an on-device test
|
# which is what a distributable universal APK needs — but for an on-device test
|
||||||
# it means three wasted Rust compiles. Pass --device (or ABI=aarch64) to build
|
# it means three wasted Rust compiles. Pass --device (or ABI=aarch64) to build
|
||||||
# only the connected device's architecture; --abi <t> targets one explicitly.
|
# only the connected device's architecture; --abi <t> targets one explicitly.
|
||||||
|
#
|
||||||
|
# Side-by-side: the `debug` build type always installs as
|
||||||
|
# com.dtourolle.jellytau.debug ("JellyTau Debug"), so it never collides with a
|
||||||
|
# real install. `release --debug` puts a *release* build — R8-minified, exactly
|
||||||
|
# what ships — into that same slot, signed with the local debug keystore. That
|
||||||
|
# is how you validate minification (R8 stripping JNI-loaded classes has broken
|
||||||
|
# release APKs here before) without the real signing key and without
|
||||||
|
# uninstalling the app you actually use.
|
||||||
BUILD_TYPE="debug"
|
BUILD_TYPE="debug"
|
||||||
CLEAN="${CLEAN:-0}"
|
CLEAN="${CLEAN:-0}"
|
||||||
ABI="${ABI:-}"
|
ABI="${ABI:-}"
|
||||||
|
SIDE_BY_SIDE="${SIDE_BY_SIDE:-0}"
|
||||||
next_is_abi=0
|
next_is_abi=0
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [ "$next_is_abi" = "1" ]; then
|
if [ "$next_is_abi" = "1" ]; then
|
||||||
@@ -37,10 +46,17 @@ for arg in "$@"; do
|
|||||||
--clean) CLEAN=1 ;;
|
--clean) CLEAN=1 ;;
|
||||||
--abi) next_is_abi=1 ;;
|
--abi) next_is_abi=1 ;;
|
||||||
--device) ABI="device" ;;
|
--device) ABI="device" ;;
|
||||||
|
--debug|--side-by-side) SIDE_BY_SIDE=1 ;;
|
||||||
debug|release) BUILD_TYPE="$arg" ;;
|
debug|release) BUILD_TYPE="$arg" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# The debug build type is side-by-side unconditionally; the flag only means
|
||||||
|
# something for a release build.
|
||||||
|
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||||
|
SIDE_BY_SIDE=1
|
||||||
|
fi
|
||||||
|
|
||||||
# Resolve --device to the attached device's Rust target triple.
|
# Resolve --device to the attached device's Rust target triple.
|
||||||
if [ "$ABI" = "device" ]; then
|
if [ "$ABI" = "device" ]; then
|
||||||
device_abi="$(adb shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r\n')"
|
device_abi="$(adb shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r\n')"
|
||||||
@@ -78,7 +94,14 @@ echo "🎨 Building frontend..."
|
|||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Step 2: Build Android APK
|
# Step 2: Build Android APK
|
||||||
if [ "$BUILD_TYPE" = "release" ]; then
|
if [ "$BUILD_TYPE" = "release" ] && [ "$SIDE_BY_SIDE" = "1" ]; then
|
||||||
|
# A release build in the debug slot: R8 still runs, but the applicationId is
|
||||||
|
# suffixed and the debug keystore signs it (read by build.gradle.kts from
|
||||||
|
# JT_SIDE_BY_SIDE), so the real key is not needed and it replaces any other
|
||||||
|
# .debug install cleanly. Deliberately does NOT write keystore.properties.
|
||||||
|
echo "📦 Building side-by-side release APK (com.dtourolle.jellytau.debug)..."
|
||||||
|
JT_SIDE_BY_SIDE=1 bun run tauri android build --apk true "${TARGET_ARGS[@]}"
|
||||||
|
elif [ "$BUILD_TYPE" = "release" ]; then
|
||||||
# Configure release signing from .env (single source of truth). Must run
|
# Configure release signing from .env (single source of truth). Must run
|
||||||
# after sync-android-sources.sh, since gen/android is (re)generated there.
|
# after sync-android-sources.sh, since gen/android is (re)generated there.
|
||||||
./scripts/write-keystore-properties.sh
|
./scripts/write-keystore-properties.sh
|
||||||
|
|||||||
@@ -13,24 +13,42 @@ if ! adb devices | grep -q "device$"; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build type: debug or release (default: debug)
|
# Build type: debug or release (default: debug). `--debug` alongside `release`
|
||||||
BUILD_TYPE="${1:-debug}"
|
# means the side-by-side release build — same APK path, but it was packaged
|
||||||
|
# under the .debug applicationId, so the package to launch differs.
|
||||||
|
BUILD_TYPE="debug"
|
||||||
|
SIDE_BY_SIDE=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--debug|--side-by-side) SIDE_BY_SIDE=1 ;;
|
||||||
|
debug|release) BUILD_TYPE="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ "$BUILD_TYPE" = "debug" ] && SIDE_BY_SIDE=1
|
||||||
|
|
||||||
# The debug build carries applicationIdSuffix ".debug" (see
|
# The .debug applicationId (see src-tauri/android/app/build.gradle.kts) is a
|
||||||
# src-tauri/android/app/build.gradle.kts), so it is a separate package and
|
# separate package, so it installs alongside a real release build — no
|
||||||
# installs alongside a release build — no uninstall dance needed.
|
# uninstall dance needed.
|
||||||
if [ "$BUILD_TYPE" = "release" ]; then
|
if [ "$BUILD_TYPE" = "release" ]; then
|
||||||
APK_PATH="src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk"
|
APK_PATH="src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk"
|
||||||
APP_PACKAGE="com.dtourolle.jellytau"
|
|
||||||
else
|
else
|
||||||
APK_PATH="src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk"
|
APK_PATH="src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SIDE_BY_SIDE" = "1" ]; then
|
||||||
APP_PACKAGE="com.dtourolle.jellytau.debug"
|
APP_PACKAGE="com.dtourolle.jellytau.debug"
|
||||||
|
else
|
||||||
|
APP_PACKAGE="com.dtourolle.jellytau"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if APK exists
|
# Check if APK exists
|
||||||
if [ ! -f "$APK_PATH" ]; then
|
if [ ! -f "$APK_PATH" ]; then
|
||||||
echo "❌ APK not found at: $APK_PATH"
|
echo "❌ APK not found at: $APK_PATH"
|
||||||
echo "Run './scripts/build-android.sh $BUILD_TYPE' first"
|
if [ "$BUILD_TYPE" = "release" ] && [ "$SIDE_BY_SIDE" = "1" ]; then
|
||||||
|
echo "Run './scripts/build-android.sh release --debug' first"
|
||||||
|
else
|
||||||
|
echo "Run './scripts/build-android.sh $BUILD_TYPE' first"
|
||||||
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,26 @@ When you need to modify Android/Kotlin files:
|
|||||||
The **debug** build type sets `applicationIdSuffix = ".debug"` in
|
The **debug** build type sets `applicationIdSuffix = ".debug"` in
|
||||||
`app/build.gradle.kts`, so a debug build is a genuinely separate Android app:
|
`app/build.gradle.kts`, so a debug build is a genuinely separate Android app:
|
||||||
|
|
||||||
| | applicationId | launcher name | versionName |
|
| build | applicationId | launcher name | versionName | signed with |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| release | `com.dtourolle.jellytau` | jellytau | `0.5.5` |
|
| `release` | `com.dtourolle.jellytau` | jellytau | `0.5.5` | real key (`.env`) |
|
||||||
| debug | `com.dtourolle.jellytau.debug` | JellyTau Debug | `0.5.5-debug` |
|
| `release --debug` | `com.dtourolle.jellytau.debug` | JellyTau Debug | `0.5.5-debug-release` | debug keystore |
|
||||||
|
| `debug` | `com.dtourolle.jellytau.debug` | JellyTau Debug | `0.5.5-debug` | debug keystore |
|
||||||
|
|
||||||
|
`release --debug` is the **side-by-side release**: fully R8-minified, exactly
|
||||||
|
what ships, but packaged into the debug slot and signed with the local debug
|
||||||
|
keystore. It exists because R8 has broken release APKs here before (stripping
|
||||||
|
JNI-loaded player/security classes), and reproducing that previously meant
|
||||||
|
building with the real key and clobbering your working install. It shares the
|
||||||
|
applicationId *and* signature with the plain debug build, so the two replace
|
||||||
|
each other cleanly; only the versionName suffix tells you which is installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-and-deploy.sh release --device --debug # build + install it
|
||||||
|
```
|
||||||
|
|
||||||
|
The flag is plumbed through as `JT_SIDE_BY_SIDE=1`, read by `build.gradle.kts`.
|
||||||
|
CI never sets it, so distributable release builds are untouched.
|
||||||
|
|
||||||
That means:
|
That means:
|
||||||
|
|
||||||
@@ -59,7 +75,10 @@ That means:
|
|||||||
different packages is just two apps.
|
different packages is just two apps.
|
||||||
- Each has **its own data directory** — separate settings, credentials,
|
- Each has **its own data directory** — separate settings, credentials,
|
||||||
downloads and offline cache. A debug experiment cannot corrupt the state of
|
downloads and offline cache. A debug experiment cannot corrupt the state of
|
||||||
the build you actually use.
|
the build you actually use. This is not optional and cannot be shared:
|
||||||
|
Android gives each applicationId its own UID and enforces the boundary in the
|
||||||
|
kernel. (`sharedUserId` is deprecated since API 29 and cannot be added to an
|
||||||
|
already-installed app anyway.) You log in again in the debug app, once.
|
||||||
- Only the *application* id changes. Kotlin classes stay in the `namespace`
|
- Only the *application* id changes. Kotlin classes stay in the `namespace`
|
||||||
package `com.dtourolle.jellytau`, so the JNI class lookups in
|
package `com.dtourolle.jellytau`, so the JNI class lookups in
|
||||||
`src-tauri/src/player/android/mod.rs`, the manifest `<service>` entry and the
|
`src-tauri/src/player/android/mod.rs`, the manifest `<service>` entry and the
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ val keystoreProperties = Properties().apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Side-by-side release: set by `scripts/build-android.sh release --debug`, which
|
||||||
|
// exports JT_SIDE_BY_SIDE=1. It puts a fully R8-minified release build into the
|
||||||
|
// debug applicationId slot, signed with the local debug keystore — so you can
|
||||||
|
// test what minification actually produces (R8 stripping JNI-loaded classes has
|
||||||
|
// broken release APKs here before) without the real signing key and without
|
||||||
|
// uninstalling your working install. Unset in CI, so distributable release
|
||||||
|
// builds are untouched.
|
||||||
|
val sideBySideRelease = System.getenv("JT_SIDE_BY_SIDE").let { it == "1" || it == "true" }
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
namespace = "com.dtourolle.jellytau"
|
namespace = "com.dtourolle.jellytau"
|
||||||
@@ -76,7 +85,18 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
getByName("release") {
|
getByName("release") {
|
||||||
if (keystoreProperties.getProperty("storeFile") != null) {
|
if (sideBySideRelease) {
|
||||||
|
// Same slot, name and version scheme as the debug build type,
|
||||||
|
// plus "-release" so you can tell from Settings > Apps which of
|
||||||
|
// the two is currently sitting there. Signed with the debug
|
||||||
|
// keystore: it shares a signature with the debug build, so the
|
||||||
|
// two replace each other cleanly instead of colliding.
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
versionNameSuffix = "-debug-release"
|
||||||
|
manifestPlaceholders["appLabel"] = "JellyTau Debug"
|
||||||
|
manifestPlaceholders["activityLabel"] = "JellyTau Debug"
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
} else if (keystoreProperties.getProperty("storeFile") != null) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
|||||||
Reference in New Issue
Block a user