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//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 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