docs: rewrite README with comprehensive toolchain usage guide

This commit is contained in:
2026-06-15 07:33:57 -04:00
parent 87a010d857
commit f75be3b757
5 changed files with 309 additions and 2 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# --- Secrets / signing (must never be committed) ---
*.keystore
*.jks
*.p12
*.pem
*.key
keystore*
*.base64
key.properties
local.properties
# --- Build / scratch ---
*.zip
*.tmp
tmp/
out/
dist/
# --- OS cruft ---
.DS_Store
Thumbs.db
desktop.ini
# --- Editors / IDEs ---
.idea/
.vscode/
*.swp
*.swo
*~
# --- Logs ---
*.log

48
Dockerfile.yml Normal file
View File

@@ -0,0 +1,48 @@
# Native linux/arm64 Android build toolchain for CI.
# Builds release artifacts only — no emulator, no test/system images.
# eclipse-temurin publishes a native linux/arm64 manifest, so this runs
# without emulation on an OCI Ampere (aarch64) runner.
FROM eclipse-temurin:21-jdk-noble
# --- Version pins (bump deliberately; this is what your app repos trust) ---
# cmdline-tools: find the current build number at
# https://developer.android.com/studio#command-line-tools-only
ARG CMDLINE_TOOLS_VERSION=13114758
ARG BUILD_TOOLS_VERSION=35.0.0
ARG PLATFORM_VERSION=android-35
ENV ANDROID_SDK_ROOT=/opt/android-sdk \
ANDROID_HOME=/opt/android-sdk \
DEBIAN_FRONTEND=noninteractive
# git is needed by some Gradle plugins; unzip/curl for SDK install.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl unzip git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Install the command-line tools into the canonical "latest" location.
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools && \
curl -fsSL -o /tmp/tools.zip \
"https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" && \
unzip -q /tmp/tools.zip -d ${ANDROID_SDK_ROOT}/cmdline-tools && \
mv ${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest && \
rm /tmp/tools.zip
ENV PATH=${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools
# Accept licenses and bake the SDK packages into the image so prod builds
# don't depend on Google's endpoint at job time.
# aapt2 and the build-tools binaries ship native arm64 for 35.x, so APK/AAB
# packaging runs natively on aarch64.
RUN yes | sdkmanager --licenses >/dev/null && \
sdkmanager --install \
"platform-tools" \
"platforms;${PLATFORM_VERSION}" \
"build-tools;${BUILD_TOOLS_VERSION}" && \
rm -rf ${ANDROID_SDK_ROOT}/.android
# Sanity: fail the image build if the toolchain isn't actually usable.
RUN java -version && sdkmanager --version && \
test -d "${ANDROID_SDK_ROOT}/platforms/${PLATFORM_VERSION}"
WORKDIR /workspace

View File

@@ -1,3 +1,94 @@
# android # android-builder
Android build Native `linux/arm64` Android build toolchain for CI, published to the Gitea
container registry at `git.helu.ca/r/android`.
App repos consume this image to build signed release artifacts. Instrumented
tests are **not** part of this toolchain — by design, CI builds are promotions
of code already tested in Dev (on Apple silicon, where the emulator runs
natively). The OCI Ampere (aarch64) runner has no `/dev/kvm` (the guest VM
boots at EL1, so KVM can't access HYP/EL2), so there's no accelerated emulator
here — and we don't need one.
## What's in the image
- Eclipse Temurin JDK 21 (native arm64)
- Android cmdline-tools, platform, and build-tools — **baked in**, so prod
builds don't depend on Google's download endpoint at job time
- `git`, `curl`, `unzip`
Pinned versions live as `ARG`s at the top of the `Dockerfile`:
| ARG | Default | Where to check |
| ----------------------- | ----------- | -------------- |
| `CMDLINE_TOOLS_VERSION` | `13114758` | https://developer.android.com/studio#command-line-tools-only |
| `BUILD_TOOLS_VERSION` | `35.0.0` | SDK Manager / release notes |
| `PLATFORM_VERSION` | `android-35`| your app's `compileSdk` |
`aapt2` and the build-tools binaries ship native arm64 for 35.x, so packaging
runs without emulation.
## Tagging model
The whole point of owning this image is that **app repos pin a tag and a new
toolchain never lands in a prod build by accident.**
- **Immutable** — a git tag like `2026.06` produces `git.helu.ca/r/android:2026.06`.
This is what app repos pin to.
- **Moving** — pushes to `main` (and any tag build) refresh
`git.helu.ca/r/android:latest` for convenience. Don't pin prod builds to this.
## Cutting a new toolchain
1. Update the `ARG` defaults in the `Dockerfile` if you're moving SDK/tool
versions. Verify `CMDLINE_TOOLS_VERSION` against the link above — a stale
number 404s the image build.
2. Commit to `main`. The workflow builds and pushes `:latest`; confirm it's green.
3. Tag the release with the year-month you want app repos to reference:
```
git tag 2026.06
git push origin 2026.06
```
This publishes the immutable `git.helu.ca/r/android:2026.06`.
4. Roll app repos forward deliberately by bumping their pinned tag (see below)
— one at a time if you want to validate, all at once if you trust the bump.
## Using it in an app repo
Drop `.gitea/workflows/build.yml` (the template alongside this repo) into the
app and pin the toolchain:
```yaml
jobs:
build:
runs-on: [self-hosted, arm64]
container:
image: git.helu.ca/r/android:2026.06
credentials:
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
```
The build task is selectable: `assembleRelease` (APK, the default) or
`bundleRelease` (AAB — only needed for Google Play distribution).
### Required secrets in each app repo (or org-level)
Signing happens at job time; nothing sensitive lives in the repo or the image.
| Secret | What it is |
| ------------------- | ---------- |
| `KEYSTORE_BASE64` | release keystore, base64-encoded: `base64 -w0 release.keystore` |
| `KEYSTORE_PASSWORD` | keystore password |
| `KEY_ALIAS` | signing key alias |
| `KEY_PASSWORD` | key password |
`GITEA_TOKEN` needs `write:package` here (to push) and `read:package` in app
repos (to pull). The built-in token usually covers this; if your instance
scopes it tightly, use a PAT.
## First-run sequencing
The image must exist in the registry before any app workflow can pull it.
So: cut and push `2026.06` from this repo first, confirm it's in the registry,
*then* the app workflows that pin it will resolve.

79
build.yml Normal file
View File

@@ -0,0 +1,79 @@
name: build-release
# Drop this into any Android app repo at .gitea/workflows/build.yml
# It runs the release build inside the pinned toolchain image and publishes
# the signed artifact. Instrumented tests are NOT run here — by design,
# prod builds are promotions of code already tested in Dev.
on:
push:
tags: ["v*"] # build prod artifacts on version tags
workflow_dispatch:
inputs:
gradle_task:
description: "Release task"
type: choice
default: assembleRelease # APK. Use bundleRelease for a Play AAB.
options: [assembleRelease, bundleRelease]
env:
# Pin the toolchain. Bump deliberately when you roll forward.
BUILDER_IMAGE: git.helu.ca/r/android:2026.06
# Default task for tag-triggered builds (workflow_dispatch overrides via input)
DEFAULT_TASK: assembleRelease
jobs:
build:
runs-on: [self-hosted, arm64]
container:
image: git.helu.ca/r/android:2026.06
credentials:
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve gradle task
id: task
run: |
TASK="${{ inputs.gradle_task }}"
[ -z "$TASK" ] && TASK="${DEFAULT_TASK}"
echo "task=$TASK" >> "$GITHUB_OUTPUT"
# --- Signing ---------------------------------------------------------
# Store the keystore as a base64-encoded Gitea Actions secret and the
# passwords as separate secrets. Nothing sensitive lives in the repo
# or the image. Decode at job time into a path Gradle reads.
#
# Create the base64 secret yourself with:
# base64 -w0 release.keystore (copy output into secret KEYSTORE_BASE64)
#
# Reference KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD from your
# signingConfig (or via -Pandroid.injected.signing.* as below).
- name: Decode keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/release.keystore"
- name: Build release
run: |
./gradlew --no-daemon ${{ steps.task.outputs.task }} \
-Pandroid.injected.signing.store.file="$RUNNER_TEMP/release.keystore" \
-Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" \
-Pandroid.injected.signing.key.alias="${{ secrets.KEY_ALIAS }}" \
-Pandroid.injected.signing.key.password="${{ secrets.KEY_PASSWORD }}"
# Collects whichever artifact the task produced — apk or aab.
- name: Collect artifact
run: |
mkdir -p out
find app/build/outputs -type f \( -name "*-release.apk" -o -name "*-release.aab" \) \
-exec cp {} out/ \;
ls -l out
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: out/*
if-no-files-found: error

57
builder-image.yml Normal file
View File

@@ -0,0 +1,57 @@
name: build-android-builder-image
# Builds the Android toolchain image and pushes it to the Gitea registry.
# Push a tag like `2026.06` to cut an immutable toolchain; pushes to main
# refresh the moving `latest` tag.
on:
push:
branches: [main]
tags: ["*"]
workflow_dispatch:
env:
REGISTRY: git.helu.ca
IMAGE: git.helu.ca/r/android
jobs:
build-image:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v4
# Determine which tags to push:
# - a git tag -> use it verbatim (e.g. 2026.06) + latest
# - main branch -> latest only
- name: Compute tags
id: tags
run: |
if [ "${{ gitea.ref_type }}" = "tag" ]; then
REF="${{ gitea.ref_name }}"
echo "tags=${IMAGE}:${REF},${IMAGE}:latest" >> "$GITHUB_OUTPUT"
else
echo "tags=${IMAGE}:latest" >> "$GITHUB_OUTPUT"
fi
- name: Log in to Gitea registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push (linux/arm64)
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
# Bump these here if you want to override the Dockerfile defaults
# build-args: |
# BUILD_TOOLS_VERSION=35.0.0
# PLATFORM_VERSION=android-35