Files
chrisandClaude Opus 5.5 c7bd8f0ad3 Faster Android builds: ccache, Gradle build cache, 64-bit only, no release lint
Most of a build was compiling the same C++ twice per APK with nothing reused between builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
2026-10-15 15:21:35 -04:00

109 lines
5.5 KiB
Markdown

# Building the Android app on the Gitea server
Pushing a version tag builds both APKs on our own server and attaches them to a Gitea release:
- `musicshare-<version>.apk`: the normal app.
- `musicshare-<version>-foss.apk`: no closed-source code, the same build F-Droid makes. The
build fails if any Google ML Kit or Play Services code ends up inside.
Both are signed with the upload key, and `SHA256SUMS` lists their checksums. Nothing leaves the server except the
downloads the build itself needs (npm packages, Gradle and its plugins).
The pieces:
| File | What it is |
| --- | --- |
| `.gitea/workflows/android.yml` | The workflow. It uses only plain shell steps, no third-party actions. |
| `apps/mobile/scripts/build-android.sh` | The build. You can also run it by hand on any machine with the Android SDK. |
| `deploy/android-builder/Dockerfile` | The build environment: Node 22, Java 17 and the Android SDK/NDK. |
| `deploy/gitea-runner/` | The runner (`act_runner`) that runs builds on the server. |
## One-time setup (on the Gitea server)
The server needs about 8 GB of free memory during a build and about 20 GB of disk for the image and
caches. A first build takes 15-40 minutes depending on the machine; later ones reuse the Gradle build
cache and ccache's compiled C++ (volumes `musicshare-gradle` and `musicshare-ccache`) and take a few
minutes. The APKs are for 64-bit phones (`arm64-v8a`); set `ABIS=armeabi-v7a,arm64-v8a` in the workflow's
Build step to include old 32-bit ones.
The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`).
Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the
server, including databases, stops writing.
1. **Turn on Actions** if it's off. In Gitea's `app.ini`, set:
```ini
[actions]
ENABLED = true
```
Restart Gitea. Then, in the repo, go to **Settings > Repository > Actions** and enable it.
2. **Build the builder image** from a checkout of this repo:
```bash
docker build -t musicshare-android-builder deploy/android-builder
```
Rebuild it when React Native's SDK or NDK versions change (the Dockerfile says where to look).
3. **Start the runner.** In the repo, open **Settings > Actions > Runners > Create new runner** and
copy the token. Use the repo's page, not the site admin's, so the runner only takes this repo's jobs.
Then:
```bash
cd deploy/gitea-runner
RUNNER_TOKEN=<token> docker compose up -d
```
It registers once (saved in `./data`), then shows as idle with the label `musicshare-android`.
The runner controls Docker on the host, so treat it like root access. The workflow only runs for
tags and manual runs, never for pull requests.
4. **Add the secrets** under **Settings > Actions > Secrets**:
- `UPLOAD_KEYSTORE_BASE64`: the output of `base64 -w0 ~/.config/musicshare/musicshare-upload.jks`.
- `MUSICSHARE_UPLOAD_STORE_PASSWORD`, `MUSICSHARE_UPLOAD_KEY_ALIAS` (`musicshare-upload`), `MUSICSHARE_UPLOAD_KEY_PASSWORD`: the values in
`~/.gradle/gradle.properties` on the machine you've been building on.
- `RELEASE_TOKEN`: create it under **User settings > Applications > Access tokens**, with
*write:repository* and *write:package* permissions.
## Releasing
1. Bump `version` and `android.versionCode` in `apps/mobile/app.json`. Add
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt`, then commit.
2. Tag and push: `git tag v1.1.0 && git push origin main v1.1.0`.
3. Watch it under the repo's **Actions** tab. The APKs appear on the **Releases** page.
The build refuses to run if the tag doesn't match the version in `app.json`. Re-running a tag's build replaces
that release's files.
**Test builds:** go to **Actions > Android > Run workflow** and pick a branch. The APKs go to the
repo's **Packages** as `musicshare-android`, versioned `<branch>-<commit>`, instead of to a release.
## App updates
The normal APK updates itself from these releases; the F-Droid build doesn't (F-Droid updates it).
1. The app asks our server (`GET /app/android/latest`) when it opens, at most every 6 hours, or when you
tap **Settings > Check for updates**.
2. The server reads the latest release from Gitea and returns its version, the APK's SHA-256 (from
`SHA256SUMS`) and the release notes. Drafts and pre-releases are skipped.
3. If it's newer, the app downloads the APK through our server and checks the SHA-256.
4. The next time you leave the app, it tries a quiet install (once per version). Android 12+ allows that
after the app has installed one update itself. Otherwise the feed shows **Update ready**, and tapping it
opens Android's install screen; the first time, Android asks you to allow musicshare to install apps.
On phones with Google Play, Play Protect may ask to scan each new version ("App scan recommended")
because the app isn't from the Play Store. Choosing **Scan app** lets the update finish.
Set these on the API server (`deploy/production.env`):
```ini
ANDROID_RELEASES_URL=https://gitea.chrisedwards.tech/api/v1/repos/chris/musicshare/releases/latest
# Only if the repo is private: a token with read:repository (User settings > Applications).
ANDROID_RELEASES_TOKEN=
```
The release notes come from `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt`: the workflow
puts that text above a `---` line in the release, and the app shows only that part.
## Building by hand
```bash
apps/mobile/scripts/build-android.sh # APKs in apps/mobile/dist
```
It uses the upload key from `~/.gradle/gradle.properties` if it's there; otherwise the APKs come out unsigned.