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
This commit is contained in:
+25
-55
@@ -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=<serial> 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=<serial> 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)"
|
||||
|
||||
Executable
+196
@@ -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 <path> --target emulator|device|any \
|
||||
# [--expect <id>] [--serial <s>] [--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-<port>"; 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=<serial>, 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 <<EOF
|
||||
|
||||
The copy of $PKG already installed was signed with a different key, and
|
||||
Android never allows that as an update.
|
||||
|
||||
The only way forward is an uninstall — **which deletes that app's data**,
|
||||
and for this app that is the user's library, irreversibly. So it is not
|
||||
done for you. If the installed copy is disposable:
|
||||
|
||||
$ADB -s $SERIAL uninstall $PKG
|
||||
|
||||
If it is not — if this is a released build with a real library on it —
|
||||
install the debug variant instead, which carries applicationIdSuffix
|
||||
".dev" and so sits beside it rather than replacing it:
|
||||
|
||||
wails3 task android:assemble:apk
|
||||
EOF
|
||||
;;
|
||||
*INSTALL_FAILED_VERSION_DOWNGRADE*)
|
||||
cat >&2 <<EOF
|
||||
|
||||
The installed copy of $PKG has a higher versionCode than this build.
|
||||
A bare 'make android' builds versionCode 1; a versioned one builds e.g.
|
||||
10301. Either build with a version:
|
||||
|
||||
YJ_VERSION=1.3.1 YJ_VERSION_CODE=10301 make android
|
||||
|
||||
or, if the installed copy is disposable, remove it:
|
||||
|
||||
$ADB -s $SERIAL uninstall $PKG
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$out"
|
||||
|
||||
[ "$LAUNCH" -eq 1 ] || exit 0
|
||||
|
||||
"$ADB" -s "$SERIAL" shell am start -n "$PKG/$ACTIVITY"
|
||||
Executable
+97
@@ -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 <apk> [--expect <id>]
|
||||
#
|
||||
# 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 <apk> [--expect <id>]"
|
||||
[ -f "$APK" ] || die "no such APK: $APK"
|
||||
|
||||
# aapt2 lives under build-tools/<version>/, 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 <<EOF
|
||||
android-pkgid: refusing to act on a package this APK does not declare.
|
||||
|
||||
the APK declares: $pkg
|
||||
the task expects: $EXPECT
|
||||
APK: $APK
|
||||
|
||||
These must agree, and when they do not it is the *expectation* that is
|
||||
wrong: the APK is what Gradle built. A debug build carries
|
||||
applicationIdSuffix ".dev" (app/build.gradle), so a task that assembles
|
||||
a debug APK and then addresses the unsuffixed id is addressing the
|
||||
released app — which on a real device is the user's install, with their
|
||||
library in it (#159).
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s\n' "$pkg"
|
||||
Reference in New Issue
Block a user