cargo-mobile2 locates Studio by scanning XDG data dirs for a .desktop entry
whose Name is exactly "Android Studio", then runs its Exec line. A standalone
install writes that name; 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, failing with an ENOENT that
names every env var except the actual problem.
android-setup.sh now writes a hidden shim entry when no exact-name match
exists, reusing the Exec line Toolbox already generated rather than hardcoding
a path. Idempotent: a later run reports Studio as discoverable and rewrites
nothing.
269 lines
10 KiB
Bash
Executable File
269 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 -------------------
|
|
#
|
|
# Fedora's default JDK is newer than AGP/Gradle supports (a JDK 25 default fails
|
|
# the Gradle build outright), so pin JAVA_HOME to 21 (or 17) for Android work.
|
|
#
|
|
# Android Studio's bundled JetBrains Runtime is preferred: it's the same JDK
|
|
# Studio itself runs Gradle with, so it's guaranteed AGP-compatible, and it
|
|
# needs no system package install. Note we test for bin/javac specifically —
|
|
# a JRE-only or leftover-empty /usr/lib/jvm directory must not count.
|
|
|
|
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).
|
|
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
|
|
do
|
|
[ -x "$d/bin/javac" ] && { echo "$d"; return 0; }
|
|
done
|
|
# Fall back to a system JDK of an accepted major version.
|
|
for v in 21 17; do
|
|
for d in "/usr/lib/jvm/java-${v}-openjdk" /usr/lib/jvm/java-${v}-openjdk-*; do
|
|
[ -x "$d/bin/javac" ] && { echo "$d"; return 0; }
|
|
done
|
|
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 21 or 17 found (Gradle rejects the newer system default)."
|
|
err "Easiest fix: install Android Studio (its bundled JBR is used automatically),"
|
|
err "or: sudo dnf install java-21-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" # Gradle rejects the newer system default
|
|
|
|
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
|