Skip to content

Android

Android #8

Workflow file for this run

name: Android
on:
pull_request:
branches: [main]
paths:
- "apps/**"
- "packages/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/android.yml"
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
env:
# Keep these in sync with docs/android-port-spike.md and the local setup.
ANDROID_PLATFORM: "platforms;android-34"
ANDROID_BUILD_TOOLS: "build-tools;34.0.0"
# NDK r27 LTS — Tauri v2's supported line. Bump deliberately.
ANDROID_NDK_VERSION: "27.3.13750724"
jobs:
build:
name: Build Android APK (debug)
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: package-lock.json
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Set up Android SDK
# Third-party action — pin to a commit SHA to match the repo's policy
# for non-official actions before relying on this in protected branches.
uses: android-actions/setup-android@v3
- name: Install Android NDK, platform, and build-tools
run: |
sdkmanager --install \
"platform-tools" \
"$ANDROID_PLATFORM" \
"$ANDROID_BUILD_TOOLS" \
"ndk;$ANDROID_NDK_VERSION"
echo "NDK_HOME=$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" >> "$GITHUB_ENV"
- name: Install Rust stable with Android targets
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
targets: >-
aarch64-linux-android,
armv7-linux-androideabi,
i686-linux-android,
x86_64-linux-android
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: apps/geolibre-desktop/src-tauri -> target
- name: Install frontend dependencies
run: npm ci
- name: Generate Android project
# gen/android is not committed; regenerate it on the clean runner. The
# Tauri CLI is resolved through the geolibre-desktop workspace.
working-directory: apps/geolibre-desktop
run: npx tauri android init
- name: Apply GeoLibre launcher icons
# `tauri android init` writes default Tauri icons; overwrite the generated
# mipmaps with the GeoLibre launcher icons checked in under src-tauri/icons.
working-directory: apps/geolibre-desktop
run: cp -r src-tauri/icons/android/. src-tauri/gen/android/app/src/main/res/
- name: Build release APKs (per ABI)
# Release (not --debug): the size-optimized + stripped Cargo profile keeps
# each .so small. --split-per-abi emits one ~40 MB APK per architecture
# instead of a single ~150 MB universal APK bundling all four ABIs.
# Release APKs are unsigned by default; the next step signs them.
working-directory: apps/geolibre-desktop
run: npx tauri android build --apk --split-per-abi
env:
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
- name: Sign APKs
# With release-keystore secrets set, the APKs are signed for distribution.
# Without them, they're signed with a throwaway debug keystore so the CI
# artifacts are still installable for testing (do NOT publish those).
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
build_tools="$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)"
if [ -n "${KEYSTORE_BASE64:-}" ]; then
echo "Signing with the release keystore from secrets."
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
keystore="$RUNNER_TEMP/release.jks"
store_pass="$KEYSTORE_PASSWORD"
key_pass="${KEY_PASSWORD:-$KEYSTORE_PASSWORD}"
alias="$KEY_ALIAS"
else
echo "::warning::No ANDROID_KEYSTORE_BASE64 secret set — signing with a throwaway debug keystore. Installable for testing only, NOT for distribution."
keystore="$RUNNER_TEMP/debug.jks"
"$JAVA_HOME/bin/keytool" -genkeypair -v -keystore "$keystore" \
-storepass android -keypass android -alias androiddebugkey \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
store_pass=android; key_pass=android; alias=androiddebugkey
fi
out="$RUNNER_TEMP/apks"; mkdir -p "$out"
found=0
while IFS= read -r unsigned; do
found=1
# e.g. app-arm64-v8a-release-unsigned.apk -> geolibre-arm64-v8a.apk
abi="$(basename "$unsigned" | sed -E 's/^app-(.*)-release-unsigned\.apk$/\1/')"
aligned="$RUNNER_TEMP/aligned-$abi.apk"
signed="$out/geolibre-android-$abi.apk"
"$build_tools/zipalign" -p -f 4 "$unsigned" "$aligned"
"$build_tools/apksigner" sign --ks "$keystore" \
--ks-pass "pass:$store_pass" --key-pass "pass:$key_pass" \
--ks-key-alias "$alias" --out "$signed" "$aligned"
"$build_tools/apksigner" verify "$signed"
echo "signed $signed ($(du -h "$signed" | cut -f1))"
done < <(find apps/geolibre-desktop/src-tauri/gen/android \
-name '*release-unsigned.apk')
if [ "$found" -eq 0 ]; then
echo "::error::No release-unsigned APKs found"; exit 1
fi
- name: Upload signed APKs
uses: actions/upload-artifact@v4
with:
name: geolibre-android-release-apks
path: ${{ runner.temp }}/apks/*.apk
if-no-files-found: error
retention-days: 14