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. 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 </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 \ diff --git a/scripts/android-pkgid.sh b/scripts/android-pkgid.sh new file mode 100755 index 0000000..fe3fa3e --- /dev/null +++ b/scripts/android-pkgid.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# +# Print the package id an APK actually declares — and, given --expect, +# refuse when that is not the id the caller was about to act on. +# +# This exists because the identity is declared twice and nothing made +# the two agree. `applicationId` in build/android/app/build.gradle is +# what Gradle installs; `APP_ID` in build/android/Taskfile.yml was what +# every adb-driven task uninstalled, launched and filtered. They differ +# for a reason nobody has to get wrong: the debug buildType carries +# `applicationIdSuffix ".dev"`, so a debug build is app.yellowjacket.dev +# while the default was app.yellowjacket — the *release* id, and on a +# real phone the released app with the user's library on it (#159). +# +# So the id is read back from the artifact rather than written down a +# third time. 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. +# +# Usage: +# android-pkgid.sh [--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 <