Android: install and launch the package the APK declares #163

Merged
logan merged 3 commits from fix/159-android-task-app-id into main 2026-08-20 19:46:35 +00:00
Collaborator

Fixes the only open Priority/Critical issue: four Android tasks that
uninstalled the released app — and the user's library — to install a
build with a different id.

Commits

commit issue what
8d2109b fix(android): install and launch the package the APK declares #159 scripts/android-pkgid.sh + scripts/android-deploy.sh, and the four Taskfile tasks reduced to one call each
4b392cb fix(android): point the emulator script at the built APK's id #159 (third declaration) scripts/android-emulator.sh's PKG derived from bin/yellowjacket.apk
998ce75 docs(android): the identity is read back, not declared twice android-tier.md's warning block becomes a note; four measurements into NOTES.md

Also filed on the way past: #162 (make android-emulator's boot
wait can be satisfied by an unrelated attached device — tripped over
while booting the emulator for this), and a comment on #62 whose
Direction sentence contradicts its own hard blocker on #57.

The four decisions

1. The uninstall goes, and that turned out not to be a trade. 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; install -r removes the reason for it rather than merely
removing it, and nothing else was relying on it. The remaining case, a
changed signing certificate, is reported with the command to run rather
than performed, because that is exactly the case where doing it
silently costs the user their library. This is not a new judgement —
scripts/android-emulator.sh already answered it that way for
make android-install; the Taskfile was the odd one out.

2. The id comes from the APK. aapt2 dump packagename on the
artifact the task has just built, so the thing installed and the thing
launched agree by construction. The cheaper fix — APP_ID + .dev on
the debug-flavoured tasks — was rejected: android-tier.md has flagged
"nothing enforces that they agree" for five phases, this is the second
time it has cashed out, and a written-down suffix leaves the class
alive for the next flavour. Reading it back costs ~40ms. An APK it
cannot read is a hard failure, never a fallback to a default —
guessing is the bug.

APP_ID survives with no default, re-purposed from a setting it
could never have been (wails3 task never reads build/config.yml,
and even set it fed only adb, never Gradle) into an assertion.

3. The guard is in scripts/, and it is on the path rather than
opt-in.
android-pkgid.sh --expect is called by every deploy, so
running these tasks is safe by construction rather than by reading them
first — an opt-in make target is a guard nobody runs. It fires
before a target is chosen, deliberately: it is a question about the
artifact, so it can be exercised with nothing plugged in, and a wrong
id should be refused whether or not there is anything to install onto.
No new make target, so make skill-check has nothing new to check.

4. Scope: the edits survive. wails3 task common:update:build-assets
does not touch build/android/ — verified by running it on this
branch: it modified build/linux/nfpm/nfpm.yaml only, and exactly the
documented homepage/license reset. generate build-assets would
rewrite the tree, and would then drop four one-line call sites and
nothing else, because the logic is in scripts/, which is ours.

The third declaration was in scope. scripts/android-emulator.sh's
PKG defaulted to app.yellowjacket while make android-install
installs whatever is in bin/ — so android-launch, android-logs
and android-smoke addressed a package the build had not produced, and
its certificate-change message named the wrong id to uninstall. Since
the resolver existed, leaving it would have been leaving the fix
half-made.

One thing beyond the issue. The emulator tasks used a bare adb,
which with one device attached picks that device whatever it is — so
wails3 task android:run, summary "in the Android Emulator", installed
on the phone when a phone was the only thing plugged in, uninstall
included. The reported data loss was reachable from the emulator
task, which is worse because nothing in the name warns you. Same rule
one level up: a task that names a target has to say so to adb.

Verification

wails3 task --dry, before

