Files
yellowjacket/build/android/Taskfile.yml
T
logan 8d2109b87e 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
2026-08-20 14:34:59 -04:00

457 lines
18 KiB
YAML

version: '3'
includes:
common: ../Taskfile.yml
vars:
# APP_ID is an *assertion*, not a setting, and it has no default.
#
# 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
HOST_ARCH:
sh: '[ "$(uname -m)" = "x86_64" ] && echo "amd64" || echo "arm64"'
# System-image ABI for the host, used in the "create an AVD" hint below.
ANDROID_ABI:
sh: '[ "$(uname -m)" = "arm64" ] && echo "arm64-v8a" || echo "x86_64"'
# SDK location: $ANDROID_HOME / $ANDROID_SDK_ROOT, else the per-OS default
# (macOS: ~/Library/Android/sdk, Linux/other: ~/Android/Sdk)
SDK_ROOT:
sh: 'echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}"'
ADB:
sh: 'command -v adb || echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}/platform-tools/adb"'
EMULATOR:
sh: 'command -v emulator || echo "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}/emulator/emulator"'
# avdmanager lives under cmdline-tools/<version>/bin; used to auto-create an
# AVD when none exists (mirrors the iOS `ensure-simulator` auto-create flow).
AVDMANAGER:
sh: 'command -v avdmanager || ls "${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$([ -d "$HOME/Library/Android/sdk" ] && echo "$HOME/Library/Android/sdk" || echo "$HOME/Android/Sdk")}}"/cmdline-tools/*/bin/avdmanager 2>/dev/null | sort -V | tail -1 || true'
tasks:
install:deps:
summary: Check and install Android development dependencies
cmds:
- go run build/android/scripts/deps/install_deps.go
env:
TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}'
prompt: This will check and install Android development dependencies. Continue?
build:
summary: Creates a debug build of the application for Android
deps:
- task: common:go:mod:tidy
- task: generate:android:overlay
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
cmds:
- echo "Building Android app {{.APP_NAME}}..."
- task: compile:go:shared
vars:
ARCH: '{{.ARCH | default .HOST_ARCH}}'
# This repo's one edit to the android scaffold, and it is not
# cosmetic. Upstream forwards ARCH here and not PRODUCTION, so
# compile:go:shared recomputed BUILD_FLAGS against an unset
# .PRODUCTION and fell back to the debug branch. package:fat
# calls compile:go:shared directly for amd64 (passing it), and
# reaches arm64 only through this task -- so a release APK
# shipped a *debug* 40 MB arm64 library beside a production
# 31 MB x86_64 one. The phone ABI, which is the only one a
# release is for, was the broken one. 34 MB APK before, 27
# after.
PRODUCTION: '{{.PRODUCTION}}'
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
env:
PRODUCTION: '{{.PRODUCTION | default "false"}}'
compile:go:shared:
summary: Compile Go code to shared library (.so)
cmds:
- |
# Locate the NDK: $ANDROID_NDK_HOME, or the newest installed NDK
NDK_ROOT="$ANDROID_NDK_HOME"
if [ -z "$NDK_ROOT" ]; then
SDK_ROOT="{{.SDK_ROOT}}"
NDK_ROOT=$(ls -d "$SDK_ROOT"/ndk/* 2>/dev/null | sort -V | tail -1)
fi
if [ -z "$NDK_ROOT" ] || [ ! -d "$NDK_ROOT" ]; then
echo "Error: Android NDK not found"
echo "Install one with: sdkmanager 'ndk;26.3.11579264' (or set ANDROID_NDK_HOME)"
exit 1
fi
# Determine toolchain based on host OS
case "$(uname -s)" in
Darwin) HOST_TAG="darwin-x86_64" ;;
Linux) HOST_TAG="linux-x86_64" ;;
*) echo "Unsupported host OS"; exit 1 ;;
esac
TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG"
# Set compiler based on architecture
case "{{.ARCH}}" in
arm64)
export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang"
export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++"
export GOARCH=arm64
JNI_DIR="arm64-v8a"
;;
amd64|x86_64)
export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang"
export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++"
export GOARCH=amd64
JNI_DIR="x86_64"
;;
*)
echo "Unsupported architecture: {{.ARCH}}"
exit 1
;;
esac
export CGO_ENABLED=1
export GOOS=android
mkdir -p {{.BIN_DIR}}
mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR
go build -buildmode=c-shared -overlay build/android/overlay.json {{.BUILD_FLAGS}} \
-o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
compile:go:all-archs:
summary: Compile Go code for all Android architectures (fat APK)
cmds:
- task: compile:go:shared
vars:
ARCH: arm64
- task: compile:go:shared
vars:
ARCH: amd64
package:
summary: Packages a production build of the application into a signed release APK
desc: |
Builds for arm64 by default (covers 99%+ of real devices). Set ARCH=amd64
for emulator-only APKs, or use package:fat for a universal APK.
deps:
- task: build
vars:
PRODUCTION: "true"
ARCH: '{{.ARCH | default "arm64"}}'
cmds:
- task: assemble:apk:release
package:fat:
summary: Packages a production build for all architectures (fat APK)
deps:
- task: build
vars:
PRODUCTION: "true"
ARCH: arm64
cmds:
- task: compile:go:shared
vars:
ARCH: amd64
PRODUCTION: "true"
- task: assemble:apk:release
bundle:
summary: Packages a production AAB (Android App Bundle) for Play Store submission
desc: |
Builds for arm64 by default. Set ARCH=amd64 for emulator builds, or use
bundle:fat for a universal AAB.
deps:
- task: build
vars:
PRODUCTION: "true"
ARCH: '{{.ARCH | default "arm64"}}'
cmds:
- task: assemble:aab:release
bundle:fat:
summary: Packages a production AAB for all architectures
deps:
- task: build
vars:
PRODUCTION: "true"
ARCH: arm64
cmds:
- task: compile:go:shared
vars:
ARCH: amd64
PRODUCTION: "true"
- task: assemble:aab:release
assemble:apk:
summary: Assembles a debug APK using Gradle
preconditions:
- sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]'
msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME"
cmds:
- |
cd build/android
# The exec bit is lost when gradlew is extracted from the embedded
# build assets, so restore it before invoking the wrapper.
chmod +x ./gradlew
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk"
echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk"
assemble:apk:release:
summary: Assembles a release APK using Gradle (signed with the debug keystore unless ANDROID_KEYSTORE_FILE is set)
preconditions:
- sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]'
msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME"
cmds:
- |
cd build/android
# The exec bit is lost when gradlew is extracted from the embedded
# build assets, so restore it before invoking the wrapper.
chmod +x ./gradlew
./gradlew assembleRelease
cp app/build/outputs/apk/release/app-release.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk"
echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk"
assemble:aab:
summary: Assembles a debug AAB (Android App Bundle) using Gradle
preconditions:
- sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]'
msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME"
cmds:
- |
cd build/android
# The exec bit is lost when gradlew is extracted from the embedded
# build assets, so restore it before invoking the wrapper.
chmod +x ./gradlew
./gradlew bundleDebug
cp app/build/outputs/bundle/debug/app-debug.aab "../../{{.BIN_DIR}}/{{.APP_NAME}}.aab"
echo "AAB created: {{.BIN_DIR}}/{{.APP_NAME}}.aab"
assemble:aab:release:
summary: Assembles a release AAB for Play Store upload (signed with the debug keystore unless ANDROID_KEYSTORE_FILE is set)
preconditions:
- sh: 'command -v java >/dev/null || [ -n "$JAVA_HOME" ]'
msg: "Java not found. Install a JDK (e.g. brew install openjdk@21) and/or set JAVA_HOME"
cmds:
- |
# With Play App Signing, the keystore configured here is your UPLOAD
# key: Google verifies the upload with it, then re-signs the app with
# the app signing key it manages for distribution.
if [ -z "$ANDROID_KEYSTORE_FILE" ]; then
echo "WARNING: ANDROID_KEYSTORE_FILE is not set, so this AAB will be"
echo "signed with the debug keystore. Google Play rejects debug-signed"
echo "bundles. Set ANDROID_KEYSTORE_FILE, ANDROID_KEYSTORE_PASSWORD,"
echo "ANDROID_KEY_ALIAS and ANDROID_KEY_PASSWORD before uploading."
fi
cd build/android
# The exec bit is lost when gradlew is extracted from the embedded
# build assets, so restore it before invoking the wrapper.
chmod +x ./gradlew
./gradlew bundleRelease
cp app/build/outputs/bundle/release/app-release.aab "../../{{.BIN_DIR}}/{{.APP_NAME}}.aab"
echo "Release AAB created: {{.BIN_DIR}}/{{.APP_NAME}}.aab"
generate:android:overlay:
internal: true
summary: Generate Go build overlay that registers the Android main
sources:
- build/config.yml
generates:
- build/android/overlay.json
- build/android/gen/main_android.gen.go
cmds:
- wails3 android overlay:gen -out build/android/overlay.json -config build/config.yml
generate:android:bindings:
internal: true
summary: Generates bindings for Android
sources:
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
cmds:
# Bindings are generated from the Go AST; CGO is disabled so the NDK
# is not required for this step
- wails3 generate bindings -f '-tags android' -clean=true
env:
GOOS: android
CGO_ENABLED: 0
ensure-emulator:
internal: true
summary: Ensure Android Emulator is running
silent: true
cmds:
- |
# Check if an emulator is already running
if "{{.ADB}}" devices | grep -q "emulator"; then
echo "Emulator already running"
exit 0
fi
# Get first available AVD
AVD_NAME=$("{{.EMULATOR}}" -list-avds | tail -1)
if [ -z "$AVD_NAME" ]; then
# No AVD yet. Mirror the iOS `ensure-simulator` flow and create one
# automatically — but ONLY from a system image that is already
# installed. We never trigger an sdkmanager download from a `run`
# task (that would be a surprise multi-GB download + license prompt).
# Pick the highest-API installed image matching the host ABI.
SDK_ROOT="{{.SDK_ROOT}}"
ABI="{{.ANDROID_ABI}}"
IMG=$(ls -d "$SDK_ROOT"/system-images/android-*/*/"$ABI" 2>/dev/null | sort -V | tail -1)
AVDMANAGER="{{.AVDMANAGER}}"
if [ -n "$IMG" ] && [ -x "$AVDMANAGER" ]; then
PKG="system-images;$(echo "$IMG" | sed "s|$SDK_ROOT/system-images/||" | tr '/' ';')"
echo "No Android Virtual Devices found. Creating 'wails' from $PKG..."
echo "no" | "$AVDMANAGER" create avd --name wails --package "$PKG" --device pixel_7 --force
AVD_NAME=wails
else
echo "No Android Virtual Devices found, and no system image is installed to create one from."
echo "Install a system image and create an AVD, e.g.:"
echo " sdkmanager 'system-images;android-35;google_apis;$ABI'"
echo " avdmanager create avd --name wails --package 'system-images;android-35;google_apis;$ABI' --device pixel_7"
exit 1
fi
fi
echo "Starting emulator: $AVD_NAME"
# Start the emulator daemonized so it outlives this task step. go-task's
# shell tracks background jobs by PID and reaps them when the command's
# interpreter finishes (which nohup/setsid alone don't prevent — the kill
# is direct), so a bare `emulator &` is gone before the later
# install/launch steps run. Launch it from a short-lived child shell that
# backgrounds the emulator and exits immediately: the emulator is then
# reparented to init/launchd and go-task's shell has no handle to reap it.
nohup sh -c "'{{.EMULATOR}}' -avd '$AVD_NAME' -no-snapshot-load </dev/null >/dev/null 2>&1 &" >/dev/null 2>&1
# Wait for emulator to boot (max 120 seconds)
echo "Waiting for emulator to boot..."
"{{.ADB}}" wait-for-device
for i in $(seq 1 120); do
BOOT_COMPLETED=$("{{.ADB}}" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
if [ "$BOOT_COMPLETED" = "1" ]; then
echo "Emulator booted successfully"
exit 0
fi
sleep 1
done
echo "Emulator boot timeout"
exit 1
preconditions:
- sh: '[ -x "{{.ADB}}" ] || command -v adb'
msg: "adb not found. Install the Android SDK platform-tools (or set ANDROID_HOME)"
- sh: '[ -x "{{.EMULATOR}}" ] || command -v emulator'
msg: "emulator not found. Install the Android SDK emulator package (or set ANDROID_HOME)"
deploy-emulator:
summary: Deploy the packaged release APK to the Android Emulator
deps:
- task: package
vars:
ARCH: '{{.ARCH | default .HOST_ARCH}}'
cmds:
- task: ensure-emulator
- './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
deps:
- task: ensure-emulator
- task: build
cmds:
- task: assemble:apk
- './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)
cmds:
- '"{{.ADB}}" devices -l'
run:device:
summary: Build, install and launch a debug build on a connected physical Android device
deps:
- task: build
vars:
ARCH: arm64
cmds:
- task: assemble:apk
- './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)"
deploy-device:
summary: Deploy the packaged release APK to a connected physical Android device
deps:
- task: package
vars:
ARCH: arm64
cmds:
- './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)"
studio:
summary: Open the generated Android project in Android Studio
cmds:
- |
if command -v studio >/dev/null 2>&1; then
studio build/android
elif [ -d "/Applications/Android Studio.app" ]; then
open -a "Android Studio" build/android
else
echo "Android Studio not found. Install it from https://developer.android.com/studio,"
echo "then open the build/android directory."
exit 1
fi
logs:
summary: Stream Android logcat filtered to this app
cmds:
- '"{{.ADB}}" logcat -v time | grep -E "(Wails|{{.APP_NAME}})" || true'
logs:all:
summary: Stream all Android logcat (verbose)
cmds:
- '"{{.ADB}}" logcat -v time'
clean:
summary: Clean build artifacts
cmds:
- rm -rf {{.BIN_DIR}}
- rm -rf build/android/app/build
- rm -rf build/android/app/src/main/jniLibs/*/libwails.so
- rm -rf build/android/.gradle