android-emulator-build

Installation
SKILL.md

Android emulator build

Turns the Expo app into a Release APK with one command, npm run build:android:emu -- --variant preview, and proves what was built: JDK 17 and the pinned Android SDK made it, the APK is the variant asked for (test-only code present in development and preview, absent in production; the variant's package; only the permissions the app declared), and the app starts and draws on an emulator this repo owns. No Android Studio, no Metro, no upload key. Verified on 2026-10-14 with Expo SDK 57 (expo 57.0.26), React Native 0.86.3 (compileSdk and targetSdk 36, build tools 36.0.0, Gradle 9.3.1 from the prebuild template), JDK 17.0.11 and Android emulator 35.1.20 on Apple silicon, booting the API 35 16 KB Play Store image (system-images;android-35;google_apis_playstore_ps16k;arm64-v8a) through the contract's systemImage; the pinned default image (system-images;android-36;google_apis;<abi>) has not had a real boot yet. The iOS twin is ios-simulator-build.

Rules that must hold

  1. Every emulator build runs through one script and starts from a clean prebuild. scripts/build/build-android-emu.ts preflights every npm script it will run (exit 2, nothing built), then npx expo prebuild --platform android --clean, the contract's post-prebuild scripts, Gradle, install, launch and screenshot. Why: android/ is generated (Continuous Native Generation, owned by expo-prebuild-and-config-plugins), and a stale android/ from another variant builds the wrong package.
  2. JDK 17 and the Android SDK are selected per process, and the pins match React Native. scripts/android/toolchain.ts pins JDK_MAJOR, ANDROID_API and BUILD_TOOLS_VERSION as literals equal to the installed React Native's version catalog; selectToolchain() finds JDK 17 by its release file, checks the SDK packages and sets JAVA_HOME and ANDROID_HOME on the child environment. Never jenv global, sdk default java, update-alternatives or sudo. Why: a machine often holds several JDKs (Android Studio's bundled one changes with Studio updates), and a global switch changes every build on it.
  3. The variant is exported once, for the whole run. variantEnv(variant) from src/config/app-variant.ts sets APP_VARIANT = EXPO_PUBLIC_APP_VARIANT on the one environment every step gets. Why: the variant enters the APK three times, each in its own process: the prebuild (package, manifest), expo-constants' createExpoConfig Gradle task (assets/app.config) and Metro (export:embed, the JS bundle).
  4. Metro's cache is keyed on the public values before any Release build. metro.config.js sets cacheVersion from the EXPO_PUBLIC_* values (the key and its proof belong to app-variants-and-environment). Why: the build runs with CI=1, so the bundling task keeps Metro's cache, and a production APK built after a preview APK can ship the preview bundle.
  5. The build is :app:assembleRelease for one ABI, with no daemon and no upload key. ./gradlew --no-daemon :app:assembleRelease -PreactNativeArchitectures=<abi> (gradleReleaseArgs), the ABI of the emulator (hostAbi(): arm64-v8a on Apple silicon, x86_64 elsewhere). Why: Release embeds the Hermes bundle users run (Debug loads JS from Metro); one ABI builds about four times faster than the template's four; a daemon would keep another run's environment and 2 GB of memory on a shared machine; the prebuild template signs Release with the public debug keystore, so no secret is touched (upload signing is android-release-play).
  6. Emulators are AVDs named <prefix>-<purpose>, started on a console port this run picked and addressed only by serial. emulator -avd <prefix>-<purpose> -port <even port>, then adb -s emulator-<port> ... after adb -s emulator-<port> emu avd name confirmed the AVD behind it; only prefixed AVDs are created, stopped (adb -s <serial> emu kill) or deleted. Never adb -e/-d, adb kill-server, gradlew --stop, a Gradle install* task, pkill of emulators or eas build:run -p android. Why: several sessions share one machine and one adb server; each of those reaches, restarts or installs on someone else's device.
  7. Every tooling process runs with EXPO_NO_TELEMETRY=1 and CI=1. Why: no telemetry, and no interactive prompt that would hang an unattended run.
  8. A fresh install and a cold start, then wait on a signal, never a fixed sleep; then check the screenshot and look at it. The previous copy is uninstalled, am start -S -W starts the launcher activity, and the run polls: the process alive (pidof), its activity resumed, the crash buffer empty, and the contract's readyLog line or two identical screenshots after at least 3 s. Why: a build that installed can still crash at launch, sit behind a system dialog, or draw a blank screen; only the picture shows the last one.
  9. The built APK proves its variant. check-apk reads the APK: an embedded Hermes bundle, the test-only sentinel (present in development and preview, absent in production), not debuggable, the variant's own package in the manifest and in expo-constants' assets/app.config, targetSdk 36 and minSdk 24 or higher, native libraries for the emulator ABI with 16 KB aligned segments, a signing block, and for production no placeholder and no cleartext traffic. Why: BUILD SUCCESSFUL says nothing about what shipped inside.
  10. The merged manifest is the permission evidence. The contract's permissions lists every permission the app may carry (seeded with the five the prebuild template always adds; one named after the app is written ${applicationId}.NAME, and the APK's own signature permission from androidx.core needs no entry); check-apk fails on any other in the APK. Why: libraries add permissions at Gradle merge time (an image picker's CAMERA, a template's SYSTEM_ALERT_WINDOW), and neither app.config.ts nor the prebuild output shows them.
  11. A change to variants, Metro, test-only code or the build tooling is proven by a preview build and a production build made back to back, without clearing any cache. Why: only that order shows a missing cache key or a broken gate.
  12. Human steps are asked for, never worked around. Installing a JDK, the SDK packages and a system image, and accepting the SDK licences, are the developer's steps (a licence is a legal agreement); the agent stops and asks. Why: a workaround (another JDK, an auto-accepted licence, a different image) builds with the wrong toolchain and hides the problem.
  13. Generated and output folders stay out of git: android/, build/ and reports/. Why: the prebuild regenerates android/, build/ holds the kept APKs and logs, reports/ the screenshots.
Installs
5
GitHub Stars
12
First Seen
13 days ago