===== android:deploy-emulator =====
"…/adb" uninstall app.yellowjacket 2>/dev/null || true
"…/adb" install "bin/yellowjacket.apk"
"…/adb" shell am start -n app.yellowjacket/com.wails.app.MainActivity
===== android:run =====
"…/adb" uninstall app.yellowjacket 2>/dev/null || true
"…/adb" install "bin/yellowjacket.apk"
"…/adb" shell am start -n app.yellowjacket/com.wails.app.MainActivity
===== android:run:device =====
"…/adb" -s "$DEVICE" uninstall app.yellowjacket 2>/dev/null || true
"…/adb" -s "$DEVICE" install "bin/yellowjacket.apk"
"…/adb" -s "$DEVICE" shell am start -n app.yellowjacket/com.wails.app.MainActivity
===== android:deploy-device =====
"…/adb" -s "$DEVICE" uninstall app.yellowjacket 2>/dev/null || true
"…/adb" -s "$DEVICE" install "bin/yellowjacket.apk"
"…/adb" -s "$DEVICE" shell am start -n app.yellowjacket/com.wails.app.MainActivity

aapt2 dump packagename bin/yellowjacket.apk on the debug APK those
first two tasks build: app.yellowjacket.dev.

wails3 task --dry, after

===== android:deploy-emulator =====
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator
===== android:run =====
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator
===== android:run:device =====
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device
===== android:deploy-device =====
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device

No uninstall, and no id in the Taskfile at all. The optional
arguments render:

$ wails3 task --dry android:run APP_ID=app.wrong
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator --expect "app.wrong"
$ wails3 task --dry android:deploy-device DEVICE_ID=S1 APP_ID=app.yellowjacket
./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device --serial "S1" --expect "app.yellowjacket"

The real run, against the emulator

make android-emulator, then wails3 task android:run with the
phone also attached
— so this exercises the target selection too:

task: [android:run] ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator
android-deploy: bin/yellowjacket.apk (app.yellowjacket.dev) -> emulator-5554
Performing Streamed Install
Success
Starting: Intent { cmp=app.yellowjacket.dev/com.wails.app.MainActivity }

Installed and launched the same package, on emulator-5554 and not
on LP3LHMA531900746. (The app then dies on launch — the emulator
cannot run it, for three reasons that are not ours. Says nothing about
this change.) Emulator stopped with make android-emulator-stop.

The negative test — the guard fires

Run with --target device while the phone was attached, which is
exactly #159's configuration (debug APK, release id, real device):

$ ./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
    APK:               bin/yellowjacket.apk
…
rc=2

No adb call was made. Before this branch, the same situation ran
adb uninstall app.yellowjacket against that phone.

And the target guard, phone attached with the 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
rc=1

The attached device, before and after

$ adb shell dumpsys package app.yellowjacket | grep -E "versionName|firstInstallTime"
    versionName=0.3.1
      firstInstallTime=2026-08-17 23:35:56     # before
    versionName=0.3.1
      firstInstallTime=2026-08-17 23:35:56     # after

Byte-identical. run:device and deploy-device were deliberately
not run against the phone
, fixed or not: the emulator demonstrates
the install/launch plumbing, which is all that changed, and the guard
above proves the id on the device path without installing anything.

The suite

make lint (3 configurations, 0 issues), make test (3 configurations,
all pass), make ui-test (949 tests, 85 files), make e2e (187
passed), tsc --noEmit in both frontend/ and e2e/,
make css-check, make bindings-check, make skill-check (47
documented targets, all present).

Which of those were formalities: all except skill-check. This
branch changes two shell scripts and one Taskfile and touches no Go, no
TypeScript and no CSS, so lint, test, ui-test, e2e, tsc, css-check and
bindings-check could not have been affected — they were run anyway, and
are reported so the claim is checkable rather than asserted.
skill-check is the one with something to say, since the commit edits
an agent-facing document.

Closes #159

