dev-updater: build an app on the machine, install it on the phone

A Rust backend that discovers Android projects under configured roots,
builds one on request, and serves the APK over pinned TLS on a WireGuard
interface; an Android client that lists what is buildable, watches a build,
and installs the result. Enrolment carries the token and the CA, so the
phone trusts exactly the machine that issued it and nothing else.

`AGENTS.md` is the working guide and `README.md` the configuration
reference. The shared tunnel-and-TLS code lives in `vendor/wg-app-link`,
which ai-app uses too.

History before this point was squashed away, and a stale `config.json` went
with it: nothing had read that file since the config moved to RON outside
the checkout, and what it still held was one machine's absolute paths and
the names of projects on it.
This commit is contained in:
iris committed 2026-08-31 20:31:08 -04:00
commit b0e83059a3
82 files changed
+20372

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
#!/bin/sh
# Builds and runs this app on an emulator, creating/booting the AVD first if
# it isn't already up.
#
# Environment setup (SDK location, PATH, ...) lives in ./android-env.sh,
# which can also be sourced directly for one-off commands; see its header
# comment. The emulator handling below is inline rather than in its own
# shared script because this repo has exactly one app, so there's no second
# caller to share it with.
#
# Goes through classic avdmanager/emulator/adb rather than the newer
# `android` CLI's emulator subsystem: that one manages its own AVD pool and
# would start a *second* instance alongside whatever is already running.
set -eu
APP_ID="com.example.devupdater"
# One AVD per repository, named after it, which is how the other Android
# projects on this machine are set up: two sessions working in two repos
# otherwise fight over one emulator instance, and neither can tell that the
# app it just installed was replaced by the other one's.
# This app installs *other* projects' builds, so exercising it needs
# something to install -- but that something should be a throwaway app you
# control, added to the list like any other project, not another repo's real
# app sharing this emulator. AVD_NAME=... overrides the name if you do need
# a second one.
AVD_NAME="${AVD_NAME:-dev-updater}"
DEVICE_PROFILE="${DEVICE_PROFILE:-pixel_10}"
SYSTEM_IMAGE="${SYSTEM_IMAGE:-system-images;android-36;google_apis;x86_64}"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
cd "$SCRIPT_DIR"
# shellcheck source=./android-env.sh
. ./android-env.sh
# Prints the adb serial of a running instance of AVD "$1", or nothing. Unlike
# `adb -e` (which only works when exactly one emulator is attached, and
# can't tell $AVD_NAME apart from some other AVD attached separately), this
# checks by name so it can't mistake someone else's emulator for ours.
avd_serial() {
for s in $(adb devices | awk '$2 == "device" {print $1}'); do
if [ "$(adb -s "$s" emu avd name 2>/dev/null | head -n1 | tr -d '\r')" = "$1" ]; then
echo "$s"
return 0
fi
done
}
echo "==> Ensuring emulator system image is installed"
android sdk install emulator "$SYSTEM_IMAGE" || echo " (non-fatal: see above)"
if [ ! -f "$ANDROID_AVD_HOME/$AVD_NAME.ini" ]; then
echo "==> Creating AVD '$AVD_NAME' ($DEVICE_PROFILE, $SYSTEM_IMAGE)"
echo no | avdmanager create avd \
-n "$AVD_NAME" \
-k "$SYSTEM_IMAGE" \
--device "$DEVICE_PROFILE" \
--sdcard 512M
else
echo "==> Reusing existing AVD '$AVD_NAME'"
fi
# avdmanager defaults new AVDs to hw.keyboard=no, which disables forwarding
# the host keyboard into the emulator and leaves you dependent on the
# on-screen keyboard. Force it on so typing works -- this app has a
# free-text path field, so that matters more here than most.
CONFIG_INI="$ANDROID_AVD_HOME/$AVD_NAME.avd/config.ini"
if [ -f "$CONFIG_INI" ]; then
grep -v '^hw\.keyboard=' "$CONFIG_INI" >"$CONFIG_INI.tmp"
echo "hw.keyboard=yes" >>"$CONFIG_INI.tmp"
mv "$CONFIG_INI.tmp" "$CONFIG_INI"
fi
SERIAL=$(avd_serial "$AVD_NAME")
if [ -n "$SERIAL" ]; then
echo "==> Emulator '$AVD_NAME' already running ($SERIAL)"
else
# Clean up a stray/crashed process for this AVD, if any, so it doesn't
# end up with two instances fighting over the same AVD directory. The
# bracketed first character keeps the pattern from matching the shell
# running this script, which has the pattern text on its own command
# line -- unbracketed, this kills that shell mid-run.
pkill -f "[e]mulator.*-avd $AVD_NAME" >/dev/null 2>&1 || true
EMU_LOG="/tmp/$AVD_NAME-emulator.log"
: >"$EMU_LOG"
# Real GPU acceleration when a display is available; headless software
# rendering otherwise (e.g. a VM with no display attached).
if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
echo "==> Starting emulator '$AVD_NAME' with GPU acceleration (-gpu host)"
emulator -avd "$AVD_NAME" -gpu host -no-audio >"$EMU_LOG" 2>&1 &
else
echo "==> No display available (DISPLAY/WAYLAND_DISPLAY unset) -- starting" \
"emulator '$AVD_NAME' headless with software rendering (-gpu swiftshader_indirect)"
emulator -avd "$AVD_NAME" -gpu swiftshader_indirect -no-audio -no-window \
>"$EMU_LOG" 2>&1 &
fi
EMU_PID=$!
i=0
booted=""
while [ "$i" -lt 150 ]; do
if ! kill -0 "$EMU_PID" 2>/dev/null; then
echo "Emulator process exited unexpectedly. Log output:" >&2
cat "$EMU_LOG" >&2
exit 1
fi
SERIAL=$(avd_serial "$AVD_NAME")
if [ -n "$SERIAL" ]; then
booted=$(adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
[ "$booted" = "1" ] && break
fi
i=$((i + 1))
sleep 2
done
if [ "$booted" != "1" ]; then
echo "Emulator did not finish booting in time. Log output:" >&2
cat "$EMU_LOG" >&2
exit 1
fi
fi
echo "==> Building debug APK"
./gradlew :androidApp:assembleDebug
APK="androidApp/build/outputs/apk/debug/androidApp-debug.apk"
echo "==> Installing and launching $APK"
adb -s "$SERIAL" install -r "$APK"
adb -s "$SERIAL" shell am start -n "$APP_ID/.MainActivity"