From 1ba836928fcd7e69a2f3e2b3235c1a7c7decde62 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 25 Aug 2026 23:48:24 +0200 Subject: [PATCH] fix(android): keep the debug applicationId out of Tauri's reach `bun run android:dev` produced an APK whose applicationId was plain com.dtourolle.jellytau, so installing it over a real release build failed with INSTALL_FAILED_UPDATE_INCOMPATIBLE -- the only obvious way out being to uninstall the release app and lose its data. `tauri android build` rewrites the getByName("debug") block in the generated copy of build.gradle.kts to inject its jniLibs.keepDebugSymbols entries. The damage is visible in the generated file, where `packaging {` ends up with the first injected line welded onto it. That rewrite drops applicationIdSuffix and nothing else -- versionNameSuffix and the manifest placeholders beside it survive -- and it happens after sync-android-sources.sh has copied the canonical file into place and before Gradle configures, so no amount of syncing beats it. The sideBySideRelease suffix in the release build type is untouched by the same rewrite, which is why `build-and-deploy.sh release --debug` kept working while the plain debug path did not. The suffix moves to a top-level statement after the android {} block, which is not inside what the rewriter looks for and survives. build-android.sh then asserts the applicationId the APK actually carries, read from AGP's output-metadata.json, so a future CLI that reaches further fails the build instead of shipping a colliding APK. Verified: a debug build now reports com.dtourolle.jellytau.debug, and the statement is still there in gen/ after the CLI has run. --- scripts/build-android.sh | 38 ++++++++++++++++++++++++++ src-tauri/android/app/build.gradle.kts | 28 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 6ac40384..58fe5c9a 100755 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -128,6 +128,44 @@ else bun run tauri android build --apk --debug "${TARGET_ARGS[@]}" fi +# The applicationId the APK actually carries — not the one build.gradle.kts asks +# for. `tauri android build` rewrites the debug `buildTypes` block in the +# generated gradle file to inject its keepDebugSymbols entries, and that rewrite +# used to drop `applicationIdSuffix` with it, silently producing a debug APK +# under the release applicationId. Installing that over a real release build +# fails with INSTALL_FAILED_UPDATE_INCOMPATIBLE, whose only obvious remedy is +# uninstalling the release app and losing its data — so this fails the build +# instead. The suffix now lives outside the rewritten block (see +# src-tauri/android/app/build.gradle.kts); this checks that it survived. +assert_application_id() { + local variant="$1" expected="$2" + local metadata="src-tauri/gen/android/app/build/outputs/apk/universal/$variant/output-metadata.json" + + [ -f "$metadata" ] || return 0 + + local actual + actual=$(sed -n 's/.*"applicationId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata" | head -1) + + if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then + echo "" + echo "❌ APK applicationId is '$actual', expected '$expected'." + echo " A build meant for the side-by-side slot came out under the" + echo " release applicationId; installing it would collide with a real" + echo " install. Check that the applicationIdSuffix at the bottom of" + echo " src-tauri/android/app/build.gradle.kts survived into" + echo " src-tauri/gen/android/app/build.gradle.kts." + exit 1 + fi +} + +if [ "$BUILD_TYPE" = "debug" ]; then + assert_application_id debug "com.dtourolle.jellytau.debug" +elif [ "$SIDE_BY_SIDE" = "1" ]; then + assert_application_id release "com.dtourolle.jellytau.debug" +else + assert_application_id release "com.dtourolle.jellytau" +fi + echo "" echo "✅ APK build complete!" echo "📱 APK location: src-tauri/gen/android/app/build/outputs/apk/" diff --git a/src-tauri/android/app/build.gradle.kts b/src-tauri/android/app/build.gradle.kts index c0c13301..2682dfb5 100644 --- a/src-tauri/android/app/build.gradle.kts +++ b/src-tauri/android/app/build.gradle.kts @@ -73,7 +73,9 @@ android { // fully-qualified class names Rust looks up over JNI, the manifest // entry and the R8 keep rules are all unaffected. The // FileProvider authority is already ${applicationId}-relative. - applicationIdSuffix = ".debug" + // + // The suffix itself is applied AFTER this block -- see the bottom of + // this file. It cannot live here. versionNameSuffix = "-debug" manifestPlaceholders["appLabel"] = "JellyTau Debug" manifestPlaceholders["activityLabel"] = "JellyTau Debug" @@ -127,6 +129,30 @@ android { } } +// The debug applicationId suffix, applied from OUTSIDE the `buildTypes` block. +// +// `tauri android build` rewrites the `getByName("debug")` block in the *generated* +// copy of this file (src-tauri/gen/android/app/build.gradle.kts) to inject its +// `jniLibs.keepDebugSymbols` entries -- you can see the damage in the generated +// file, where `packaging {` ends up with the first injected line welded onto it. +// That rewrite drops `applicationIdSuffix` and nothing else: `versionNameSuffix` +// and the manifest placeholders beside it survive. It happens after +// sync-android-sources.sh has copied this file into place and before Gradle +// configures, so no amount of syncing can beat it. +// +// The result was a debug APK whose applicationId was plain +// `com.dtourolle.jellytau`, colliding with a real release install: +// INSTALL_FAILED_UPDATE_INCOMPATIBLE, with the only obvious way out being to +// uninstall the release app and lose its data. The `sideBySideRelease` suffix in +// the *release* build type is untouched by the same rewrite, which is why that +// path kept working and this one did not. +// +// A top-level statement is not inside the block the rewriter looks for, so it +// survives. scripts/build-android.sh asserts the built applicationId afterwards, +// so a future CLI that reaches further fails the build instead of shipping a +// colliding APK. +android.buildTypes.getByName("debug").applicationIdSuffix = ".debug" + rust { rootDirRel = "../../../" }