mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Build production-signed APKs on GitHub while leaving AAB store builds on EAS. Manual dispatches default to artifact-only dry runs so arbitrary refs can be verified without publishing.
171 lines
9.2 KiB
Markdown
171 lines
9.2 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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](https://mise.jdx.dev):
|
|
|
|
```bash
|
|
mise install # java 21 + android-sdk 21.0 command-line tools
|
|
```
|
|
|
|
> **Pin a real `android-sdk` version, not `latest`.** The mise `android-sdk` plugin's `latest` resolved to the ancient `1.0` bundle, whose `sdkmanager` (3.6.0) predates the `emulator` package and fails with `Failed to find package emulator`. `21.0` ships a current `sdkmanager`. If you bump it, update the version in `.tool-versions` and 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
npm run android:development # Debug build
|
|
npm run android:production # Release build
|
|
npm run android:clear # Remove generated Android project
|
|
```
|
|
|
|
Or from `packages/app`:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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 with `Failed to connect to /<lan-ip>:8081` before 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 to `localhost:6767`, the production daemon. Use `$PASEO_SERVICE_DAEMON_PORT` for a worktree daemon running as a Paseo service, or `6768` for a standalone `npm 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):
|
|
|
|
```bash
|
|
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](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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.yml` on GitHub Actions (locally built, production-signed APK asset on GitHub Release). The APK build uses EAS local tooling for build parity but runs entirely on the GitHub runner, so it does not consume an EAS cloud build.
|
|
|
|
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 also supports `workflow_dispatch`:
|
|
|
|
- Leave `publish` disabled and optionally provide `ref` to build any branch, tag, or commit as a workflow artifact without changing a GitHub release.
|
|
- Enable `publish` and provide an existing `tag` to rebuild that tag and upload the APK to its GitHub release. Manual publishes always build the tag itself; they cannot publish an arbitrary ref under a release tag.
|
|
|
|
The workflow restores the production keystore and Firebase configuration from protected GitHub repository secrets. Keep those values synchronized with the production Android credentials in EAS. The APK profile uses `credentialsSource: local`; the AAB profile continues using EAS-managed credentials and store submission.
|
|
|
|
### Useful commands
|
|
|
|
```bash
|
|
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](release.md) for the full mobile-build babysitting flow.
|