Files
mtgc/scripts/android-setup.sh
Garrett Mills d3154738d2 Add Android support via Tauri
The Rust core was already portable (caller-provided db paths, rustls rather
than openssl) and the shell already had the mobile entry point, so this is
mostly toolchain, provisioning, and platform-gating.

Toolchain:
- scripts/android-setup.sh installs what Android Studio doesn't: SDK
  cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is
  pinned rather than "latest" because sdkmanager's newest is often a beta
  (30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers
  Android Studio's bundled JDK 21, since Gradle rejects newer JDKs.
- Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is
  hand-edited and Android permissions have no tauri.conf.json equivalent.
- vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server.

Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a
phone. Add import_reference_db to adopt a desktop-built file instead, validated
by db::inspect_reference_db first so picking the wrong file is a clean error
rather than a broken install. Installs via copy-then-rename so an interrupted
copy can't leave a partial db.

Ingest hardening (helps desktop too):
- Give ureq connect/read timeouts. Cancellation is only polled per parsed
  record, so a stalled socket previously hung forever with no way to cancel.
- Drop the remove_file before rename; POSIX rename already replaces atomically,
  and the delete-first window could leave no reference.db at all if the process
  was killed -- much likelier on Android.

UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path
equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile)
flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard
doesn't break the shell.

Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds
(bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a
device.
2026-08-02 13:16:34 -05:00

197 lines
7.6 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. 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