Development
How to build, run, and test every part — and how to drive the whole loop end to end.
Prerequisites
- JDK — the Foojay toolchain resolver (
settings.gradle.kts) downloads a matching JDK automatically, so no manualJAVA_HOMEis required. Modules compile to JVM 11 bytecode. - Android SDK — for the agent. Point
local.propertiesat it (sdk.dir=…); this file is git-ignored. - An emulator — to run the agent as Device Owner: a Google APIs image with no account (covers both Device Owner and FCM — see firebase-setup.md).
- Everything is driven by the Gradle wrapper (
./gradlew); the configuration cache and build cache are enabled ingradle.properties.
Version catalog
All versions live in gradle/libs.versions.toml and are referenced via libs.* / libs.plugins.*.
| Component | Version |
|---|---|
| Kotlin | 2.4.0 |
| Android Gradle Plugin (AGP) | 9.0.1 |
| Compose Multiplatform | 1.11.1 |
| Ktor | 3.5.0 |
| kotlinx-serialization | 1.11.0 |
| kotlinx-coroutines | 1.11.0 |
| Firebase Admin SDK (server) | 9.9.0 |
| Firebase BoM (agent) | 34.14.0 |
firebase-messaging (agent) | pinned by the BoM |
WorkManager (androidx.work) | 2.10.5 |
google-services plugin | 4.4.4 |
| Foojay toolchain resolver | 1.0.0 |
| JVM target | 11 |
| Android compile / target / min SDK | 36 / 36 / 24 |
A few conventions worth knowing (more in ../CLAUDE.md):
- Ktor server artifacts use the
-jvmsuffix; client artifacts use bare module ids (Gradle metadata picks the variant). - The
kotlin.plugin.serializationplugin is applied only in:protocol(where@Serializableis declared). Consumers just need the types on the classpath. - Project dependencies use type-safe accessors (
projects.protocol, …).
Build & run
| Target | Command |
|---|---|
| Agent — build APK | ./gradlew :agent:assembleDebug |
| Agent — install to device/emulator | ./gradlew :agent:installDebug |
| Server (Netty, port 8081) | ./gradlew :server:run |
| Desktop console | ./gradlew :app:desktopApp:run |
| Desktop console — hot reload | ./gradlew :app:desktopApp:hotRun --auto |
| Web console (Wasm) | ./gradlew :app:webApp:wasmJsBrowserDevelopmentRun |
| Web console (JS) | ./gradlew :app:webApp:jsBrowserDevelopmentRun |
| iOS | open ../app/iosApp in Xcode |
The built agent APK lands at agent/build/outputs/apk/debug/agent-debug.apk.
The Gradle project lock
:server:run and the console dev-runs are long-lived tasks that hold the Gradle project lock — any other ./gradlew command blocks until they stop. So for end-to-end work, build artifacts first, then start the server, then drive everything else with adb/curl (not Gradle). Or run the server from a second checkout/terminal that doesn’t contend for the lock.
Tests
There’s no single all-targets task; run per target.
| Scope | Command |
|---|---|
| Protocol | ./gradlew :protocol:jvmTest |
| Server | ./gradlew :server:test (e.g. --tests "com.arganaemre.mdmcore.StatusTest") |
| Shared — Android host | ./gradlew :app:shared:testAndroidHostTest |
| Shared — JVM | ./gradlew :app:shared:jvmTest |
| Shared — Wasm | ./gradlew :app:shared:wasmJsTest |
| Shared — JS | ./gradlew :app:shared:jsTest |
| Shared — iOS sim | ./gradlew :app:shared:iosSimulatorArm64Test |
What the suites cover:
:protocol—ComplianceRuleTest(theisSatisfiedByrule) andProtocolSerializationTest(wire round-trips; missing optional fields fall back to defaults). These run on every common target.:server(KtortestApplication) —ApplicationTest(health),WireTest(enroll → pull → report),PolicyEditTest(PUT bumps the version and flips a device to STALE),StatusTest(all fourComplianceStatevalues; the server recomputes and ignores the agent’scompliantflag),TokenTest(202 on enrolled, 404 on unknown).
DeviceStoreis a shared in-memory singleton, so each server test enrolls and uses its own returnedDeviceIdrather than assumingdevice-1.
End-to-end recipe
Run one full enroll → enforce → report → edit → drift → re-comply cycle.
# 1) Build the agent APK FIRST, before the server grabs the project lock.
./gradlew :agent:assembleDebug
# 2) Start a fresh Google APIs emulator with no account signed in.
# 3) Start the server (holds the lock — leave it running in this terminal).
./gradlew :server:run # http://localhost:8081
# 4) In ANOTHER terminal: install the agent and make it Device Owner.
adb install -r agent/build/outputs/apk/debug/agent-debug.apk
adb shell dpm set-device-owner com.arganaemre.mdmcore/.MdmDeviceAdminReceiver
# 5) Launch the agent. It enrolls (device-1), enforces the default policy
# (cameraDisabled=true), and reports. Confirm from the host:
curl -s localhost:8081/devices # device-1 -> COMPLIANT
# 6) Change the desired policy (via curl, or the desktop console's camera toggle).
curl -s -X PUT localhost:8081/devices/device-1/policy \
-H 'Content-Type: application/json' -d '{"cameraDisabled":false}'
# Version bumps; if FCM is configured the doorbell wakes the agent now.
# 7) Watch device-1 go STALE -> COMPLIANT as the agent re-syncs.
curl -s localhost:8081/devices/device-1/status
Prefer the GUI for step 6? Run ./gradlew :app:desktopApp:run (from a checkout that isn’t holding the lock) and flip the camera switch on the device card — see console.md.
If something doesn’t behave, see troubleshooting.md.