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
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.
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 bothfrontend/ 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.
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
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.
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 main2026-08-20 19:46:35 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the only open
Priority/Criticalissue: four Android tasks thatuninstalled the released app — and the user's library — to install a
build with a different id.
Commits
8d2109bfix(android): install and launch the package the APK declaresscripts/android-pkgid.sh+scripts/android-deploy.sh, and the four Taskfile tasks reduced to one call each4b392cbfix(android): point the emulator script at the built APK's idscripts/android-emulator.sh'sPKGderived frombin/yellowjacket.apk998ce75docs(android): the identity is read back, not declared twiceandroid-tier.md's warning block becomes a note; four measurements intoNOTES.mdAlso filed on the way past: #162 (
make android-emulator's bootwait 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 installwith no-r— and Android refusesan 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 -rremoves the reason for it rather than merelyremoving 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.shalready answered it that way formake android-install; the Taskfile was the odd one out.2. The id comes from the APK.
aapt2 dump packagenameon theartifact the task has just built, so the thing installed and the thing
launched agree by construction. The cheaper fix —
APP_ID+.devonthe debug-flavoured tasks — was rejected:
android-tier.mdhas 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_IDsurvives with no default, re-purposed from a setting itcould never have been (
wails3 tasknever readsbuild/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 thanopt-in.
android-pkgid.sh --expectis called by every deploy, sorunning these tasks is safe by construction rather than by reading them
first — an opt-in
maketarget is a guard nobody runs. It firesbefore 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
maketarget, somake skill-checkhas nothing new to check.4. Scope: the edits survive.
wails3 task common:update:build-assetsdoes not touch
build/android/— verified by running it on thisbranch: it modified
build/linux/nfpm/nfpm.yamlonly, and exactly thedocumented
homepage/licensereset.generate build-assetswouldrewrite 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'sPKGdefaulted toapp.yellowjacketwhilemake android-installinstalls whatever is in
bin/— soandroid-launch,android-logsand
android-smokeaddressed a package the build had not produced, andits 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", installedon 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, beforeaapt2 dump packagename bin/yellowjacket.apkon the debug APK thosefirst two tasks build:
app.yellowjacket.dev.wails3 task --dry, afterNo
uninstall, and no id in the Taskfile at all. The optionalarguments render:
The real run, against the emulator
make android-emulator, thenwails3 task android:runwith thephone also attached — so this exercises the target selection too:
Installed and launched the same package, on
emulator-5554and noton
LP3LHMA531900746. (The app then dies on launch — the emulatorcannot 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 devicewhile the phone was attached, which isexactly #159's configuration (debug APK, release id, real device):
No adb call was made. Before this branch, the same situation ran
adb uninstall app.yellowjacketagainst that phone.And the target guard, phone attached with the emulator stopped:
The attached device, before and after
Byte-identical.
run:deviceanddeploy-devicewere deliberatelynot 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(187passed),
tsc --noEmitin bothfrontend/ande2e/,make css-check,make bindings-check,make skill-check(47documented targets, all present).
Which of those were formalities: all except
skill-check. Thisbranch 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-checkis the one with something to say, since the commit editsan agent-facing document.
Closes #159
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