Files
jellytau/scripts/build-android.sh
T
dtourolle 8ecf74a2af
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 25m24s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 54s
📱 Test APK / Build test APK (push) Failing after 50m57s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 9m47s
Traceability Validation / Check Requirement Traces (push) Successful in 25s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 7m18s
fix(ci): let the Android build scripts respect a caller-set ANDROID_HOME
build-android.sh hardcoded `export ANDROID_HOME="$HOME/Android/Sdk"`,
which discarded whatever the caller had set. In the builder image the SDK
is at /opt/android-sdk and the job exports exactly that, so the script
looked for an NDK under /root/Android/Sdk, found nothing, and the build
died a minute in with "Android SDK not found" -- the first automatic
`latest` APK build failed on this and nothing else.

Now a default rather than an override, matching what
test-player-conformance.sh already did. NDK_HOME likewise prefers an
explicitly pinned ANDROID_NDK_HOME over guessing with `ls | head -1`,
which is how CI pins an exact NDK revision.

A missing SDK now fails immediately and says which variable to set,
instead of letting `ls` print its own error and the real failure surface
a minute later inside the tauri CLI.

android-dev.sh had the same override and gets the same treatment.
2026-09-05 14:33:33 +02:00

193 lines
8.0 KiB
Bash
Executable File