Fixes the only open `Priority/Critical` issue: four Android tasks that uninstalled the released app — and the user's library — to install a build with a different id. ## Commits | commit | issue | what | |---|---|---| | `8d2109b` fix(android): install and launch the package the APK declares | **#159** | `scripts/android-pkgid.sh` + `scripts/android-deploy.sh`, and the four Taskfile tasks reduced to one call each | | `4b392cb` fix(android): point the emulator script at the built APK's id | #159 (third declaration) | `scripts/android-emulator.sh`'s `PKG` derived from `bin/yellowjacket.apk` | | `998ce75` docs(android): the identity is read back, not declared twice | — | `android-tier.md`'s warning block becomes a note; four measurements into `NOTES.md` | Also filed on the way past: **#162** (`make android-emulator`'s boot wait can be satisfied by an unrelated attached device — tripped over while booting the emulator for this), and a comment on **#62** whose Direction sentence contradicts its own hard blocker on #57. ## The four decisions **1. The uninstall goes, and that turned out not to be a trade.** 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; `install -r` removes the *reason* for it rather than merely removing it, and nothing else was relying on it. The remaining case, a changed signing certificate, is reported with the command to run rather than performed, because that is exactly the case where doing it silently costs the user their library. This is not a new judgement — `scripts/android-emulator.sh` already answered it that way for `make android-install`; the Taskfile was the odd one out. **2. The id comes from the APK.** `aapt2 dump packagename` on the artifact the task has just built, so the thing installed and the thing launched agree by construction. The cheaper fix — `APP_ID` + `.dev` on the debug-flavoured tasks — was rejected: `android-tier.md` has flagged "nothing enforces that they agree" for five phases, this is the second time it has cashed out, and a written-down suffix leaves the class alive for the next flavour. Reading it back costs ~40ms. An APK it cannot read is a **hard failure**, never a fallback to a default — guessing is the bug. `APP_ID` survives with **no default**, re-purposed from a setting it could never have been (`wails3 task` never reads `build/config.yml`, and even set it fed only adb, never Gradle) into an assertion. **3. The guard is in `scripts/`, and it is on the path rather than opt-in.** `android-pkgid.sh --expect` is called by every deploy, so running these tasks is safe by construction rather than by reading them first — an opt-in `make` target is a guard nobody runs. It fires **before a target is chosen**, deliberately: it is a question about the artifact, so it can be exercised with nothing plugged in, and a wrong id should be refused whether or not there is anything to install onto. No new `make` target, so `make skill-check` has nothing new to check. **4. Scope: the edits survive.** `wails3 task common:update:build-assets` does not touch `build/android/` — verified by running it on this branch: it modified `build/linux/nfpm/nfpm.yaml` only, and exactly the documented `homepage`/`license` reset. `generate build-assets` would rewrite the tree, and would then drop **four one-line call sites** and nothing else, because the logic is in `scripts/`, which is ours. **The third declaration was in scope.** `scripts/android-emulator.sh`'s `PKG` defaulted to `app.yellowjacket` while `make android-install` installs whatever is in `bin/` — so `android-launch`, `android-logs` and `android-smoke` addressed a package the build had not produced, and its certificate-change message named the wrong id to uninstall. Since the resolver existed, leaving it would have been leaving the fix half-made. **One thing beyond the issue.** The emulator tasks used a bare `adb`, which with one device attached picks that device whatever it is — so `wails3 task android:run`, summary "in the Android Emulator", installed on the **phone** when a phone was the only thing plugged in, uninstall included. The reported data loss was reachable from the *emulator* task, which is worse because nothing in the name warns you. Same rule one level up: a task that names a target has to say so to adb. ## Verification ### `wails3 task --dry`, before ``` ===== android:deploy-emulator ===== "…/adb" uninstall app.yellowjacket 2>/dev/null || true "…/adb" install "bin/yellowjacket.apk" "…/adb" shell am start -n app.yellowjacket/com.wails.app.MainActivity ===== android:run ===== "…/adb" uninstall app.yellowjacket 2>/dev/null || true "…/adb" install "bin/yellowjacket.apk" "…/adb" shell am start -n app.yellowjacket/com.wails.app.MainActivity ===== android:run:device ===== "…/adb" -s "$DEVICE" uninstall app.yellowjacket 2>/dev/null || true "…/adb" -s "$DEVICE" install "bin/yellowjacket.apk" "…/adb" -s "$DEVICE" shell am start -n app.yellowjacket/com.wails.app.MainActivity ===== android:deploy-device ===== "…/adb" -s "$DEVICE" uninstall app.yellowjacket 2>/dev/null || true "…/adb" -s "$DEVICE" install "bin/yellowjacket.apk" "…/adb" -s "$DEVICE" shell am start -n app.yellowjacket/com.wails.app.MainActivity ``` `aapt2 dump packagename bin/yellowjacket.apk` on the debug APK those first two tasks build: `app.yellowjacket.dev`. ### `wails3 task --dry`, after ``` ===== android:deploy-emulator ===== ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator ===== android:run ===== ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator ===== android:run:device ===== ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device ===== android:deploy-device ===== ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device ``` No `uninstall`, and no id in the Taskfile at all. The optional arguments render: ``` $ wails3 task --dry android:run APP_ID=app.wrong ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator --expect "app.wrong" $ wails3 task --dry android:deploy-device DEVICE_ID=S1 APP_ID=app.yellowjacket ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target device --serial "S1" --expect "app.yellowjacket" ``` ### The real run, against the emulator `make android-emulator`, then `wails3 task android:run` **with the phone also attached** — so this exercises the target selection too: ``` task: [android:run] ./scripts/android-deploy.sh --apk "bin/yellowjacket.apk" --target emulator android-deploy: bin/yellowjacket.apk (app.yellowjacket.dev) -> emulator-5554 Performing Streamed Install Success Starting: Intent { cmp=app.yellowjacket.dev/com.wails.app.MainActivity } ``` Installed and launched **the same package**, on `emulator-5554` and not on `LP3LHMA531900746`. (The app then dies on launch — the emulator cannot run it, for three reasons that are not ours. Says nothing about this change.) Emulator stopped with `make android-emulator-stop`. ### The negative test — the guard fires Run with `--target device` while the phone was attached, which is exactly #159's configuration (debug APK, release id, real device): ``` $ ./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 APK: bin/yellowjacket.apk … rc=2 ``` No adb call was made. Before this branch, the same situation ran `adb uninstall app.yellowjacket` against that phone. And the target guard, phone attached with the 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 rc=1 ``` ### The attached device, before and after ``` $ adb shell dumpsys package app.yellowjacket | grep -E "versionName|firstInstallTime" versionName=0.3.1 firstInstallTime=2026-08-17 23:35:56 # before versionName=0.3.1 firstInstallTime=2026-08-17 23:35:56 # after ``` Byte-identical. **`run:device` and `deploy-device` were deliberately not run against the phone**, fixed or not: the emulator demonstrates the install/launch plumbing, which is all that changed, and the guard above proves the id on the device path without installing anything. ### The suite `make lint` (3 configurations, 0 issues), `make test` (3 configurations, all pass), `make ui-test` (949 tests, 85 files), `make e2e` (187 passed), `tsc --noEmit` in **both** `frontend/` and `e2e/`, `make css-check`, `make bindings-check`, `make skill-check` (47 documented targets, all present). **Which of those were formalities:** all except `skill-check`. This branch changes two shell scripts and one Taskfile and touches no Go, no TypeScript and no CSS, so lint, test, ui-test, e2e, tsc, css-check and bindings-check could not have been affected — they were run anyway, and are reported so the claim is checkable rather than asserted. `skill-check` is the one with something to say, since the commit edits an agent-facing document. Closes #159
logan added 3 commits 2026-08-20 19:34:36 +00:00
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
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.
docs(android): the identity is read back, not declared twice
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m32s
CI / e2e (pull_request) Successful in 8m22s
998ce75fb6
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.
logan merged commit de04339494 into main 2026-08-20 19:46:35 +00:00
Sign in to join this conversation.