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.
272 lines
10 KiB
Bash
Executable File
272 lines
10 KiB
Bash
Executable File
#!/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
|