* Pin android-sdk to 21.0 and unify Java on 21 for mise The `.tool-versions` `android-sdk latest` pin had resolved to the ancient `1.0` cmdline-tools bundle, whose sdkmanager (3.6.0) predates the `emulator` package and fails with `Failed to find package emulator`. Pin `android-sdk 21.0`, point the hardcoded install paths in `.mise.toml` at it, and add `cmdline-tools/21.0/bin` to PATH so sdkmanager and avdmanager resolve. Also unify the Java version on 21: `.tool-versions` already pinned 21, but `.mise.toml` overrode it to 17. A local `npm run android` Gradle build succeeds on 21. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Document iOS simulator and Android emulator local dev setup Add local-dev docs for running the app against a worktree daemon: - development.md: prerequisites for the ios-simulator preview service (Xcode, an iOS runtime, automatic CocoaPods install), the xcode-select/simctl troubleshooting fix, and a "Running the iOS app on a local simulator" section covering `npm run ios` and pointing the app at a worktree daemon via EXPO_PUBLIC_LOCAL_DAEMON. - android.md: a "Prerequisites (local dev)" section (mise java 21 + android-sdk 21.0, sdkmanager components, AVD creation) and a "Running on an emulator against a worktree daemon" section covering REACT_NATIVE_PACKAGER_HOSTNAME and EXPO_PUBLIC_LOCAL_DAEMON=10.0.2.2 for reaching the host daemon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Give full Intel x86_64 emulator setup commands in Android docs Greptile flagged that the arm64-v8a -> x86_64 substitution for Intel Macs was only a parenthetical. Provide a complete, copy-pasteable x86_64 command block instead of asking readers to hand-edit the arm64 one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
8.4 KiB
Android
App variants
Controlled by APP_VARIANT in packages/app/app.config.js (vanilla Expo, no custom Gradle plugin):
| Variant | App name | Package ID |
|---|---|---|
production |
Paseo | sh.paseo |
development |
Paseo Debug | sh.paseo.debug |
EAS profiles: development, production, and production-apk in packages/app/eas.json.
development uses Android debug.
Version codes
packages/app/app.config.js derives Android versionCode from the package version with:
major * 1_000_000 + minor * 1_000 + patch
Prerelease metadata is ignored, so 0.1.102-beta.1 and 0.1.102 both produce 1102. The same value is used as the iOS buildNumber because packages/app/eas.json uses EAS's local app version source. Do not re-enable EAS remote version counters or Android autoIncrement; F-Droid and other source-based builders need the native build number to be visible in the repo.
The formula reserves three digits each for minor and patch. If either reaches 1000, change the formula before cutting that release.
Prerequisites (local dev)
Local Android builds run on macOS (or Linux) and need the Android toolchain, pinned in .tool-versions (java 21, android-sdk 21.0) and wired up by .mise.toml (which sets ANDROID_HOME and puts cmdline-tools/21.0/bin, platform-tools, and emulator on PATH). With mise:
mise install # java 21 + android-sdk 21.0 command-line tools
Pin a real
android-sdkversion, notlatest. The miseandroid-sdkplugin'slatestresolved to the ancient1.0bundle, whosesdkmanager(3.6.0) predates theemulatorpackage and fails withFailed to find package emulator.21.0ships a currentsdkmanager. If you bump it, update the version in.tool-versionsand in all four paths in.mise.toml.
mise install only lays down the command-line tools. Install the rest and create an emulator. On Apple Silicon:
sdkmanager --licenses
sdkmanager "platform-tools" "emulator" "platforms;android-35" "build-tools;35.0.0" \
"system-images;android-35;google_apis;arm64-v8a"
avdmanager create avd -n paseo -k "system-images;android-35;google_apis;arm64-v8a" -d pixel_7
emulator @paseo # start it; leave running
On an Intel Mac, use the x86_64 system image:
sdkmanager --licenses
sdkmanager "platform-tools" "emulator" "platforms;android-35" "build-tools;35.0.0" \
"system-images;android-35;google_apis;x86_64"
avdmanager create avd -n paseo -k "system-images;android-35;google_apis;x86_64" -d pixel_7
emulator @paseo # start it; leave running
Gradle auto-fetches the platform/build-tools it needs once licenses are accepted, so adjust android-35 only if it asks for a different level.
Local build + install
From repo root:
npm run android:development # Debug build
npm run android:production # Release build
npm run android:clear # Remove generated Android project
Or from packages/app:
# Debug
npx cross-env APP_VARIANT=development expo prebuild --platform android --non-interactive
npx cross-env APP_VARIANT=development expo run:android --variant=debug
# Release
npx cross-env APP_VARIANT=production expo prebuild --platform android --non-interactive
npx cross-env APP_VARIANT=production expo run:android --variant=release
# Clear generated Android project
rm -rf android
Running on an emulator against a worktree daemon
npm run android builds and installs the dev client, but two connections have to reach your Mac from inside the emulator — Metro (the JS bundle) and the Paseo daemon — and the emulator does not share the host's loopback: localhost inside the emulator is the emulator itself. Reach the host at 10.0.2.2 (the standard AVD's host alias) for both:
REACT_NATIVE_PACKAGER_HOSTNAME=10.0.2.2 \
EXPO_PUBLIC_LOCAL_DAEMON=10.0.2.2:$PASEO_SERVICE_DAEMON_PORT \
npm run android
REACT_NATIVE_PACKAGER_HOSTNAME=10.0.2.2— without it, Expo bakes your Mac's LAN IP into the dev client's Metro URL, which the emulator can't route to, and the app dies withFailed to connect to /<lan-ip>:8081before any JS loads.EXPO_PUBLIC_LOCAL_DAEMON=10.0.2.2:<port>— the client's daemon endpoint (packages/app/src/runtime/host-runtime.ts); when unset it defaults tolocalhost:6767, the production daemon. Use$PASEO_SERVICE_DAEMON_PORTfor a worktree daemon running as a Paseo service, or6768for a standalonenpm run dev:server. It is inlined into the JS bundle at Metro bundle time, so set it on the build command and clear the Metro cache (npx expo start -c) if a change doesn't take.
Alternative — adb reverse + localhost (if 10.0.2.2 misbehaves):
adb reverse tcp:8081 tcp:8081
adb reverse tcp:$PASEO_SERVICE_DAEMON_PORT tcp:$PASEO_SERVICE_DAEMON_PORT
REACT_NATIVE_PACKAGER_HOSTNAME=localhost \
EXPO_PUBLIC_LOCAL_DAEMON=localhost:$PASEO_SERVICE_DAEMON_PORT \
npm run android
This is the Android counterpart of the iOS local-simulator flow in development.md: on iOS the simulator shares the Mac's loopback so localhost:<port> works directly; on Android you need 10.0.2.2 or adb reverse.
F-Droid / source-only Android builds
F-Droid builds should set PASEO_FDROID_BUILD=1 when running Expo prebuild:
cd packages/app
PASEO_FDROID_BUILD=1 APP_VARIANT=production npx expo prebuild --platform android --clean --non-interactive
cd android
PASEO_FDROID_BUILD=1 ./gradlew assembleRelease --no-daemon --max-workers=1 -Dorg.gradle.parallel=false
The flag must be present for both prebuild and Gradle because Gradle starts Metro for the release bundle. Keep the source build serial and daemon-free as shown above: compiling every Expo module can exhaust memory when Gradle workers run in parallel. The profile enables source-built Expo modules, excludes the proprietary camera, Firebase notification, and Expo development-client native modules, disables EAS updates and Gradle dependency metadata, and substitutes JavaScript stubs for camera and notifications. The resulting app supports direct and pasted-link pairing but not QR scanning or push notifications.
Keep the excluded npm packages installed. Normal builds use them, while the F-Droid profile removes only their Android native modules and config plugins. Paseo always applies expo-gradle-jvmargs with -Xmx4096m and -XX:MaxMetaspaceSize=1024m so local Expo prebuilds have enough Gradle heap whether they use precompiled AARs or source-built Expo modules.
React version lockstep
Keep react and react-dom pinned to the React version embedded by the current react-native release. React Native 0.81.x embeds react-native-renderer 19.1.0, so packages/app must use React 19.1.0. Bumping React to a newer patch can build successfully but crash at JS startup on Android with Incompatible React versions, leaving the app on the native splash screen.
Screenshots
adb exec-out screencap -p > screenshot.png
Cloud build + submit (EAS)
Stable tag pushes like v0.1.0 trigger:
- The EAS GitHub app on Expo servers (iOS + Android production builds + store submit). There is no workflow file in this repo for it.
.github/workflows/android-apk-release.ymlon GitHub Actions (APK asset on GitHub Release).
iOS auto-submits to App Store review via a Fastlane lane after EAS uploads to TestFlight. Android auto-submits to the Play Store via EAS-managed credentials.
Beta tags like v0.1.1-beta.1 only trigger the GitHub APK workflow. They publish a GitHub prerelease APK for testing and do not submit to the stores.
android-v* tags also trigger only the GitHub APK workflow — useful when you want to ship an APK without going through stores. The GitHub APK workflow supports workflow_dispatch with an existing tag input so you can rebuild without cutting a new tag.
Useful commands
cd packages/app
# Recent builds
npx eas build:list --limit 10 --non-interactive --json | jq '.[] | {platform, status, appVersion, gitCommitHash}'
# Inspect a build (the printed `Logs` URL opens the build's Expo dashboard page,
# which has a Submissions section showing the auto-submit to the Play Store).
npx eas build:view <build-id>
The Play Console (Internal testing → Production tracks) is the final confirmation that the binary reached the store.
See docs/release.md for the full mobile-build babysitting flow.