#!/bin/bash
# Build Android APK
set -e
# Source Rust environment
source "$HOME/.cargo/env.fish" 2>/dev/null || source "$HOME/.cargo/env" 2>/dev/null || true
# Set Android environment variables.
#
# Defaults, not overrides. A developer's SDK is at ~/Android/Sdk, but CI runs in
# the builder image where it lives at /opt/android-sdk and the job sets
# ANDROID_HOME accordingly — hardcoding the home-directory path here silently
# discarded that and the build died with "Android SDK not found" a minute in.
# `test-player-conformance.sh` already had this right; this script did not.
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
if [ ! -d "$ANDROID_HOME/ndk" ]; then
echo "❌ No NDK directory at $ANDROID_HOME/ndk" >&2
echo " Set ANDROID_HOME to your SDK location, or install the NDK." >&2
exit 1
fi
# Respect an NDK the caller has already picked (CI pins an exact revision via
# ANDROID_NDK_HOME); otherwise take whatever is installed.
export NDK_HOME="${NDK_HOME:-${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/$(ls "$ANDROID_HOME/ndk" | head -1)}}"
export ANDROID_NDK_HOME="$NDK_HOME"
echo "🤖 Building Android APK..."
echo "Android SDK: $ANDROID_HOME"
echo "NDK: $NDK_HOME"
echo ""
# Parse args: build type (debug/release) and optional --clean flag.
# By default the build is INCREMENTAL — Cargo and Vite reuse their caches.
# Pass --clean (or CLEAN=1) to wipe all caches for a from-scratch build.
#
# ABI selection: by default Tauri builds all four ABIs (arm64/arm/x86/x86_64),
# which is what a distributable universal APK needs — but for an on-device test
# it means three wasted Rust compiles. Pass --device (or ABI=aarch64) to build
# only the connected device's architecture; --abi <t> targets one explicitly.
#
# Side-by-side: the `debug` build type always installs as
# com.dtourolle.jellytau.debug ("JellyTau Debug"), so it never collides with a
# real install. `release --debug` puts a *release* build — R8-minified, exactly
# what ships — into that same slot, signed with the local debug keystore. That
# is how you validate minification (R8 stripping JNI-loaded classes has broken
# release APKs here before) without the real signing key and without
# uninstalling the app you actually use.
BUILD_TYPE="debug"
CLEAN="${CLEAN:-0}"
ABI="${ABI:-}"
SIDE_BY_SIDE="${SIDE_BY_SIDE:-0}"
next_is_abi=0
for arg in "$@"; do
if [ "$next_is_abi" = "1" ]; then
ABI="$arg"
next_is_abi=0
continue
fi
case "$arg" in
--clean) CLEAN=1 ;;
--abi) next_is_abi=1 ;;
--device) ABI="device" ;;
--debug|--side-by-side) SIDE_BY_SIDE=1 ;;
debug|release) BUILD_TYPE="$arg" ;;
esac
done
# The debug build type is side-by-side unconditionally; the flag only means
# something for a release build.
if [ "$BUILD_TYPE" = "debug" ]; then
SIDE_BY_SIDE=1
fi
# Resolve --device to the attached device's Rust target triple.
if [ "$ABI" = "device" ]; then
device_abi="$(adb shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r\n')"
case "$device_abi" in
arm64-v8a) ABI="aarch64" ;;
armeabi-v7a) ABI="armv7" ;;
x86_64) ABI="x86_64" ;;
x86) ABI="i686" ;;
*)
echo "⚠️ Could not detect device ABI (got '${device_abi:-none}') — building all targets."
ABI=""
;;
esac
[ -n "$ABI" ] && echo "🎯 Device ABI $device_abi → building only '$ABI'"
fi
TARGET_ARGS=()
if [ -n "$ABI" ]; then
TARGET_ARGS=(--target "$ABI")
fi
# Step 0: Optionally clear build caches for a fully fresh build.
if [ "$CLEAN" = "1" ]; then
echo "🧹 Clearing build caches (clean build)..."
rm -rf node_modules/.vite dist .svelte-kit .next build target src-tauri/target 2>/dev/null || true
# `bun install`, NOT `npm install`. This is a bun project (see packageManager
# in package.json) and bun.lock is the lockfile that is committed; npm
# ignores it, re-resolves the tree from package.json alone, and writes a
# package-lock.json that .gitignore then hides.
#
# That is not cosmetic. The Tauri CLI refuses to build when a plugin's Rust
# crate and npm package differ by minor version, so the JS side is pinned
# exactly to match Cargo.lock; a re-resolve is precisely how those halves
# drift apart again. A clean build must not be able to change what gets
# installed.
bun install > /dev/null 2>&1
fi
# Step 1: Sync Android source files
echo "🔄 Syncing Android sources..."
./scripts/sync-android-sources.sh
# Step 2: Build the frontend first to avoid dev server issues
echo "🎨 Building frontend..."
bun run build
# Step 2: Build Android APK
# `--apk` is a boolean flag, NOT `--apk true`.
#
# tauri-cli took a value here until 2.10; from 2.11 it is a plain flag and the
# stray `true` is parsed as a positional argument, failing with
# "error: unexpected argument 'true' found" before the build starts. Found by
# deploying to a device after the Tauri 2.9.5 -> 2.11.5 upgrade.
if [ "$BUILD_TYPE" = "release" ] && [ "$SIDE_BY_SIDE" = "1" ]; then
# A release build in the debug slot: R8 still runs, but the applicationId is
# suffixed and the debug keystore signs it (read by build.gradle.kts from
# JT_SIDE_BY_SIDE), so the real key is not needed and it replaces any other
# .debug install cleanly. Deliberately does NOT write keystore.properties.
echo "📦 Building side-by-side release APK (com.dtourolle.jellytau.debug)..."
JT_SIDE_BY_SIDE=1 bun run tauri android build --apk "${TARGET_ARGS[@]}"
elif [ "$BUILD_TYPE" = "release" ]; then
# Configure release signing from .env (single source of truth). Must run
# after sync-android-sources.sh, since gen/android is (re)generated there.
./scripts/write-keystore-properties.sh
echo "📦 Building release APK..."
bun run tauri android build --apk "${TARGET_ARGS[@]}"
else
echo "📦 Building debug APK..."
bun run tauri android build --apk --debug "${TARGET_ARGS[@]}"
fi
# The applicationId the APK actually carries — not the one build.gradle.kts asks
# for. `tauri android build` rewrites the debug `buildTypes` block in the
# generated gradle file to inject its keepDebugSymbols entries, and that rewrite
# used to drop `applicationIdSuffix` with it, silently producing a debug APK
# under the release applicationId. Installing that over a real release build
# fails with INSTALL_FAILED_UPDATE_INCOMPATIBLE, whose only obvious remedy is
# uninstalling the release app and losing its data — so this fails the build
# instead. The suffix now lives outside the rewritten block (see
# src-tauri/android/app/build.gradle.kts); this checks that it survived.
assert_application_id() {
local variant="$1" expected="$2"
local metadata="src-tauri/gen/android/app/build/outputs/apk/universal/$variant/output-metadata.json"
[ -f "$metadata" ] || return 0
local actual
actual=$(sed -n 's/.*"applicationId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata" | head -1)
if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then
echo ""
echo "❌ APK applicationId is '$actual', expected '$expected'."
echo " A build meant for the side-by-side slot came out under the"
echo " release applicationId; installing it would collide with a real"
echo " install. Check that the applicationIdSuffix at the bottom of"
echo " src-tauri/android/app/build.gradle.kts survived into"
echo " src-tauri/gen/android/app/build.gradle.kts."
exit 1
fi
}
if [ "$BUILD_TYPE" = "debug" ]; then
assert_application_id debug "com.dtourolle.jellytau.debug"
elif [ "$SIDE_BY_SIDE" = "1" ]; then
assert_application_id release "com.dtourolle.jellytau.debug"
else
assert_application_id release "com.dtourolle.jellytau"
fi
echo ""
echo "✅ APK build complete!"
echo "📱 APK location: src-tauri/gen/android/app/build/outputs/apk/"
# Containerised builds run as root against a bind-mounted tree; hand the
# artifacts back to the host user. No-op when not root. See DR-213.
"$(dirname "$0")/restore-ownership.sh"