The phone has been running the debug build: build-apk.sh assembled it, and nothing in the app or its report said which build a frame time came from. A debuggable build runs Compose at a fraction of release speed, so the tuning so far was measured against the wrong number. On the emulator, the same fixture and gestures: measure 1.4ms mean / 14.7ms worst on debug, 0.8ms / 6.6ms on release. build-apk.sh now assembles the release variant, signed with a key it generates once under ~/.config/ai-app (beside the pinned CA, outside any checkout). The report's header names the build. The one native library is declared kept-with-symbols so packaging stops warning about an NDK the build does not need. Also: ui-sandbox.sh keep now keeps the config too. The server appends spawned sessions and enrolled tokens to it, so regenerating it left the transcripts on disk and the registry empty. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
136 lines
6.2 KiB
Bash
Executable File
136 lines
6.2 KiB
Bash
Executable File
#!/bin/sh
|
|
# Builds the app's APK, ready to install on a phone through Dev Updater.
|
|
#
|
|
# ./build-apk.sh
|
|
#
|
|
# 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.
|
|
set -eu
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# 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
|
|
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 [ ! -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
|
|
AI_APP_KEYSTORE="$KEYSTORE"
|
|
AI_APP_KEYSTORE_PASSWORD=$(cat "$KEYSTORE.password")
|
|
export AI_APP_KEYSTORE AI_APP_KEYSTORE_PASSWORD
|
|
echo "==> Signing with $KEYSTORE"
|
|
|
|
# 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 -- compileReleaseKotlin 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:assembleRelease --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:assembleRelease --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:assembleRelease --console=plain >/dev/null
|
|
else
|
|
./gradlew :androidApp:assembleRelease
|
|
fi
|
|
|
|
APK="$SCRIPT_DIR/androidApp/build/outputs/apk/release/androidApp-release.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 "If the phone still has the old debug build, uninstall that first:"
|
|
echo "it is signed with a different key, so Android will refuse the update."
|
|
echo "Then start the backend and scan the enrollment QR it prints:"
|
|
echo " ./server/target/release/ai-server --rotate-token"
|