Files
mtgc/scripts/android-setup.sh
Garrett Mills 1641d9aa47 Android: build on JDK 25 with Gradle 9.6.1, AGP 9.4.1, Kotlin 2.2.10
Android Studio now bundles JBR 25, which Gradle 8.14's embedded Kotlin
can't parse. Follow Tauri's current template: Gradle 9.6.1, Kotlin 2.2.10,
the builtInKotlin/newDsl/failOnMissingFiles compatibility flags, the
compilerOptions DSL, and ExecOperations in BuildTask (project.exec is gone
in Gradle 9). AGP goes to 9.4.1 rather than the template's 9.3.1.

Release lint vital is turned off: AGP 9.3/9.4's lint crashes analysing the
.gradle.kts scripts (findFirCompiledSymbol), failing every release build.

The env scripts now accept JDK 17 through 25, preferring Studio's JBR.
2026-10-16 22:39:36 -05:00

272 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# android-setup.sh — one-time Android build environment setup for mtgc.
#
# Installs the pieces `cargo tauri android` needs that Android Studio does NOT
# install by default: the SDK command-line tools, the NDK (required to
# cross-compile the Rust core — see "Why the NDK is mandatory" below), and the
# Rust Android targets.
#
# Assumes the Android SDK itself already exists (Android Studio's first-run
# wizard creates it at ~/Android/Sdk). Set ANDROID_HOME to override.
#
# Safe to re-run: every step is skipped if already satisfied.
#
# Usage:
# ./scripts/android-setup.sh
# ANDROID_HOME=/opt/android-sdk ./scripts/android-setup.sh
# NDK_VERSION=27.2.12479018 ./scripts/android-setup.sh # pin instead of latest
#
# Why the NDK is mandatory (not just nice to have): mtgc-core builds rusqlite
# with the `bundled` feature, which compiles the SQLite C amalgamation, and
# rustls pulls in `ring` (C/asm). Both need a C cross-toolchain. The bundled
# SQLite can't be swapped for Android's system libsqlite3 either — the card
# schema uses FTS5 with the `trigram` tokenizer, which platform SQLite on many
# Android versions lacks entirely.
#
set -euo pipefail
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; }
ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
# Bump if Google publishes a newer bundle; the URL pattern is stable.
CMDLINE_TOOLS_BUILD="${CMDLINE_TOOLS_BUILD:-13114758}"
CMDLINE_TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_BUILD}_latest.zip"
# Pinned to a known-good *stable* NDK on purpose. sdkmanager also publishes beta
# and canary channels whose version numbers sort higher (e.g. 30.0.15729638 is
# r30-beta2), so "install the newest" silently lands you on a prerelease
# toolchain — not what you want compiling SQLite and ring. Override with
# NDK_VERSION=... if you need a different one.
DEFAULT_NDK_VERSION="28.2.13676358"
RUST_TARGETS=(
aarch64-linux-android # 64-bit ARM — virtually all real devices
armv7-linux-androideabi # 32-bit ARM — older devices
i686-linux-android # 32-bit x86 emulator
x86_64-linux-android # 64-bit x86 emulator (the fast one on a desktop)
)
# --- 1. Prerequisites ------------------------------------------------------
if ! command -v rustc >/dev/null 2>&1; then
err "Rust not found. Install from https://rustup.rs and re-run."
exit 1
fi
info "Rust: $(rustc --version)"
if cargo tauri --version >/dev/null 2>&1; then
info "Tauri CLI: $(cargo tauri --version 2>&1 | head -1)"
else
err "Tauri CLI not found. Run ./scripts/devel-setup.sh first."
exit 1
fi
# --- 2. A JDK the Android Gradle Plugin actually accepts -------------------
#
# Gradle 9.6 / AGP 9 run on JDK 17 through 25 (25 is the newest tested; an
# older Gradle choked on 25, so an untested newer JDK is skipped, not trusted).
#
# Android Studio's bundled JetBrains Runtime is preferred, then the newest
# system JDK in range. Every candidate's major version is checked (from its
# `release` file) rather than assumed. Note we test for bin/javac specifically —
# a JRE-only or leftover-empty /usr/lib/jvm directory must not count.
# Succeeds if $1 is a JDK of a version Gradle accepts (17 through 25).
jdk_ok() {
[ -x "$1/bin/javac" ] || return 1
local v
v="$(sed -n 's/^JAVA_VERSION="\([0-9]*\).*/\1/p' "$1/release" 2>/dev/null)"
[ -n "$v" ] && [ "$v" -ge 17 ] && [ "$v" -le 25 ]
}
find_jdk() {
# Explicit override wins.
if [ -n "${ANDROID_JAVA_HOME:-}" ] && [ -x "${ANDROID_JAVA_HOME}/bin/javac" ]; then
echo "$ANDROID_JAVA_HOME"; return 0
fi
# Android Studio's bundled JBR (Toolbox or standalone install locations),
# then the newest system JDK (Fedora or Adoptium packages) in range.
for d in \
"$HOME/.local/share/JetBrains/Toolbox/apps/android-studio/jbr" \
"$HOME/.local/share/JetBrains/Toolbox/apps/android-studio"/*/jbr \
"$HOME/android-studio/jbr" \
/opt/android-studio/jbr \
$(ls -d /usr/lib/jvm/* 2>/dev/null | sort -rV)
do
jdk_ok "$d" && { echo "$d"; return 0; }
done
return 1
}
if JDK_PATH="$(find_jdk)"; then
export JAVA_HOME="$JDK_PATH"
info "Android JDK: $JAVA_HOME ($("$JAVA_HOME/bin/java" -version 2>&1 | head -1))"
else
err "No JDK 17 through 25 found."
err "Fix: sudo dnf install java-25-openjdk-devel"
err "or: ANDROID_JAVA_HOME=/path/to/jdk ./scripts/android-setup.sh"
exit 1
fi
# --- 3. Android SDK --------------------------------------------------------
if [ ! -d "$ANDROID_HOME" ]; then
err "No Android SDK at $ANDROID_HOME"
err "Install Android Studio and run it once (its setup wizard downloads the SDK),"
err "or set ANDROID_HOME to an existing SDK."
exit 1
fi
info "Android SDK: $ANDROID_HOME"
# --- 4. SDK command-line tools (provides sdkmanager) -----------------------
SDKMANAGER="$(find "$ANDROID_HOME/cmdline-tools" -name sdkmanager -type f 2>/dev/null | head -1 || true)"
if [ -z "$SDKMANAGER" ]; then
info "cmdline-tools not present — installing (build $CMDLINE_TOOLS_BUILD)…"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fL --progress-bar -o "$tmp/cmdline-tools.zip" "$CMDLINE_TOOLS_URL"
unzip -q "$tmp/cmdline-tools.zip" -d "$tmp"
# sdkmanager expects to live at cmdline-tools/latest/, not cmdline-tools/.
mkdir -p "$ANDROID_HOME/cmdline-tools"
rm -rf "$ANDROID_HOME/cmdline-tools/latest"
mv "$tmp/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
fi
info "sdkmanager: $SDKMANAGER"
sdk() { "$SDKMANAGER" --sdk_root="$ANDROID_HOME" "$@"; }
# Accept licenses up front so the installs below don't block on a prompt.
info "Accepting SDK licenses…"
yes 2>/dev/null | sdk --licenses >/dev/null || true
# --- 5. NDK ----------------------------------------------------------------
NDK_VERSION="${NDK_VERSION:-$DEFAULT_NDK_VERSION}"
NDK_DIR="$ANDROID_HOME/ndk/$NDK_VERSION"
if [ -d "$NDK_DIR" ]; then
info "NDK already installed: $NDK_DIR"
else
info "Installing NDK $NDK_VERSION (large download)…"
sdk --install "ndk;$NDK_VERSION"
fi
[ -d "$NDK_DIR" ] || { err "NDK install did not produce $NDK_DIR"; exit 1; }
# Surface the actual release name so a prerelease can't slip by unnoticed.
NDK_RELEASE="$(sed -n 's/^Pkg.Revision *= *//p' "$NDK_DIR/source.properties" 2>/dev/null)"
info "NDK: $NDK_DIR (revision ${NDK_RELEASE:-unknown})"
case "$NDK_RELEASE" in
*beta*|*rc*|*canary*) warn "This NDK is a prerelease — pin a stable NDK_VERSION if builds misbehave." ;;
esac
# --- 6. Rust Android targets ----------------------------------------------
installed="$(rustup target list --installed)"
for t in "${RUST_TARGETS[@]}"; do
if grep -qx "$t" <<<"$installed"; then
info "rust target present: $t"
else
info "adding rust target: $t"
rustup target add "$t"
fi
done
# --- 7. Make Android Studio findable by its canonical XDG name -------------
#
# `cargo tauri android dev --open` (cargo-mobile2) locates Studio by scanning
# XDG data dirs for a .desktop entry whose Name is *exactly* "Android Studio",
# then running its Exec line. A standalone install writes that name, but
# JetBrains Toolbox writes a versioned one ("Android Studio Quail 2 2026.1.2
# Patch 1"), so the lookup misses and it falls back to running "Android Studio"
# as a literal command — which fails with a confusing ENOENT. Drop in a hidden
# shim entry when no exact match exists.
xdg_app_dirs() {
echo "${XDG_DATA_HOME:-$HOME/.local/share}/applications"
local IFS=:
for d in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do echo "$d/applications"; done
}
has_studio_entry() {
local d f
while read -r d; do
[ -d "$d" ] || continue
for f in "$d"/*.desktop; do
[ -f "$f" ] || continue
# Name= must match exactly, and only within the [Desktop Entry] section.
if grep -qx 'Name=Android Studio' "$f"; then return 0; fi
done
done < <(xdg_app_dirs)
return 1
}
find_studio_exec() {
# Prefer an Exec line already written by Toolbox (it tracks the real path);
# otherwise fall back to well-known install locations.
local f exec_line d
for f in "${XDG_DATA_HOME:-$HOME/.local/share}"/applications/jetbrains-studio-*.desktop; do
[ -f "$f" ] || continue
exec_line="$(sed -n 's/^Exec=//p' "$f" | head -1)"
[ -n "$exec_line" ] && { echo "$exec_line"; return 0; }
done
for d in \
"$HOME/.local/share/JetBrains/Toolbox/apps/android-studio/bin/studio" \
"$HOME/android-studio/bin/studio.sh" \
/opt/android-studio/bin/studio.sh
do
[ -x "$d" ] && { echo "\"$d\" %u"; return 0; }
done
return 1
}
if has_studio_entry; then
info "Android Studio is discoverable by name (--open will work)"
elif STUDIO_EXEC="$(find_studio_exec)"; then
APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
mkdir -p "$APPS"
cat > "$APPS/android-studio.desktop" <<DESKTOP
[Desktop Entry]
# Shim so tools that look up Android Studio by its canonical XDG name can find
# a JetBrains Toolbox install, which names its entry with the release version.
# Written by mtgc's scripts/android-setup.sh. Safe to delete.
Type=Application
Name=Android Studio
Exec=$STUDIO_EXEC
Categories=Development;IDE;
Terminal=false
StartupWMClass=jetbrains-studio
NoDisplay=true
DESKTOP
info "wrote $APPS/android-studio.desktop so 'tauri android dev --open' can find Studio"
else
warn "Android Studio not found — 'cargo tauri android dev --open' won't be able to launch it."
warn "Run 'cargo tauri android dev' and open src-tauri/gen/android in Studio yourself."
fi
# --- 8. The environment the build needs ------------------------------------
cat <<EOF
$(info "Setup complete.")
Add these to your shell profile (or export them before building):
export ANDROID_HOME="$ANDROID_HOME"
export NDK_HOME="$NDK_DIR"
export JAVA_HOME="$JAVA_HOME" # a JDK 17–25 that Gradle accepts
Then, from the project root:
cargo tauri android init # once — scaffolds src-tauri/gen/android
cargo tauri android dev # run on an emulator or adb-connected device
cargo tauri android build # build APK/AAB
Devices/emulators: $ANDROID_HOME/platform-tools/adb devices
EOF