Make the Rust client the sole app
This commit is contained in:
1 parent
a8602c1626
commit
d8bb1699a8
230 files changed
+762
-27300
No files matched your search
+100
-151
@@ -1,159 +1,108 @@
|
||||
#!/bin/sh
|
||||
# Builds the app's APK, ready to install on a phone through Dev Updater.
|
||||
# Builds the Rust cdylib with cargo-ndk, packages it with Gradle, and
|
||||
# verifies the resulting APK.
|
||||
#
|
||||
# ./build-apk.sh the release build, signed (what the phone runs)
|
||||
# ./build-apk.sh debug the debug build, for reproducing something the
|
||||
# emulator scripts would build anyway
|
||||
# ./build-apk.sh bench P0's benchmark build (own app id, "AI Sessions
|
||||
# bench" label, opens straight onto the fixture
|
||||
# session -- see docs/RUST.md's P0 box and
|
||||
# app/bench-fixture/README.md). Signed the same
|
||||
# as release; never touches the CA it pins.
|
||||
#
|
||||
# Dev Updater's `.dev-updater.ron` at the checkout root spells these out as
|
||||
# build modes, one command line each; it passes nothing else, so the word
|
||||
# here is the whole interface.
|
||||
#
|
||||
# The APK pins the CA on *this* machine ($XDG_CONFIG_HOME/ai-app/certs/ca.pem,
|
||||
# or AI_APP_CA), so build it on the machine that runs the backend: an app
|
||||
# built somewhere else trusts a CA that backend can't present, and simply
|
||||
# won't connect. Start ai-server once first if there are no certificates
|
||||
# yet -- it generates them; the build stops with that instruction if it
|
||||
# can't find one.
|
||||
#
|
||||
# Unlike ./run-android.sh, this touches no emulator: it only produces the
|
||||
# file. Installing on a real phone goes through Dev Updater, which serves
|
||||
# whatever is under this project's build directory.
|
||||
# Usage: ./build-apk.sh [debug|release] [--abi arm64-v8a|x86_64] [--features "a b c"]
|
||||
# debug/release default to debug (matches this-machine-android's "the
|
||||
# emulator stays on debug" rule -- pass `release` explicitly for a phone
|
||||
# build). --abi defaults to arm64-v8a (a phone/real device); pass
|
||||
# x86_64 for this checkout's own AVD. --features defaults to
|
||||
# "transcript-screen". Pass "transcript-screen bench" for the retained
|
||||
# benchmark app. Never force GLES for a phone or emulator build; Iris's
|
||||
# runtime selects the available hardware backend.
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
VARIANT=${1:-release}
|
||||
case "$VARIANT" in
|
||||
release) TASK=assembleRelease ;;
|
||||
debug) TASK=assembleDebug ;;
|
||||
bench) TASK=assembleBench ;;
|
||||
*)
|
||||
echo "build-apk.sh: unknown variant '$VARIANT' (release, debug, bench)" >&2
|
||||
exit 2
|
||||
;;
|
||||
BUILD_TYPE="debug"
|
||||
ABI="arm64-v8a"
|
||||
FEATURES="transcript-screen"
|
||||
case "${1:-}" in
|
||||
debug|release) BUILD_TYPE="$1"; shift ;;
|
||||
esac
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--abi) ABI="$2"; shift 2 ;;
|
||||
--features) FEATURES="$2"; shift 2 ;;
|
||||
*) echo "build-apk.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SDK_ROOT="$HOME/Android/Sdk"
|
||||
export ANDROID_HOME="$SDK_ROOT"
|
||||
export ANDROID_SDK_ROOT="$SDK_ROOT"
|
||||
NDK_DIR=$(ls -d "$SDK_ROOT"/ndk/*/ 2>/dev/null | sort -V | tail -1)
|
||||
if [ -z "$NDK_DIR" ]; then
|
||||
echo "build-apk.sh: no NDK found under $SDK_ROOT/ndk" >&2
|
||||
exit 1
|
||||
fi
|
||||
export ANDROID_NDK_HOME="$NDK_DIR"
|
||||
|
||||
# Only the ABI asked for goes into the APK. cargo ndk adds its output beside
|
||||
# whatever earlier builds left here, and Gradle packages every directory it
|
||||
# finds -- a debug x86_64 emulator build left behind made an arm64 "release"
|
||||
# 339 MB on 2026-09-06.
|
||||
rm -rf android-project/app/src/main/jniLibs
|
||||
# ...and Gradle's own copy of them, which `rm -rf jniLibs` does not reach.
|
||||
# `mergeReleaseNativeLibs` is *up to date* against its cached inputs, so a
|
||||
# build that switches ABI packages the previous ABI: an `--abi x86_64`
|
||||
# release APK containing `lib/arm64-v8a/libmain.so` installed fine and
|
||||
# aborted at startup with `Could not get adapter!: NotFound {
|
||||
# active_backends: VULKAN }` under libndk_translation -- which reads
|
||||
# exactly like the phone's own Vulkan problem and is nothing of the kind.
|
||||
# Scoped to the merge task's directory rather than all of `app/build`, so
|
||||
# an ABI change costs the native merge and not the whole Gradle build.
|
||||
rm -rf android-project/app/build/intermediates/merged_native_libs \
|
||||
android-project/app/build/intermediates/stripped_native_libs \
|
||||
android-project/app/build/intermediates/merged_jni_libs
|
||||
echo "build-apk.sh: cargo ndk -t $ABI build ${BUILD_TYPE:+(${BUILD_TYPE})} --features \"$FEATURES\""
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
cargo ndk -t "$ABI" -P 29 -o android-project/app/src/main/jniLibs/ build --lib \
|
||||
--profile android-release --no-default-features --features "$FEATURES"
|
||||
else
|
||||
cargo ndk -t "$ABI" -P 29 -o android-project/app/src/main/jniLibs/ build --lib \
|
||||
--profile android-dev --no-default-features --features "$FEATURES"
|
||||
fi
|
||||
|
||||
GRADLE_TASK="assembleDebug"
|
||||
APK_DIR="android-project/app/build/outputs/apk/debug"
|
||||
APK_NAME="app-debug.apk"
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
GRADLE_TASK="assembleRelease"
|
||||
APK_DIR="android-project/app/build/outputs/apk/release"
|
||||
APK_NAME="app-release.apk"
|
||||
export AI_APP_KEYSTORE="${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/release.jks"
|
||||
if [ ! -f "$AI_APP_KEYSTORE" ]; then
|
||||
KEYTOOL="${JAVA_HOME:+$JAVA_HOME/bin/keytool}"
|
||||
KEYTOOL="${KEYTOOL:-keytool}"
|
||||
if ! command -v "$KEYTOOL" >/dev/null 2>&1; then
|
||||
echo "build-apk.sh: no release key and no keytool to create one" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$AI_APP_KEYSTORE")"
|
||||
AI_APP_KEYSTORE_PASSWORD=$(head -c 24 /dev/urandom | base64 | tr -d '/+=')
|
||||
(umask 077 && printf '%s\n' "$AI_APP_KEYSTORE_PASSWORD" > "$AI_APP_KEYSTORE.password")
|
||||
(umask 077 && "$KEYTOOL" -genkeypair -keystore "$AI_APP_KEYSTORE" \
|
||||
-alias ai-app -keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-storepass "$AI_APP_KEYSTORE_PASSWORD" -keypass "$AI_APP_KEYSTORE_PASSWORD" \
|
||||
-dname "CN=ai-app" >/dev/null 2>&1)
|
||||
fi
|
||||
export AI_APP_KEYSTORE_PASSWORD
|
||||
AI_APP_KEYSTORE_PASSWORD=$(cat "$AI_APP_KEYSTORE.password")
|
||||
fi
|
||||
|
||||
unset AI_APP_BENCH
|
||||
case " $FEATURES " in
|
||||
*" bench "*) export AI_APP_BENCH=1 ;;
|
||||
esac
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "$SCRIPT_DIR"
|
||||
(cd android-project && gradle ":app:$GRADLE_TASK" --console=plain)
|
||||
|
||||
# Prefer an SDK this machine has already configured -- the host and the dev
|
||||
# VM don't keep it in the same place, and android-env.sh is written for the
|
||||
# VM's layout (it also installs missing packages, which isn't wanted here).
|
||||
if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}" ]; then
|
||||
echo "==> Using ANDROID_HOME=$ANDROID_HOME"
|
||||
elif [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "${ANDROID_SDK_ROOT}" ]; then
|
||||
ANDROID_HOME="$ANDROID_SDK_ROOT"
|
||||
export ANDROID_HOME
|
||||
echo "==> Using ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT"
|
||||
elif [ -d "$HOME/Android/Sdk" ]; then
|
||||
ANDROID_HOME="$HOME/Android/Sdk"
|
||||
ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export ANDROID_HOME ANDROID_SDK_ROOT
|
||||
echo "==> Using $ANDROID_HOME"
|
||||
else
|
||||
echo "No Android SDK found. Set ANDROID_HOME to it, or install one" >&2
|
||||
echo "(Android Studio's default location is ~/Android/Sdk)." >&2
|
||||
exit 1
|
||||
APK_PATH="$(pwd)/$APK_DIR/$APK_NAME"
|
||||
BUILD_TOOLS=$(ls -d "$SDK_ROOT"/build-tools/*/ | sort -V | tail -1)
|
||||
echo "--- aapt2 dump badging ---"
|
||||
"${BUILD_TOOLS}aapt2" dump badging "$APK_PATH" | head -5
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
echo "--- apksigner verify ---"
|
||||
"${BUILD_TOOLS}apksigner" verify --print-certs "$APK_PATH"
|
||||
fi
|
||||
|
||||
CA="${AI_APP_CA:-${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs/ca.pem}"
|
||||
if [ -f "$CA" ]; then
|
||||
# Printed so a wrong or stale certificate is visible here rather than
|
||||
# as a handshake failure on the phone -- compare it against the CA the
|
||||
# backend is actually presenting.
|
||||
FINGERPRINT=$(openssl x509 -in "$CA" -pubkey -noout 2>/dev/null \
|
||||
| openssl pkey -pubin -outform der 2>/dev/null \
|
||||
| openssl dgst -sha256 -binary 2>/dev/null \
|
||||
| openssl base64 2>/dev/null || echo "(openssl unavailable)")
|
||||
echo "==> Pinning the CA at $CA"
|
||||
echo " fingerprint: $FINGERPRINT"
|
||||
else
|
||||
echo "No CA certificate at $CA -- start ai-server once on this machine" >&2
|
||||
echo "(it generates them), or set AI_APP_CA. The APK embeds it at build time." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The phone runs the release build. A debuggable build runs Compose at a
|
||||
# fraction of the speed -- ART keeps the process debugger-friendly and the
|
||||
# compiler leaves its inspection hooks in -- so a frame time measured on one
|
||||
# says little about the app; that cost a day of tuning against the wrong
|
||||
# number. A release build must be signed, and the key is what the phone
|
||||
# recognises the app by, so it lives beside the CA, outside any checkout,
|
||||
# and is generated once here. Switching from an installed debug build means
|
||||
# uninstalling it first: the signatures differ, and Android refuses to
|
||||
# update across them.
|
||||
KEYSTORE="${AI_APP_KEYSTORE:-${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/release.jks}"
|
||||
if { [ "$VARIANT" = release ] || [ "$VARIANT" = bench ]; } && [ ! -f "$KEYSTORE" ]; then
|
||||
KEYTOOL="${JAVA_HOME:+$JAVA_HOME/bin/keytool}"
|
||||
KEYTOOL="${KEYTOOL:-keytool}"
|
||||
if ! command -v "$KEYTOOL" >/dev/null 2>&1; then
|
||||
echo "No signing key at $KEYSTORE and no keytool to make one -- set" >&2
|
||||
echo "JAVA_HOME to the JDK Gradle uses, or AI_APP_KEYSTORE to an existing key." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> No signing key at $KEYSTORE -- generating one"
|
||||
mkdir -p "$(dirname "$KEYSTORE")"
|
||||
PASSWORD=$(head -c 24 /dev/urandom | base64 | tr -d '/+=')
|
||||
(umask 077 && printf '%s\n' "$PASSWORD" > "$KEYSTORE.password")
|
||||
(umask 077 && "$KEYTOOL" -genkeypair -keystore "$KEYSTORE" -alias ai-app \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-storepass "$PASSWORD" -keypass "$PASSWORD" -dname "CN=ai-app" >/dev/null 2>&1)
|
||||
fi
|
||||
if [ "$VARIANT" = release ] || [ "$VARIANT" = bench ]; then
|
||||
AI_APP_KEYSTORE="$KEYSTORE"
|
||||
AI_APP_KEYSTORE_PASSWORD=$(cat "$KEYSTORE.password")
|
||||
export AI_APP_KEYSTORE AI_APP_KEYSTORE_PASSWORD
|
||||
echo "==> Signing with $KEYSTORE"
|
||||
fi
|
||||
|
||||
# Dev Updater draws a real progress bar from "@@progress done/total" lines,
|
||||
# and ignores anything that isn't exactly that shape. Gradle can't be asked
|
||||
# for this directly: an init script using taskGraph.afterTask is rejected
|
||||
# outright by the configuration cache, and whenReady never fires on a cache
|
||||
# hit. --dry-run costs about a second, is cache-friendly, and prints one
|
||||
# ":task SKIPPED" line per task the real build will run, which is exactly
|
||||
# the total. The build then prints one "> Task :x" line per task as it
|
||||
# goes, so counting those against it is the whole mechanism.
|
||||
#
|
||||
# Task count is not time -- the Kotlin compile and dexBuilder are most of
|
||||
# the wall clock -- so the bar moves unevenly. It is still counted work
|
||||
# rather than a guess at how long last time took.
|
||||
TASKS=$(./gradlew :androidApp:$TASK --dry-run --console=plain 2>/dev/null \
|
||||
| grep -c '^:[A-Za-z:]* SKIPPED' || true)
|
||||
|
||||
echo "==> Building"
|
||||
if [ "${TASKS:-0}" -gt 0 ]; then
|
||||
echo "@@progress 0/$TASKS"
|
||||
DONE=0
|
||||
./gradlew :androidApp:$TASK --console=plain 2>&1 | while IFS= read -r line; do
|
||||
echo "$line"
|
||||
case "$line" in
|
||||
"> Task "*)
|
||||
DONE=$((DONE + 1))
|
||||
echo "@@progress $DONE/$TASKS"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# The pipeline's exit status is the shell's, not gradle's, so ask
|
||||
# gradle again rather than reporting a failed build as a success. It is
|
||||
# up to date by now, so this is a second or two.
|
||||
./gradlew :androidApp:$TASK --console=plain >/dev/null
|
||||
else
|
||||
./gradlew :androidApp:$TASK
|
||||
fi
|
||||
|
||||
APK="$SCRIPT_DIR/androidApp/build/outputs/apk/$VARIANT/androidApp-$VARIANT.apk"
|
||||
echo
|
||||
echo "==> Built $APK"
|
||||
[ -f "$APK" ] && ls -lh "$APK" | awk '{print " " $5}'
|
||||
echo
|
||||
echo "To get it onto the phone: add this project to Dev Updater (or hit"
|
||||
echo "Update on it if it's already there) and install from there."
|
||||
echo "Debug and release are signed with different keys, so switching from"
|
||||
echo "one to the other means uninstalling the installed one first."
|
||||
echo "Then start the backend and scan the enrollment QR it prints:"
|
||||
echo " ./server/target/release/ai-server --rotate-token"
|
||||
echo "$APK_PATH"
|
||||
Reference in new issue
Block a user