chore(android): streamline local dev loop

This commit is contained in:
Bailey Dixon
2026-08-05 19:16:36 -04:00
parent 217746a243
commit ebe1a8ebc4
9 changed files with 177 additions and 52 deletions
+32 -7
View File
@@ -17,17 +17,41 @@ That's it — no extra setup or credentials required for a debug build.
Helper scripts for common development tasks:
```bash
scripts/dev.bat build # Build debug APK
scripts/dev.bat build # Build the sideload debug APK
scripts/dev.bat compile # Compile sideload Kotlin only
scripts/dev.bat test-one "com.hermesandroid.relay.SomeTest" # Run one test class
scripts/dev.bat install-fast # Build arm64 only + install + launch
scripts/dev.bat release # Build signed release APK
scripts/dev.bat bundle # Build release AAB for Google Play
scripts/dev.bat run # Build + install + launch + logcat
scripts/dev.bat test # Run unit tests
scripts/dev.bat run # Build sideload + install + launch + logcat
scripts/dev.bat test # Run sideload debug unit tests
scripts/dev.bat version # Show current version
scripts/dev.bat relay # Start relay server (dev, no TLS)
```
Linux/macOS equivalent lives at `scripts/dev.sh`.
### Fast Android iteration
Gradle's daemon, local build cache, configuration cache, and parallel task
execution are enabled for repeat local builds. Keep the same Gradle JVM
configuration between invocations and do not add `--no-daemon` to normal dev
commands; a different heap or Java home starts a separate daemon and discards
the warm-process benefit.
Use the narrowest command that proves the change:
1. `scripts/dev.bat compile` for a Kotlin compile check.
2. `scripts/dev.bat test-one "<fully-qualified-class-or-pattern>"` for a focused regression.
3. `scripts/dev.bat install-fast` when the result must run on the connected
arm64 phone. This passes `-Phermes.devAbi=arm64-v8a`, avoiding the x86,
x86_64, and armeabi-v7a native libraries in the local APK.
4. `scripts/dev.bat prepush` before pushing Android work.
`install-fast` is intentionally phone-specific. Use `install` for a universal
sideload debug APK or when the target ABI is not arm64. Release builds remain
universal and are unaffected unless `-Phermes.devAbi` is explicitly supplied.
## Repository Structure
```
@@ -51,12 +75,12 @@ The legacy `relay_server/` directory is a thin compatibility shim around `plugin
| Component | Stack |
|-----------|-------|
| **Android App** | Kotlin 2.0, Jetpack Compose, Material 3, OkHttp |
| **Android App** | Kotlin 2.4, Jetpack Compose, Material 3, OkHttp |
| **Relay Server** | Python 3.11+, aiohttp |
| **Serialization** | kotlinx.serialization |
| **Build** | AGP 9, Gradle 8.13, JVM toolchain 17 |
| **Build** | AGP 9.3.1, Gradle 9.6.1, JVM toolchain 17 |
| **CI/CD** | GitHub Actions (lint, build, test, signed APK artifacts) |
| **Min SDK** | 26 (Android 8.0) / Target SDK 35 |
| **Min SDK** | 26 (Android 8.0) / Target SDK 36 |
## Running the Relay Locally
@@ -174,7 +198,8 @@ Release notes (`RELEASE_NOTES.md`, `app/src/main/assets/whats_new.txt`, `docs/pl
CI in one cached Gradle invocation. Run it before pushing Android PR updates
to catch common hosted failures without waiting for another full Actions
cycle; hosted CI remains the exhaustive all-variant gate.
- **Android unit tests:** `scripts/dev.bat test` (runs JUnit + MockK + Compose testing)
- **Focused Android unit test:** `scripts/dev.bat test-one "<fully-qualified-class-or-pattern>"`
- **Android unit tests:** `scripts/dev.bat test` (runs the sideload debug JUnit + MockK + Compose suite)
- **Python tests:** `python -m unittest plugin.tests.test_<name>` from the repo root with the hermes-agent venv active. `pytest` works too but the pre-existing `conftest.py` imports a module that isn't always installed — `unittest` avoids that entirely.
CI is split into path-filtered workflows: `.github/workflows/ci-android.yml` (lint + build + test on app/Gradle changes), `.github/workflows/ci-server.yml` (syntax check + focused server tests on plugin/Python changes), and `.github/workflows/ci-desktop.yml` (desktop type/build/smoke checks). They run on pushes to `main` and `dev` and on PRs targeting either when their paths are touched.
+9 -6
View File
@@ -267,11 +267,14 @@ Already installed? The same recipe is auto-loaded as a Hermes skill — invoke `
```bash
# Android: open the repo root in Android Studio, wait for Gradle sync, Run (Shift+F10).
scripts/dev.bat build # Build debug APK
scripts/dev.bat build # Build sideload debug APK
scripts/dev.bat compile # Compile sideload Kotlin only
scripts/dev.bat test-one "com.hermesandroid.relay.SomeTest" # Focused unit test
scripts/dev.bat install-fast # arm64 phone build + install + launch
scripts/dev.bat release # Build signed release APK
scripts/dev.bat bundle # Build release AAB for Google Play
scripts/dev.bat run # Build + install + launch + logcat
scripts/dev.bat test # Run unit tests
scripts/dev.bat run # Build sideload + install + launch + logcat
scripts/dev.bat test # Run sideload debug unit tests
scripts/dev.bat version # Show current version
scripts/dev.bat relay # Start the relay server (dev, no TLS)
```
@@ -280,13 +283,13 @@ scripts/dev.bat relay # Start the relay server (dev, no TLS)
| Component | Stack |
|-----------|-------|
| **Android app** | Kotlin 2.0, Jetpack Compose, Material 3, OkHttp |
| **Android app** | Kotlin 2.4, Jetpack Compose, Material 3, OkHttp |
| **Hermes-Relay CLI** | TypeScript, Bun-compiled native binary, Node ≥21 (source/dev), zero runtime deps |
| **Server / plugin** | Python 3.11+, aiohttp |
| **Serialization** | kotlinx.serialization (Android) |
| **Build** | AGP 9, Gradle 8.13, JVM toolchain 17 (Android); `tsc` + `bun build --compile` (CLI) |
| **Build** | AGP 9.3.1, Gradle 9.6.1, JVM toolchain 17 (Android); `tsc` + `bun build --compile` (CLI) |
| **CI/CD** | GitHub Actions — lint, build, test, APK artifact, CLI binaries per platform |
| **Min SDK** | 26 (Android 8.0) · Target SDK 35 |
| **Min SDK** | 26 (Android 8.0) · Target SDK 36 |
<details>
<summary><b>Repository structure</b></summary>
+16
View File
@@ -24,6 +24,22 @@ multi-session protocol before treating `lifecycle=session` as an isolation claim
---
## Split fast Android unit tests from resource and screenshot tests
The quick-loop commands now narrow execution to the sideload debug variant and
support one-class filtering, but all `:app` unit tests still share one Android
test variant. That variant includes merged Android resources, gives every test
worker a 2 GiB heap, runs on JDK 21, and enables Roborazzi recording because a
small subset of Robolectric/screenshot tests requires those settings.
Create a separate resource/screenshot test lane so pure state, parser, routing,
and formatting tests can run as ordinary JVM tests without Android resource
packaging. Keep golden-image recording explicit rather than applying it to all
unit tests, preserve a CI task that runs both lanes, and benchmark cold plus
warm focused-test latency before adopting the split.
---
## Verify Android native dashboard sign-in on device
Android now selects Custom Tab + PKCE for HTTPS gateways that advertise
+20
View File
@@ -7,6 +7,15 @@ plugins {
alias(libs.plugins.play.publisher)
}
val supportedHermesDevAbis = setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
val hermesDevAbi = providers.gradleProperty("hermes.devAbi").orNull
hermesDevAbi?.let { requestedAbi ->
require(requestedAbi in supportedHermesDevAbis) {
"Unsupported hermes.devAbi '$requestedAbi'. Expected one of: " +
supportedHermesDevAbis.sorted().joinToString()
}
}
// Rename output artifacts to include the app version. AGP respects
// `archivesName` for both APK (assemble*) and AAB (bundle*) outputs, so
// this single line produces `hermes-relay-<version>-<flavor>-<buildType>`
@@ -43,6 +52,17 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Optional local-only fast path for device iteration. Native voice/VAD
// dependencies make the universal sideload APK very large, while a
// connected phone needs only its own ABI. Release and normal debug
// builds remain universal unless the developer explicitly supplies
// -Phermes.devAbi=<abi>.
hermesDevAbi?.let { requestedAbi ->
ndk {
abiFilters += requestedAbi
}
}
// Feature flags — DEV_MODE enables all experimental features in debug builds
buildConfigField("boolean", "DEV_MODE", "false")
}
+9 -2
View File
@@ -83,8 +83,15 @@ git worktree remove <path> # delete a worktree (must be clean, or pass --force)
(`../hermes-feat-x`), not under the repo root, or it gets swept into globs and
IDE indexing.
- **Build outputs are per-folder.** That's the point (warm caches), but it also
means three worktrees ≈ three `build/` trees on disk. Prune merged worktrees so
they don't accumulate.
means three worktrees ≈ three `build/` trees on disk. Gradle's local build
cache is shared through the Gradle user home and can reuse compatible task
outputs across worktrees. Prune merged worktrees so build trees don't
accumulate.
- **Serialize heavy Gradle invocations on one host.** Worktrees isolate source
and output directories, but concurrent Android compiles, lint, tests, and APK
packaging still compete for the same CPU, memory, daemon pool, and local
cache. Keep one heavy Gradle lane active at a time; parallelism inside that
invocation remains enabled.
## How this maps to releasing
+4
View File
@@ -1,4 +1,8 @@
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
org.gradle.daemon=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=37
kotlin.code.style=official
-1
View File
@@ -88,7 +88,6 @@ def main() -> int:
str(wrapper),
"--console=plain",
"--configuration-cache",
"-Dorg.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8",
*tasks,
]
if not args.skip_tests:
+48 -18
View File
@@ -10,6 +10,9 @@ if "%1"=="release" goto release
if "%1"=="bundle" goto bundle
if "%1"=="install" goto install
if "%1"=="run" goto run
if "%1"=="compile" goto compile
if "%1"=="test-one" goto testone
if "%1"=="install-fast" goto installfast
if "%1"=="test" goto test
if "%1"=="lint" goto lint
if "%1"=="prepush" goto prepush
@@ -23,9 +26,9 @@ if "%1"=="help" goto help
goto help
:build
echo Building debug APK...
call gradlew.bat assembleDebug
echo APK: app\build\outputs\apk\debug\app-debug.apk
echo Building sideload debug APK...
call gradlew.bat :app:assembleSideloadDebug --console=plain
echo APK: app\build\outputs\apk\sideload\debug\
goto end
:release
@@ -49,33 +52,57 @@ echo Location: app\build\outputs\bundle\release\
goto end
:install
echo Building and installing to connected device...
call gradlew.bat installDebug
echo Building and installing sideload debug to connected device...
call gradlew.bat :app:installSideloadDebug --console=plain
if errorlevel 1 goto end
echo Launching app...
REM Explicit FQCN: applicationId is com.axiomlabs.hermesrelay but the
REM Explicit FQCN: the sideload applicationId includes the flavor suffix but the
REM namespace (and thus the real class FQCN) is still com.hermesandroid.relay,
REM so the `.MainActivity` shorthand no longer resolves correctly.
adb shell am start -n com.axiomlabs.hermesrelay/com.hermesandroid.relay.MainActivity
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
goto end
:run
echo Building, installing, and launching...
call gradlew.bat installDebug
REM Explicit FQCN: applicationId is com.axiomlabs.hermesrelay but the
echo Building, installing, and launching sideload debug...
call gradlew.bat :app:installSideloadDebug --console=plain
if errorlevel 1 goto end
REM Explicit FQCN: the sideload applicationId includes the flavor suffix but the
REM namespace (and thus the real class FQCN) is still com.hermesandroid.relay,
REM so the `.MainActivity` shorthand no longer resolves correctly.
adb shell am start -n com.axiomlabs.hermesrelay/com.hermesandroid.relay.MainActivity
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
adb logcat -s HermesRelay:* --format=brief
goto end
:compile
echo Compiling sideload debug Kotlin...
call gradlew.bat :app:compileSideloadDebugKotlin --console=plain
goto end
:testone
if "%~2"=="" (
echo Usage: scripts\dev.bat test-one ^<fully-qualified-test-class-or-pattern^>
exit /b 2
)
echo Running focused sideload test: %~2
call gradlew.bat :app:testSideloadDebugUnitTest --tests "%~2" --console=plain
goto end
:installfast
echo Building arm64 sideload debug and installing to connected phone...
call gradlew.bat :app:installSideloadDebug -Phermes.devAbi=arm64-v8a --console=plain
if errorlevel 1 goto end
echo Launching app...
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
goto end
:test
echo Running unit tests...
call gradlew.bat test
echo Running sideload debug unit tests...
call gradlew.bat :app:testSideloadDebugUnitTest --console=plain
goto end
:lint
echo Running lint...
call gradlew.bat lint
echo Running sideload debug lint...
call gradlew.bat :app:lintSideloadDebug --console=plain
goto end
:prepush
@@ -125,12 +152,15 @@ goto end
:help
echo Hermes-Relay Dev Scripts
echo.
echo build Build debug APK
echo build Build sideload debug APK
echo release Build signed release APK
echo bundle Build release AAB (for Google Play upload)
echo install Build + install to connected device
echo install Build universal sideload + install
echo run Build + install + launch + logcat
echo test Run unit tests
echo compile Compile sideload debug Kotlin only
echo test-one Run one test class or wildcard pattern
echo install-fast Build arm64 only + install + launch
echo test Run sideload debug unit tests
echo lint Run lint checks
echo prepush Run Android repository checks, lint, and focused CI tests
echo clean Clean build outputs
+39 -18
View File
@@ -7,35 +7,53 @@ cd "$(dirname "$0")/.."
case "${1:-help}" in
build)
echo "Building debug APK..."
./gradlew assembleDebug
echo "APK: app/build/outputs/apk/debug/app-debug.apk"
echo "Building sideload debug APK..."
./gradlew :app:assembleSideloadDebug --console=plain
echo "APK: app/build/outputs/apk/sideload/debug/"
;;
install)
echo "Building and installing to connected device..."
./gradlew installDebug
echo "Building and installing sideload debug to connected device..."
./gradlew :app:installSideloadDebug --console=plain
echo "Launching app..."
# Explicit FQCN: applicationId is com.axiomlabs.hermesrelay but the
# Explicit FQCN: the sideload applicationId includes the flavor suffix but the
# namespace (and thus the real class FQCN) is still com.hermesandroid.relay,
# so the `.MainActivity` shorthand no longer resolves correctly.
adb shell am start -n com.axiomlabs.hermesrelay/com.hermesandroid.relay.MainActivity
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
;;
run)
echo "Building, installing, and launching..."
./gradlew installDebug
# Explicit FQCN: applicationId is com.axiomlabs.hermesrelay but the
echo "Building, installing, and launching sideload debug..."
./gradlew :app:installSideloadDebug --console=plain
# Explicit FQCN: the sideload applicationId includes the flavor suffix but the
# namespace (and thus the real class FQCN) is still com.hermesandroid.relay,
# so the `.MainActivity` shorthand no longer resolves correctly.
adb shell am start -n com.axiomlabs.hermesrelay/com.hermesandroid.relay.MainActivity
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
adb logcat -s HermesRelay:* --format=brief
;;
compile)
echo "Compiling sideload debug Kotlin..."
./gradlew :app:compileSideloadDebugKotlin --console=plain
;;
test-one)
if [ -z "${2:-}" ]; then
echo "Usage: ./scripts/dev.sh test-one <fully-qualified-test-class-or-pattern>"
exit 2
fi
echo "Running focused sideload test: $2"
./gradlew :app:testSideloadDebugUnitTest --tests "$2" --console=plain
;;
install-fast)
echo "Building arm64 sideload debug and installing to connected phone..."
./gradlew :app:installSideloadDebug -Phermes.devAbi=arm64-v8a --console=plain
echo "Launching app..."
adb shell am start -n com.axiomlabs.hermesrelay.sideload/com.hermesandroid.relay.MainActivity
;;
test)
echo "Running unit tests..."
./gradlew test
echo "Running sideload debug unit tests..."
./gradlew :app:testSideloadDebugUnitTest --console=plain
;;
lint)
echo "Running lint..."
./gradlew lint
echo "Running sideload debug lint..."
./gradlew :app:lintSideloadDebug --console=plain
;;
prepush)
echo "Running Android pre-push checks..."
@@ -77,10 +95,13 @@ case "${1:-help}" in
help|*)
echo "Hermes-Relay Dev Scripts"
echo ""
echo " build Build debug APK"
echo " install Build + install to connected device"
echo " build Build sideload debug APK"
echo " install Build universal sideload + install"
echo " run Build + install + launch + logcat"
echo " test Run unit tests"
echo " compile Compile sideload debug Kotlin only"
echo " test-one Run one test class or wildcard pattern"
echo " install-fast Build arm64 only + install + launch"
echo " test Run sideload debug unit tests"
echo " lint Run lint checks"
echo " prepush Run Android repository checks, lint, and focused CI tests"
echo " clean Clean build outputs"