diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15057121..00b48781 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ""` 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 ""` +- **Android unit tests:** `scripts/dev.bat test` (runs the sideload debug JUnit + MockK + Compose suite) - **Python tests:** `python -m unittest plugin.tests.test_` 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. diff --git a/README.md b/README.md index 7d5ae4ae..b5eb2409 100644 --- a/README.md +++ b/README.md @@ -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 |
Repository structure diff --git a/TODO.md b/TODO.md index edc85e01..5bc07d13 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f558f28d..3ca8c7d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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---` @@ -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=. + hermesDevAbi?.let { requestedAbi -> + ndk { + abiFilters += requestedAbi + } + } + // Feature flags — DEV_MODE enables all experimental features in debug builds buildConfigField("boolean", "DEV_MODE", "false") } diff --git a/docs/worktree-workflow.md b/docs/worktree-workflow.md index 5385439a..a2e3cdba 100644 --- a/docs/worktree-workflow.md +++ b/docs/worktree-workflow.md @@ -83,8 +83,15 @@ git worktree remove # 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 diff --git a/gradle.properties b/gradle.properties index 373219ea..a95db285 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/scripts/android-prepush.py b/scripts/android-prepush.py index 4278ebe1..17545d90 100644 --- a/scripts/android-prepush.py +++ b/scripts/android-prepush.py @@ -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: diff --git a/scripts/dev.bat b/scripts/dev.bat index b01edb21..d905e7e6 100644 --- a/scripts/dev.bat +++ b/scripts/dev.bat @@ -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 ^ + 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 diff --git a/scripts/dev.sh b/scripts/dev.sh index 256697b5..f88d1bca 100644 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -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 " + 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"