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:
2026-08-16 10:42:59 +02:00
parent 886cbcb29a
commit 521acc75fd
6 changed files with 106 additions and 21 deletions
+24 -5
View File
@@ -46,10 +46,26 @@ When you need to modify Android/Kotlin files:
The **debug** build type sets `applicationIdSuffix = ".debug"` in
`app/build.gradle.kts`, so a debug build is a genuinely separate Android app:
| | applicationId | launcher name | versionName |
|---|---|---|---|
| release | `com.dtourolle.jellytau` | jellytau | `0.5.5` |
| debug | `com.dtourolle.jellytau.debug` | JellyTau Debug | `0.5.5-debug` |
| build | applicationId | launcher name | versionName | signed with |
|---|---|---|---|---|
| `release` | `com.dtourolle.jellytau` | jellytau | `0.5.5` | real key (`.env`) |
| `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:
@@ -59,7 +75,10 @@ That means:
different packages is just two apps.
- Each has **its own data directory** — separate settings, credentials,
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`
package `com.dtourolle.jellytau`, so the JNI class lookups in
`src-tauri/src/player/android/mod.rs`, the manifest `<service>` entry and the
+21 -1
View File
@@ -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 {
compileSdk = 36
namespace = "com.dtourolle.jellytau"
@@ -76,7 +85,18 @@ android {
}
}
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")
}
isMinifyEnabled = true