From 8d2109b87ede28ac823e34d683607f6a4b9861b6 Mon Sep 17 00:00:00 2001 From: Logan Date: Thu, 20 Aug 2026 14:34:59 -0400 Subject: [PATCH 1/3] fix(android): install and launch the package the APK declares The four adb-driven tasks in build/android/Taskfile.yml began with `adb uninstall {{.APP_ID}}`, where APP_ID defaulted to "app.yellowjacket" -- the release id. `run` and `run:device` build the *debug* variant, whose applicationIdSuffix makes it "app.yellowjacket.dev", so both uninstalled the user's released app, took the library with it, installed a different package, and then failed to launch the one they had just removed. The id is read back from the built APK now (scripts/android-pkgid.sh, `aapt2 dump packagename`) rather than written down a second time, so the thing installed and the thing launched agree by construction -- whatever Gradle resolved the applicationId to, suffixes included, is in the file. An APK it cannot read is a hard failure and never a fallback to a default; guessing is the bug. APP_ID survives with no default as an *assertion*: it is checked against the artifact and refused, naming both, before anything is installed or a target is even chosen. The uninstall is gone rather than corrected. It was there to make the bare `install` on the next line work at all -- Android refuses an install over an existing package without -r -- so `install -r` removes the reason for it. What is left is the one case an uninstall is really the remedy, a changed signing certificate, and that is exactly the case where doing it silently costs the user their library. So it is reported with the command to run, which is the answer scripts/android-emulator.sh had already reached for `make android-install`. And the emulator tasks now say "emulator" to adb. A bare `adb install` with one device attached picks that device whatever it is, so with a phone plugged in and no emulator running, the task whose summary reads "in the Android Emulator" installed on the phone -- the same data loss, from the task whose name gives no warning. Several matching targets is an error naming them rather than a silent pick of the first. Closes #159 --- build/android/Taskfile.yml | 80 +++++---------- scripts/android-deploy.sh | 196 +++++++++++++++++++++++++++++++++++++ scripts/android-pkgid.sh | 97 ++++++++++++++++++ 3 files changed, 318 insertions(+), 55 deletions(-) create mode 100755 scripts/android-deploy.sh create mode 100755 scripts/android-pkgid.sh diff --git a/build/android/Taskfile.yml b/build/android/Taskfile.yml index b157ca3..c486463 100644 --- a/build/android/Taskfile.yml +++ b/build/android/Taskfile.yml @@ -4,18 +4,28 @@ includes: common: ../Taskfile.yml vars: - # 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. + # APP_ID is an *assertion*, not a setting, and it has no default. # - # 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"}}' + # It used to be the id every adb-driven task below uninstalled, + # launched and filtered, defaulting to "app.yellowjacket". It could + # never have been a setting: `wails3 task` builds its var set from CLI + # KEY=VALUE arguments and the Taskfile tree only -- nothing reads + # build/config.yml, contrary to ANDROID.md, checked with --dry -- and + # even when set it fed only the adb commands, never Gradle. So the + # identity was declared twice, here and as `applicationId` in + # app/build.gradle, with nothing enforcing that they agree. + # + # They did not agree. The debug buildType carries + # `applicationIdSuffix ".dev"`, so the tasks that assemble a debug APK + # addressed the *release* id -- on a device, the user's installed app + # and their library (#159). + # + # The id is now read back from the built APK by scripts/android- + # pkgid.sh, so the thing installed and the thing launched agree by + # construction. Passing APP_ID= says "this build had better declare + # that id", and the deploy refuses before touching anything if it does + # not -- which is the check that would have caught #159 statically. + APP_ID: '{{.APP_ID | default ""}}' MIN_SDK: '21' TARGET_SDK: '35' # The emulator runs the host architecture; physical devices are arm64 @@ -372,9 +382,7 @@ tasks: ARCH: '{{.ARCH | default .HOST_ARCH}}' cmds: - task: ensure-emulator - - '"{{.ADB}}" uninstall {{.APP_ID}} 2>/dev/null || true' - - '"{{.ADB}}" install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"' - - '"{{.ADB}}" shell am start -n {{.APP_ID}}/com.wails.app.MainActivity' + - './scripts/android-deploy.sh --apk "{{.BIN_DIR}}/{{.APP_NAME}}.apk" --target emulator{{if .APP_ID}} --expect "{{.APP_ID}}"{{end}}' run: summary: Build, install and launch a debug build in the Android Emulator @@ -383,9 +391,7 @@ tasks: - task: build cmds: - task: assemble:apk - - '"{{.ADB}}" uninstall {{.APP_ID}} 2>/dev/null || true' - - '"{{.ADB}}" install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"' - - '"{{.ADB}}" shell am start -n {{.APP_ID}}/com.wails.app.MainActivity' + - './scripts/android-deploy.sh --apk "{{.BIN_DIR}}/{{.APP_NAME}}.apk" --target emulator{{if .APP_ID}} --expect "{{.APP_ID}}"{{end}}' device:list: summary: Lists connected Android devices and emulators (serials) @@ -400,25 +406,7 @@ tasks: ARCH: arm64 cmds: - task: assemble:apk - - | - DEVICE='{{.DEVICE_ID | default ""}}' - if [ -z "$DEVICE" ]; then - DEVICE="${DEVICE_ID:-}" - fi - if [ -z "$DEVICE" ]; then - DEVICE=$("{{.ADB}}" devices | awk 'NR > 1 && $2 == "device" && $1 !~ /^emulator-/ { print $1; exit }') - fi - if [ -z "$DEVICE" ]; then - echo "Error: no connected physical Android device found." - echo "Pass DEVICE_ID= to target a device explicitly." - echo "Find connected device serials with: {{.ADB}} devices" - exit 1 - fi - - echo "Deploying {{.BIN_DIR}}/{{.APP_NAME}}.apk to device $DEVICE..." - "{{.ADB}}" -s "$DEVICE" uninstall {{.APP_ID}} 2>/dev/null || true - "{{.ADB}}" -s "$DEVICE" install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" - "{{.ADB}}" -s "$DEVICE" shell am start -n {{.APP_ID}}/com.wails.app.MainActivity + - './scripts/android-deploy.sh --apk "{{.BIN_DIR}}/{{.APP_NAME}}.apk" --target device{{if .DEVICE_ID}} --serial "{{.DEVICE_ID}}"{{end}}{{if .APP_ID}} --expect "{{.APP_ID}}"{{end}}' preconditions: - sh: '[ -x "{{.ADB}}" ] || command -v adb' msg: "adb not found. Install the Android SDK platform-tools (or set ANDROID_HOME)" @@ -430,25 +418,7 @@ tasks: vars: ARCH: arm64 cmds: - - | - DEVICE='{{.DEVICE_ID | default ""}}' - if [ -z "$DEVICE" ]; then - DEVICE="${DEVICE_ID:-}" - fi - if [ -z "$DEVICE" ]; then - DEVICE=$("{{.ADB}}" devices | awk 'NR > 1 && $2 == "device" && $1 !~ /^emulator-/ { print $1; exit }') - fi - if [ -z "$DEVICE" ]; then - echo "Error: no connected physical Android device found." - echo "Pass DEVICE_ID= to target a device explicitly." - echo "Find connected device serials with: {{.ADB}} devices" - exit 1 - fi - - echo "Deploying {{.BIN_DIR}}/{{.APP_NAME}}.apk to device $DEVICE..." - "{{.ADB}}" -s "$DEVICE" uninstall {{.APP_ID}} 2>/dev/null || true - "{{.ADB}}" -s "$DEVICE" install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" - "{{.ADB}}" -s "$DEVICE" shell am start -n {{.APP_ID}}/com.wails.app.MainActivity + - './scripts/android-deploy.sh --apk "{{.BIN_DIR}}/{{.APP_NAME}}.apk" --target device{{if .DEVICE_ID}} --serial "{{.DEVICE_ID}}"{{end}}{{if .APP_ID}} --expect "{{.APP_ID}}"{{end}}' preconditions: - sh: '[ -x "{{.ADB}}" ] || command -v adb' msg: "adb not found. Install the Android SDK platform-tools (or set ANDROID_HOME)" diff --git a/scripts/android-deploy.sh b/scripts/android-deploy.sh new file mode 100755 index 0000000..4670cbc --- /dev/null +++ b/scripts/android-deploy.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# +# Install a built APK onto an Android target and launch it, under the +# package id the APK itself declares. +# +# This is the whole body of build/android/Taskfile.yml's four adb-driven +# tasks — deploy-emulator, run, run:device, deploy-device — which were +# three lines each, written out four times, and wrong in two ways in all +# four (#159): +# +# adb uninstall app.yellowjacket # the RELEASE id, unconditionally +# adb install bin/yellowjacket.apk +# adb shell am start -n app.yellowjacket/com.wails.app.MainActivity +# +# **The uninstall is not here and does not come back.** It was there to +# make the bare `install` on the next line work at all — without -r, +# Android refuses an install over an existing package — so `install -r` +# removes the reason for it rather than merely removing it. What is +# left is the one case an uninstall really is the remedy, a changed +# signing certificate, and that is exactly the case where performing it +# silently costs the user their library. So it is *named* and not done: +# an error message carrying the command is a decision the person at the +# keyboard gets to make, which is the same answer scripts/android- +# emulator.sh already reached for `make android-install`. +# +# **The id is read back from the artifact**, never defaulted, so the +# thing installed and the thing launched cannot disagree — see +# scripts/android-pkgid.sh for why that is by construction rather than +# by discipline. +# +# **The target is checked against the task's own name.** The emulator +# tasks used a bare `adb`, which with one device attached picks that +# device whatever it is — so `wails3 task android:run`, whose summary +# says "in the Android Emulator", installed on the phone when a phone +# was the only thing plugged in. A task addressing something other than +# what it says is the same fault as the package id, one level up. +# +# Usage: +# android-deploy.sh --apk --target emulator|device|any \ +# [--expect ] [--serial ] [--no-launch] +set -euo pipefail + +cd "$(dirname "$0")/.." + +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" +ADB="$(command -v adb || echo "$SDK/platform-tools/adb")" + +# **Not "$PKG/.MainActivity".** A leading-dot activity is resolved +# against the applicationId, and the scaffold's activity lives in the +# Java package com.wails.app, which is deliberately not it. The short +# form fails with a class-not-found that reads like a broken build. +ACTIVITY="${YJ_ANDROID_ACTIVITY:-com.wails.app.MainActivity}" + +APK="" +TARGET="any" +EXPECT="" +SERIAL="${ANDROID_SERIAL:-${DEVICE_ID:-}}" +LAUNCH=1 + +die() { echo "android-deploy: $*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --apk) APK="${2:-}"; shift 2 ;; + --target) TARGET="${2:-}"; shift 2 ;; + --expect) EXPECT="${2:-}"; shift 2 ;; + --serial) SERIAL="${2:-}"; shift 2 ;; + --no-launch) LAUNCH=0; shift ;; + *) die "unknown option $1" ;; + esac +done + +[ -n "$APK" ] || die "--apk is required" +[ -f "$APK" ] || die "no such APK: $APK + Build one first: wails3 task android:assemble:apk (debug) + wails3 task android:package (release)" +[ -x "$ADB" ] || command -v adb >/dev/null || + die "adb not found. Install the Android SDK platform-tools (or set ANDROID_HOME)" + +case "$TARGET" in +emulator | device | any) ;; +*) die "--target must be emulator, device or any (got '$TARGET')" ;; +esac + +# ---------------------------------------------------------------- # +# Which package +# ---------------------------------------------------------------- # + +# This runs *before* a target is chosen, deliberately: the guard is a +# question about the artifact, so it can be answered — and exercised — +# with nothing plugged in, and a build whose id is wrong should be +# refused whether or not there is anything to install it onto. +# +# An unreadable APK, or an id that is not the one the caller named, is a +# hard stop before anything is installed or launched. Spelled as two +# calls rather than one with a conditional argument: an empty array under +# `set -u` is an unbound variable in bash 3.2, which is what macOS ships. +if [ -n "$EXPECT" ]; then + PKG="$(./scripts/android-pkgid.sh "$APK" --expect "$EXPECT")" +else + PKG="$(./scripts/android-pkgid.sh "$APK")" +fi + +# ---------------------------------------------------------------- # +# Which target +# ---------------------------------------------------------------- # + +# An emulator serial is "emulator-"; anything else online is a +# physical device. That is the same test the device tasks already made, +# and the emulator tasks did not make at all. +online_matching() { + case "$TARGET" in + emulator) "$ADB" devices | awk 'NR > 1 && $2 == "device" && $1 ~ /^emulator-/ { print $1 }' ;; + device) "$ADB" devices | awk 'NR > 1 && $2 == "device" && $1 !~ /^emulator-/ { print $1 }' ;; + any) "$ADB" devices | awk 'NR > 1 && $2 == "device" { print $1 }' ;; + esac +} + +if [ -z "$SERIAL" ]; then + matches="$(online_matching)" + count="$(printf '%s' "$matches" | grep -c . || true)" + + if [ "$count" -eq 0 ]; then + echo "android-deploy: no ${TARGET/any/attached} target is online." >&2 + "$ADB" devices | sed '1d;/^$/d;s/^/ /' >&2 || true + if [ "$TARGET" = "emulator" ]; then + echo " Start one with: make android-emulator" >&2 + elif [ "$TARGET" = "device" ]; then + echo " Plug a phone in and authorise the adb key." >&2 + fi + exit 1 + fi + + # Several is ambiguous, and picking the first silently is how a + # build lands on a target nobody named. The old run:device did + # exactly that. + if [ "$count" -gt 1 ]; then + echo "android-deploy: several $TARGET targets are online — name one." >&2 + printf '%s\n' "$matches" | sed 's/^/ /' >&2 + echo " Pass DEVICE_ID=, or set ANDROID_SERIAL." >&2 + exit 1 + fi + + SERIAL="$matches" +fi + +# ---------------------------------------------------------------- # +# Install +# ---------------------------------------------------------------- # + +echo "android-deploy: $APK ($PKG) -> $SERIAL" + +if ! out="$("$ADB" -s "$SERIAL" install -r "$APK" 2>&1)"; then + printf '%s\n' "$out" + case "$out" in + *INSTALL_FAILED_UPDATE_INCOMPATIBLE* | *"signatures do not match"*) + cat >&2 <&2 < [--expect ] +# +# Exit codes: 0 printed the id; 1 could not read it; 2 --expect failed. +set -euo pipefail + +die() { echo "android-pkgid: $*" >&2; exit 1; } + +APK="" +EXPECT="" +while [ $# -gt 0 ]; do + case "$1" in + --expect) EXPECT="${2:-}"; shift 2 ;; + -*) die "unknown option $1" ;; + *) APK="$1"; shift ;; + esac +done + +[ -n "$APK" ] || die "usage: android-pkgid.sh [--expect ]" +[ -f "$APK" ] || die "no such APK: $APK" + +# aapt2 lives under build-tools//, which is versioned, so it is +# resolved rather than pinned. PATH first, so a system aapt2 (Arch ships +# one) works without an SDK layout at all. +find_aapt() { + local sdk name + for name in "$@"; do + command -v "$name" 2>/dev/null && return 0 + done + sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" + for name in "$@"; do + ls "$sdk"/build-tools/*/"$name" 2>/dev/null | sort -V | tail -1 | grep . && return 0 + done + return 1 +} + +pkg="" + +# `aapt2 dump packagename` answers in one word and is the cheapest of +# the three. aapt1 is the fallback because it is what older build-tools +# carry and what the issue's own measurement used. +if AAPT2="$(find_aapt aapt2)"; then + pkg="$("$AAPT2" dump packagename "$APK" 2>/dev/null | head -1 | tr -d '\r')" || true +fi + +if [ -z "$pkg" ] && AAPT="$(find_aapt aapt)"; then + pkg="$("$AAPT" dump badging "$APK" 2>/dev/null | + sed -n "s/^package: name='\([^']*\)'.*/\1/p" | head -1)" || true +fi + +# Guessing here is the bug this file exists to prevent, so an unreadable +# APK is a hard failure and never a fallback to a written-down default. +if [ -z "$pkg" ]; then + die "could not read a package name from $APK. + Install the SDK build-tools (aapt2), or set ANDROID_HOME to an SDK + that carries them: sdkmanager 'build-tools;34.0.0'" +fi + +if [ -n "$EXPECT" ] && [ "$EXPECT" != "$pkg" ]; then + cat >&2 < Date: Thu, 20 Aug 2026 14:35:08 -0400 Subject: [PATCH 2/3] fix(android): point the emulator script at the built APK's id The third declaration of the app's identity, and the one #159 did not cash out in: PKG defaulted to "app.yellowjacket" while `make android-install` installs whatever is in bin/, which after `wails3 task android:assemble:apk` is app.yellowjacket.dev. So android-launch, android-logs and android-smoke addressed a package the build had not produced, and the certificate-change message named the wrong id to uninstall -- the release one. It is derived from bin/yellowjacket.apk the same way the tasks are, so it follows whichever variant was built last. YJ_ANDROID_PKG still overrides, and the literal survives only for a tree with no APK yet, where these commands are asking about whatever is already installed and there is nothing to read. cmd_inspect's probe order goes with it: "$PKG.dev" would append a second suffix to an id that already carries one, so the candidates are derived from the resolved id in either direction -- debug sibling first, release second, as before. --- scripts/android-emulator.sh | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/android-emulator.sh b/scripts/android-emulator.sh index 578528f..025b57c 100755 --- a/scripts/android-emulator.sh +++ b/scripts/android-emulator.sh @@ -34,7 +34,21 @@ cd "$(dirname "$0")/.." AVD="${YJ_AVD:-yj-test}" SDK="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}" -PKG="${YJ_ANDROID_PKG:-app.yellowjacket}" +# The third declaration of the app's identity, and the one #159 did not +# cash out in -- but the same hazard, so it is derived rather than +# written down too. The APK in bin/ is what `make android-install` is +# about to install and what `android-launch`, `logs` and `smoke` are +# about to address, so it is the authority; whatever Gradle resolved the +# applicationId to, suffix included, is in the file. +# +# The literal survives only as the answer for a tree with no APK built +# yet, where these commands are asking about whatever is already on the +# device and there is nothing to read. YJ_ANDROID_PKG still overrides. +PKG="${YJ_ANDROID_PKG:-}" +if [ -z "$PKG" ] && [ -f bin/yellowjacket.apk ]; then + PKG="$(./scripts/android-pkgid.sh bin/yellowjacket.apk 2>/dev/null || true)" +fi +PKG="${PKG:-app.yellowjacket}" # Where `make android-inspect` forwards the WebView's devtools socket. CDP_PORT="${YJ_ANDROID_CDP_PORT:-9222}" # **Not "$PKG/.MainActivity".** A leading-dot activity is resolved @@ -281,15 +295,24 @@ cmd_inspect() { need_sdk pick_device || die "no device -- plug a phone in (USB debugging on) or run 'make android-emulator'" - local pkg pid + local pkg pid candidates pid="" - for pkg in "$PKG.dev" "$PKG"; do + # Debug sibling first, release second, whichever way round $PKG was + # resolved -- it is read from the built APK now, so it is already the + # .dev id whenever a debug build is what is in bin/, and appending a + # second ".dev" to it would probe a package that cannot exist. + case "$PKG" in + *.dev) candidates="$PKG ${PKG%.dev}" ;; + *) candidates="$PKG.dev $PKG" ;; + esac + + for pkg in $candidates; do pid=$("$ADB" shell pidof "$pkg" 2>/dev/null | tr -d '\r' | awk '{print $1}') [ -n "$pid" ] && break done - [ -n "$pid" ] || die "neither $PKG.dev nor $PKG is running; launch it first" + [ -n "$pid" ] || die "none of: $candidates is running; launch it first" "$ADB" forward --remove-all >/dev/null 2>&1 || true "$ADB" forward "tcp:$CDP_PORT" "localabstract:webview_devtools_remote_$pid" >/dev/null \ From 998ce75fb64e7b0ef08789a14fcfc6edc3c75a8b Mon Sep 17 00:00:00 2001 From: Logan Date: Thu, 20 Aug 2026 14:35:16 -0400 Subject: [PATCH 3/3] docs(android): the identity is read back, not declared twice android-tier.md carried a warning block telling the reader not to use run:device or deploy-device, and offered a manual sequence instead. Both are wrong now: the tasks are the way in, and the warning would read as a live hazard. It becomes a note about what changed, and the manual sequence stays as the smallest thing that works when you want no script between you and adb. "The identity is declared twice" was the section this file had carried for five phases saying nothing enforced that the two ids agree. It describes the enforcement now, plus what APP_ID means since it stopped being a setting it never was. NOTES.md takes the four measurements: that the uninstall existed only to cover a missing -r (which is what makes deleting it a fix rather than a trade), that the emulator tasks installed on a phone, what reading the id back costs, and the boot-wait race filed as #162. --- .../references/android-tier.md | 106 ++++++++++++++---- .planning/NOTES.md | 87 ++++++++++++++ 2 files changed, 169 insertions(+), 24 deletions(-) diff --git a/.pi/skills/yellowjacket-dev/references/android-tier.md b/.pi/skills/yellowjacket-dev/references/android-tier.md index 1e417f5..62ff776 100644 --- a/.pi/skills/yellowjacket-dev/references/android-tier.md +++ b/.pi/skills/yellowjacket-dev/references/android-tier.md @@ -258,20 +258,25 @@ one. `build/android/Taskfile.yml` ships more than the Makefile wraps, and they are the right thing to reach for when you want something one-off: -> **Do not run `android:run:device` or `android:deploy-device` -> against a device that has the released app on it (#159).** Both begin -> with `adb uninstall {{.APP_ID}}`, and `APP_ID` defaults to -> `app.yellowjacket` — the **release** id — while `run:device` builds -> the **debug** variant, whose id is `app.yellowjacket.dev`. So it -> uninstalls the user's app, taking the library with it, installs a -> different package, and then fails to launch the one it removed. This -> is "the identity is declared twice" (below) cashing out. The safe -> sequence is at the end of this section. +> **These four were unsafe until #159 and are now the way in.** All of +> them began with `adb uninstall {{.APP_ID}}`, where `APP_ID` defaulted +> to `app.yellowjacket` — the **release** id — while `run` and +> `run:device` build the **debug** variant, whose id is +> `app.yellowjacket.dev`. So they uninstalled the user's app, taking +> the library with it, installed a different package, and then failed +> to launch the one they had removed. +> +> They share `scripts/android-deploy.sh` now, which **never** +> uninstalls (`install -r`, and a changed signing certificate is +> reported with the command rather than acted on), reads the package id +> back out of the built APK, and refuses a target that is not the kind +> the task names. There is nothing left to avoid; the manual sequence +> below is kept because it is still the smallest thing that works. ``` wails3 task android:run # debug build + emulator install + launch -wails3 task android:run:device # UNSAFE, see #159 -wails3 task android:deploy-device # UNSAFE, see #159 +wails3 task android:run:device # debug build + install + launch on a phone +wails3 task android:deploy-device # release build, same wails3 task android:bundle:fat # AAB, for a Play Store upload wails3 task android:studio # open build/android/ in Android Studio wails3 task android:device:list @@ -279,6 +284,16 @@ wails3 task android:logs:all wails3 task android:clean ``` +**`run` and `deploy-emulator` mean the emulator, and now say so to +adb.** They used a bare `adb install`, which with exactly one device +attached picks that device whatever it is — so with a phone plugged in +and no emulator running, the task whose summary reads "in the Android +Emulator" installed on the phone. They pass `--target emulator` and +refuse with `make android-emulator` as the remedy. + +**`DEVICE_ID=` still names a device, and several attached +devices is now an error rather than a silent pick of the first.** + Two are deliberately **not** wrapped. `android:logs` greps logcat for `(Wails|yellowjacket)`, which catches the `WailsBridge` tag but misses the app's own process tag (`app.yellowjacket` — lowercase, so `Wails` @@ -288,16 +303,51 @@ instead. And `ensure-emulator` boots whatever `-list-avds | tail -1` returns, with no pidfile and no boot wait, so it cannot be stopped or sequenced. -## The identity is declared twice +## The identity is read back from the APK +It used to be **declared twice**, and that is what #159 was. `applicationId` in `build/android/app/build.gradle` is what Gradle -installs. `APP_ID` in `build/android/Taskfile.yml` is what every -adb-driven task uninstalls, launches and filters. **Nothing enforces -that they agree**, and `ANDROID.md`'s advice to set `APP_ID` in -`build/config.yml` does not work in beta.8 — `wails3 task` never reads -that file (verified with `--dry`), and even when set it feeds only the -adb commands, never Gradle. Change both or the official `run`/`deploy` -tasks address a package that is not installed. +installs; `APP_ID` in `build/android/Taskfile.yml` was what every +adb-driven task uninstalled, launched and filtered, and nothing +enforced that they agree. They did not: the debug buildType carries +`applicationIdSuffix ".dev"`, so every task that assembles a debug APK +addressed the release id. This file flagged the hazard for five phases +and it cashed out twice — once as a wrong `am start`, once as an +uninstall of the user's library. + +**`scripts/android-pkgid.sh` is the one answer now.** It prints the +package id an APK declares (`aapt2 dump packagename`, falling back to +`aapt dump badging`), and the deploy path installs and launches *that*. +The APK is the authority because the task that installs it has just +built it: whatever Gradle resolved the applicationId to, suffixes and +flavours included, is in the file, and no default can disagree with it. +An APK it cannot read is a hard failure, never a fallback to a written +down default — guessing is the bug. + +**`APP_ID` survives as an assertion, not a setting**, and has no +default. `wails3 task android:run APP_ID=app.yellowjacket` says "this +build had better declare that id" and is refused, naming both, *before* +anything is installed or a device is even chosen. It could never have +been a setting: `ANDROID.md`'s advice to put it in `build/config.yml` +does not work in beta.8 — `wails3 task` never reads that file (verified +with `--dry`) — and even when set it fed only the adb commands, never +Gradle. + +`scripts/android-emulator.sh` derives `PKG` the same way, from +`bin/yellowjacket.apk` when one is built, so `make android-install`, +`android-launch`, `android-logs` and `android-smoke` follow whichever +variant is actually in `bin/`. `YJ_ANDROID_PKG` still overrides, and +the old literal survives only for a tree with no APK built yet. + +**The uninstall is gone and is not coming back.** It existed to make +the bare `install` on the next line work at all — without `-r` Android +refuses an install over an existing package — so `install -r` removes +the *reason* for it rather than merely removing it. What is left is the +one case an uninstall really is the remedy, a changed signing +certificate, and that is exactly the case where performing it silently +costs the user their library. So it is named and not done, which is the +answer `scripts/android-emulator.sh` had already reached for +`make android-install`. Related, and it will bite once: the launcher activity is `com.wails.app.MainActivity` and the applicationId is @@ -306,8 +356,10 @@ resolves the leading dot against the *applicationId* and fails with a class-not-found that reads like a broken build. Always the fully-qualified form. -**The safe way to put a debug build on a real device**, which is what -#52 used and what #159 exists to make unnecessary: +**`wails3 task android:run:device` is the way to put a debug build on a +real device**, since #159. What #52 used, before it was safe, was the +longer form, and it is still the smallest thing that works if you want +no script between you and adb: ```bash wails3 task android:build ARCH=arm64 && wails3 task android:assemble:apk @@ -315,9 +367,15 @@ adb install -r bin/yellowjacket.apk # -r, never uninstall adb shell am start -n app.yellowjacket.dev/com.wails.app.MainActivity ``` -`YJ_ANDROID_PKG=app.yellowjacket.dev` points `scripts/android-emulator.sh` -— and therefore `make android-smoke`, `android-logs`, `android-launch` -— at the debug id, which is otherwise `app.yellowjacket`. +The id in that last line is the one thing to keep an eye on by hand — +`./scripts/android-pkgid.sh bin/yellowjacket.apk` is what the tasks ask, +and it is a good habit before any `am start` written out in full. + +`YJ_ANDROID_PKG=app.yellowjacket.dev` still overrides what +`scripts/android-emulator.sh` — and therefore `make android-smoke`, +`android-logs`, `android-launch` — addresses, but it is rarely needed +now: that default is read from `bin/yellowjacket.apk`, so it already +follows whichever variant was built last. ## What only a device can answer diff --git a/.planning/NOTES.md b/.planning/NOTES.md index 630793f..7c507e1 100644 --- a/.planning/NOTES.md +++ b/.planning/NOTES.md @@ -4155,3 +4155,90 @@ This is the hazard that file already names — "The identity is declared twice ... **Nothing enforces that they agree**" — reached by a second route: the two ids differ not because someone edited one, but because the debug buildType suffixes it. + +## The uninstall was there to make a bare `install` work (measured 2026-08-20) + +Fixing #159 turned up *why* the `adb uninstall` was in all four tasks, +which the issue does not say and which decides whether it can simply be +deleted. The line under it was `adb install`, with **no `-r`** — and +Android refuses an install over an existing package without it. So the +uninstall was not a deliberate clean-slate step; it was the price of +the missing flag, paid on every run, and `install -r` removes the +reason for it rather than merely removing it. + +That matters because "should the uninstall go at all" looked like a +trade — drop it and a signing-certificate change fails with +`INSTALL_FAILED_UPDATE_INCOMPATIBLE` instead of being handled. It is +not a trade: nothing else was relying on it. The certificate case is +reported with the command to run, which is what +`scripts/android-emulator.sh` already did for `make android-install`, +so this is one existing judgement applied consistently rather than a +new one. + +## `wails3 task android:run` installs on a phone (measured 2026-08-20) + +The emulator tasks (`run`, `deploy-emulator`) used a bare `adb install` +with no `-s`. adb with exactly one device attached uses that device +whatever kind it is, so with a phone plugged in and no emulator +running, the task whose summary reads "in the Android Emulator" +installed on the phone — and, before #159 was fixed, ran +`adb uninstall app.yellowjacket` against it first. The reported data +loss was reachable from the *emulator* task, which is not what the +issue describes and is worse, because nothing in the name warns you. + +Measured after the fix, phone attached and emulator stopped: + +``` +$ ./scripts/android-deploy.sh --apk bin/yellowjacket.apk --target emulator +android-deploy: no emulator target is online. + LP3LHMA531900746 device + Start one with: make android-emulator +``` + +The general form: **a task that names a target has to say so to adb.** +The device tasks always filtered on `$1 !~ /^emulator-/`; the emulator +tasks filtered on nothing at all. + +## The package id can be read back, and costs nothing (2026-08-20) + +`aapt2 dump packagename ` answers in one word and ~40 ms, from +`$ANDROID_HOME/build-tools/*/aapt2` (versioned, so resolved not +pinned); `aapt dump badging` is the fallback for older build-tools and +is what #159's own measurement used. That is cheap enough to do on +every deploy, which is what makes "the two ids agree by construction" +affordable rather than aspirational — the alternative considered was +giving the debug-flavoured tasks `APP_ID` + `.dev`, which is one line +and leaves the class of bug alive for the next flavour or suffix. + +The guard runs **before** a target is chosen, deliberately: it is a +question about the artifact, so it can be exercised with nothing +plugged in, and a build whose id is wrong should be refused whether or +not there is anything to install it onto. That is what let the negative +test run safely with the user's phone attached: + +``` +$ ./scripts/android-deploy.sh --apk bin/yellowjacket.apk \ + --target device --expect app.yellowjacket +android-pkgid: refusing to act on a package this APK does not declare. + the APK declares: app.yellowjacket.dev + the task expects: app.yellowjacket +rc=2 +``` + +That is exactly #159's configuration — debug APK, release id, real +device — refused with no adb call made. + +## `make android-emulator`'s boot wait can be satisfied by a phone (2026-08-20) + +Noticed while booting the emulator for #159's verification, with a +phone also attached. `scripts/android-emulator.sh start` reported +`waiting for boot ok / android 14` about **eight seconds** after +launching the emulator, which had not appeared in `adb devices` yet — +`pick_device`'s last resort is "exactly one device online", and at that +moment the one online device was the phone. So it waited for the +phone's boot, found it long since booted, and returned. The emulator +took another ~10 s to come up. + +Harmless here (the emulator was up before anything used it) and a +straightforward race otherwise: `start` should wait for a device that +is an emulator, not for whatever `pick_device` returns. Filed as #162.