From 6fbb62730daf0b2ee66839f0e6b96e5e229f0432 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 16 Aug 2026 15:30:35 -0400 Subject: [PATCH] fix(android): build a release APK that is releasable Three edits to the scaffold, each of which the generated tree gets wrong for a shipped app. **The phone ABI got a debug library.** Upstream's `build` task forwards ARCH to compile:go:shared but not PRODUCTION, so the arm64 leg recomputed BUILD_FLAGS against an unset variable and took the debug branch -- while amd64, which package:fat calls directly with PRODUCTION: "true", was correct. A release APK therefore shipped a 40MB unstripped debug library for the only ABI a release is for, beside a 31MB production one for the emulator. 34MB APK before, 27MB after. **The APK could be installed once and never updated.** Android orders releases by versionCode and refuses anything not greater than what is installed; the scaffold hardcodes 1, so the first install would have been the last and the only way out is an uninstall, which takes the user's library with it. It comes from YJ_VERSION_CODE now, which CI derives from the tag (1.3.1 -> 10301, monotonic while minor and patch stay under 100), with a default that keeps a local build working. Integer.parseInt, not `(...) as Integer`: Groovy binds the call parentheses to versionCode before the cast, so the latter reads as `versionCode("1") as Integer` -- it sets a String, then casts the setter's null return, and Gradle fails the whole project with "Value is null" pointing at that line. **And it identified itself as com.wails.app.** applicationId is app.yellowjacket now, matching build/config.yml's productIdentifier, and the label is YellowJacket rather than "Wails App". Two things follow from that rename and both bite: The identity is declared twice. applicationId is what Gradle installs; APP_ID in build/android/Taskfile.yml is what every adb-driven task uninstalls, launches and filters, and nothing enforces agreement. ANDROID.md says to set APP_ID in build/config.yml -- that does nothing in beta.8, checked both ways: `wails3 task` builds its var set from CLI KEY=VALUE arguments and the Taskfile tree and never reads config.yml, and even when set it feeds only those adb commands, never Gradle. And `namespace` deliberately stays com.wails.app, because that is the Java package MainActivity and WailsBridge live in and renaming it means renaming their source. So the launcher activity is app.yellowjacket/com.wails.app.MainActivity, and the short `.MainActivity` form resolves the dot against the applicationId and fails with a class-not-found that reads like a broken build. --- build/android/Taskfile.yml | 24 ++++++++++++++- build/android/app/build.gradle | 29 +++++++++++++++++-- .../app/src/main/res/values/strings.xml | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/build/android/Taskfile.yml b/build/android/Taskfile.yml index 980b700..b157ca3 100644 --- a/build/android/Taskfile.yml +++ b/build/android/Taskfile.yml @@ -4,7 +4,18 @@ includes: common: ../Taskfile.yml vars: - APP_ID: '{{.APP_ID | default "com.wails.app"}}' + # The *installed* package name, which every adb-driven task below uses + # to uninstall, launch and filter. It must agree with `applicationId` + # in app/build.gradle, and nothing enforces that. + # + # ANDROID.md says to set this in build/config.yml. That does not work + # in beta.8, checked both ways: `wails3 task` builds its var set from + # CLI KEY=VALUE arguments and the Taskfile tree only -- nothing reads + # config.yml -- and even when set it feeds only these adb commands, + # never Gradle. So the identity is declared twice, here and in + # build.gradle, and a change to one alone means the official run and + # deploy tasks address a package that is not installed. + APP_ID: '{{.APP_ID | default "app.yellowjacket"}}' MIN_SDK: '21' TARGET_SDK: '35' # The emulator runs the host architecture; physical devices are arm64 @@ -51,6 +62,17 @@ tasks: - task: compile:go:shared vars: ARCH: '{{.ARCH | default .HOST_ARCH}}' + # This repo's one edit to the android scaffold, and it is not + # cosmetic. Upstream forwards ARCH here and not PRODUCTION, so + # compile:go:shared recomputed BUILD_FLAGS against an unset + # .PRODUCTION and fell back to the debug branch. package:fat + # calls compile:go:shared directly for amd64 (passing it), and + # reaches arm64 only through this task -- so a release APK + # shipped a *debug* 40 MB arm64 library beside a production + # 31 MB x86_64 one. The phone ABI, which is the only one a + # release is for, was the broken one. 34 MB APK before, 27 + # after. + PRODUCTION: '{{.PRODUCTION}}' vars: BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' env: diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle index 9c69943..b8c2cd0 100644 --- a/build/android/app/build.gradle +++ b/build/android/app/build.gradle @@ -11,11 +11,34 @@ android { } defaultConfig { - applicationId "com.wails.app" + // The app's identity on the device. `namespace` above stays + // com.wails.app -- that is the *Java package* the scaffold's + // MainActivity/WailsBridge live in, and renaming it would mean + // renaming their source. The two being different is normal and is + // why every `am start` needs the fully-qualified activity name + // (app.yellowjacket/com.wails.app.MainActivity), not `.MainActivity`. + // + // Matches build/config.yml's productIdentifier. + applicationId "app.yellowjacket" minSdk 21 targetSdk 35 - versionCode 1 - versionName "1.0" + + // **Android orders releases by this integer, not by the version + // string, and refuses to install anything not greater than what is + // already there.** A hardcoded 1 means the first install is the + // last: every later build is rejected as a downgrade and the only + // way out is an uninstall, which takes the user's library with it. + // CI derives it from the tag (1.3.1 -> 10301), monotonic as long as + // minor and patch stay under 100. The defaults keep a local build + // working with no environment at all. + // + // `Integer.parseInt`, not `(...) as Integer`: Groovy binds the call + // parentheses to `versionCode` before the cast, so the latter reads + // as `versionCode("1") as Integer` -- it sets a String, then casts + // the setter's null return, and Gradle fails the entire project + // with "Value is null" pointing at this line. + versionCode Integer.parseInt(System.getenv("YJ_VERSION_CODE") ?: "1") + versionName System.getenv("YJ_VERSION") ?: "0.0.0" // Configure supported ABIs ndk { diff --git a/build/android/app/src/main/res/values/strings.xml b/build/android/app/src/main/res/values/strings.xml index 3ed9e47..887ff9d 100644 --- a/build/android/app/src/main/res/values/strings.xml +++ b/build/android/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - Wails App + YellowJacket