Files
mantra-kmp/docs/jvm-target.md

402 lines
19 KiB
Markdown
Raw Normal View History

docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
# Bringing up the JVM target
What it would actually take to build Mantra for desktop, phased, with the
blocking work separated from the mechanical work.
The headline is not what you would expect. The four-deep native chain — secp256k1
→ bitcoin-kmp → lightning-kmp → lightning-kmp-app — is **already building for
JVM**, and has been all along. The thing standing in the way is an empty source
set in our own phoenix fork.
One scoping note before anything else: **none of this is needed to test the
DAOs.** Host-speed Room tests run under `androidUnitTest` today, given one extra
dependency. The JVM target is a product decision — a desktop Mantra — not a
testing prerequisite. See [Room DAO tests](#appendix-room-dao-tests-do-not-need-this)
at the end.
## What is already done for you
**The native chain is already JVM.** This is the expensive part, and it is
finished. The comment at [composeApp/build.gradle.kts:50](../composeApp/build.gradle.kts)
records why: `lightning-kmp-core` publishes no android variant, so our android
target resolves it to the **jvm** one, which in turn pulls
`secp256k1-kmp-jni-jvm` — desktop `.so`/`.dylib`/`.dll` files. That is why the
build has to name `secp256k1-kmp-jni-android` by hand.
Read that the other way round and it is good news: every JVM artifact in the
chain is already compiled from source, by the composite build, on every android
build we do. Turning on a JVM target adds no cinterop, no C compilation, and no
new native constraints.
**The third-party dependencies all have JVM variants.** Verified against the
repositories the build actually resolves from:
| dependency | JVM artifact | status |
|---|---|---|
| quartz 1.14.0 | `com.vitorpamplona.quartz:quartz-jvm` | on Maven Central |
| room3 3.0.1 | `androidx.room3:room3-runtime-jvm` | on Google Maven |
| sqlite 2.7.0 | `androidx.sqlite:sqlite-bundled-jvm` | on Google Maven |
| sqldelight 2.3.2 | `app.cash.sqldelight:jdbc-driver`, `:sqlite-driver` | on Maven Central |
**`commonMain` is clean.** No `android.*` and no `java.*` imports anywhere in it.
The three NFC files in `androidMain` are not referenced from common code either,
so there is nothing to stub out and no android-only API to route around. The
shared tree will compile for JVM as-is.
## What actually blocks it
`lightning-kmp-app/library` declares 25 `expect` symbols in `commonMain` and
implements them across 35 files in `androidMain`. Its `jvmMain` contains exactly
one file:
```kotlin
// lightning-kmp-app/library/src/jvmMain/kotlin/fibiprops.jvm.kt
package io.github.kotlin.fibonacci
actual val firstElement: Int = 2
actual val secondElement: Int = 3
```
That is the Kotlin multiplatform library template's Fibonacci boilerplate, left
over from whenever the module was scaffolded. It implements two of the 25
expects, and both of them are the template's own.
So **23 JVM actuals are missing**, one level down from us, and
`lightning-kmp-app/library/build.gradle.kts:18` has `jvm()` commented out because
of it. Mantra cannot declare `jvm()` — commented out in turn at
[composeApp/build.gradle.kts:46](../composeApp/build.gradle.kts) — until phoenix
does.
Everything below is ordered by that dependency.
---
## Phase 0 — build configuration
**~half a day. No blockers.**
Nothing here needs a decision; it is the groundwork the later phases assume.
1. **Delete the stale `jvmMain` tree.** Six files under
`composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/` survive from the old Aux
project. They are an orphan source set that nothing currently compiles, which
is why they have gone unnoticed — they use the wrong package, import
`androidx.room` (Room 2), and reference a long-gone `AuxDatabase`. The moment
`jvm()` is declared they become compile errors.
Keep them open in a scratch buffer while doing Phase 4: five of them are a
usable skeleton for the actuals we still need.
2. **Add the missing catalog entries** to `gradle/libs.versions.toml`:
```toml
androidx-sqlite-bundled-jvm = { module = "androidx.sqlite:sqlite-bundled-jvm", version.ref = "sqlite" }
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
```
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
Only these two, and only because neither can be reached any other way.
`sqlite-bundled-jvm` has to be named explicitly because variant-aware
resolution hands the *android* artifact to anything running on the host —
that is the whole trap described in the appendix. `sqlite-driver` is the jvm
counterpart to the `android-driver` and `native-driver` entries already here.
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
No `room3-runtime-jvm` entry: once `jvm()` exists, `commonMain`'s existing
`androidx-room3-runtime` resolves to the `-jvm` variant on its own. And the
SQLDelight drivers Phase 2 needs belong in **lightning-kmp-app's own
catalog**, not this one — that is a separate gradle build with a separate
version catalog, and putting them here would not make them visible there.
3. **Do not wire `kspJvm` yet.** It cannot be done at this point, and the reason
is already written down a few lines above it in the build file:
> These configurations only exist when the ios targets are declared, which
> the kotlin block above does only on a mac.
The same rule governs `kspJvm``dependencies { add("kspJvm", ...) }` throws
`UnknownConfigurationException` until a `jvm()` target creates that
configuration. So uncommenting [composeApp/build.gradle.kts:194](../composeApp/build.gradle.kts)
belongs in **Phase 4**, in the same edit that turns the target on, not here.
4. **Leave `jvm()` commented out**, in both builds. It goes on at the start of
Phase 4, once there is something for it to resolve against. Turning it on
earlier just means living with a broken build through Phases 13.
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
**Verification:** `./gradlew :composeApp:compileDebugKotlinAndroid` still passes.
This phase changes nothing observable; the point is that it changes nothing
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
observable — a deleted orphan source set and two unreferenced catalog entries
cannot alter a build.
**If you are working in a git worktree, no gradle task will run at all** until
the submodule is checked out there. Worktrees do not get submodules
automatically, so `lightning-kmp-app/` is empty and the composite build fails
during configuration:
```
Project with path ':library' not found in build ':lightning-kmp-app'
```
`git submodule update --init --recursive` fixes it, but note that a linked
worktree shares `.git/modules/` with the main checkout, so both trees end up
sharing one submodule git dir. That is fine while both want the same commit —
check with `git submodule status` in each — and worth being careful about when
they do not.
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
---
## Phase 1 — phoenix: the mechanical actuals
**~12 days. Blocked by nothing. Do this first.**
Fourteen of the 23. None of them requires a decision — each is either a direct
copy of the android implementation or a handful of lines of JVM file handling.
**`DbHooks.jvm.kt` — six functions, and this one is free.** The android
implementation is 15 lines and every function is an empty body; the hooks only do
work on Apple platforms, where they drive CloudKit sync. Copy the file, change
the suffix.
```kotlin
actual fun didSaveWalletPayment(id: UUID, database: PaymentsDatabase) {}
actual fun didDeleteWalletPayment(id: UUID, database: PaymentsDatabase) {}
actual fun didUpdateWalletPaymentMetadata(id: UUID, database: PaymentsDatabase) {}
actual fun didSaveContact(contactId: UUID, database: PaymentsDatabase) {}
actual fun didDeleteContact(contactId: UUID, database: PaymentsDatabase) {}
actual fun makeCloudKitDb(appDb: SqliteAppDb, paymentsDb: SqlitePaymentsDb): CloudKitInterface? = null
```
**`PlatformContext.jvm.kt` — the class plus four directory paths.** On android
these come off a `Context`; on desktop there is no context object, so
`PlatformContext` becomes either an empty class or one holding an explicit root
directory. Prefer the latter — it makes tests and multi-profile desktop installs
possible later, and it costs nothing now.
The four paths (`getApplicationFilesDirectoryPath`,
`getDatabaseFilesDirectoryPath`, `getApplicationCacheDirectoryPath`,
`getTemporaryDirectoryPath`) should resolve to a per-OS application data
directory, not `java.io.tmpdir`. The old Aux code used tmpdir and left a `TODO`
about it; do not inherit that.
**The remaining singles:** `platformElectrumRegtestConf`, `AppVersion`,
`phoenixLogWriters` (a console writer is fine), `computePreferencePath`, and the
two Fibonacci template properties — which should be deleted along with the
template file rather than reimplemented, assuming nothing references them.
**Verification:** still nothing compiles for JVM at this point, because Phases 2
and 3 are outstanding. Work against the expect list, not the compiler.
---
## Phase 2 — phoenix: drivers and connectivity
**~23 days. Blocked by nothing, but do it after Phase 1.**
Three of the 23 are database drivers and one is the network monitor. These are
real implementations, but they are bounded — the shape is known and the failure
modes are ordinary.
**`DbFactory.jvm.kt``createChannelsDbDriver`, `createPaymentsDbDriver`,
`createAppDbDriver`.** Use SQLDelight's `sqlite-driver` (the JDBC driver
specialised for SQLite). The one real difference from android: the android driver
creates and migrates the schema for you via `AndroidSqliteDriver`'s callback, and
the JDBC driver does not. You call `Schema.create(driver)` and the migration path
explicitly, and you have to track the applied version yourself. Budget for that,
not for the driver construction.
`createPaymentsDbDriver` also takes an `onError: (String) -> Unit` — make sure
corruption and migration failures actually reach it rather than throwing past it,
because on android that callback is what surfaces the problem to the user.
**`NetworkMonitor.jvm.kt`.** The android implementation is 90 lines built on
`ConnectivityManager` and its `NetworkCallback` — genuine push notification of
connectivity changes. The JVM has no equivalent. The options are a polling
reachability check, or treating the connection as always-available and letting
the lightning stack's own reconnect logic handle reality.
Start with polling on a slow interval. It is worse than the android behaviour and
that is acceptable — the alternative is pretending the network never changes,
which produces confusing UI on a laptop that gets closed and reopened.
**Verification:** none of this is exercisable until Phase 4. Write the SQLDelight
schema-creation path against a scratch `main()` if you want feedback sooner.
---
## Phase 3 — phoenix: key storage
**A decision, not a port. Unbounded until the decision is made.**
Four of the 23: `keyStoreEncryption`, `keyStoreDecryption`, and the two inline
wrappers `gracefulSingleSeedDecryption` and `gracefulMultiSeedDecryption` that
translate keystore exceptions into a `DecryptSeedResult`.
The android implementation delegates to `KeystoreHelper.kt` — 116 lines against
`AndroidKeyStore`, with `KeyGenParameterSpec`, and `setIsStrongBoxBacked(true)`
attempted first and fallen back from when the device has no secure element. The
key material never leaves hardware.
**Desktop JVM has no equivalent.** There is no portable, hardware-backed keystore
on the JVM. The realistic options:
| approach | protects against | cost |
|---|---|---|
| passphrase-derived KEK (Argon2id → AES-GCM) | disk theft, if the passphrase is strong | low; but prompts the user on every launch |
| OS keychain via JNA (Keychain / DPAPI / libsecret) | other users on the machine, at rest | three separate platform integrations, three failure modes |
| JCEKS/PKCS12 file with a fixed key | nothing meaningful | low, and misleading |
This is wallet seed material. The third option is not a stopgap, it is a
liability, and it interacts directly with the plaintext-key finding already open
against this codebase — do not let a desktop build quietly become the weakest
place the seed lives.
**Recommendation for sequencing:** implement the passphrase-derived KEK, mark the
desktop build clearly as unsuitable for real funds, and treat OS-keychain
integration as its own piece of work with its own review. That unblocks Phases 4
and 5 without pretending the security question is answered.
**This phase is the only one in the plan with no honest day estimate**, because
the estimate is a function of which row of that table gets chosen and how much
review it attracts.
---
## Phase 4 — mantra's own actuals
**~12 days. Blocked by Phases 13.**
Now turn on `jvm()` — [composeApp/build.gradle.kts:46](../composeApp/build.gradle.kts)
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
and `lightning-kmp-app/library/build.gradle.kts:18` — and, in the same edit,
uncomment `kspJvm` at [composeApp/build.gradle.kts:194](../composeApp/build.gradle.kts).
Those two go together: the KSP configuration does not exist until the target
does, which is why Phase 0 deliberately left it alone. Then let the compiler
drive.
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
Mantra declares 16 expects across 8 files. They split cleanly:
**Six platform basics.** `getPlatform` ([Platform.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/Platform.kt)),
`PlatformContext`, `AppVersion`, `themeColorScheme`
([Theme.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/ui/theme/Theme.kt)),
and `PlatformDatabaseBuilder`'s two functions. The deleted Aux files from Phase 0
are a working skeleton for five of these — repackage to `press.mantra.compose`,
update Room 2 → Room 3 (`androidx.room``androidx.room3`), and point at
`MantraDatabase` instead of `AuxDatabase`.
`MantraDatabaseConstructor` needs no hand-written actual; Room's KSP generates it
build: phase 0 of the jvm target -- clear the ground, correct the plan First phase of docs/jvm-target.md. Nothing here turns the target on; it removes what would break the moment it is turned on, and stages the two catalog entries that cannot be derived automatically. Two of the four steps as written in the doc turned out to be wrong, and implementing them is how that surfaced -- both are corrected in the doc in this commit. **Deleted the stale jvmMain tree.** Six files under composeApp/src/jvmMain/kotlin/ac/cord/auxiliary/ survived from the Aux project this codebase grew out of. They have gone unnoticed because `jvmMain` is currently an orphan source set -- the accessor creates it, no target compiles it -- so the wrong package, the Room 2 imports (androidx.room, not androidx.room3), and the references to a long-gone AuxDatabase and AuxGlobal have never had to resolve. They would all become compile errors in phase 4. They are not lost: they are the closest thing to a skeleton for five of the six platform actuals phase 4 needs, and main.kt is a reasonable starting shape for the phase 5 desktop entry point. `git show HEAD~1` has them. **Added two catalog entries, not four.** sqlite-bundled-jvm and sqldelight-sqlite-driver. Both earn their place by being unreachable otherwise: sqlite-bundled-jvm has to be named explicitly because variant-aware resolution hands the *android* artifact to anything running on the host, and sqldelight-sqlite-driver is the jvm counterpart to the android-driver and native-driver entries already there. The doc also listed room3-runtime-jvm and sqldelight-jdbc-driver. Neither is right. Once jvm() exists, commonMain's existing androidx-room3-runtime resolves to the -jvm variant on its own, so an explicit entry is redundant and would drift. And the SQLDelight drivers phase 2 needs are for DbFactory, which lives in lightning-kmp-app -- a separate gradle build with its own version catalog, where an entry here is simply not visible. **kspJvm cannot be wired yet, and the build file already said so.** The doc's phase 0 told you to uncomment composeApp/build.gradle.kts:194. It contradicted its own phase 4, which is where jvm() gets turned on. The comment three lines above it states the rule: These configurations only exist when the ios targets are declared, which the kotlin block above does only on a mac. The same holds for kspJvm -- `dependencies { add("kspJvm", ...) }` throws UnknownConfigurationException until a jvm() target creates the configuration. So it moves into phase 4, into the same edit that declares the target. composeApp/build.gradle.kts is deliberately untouched by this commit. **Also documented: gradle does not run in a worktree here at all** until the submodule is checked out, which worktrees do not do automatically. `lightning-kmp-app/` is empty and configuration fails with "Project with path ':library' not found in build ':lightning-kmp-app'". Recorded in the phase 0 verification section along with the caveat that a linked worktree shares .git/modules/ with the main checkout, so both trees end up on one submodule git dir. **Not verified by a build.** For that reason. The deletion is an orphan source set and the additions are unreferenced catalog lines, so neither can change a build's outcome -- but that is an argument, not a green check, and it is the second commit in a row on this branch that has not compiled anything. Phase 4 is the first phase that genuinely cannot be done without a working gradle invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:44:10 +02:00
once `kspJvm` is wired above.
docs: scope the jvm target, and separate it from testing the daos Two questions arrived together -- whether Room's own testing guidance applies to this project, and what desktop support would cost -- and they turned out to have opposite answers. Both are now in docs/jvm-target.md, phased, with the blocking work separated from the mechanical work. **The expensive part is already done.** The four-deep native chain -- secp256k1 -> bitcoin-kmp -> lightning-kmp -> lightning-kmp-app -- already builds for JVM, on every android build we do. The comment at composeApp/build.gradle.kts:50 records the mechanism without drawing the conclusion: lightning-kmp-core publishes no android variant, so our android target resolves it to the *jvm* one, which pulls secp256k1-kmp-jni-jvm desktop natives, which is exactly why the build has to name the android artifact by hand. Read the other way round, every JVM artifact in the chain is already compiled from source by the composite build. A jvm target adds no cinterop, no C compilation and no new native constraints. That was the part worth being afraid of, and it is finished. **The blocker is one level down, and smaller than it looks.** lightning-kmp-app/library declares 25 expects and implements them across 35 androidMain files. Its jvmMain holds exactly one: fibiprops.jvm.kt, the Kotlin multiplatform library template's Fibonacci boilerplate, satisfying two of the 25 -- both of them the template's own. So 23 actuals are missing, which is why jvm() is commented out there (library/build.gradle.kts:18), which is why it is commented out here (composeApp/build.gradle.kts:46). Mantra cannot declare the target until the fork does. Six phases, ordered by that dependency. 0 build config; 1 the fourteen mechanical phoenix actuals; 2 the three SQLDelight JDBC drivers and NetworkMonitor; 3 key storage; 4 mantra's own sixteen expects; 5 the desktop entry point. 1-3 are independent and parallelisable, 4 is where the compiler finally checks the whole thing. Roughly a week to a launchable build. **Phase 3 has no day estimate, deliberately.** keyStoreEncryption / keyStoreDecryption and their two graceful* wrappers delegate on android to KeystoreHelper.kt -- 116 lines against AndroidKeyStore, StrongBox attempted first and fallen back from, key material never leaving hardware. Desktop JVM has no equivalent, so this is a decision rather than a port, and the doc gives the three real options against what each actually protects. A fixed-key JCEKS file is named there as a liability rather than a stopgap: this is wallet seed material, and it lands on top of the plaintext-key finding already open against this codebase. Recommended sequencing is a passphrase-derived KEK with the desktop build marked unsuitable for real funds, so phases 4 and 5 can proceed without the security question being quietly treated as answered. Two inherited mistakes are called out rather than carried forward. The old Aux jvmMain put the database in java.io.tmpdir behind a TODO -- the doc says not to inherit that in either phase that touches it. And schedulePlatformLogic goes through WorkManager on android with no desktop counterpart, so the doc asks for an explicit choice between a no-op and an in-process coroutine, written down. **The DAO answer is an appendix, because it is the opposite answer.** None of the above is needed to test the DAOs, and burying that would have been misleading. room3-runtime-android:3.0.1 already exposes the no-Context inMemoryDatabaseBuilder(Function0<T>) overload, and MantraDatabaseConstructor already supplies what it needs, so Room's recommended host-machine form compiles in commonTest and runs under testDebugUnitTest today. The one trap is native and is the secp256k1 problem mirrored: sqlite-bundled-android ships only android-ABI .so under jni/, so a local unit test's JVM cannot load it and BundledSQLiteDriver fails at construction; sqlite-bundled-jvm on the androidUnitTest classpath is the fix. Robolectric neither helps nor is needed -- it cannot load android .so on the host either. Everything structural here was checked against the artifacts rather than recalled: the Room builder overloads by javap on room3-runtime-android, the two sqlite-bundled native layouts by unzipping both, and the availability of room3-runtime-jvm, room3-testing, quartz-jvm and the two SQLDelight drivers by request against the repositories this build actually resolves from. The absence of android.* and java.* imports in commonMain, and of any NFC reference from it, was likewise grepped rather than assumed. **Not verified: anything that requires compiling.** No jvm target was turned on, nothing was built, and the day estimates are estimates. Phase 4 is where dependency-substitution surprises would surface if there are any, and it is precisely the phase nothing here exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:38:11 +02:00
For `PlatformDatabaseBuilder.getDatabaseBuilder`, use the real application data
directory from Phase 1 — not `java.io.tmpdir`, which is what the old Aux
implementation did and which silently loses the database on reboot on most
systems.
**Nine lightning wrappers.** Four in
[Phoenix.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/extensions/Phoenix.kt)
(`platformStartupLogic`, `schedulePlatformLogic`, `getShowIntroFlow`,
`getGlobalPrefs`) and five declared in
[SovereignWalletViewModel.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt)
(`updateBusinessActiveInUI`, `loadAndDecryptSeed`, `getAvailableWalletsMeta`, and
the two `saveAvailableWalletMeta` overloads, plus `platformWriteSeed`) whose
android actuals live in `NavigationViewModel.android.kt`.
These are thin — they mostly forward into the phoenix library. They are thin
*because* Phases 13 did the work, which is why they are last.
`schedulePlatformLogic` is the one to look at properly: on android it schedules
background work through WorkManager. On desktop there is no equivalent and no
process that outlives the window. Decide explicitly whether it becomes a no-op or
an in-process coroutine, and write down which.
**Verification:** `./gradlew :composeApp:compileKotlinJvm`. This is the first
point in the plan where the JVM target has to actually resolve, so expect the
dependency-substitution surprises to land here rather than earlier.
---
## Phase 5 — desktop entry point and shakeout
**~12 days. Blocked by Phase 4.**
[composeApp/build.gradle.kts:221](../composeApp/build.gradle.kts) already names
`press.mantra.desktop.MainKt` as the desktop main class. **That file does not
exist.** Write it: a `application { Window { ... } }` entry point constructing
`PlatformContext` and handing it to the same root composable android uses.
The UI itself is Compose Multiplatform and should largely come up as-is. What to
expect anyway:
- **Window sizing.** The layouts have only ever been laid out at phone widths.
Nothing will crash; plenty will look wrong.
- **Back handling.** Android's system back has no desktop counterpart.
- **NFC.** The three `androidMain` NFC files are correctly android-only and are
not referenced from `commonMain` — but any UI that offers an NFC affordance
needs to not offer it here.
- **`Dispatchers.IO`.** Used in `getRoomDatabase` and available on JVM, so no
change; noted because it is not available on all KMP targets and is easy to
trip over later.
**Verification:** `./gradlew :composeApp:run`.
---
## Estimate
| phase | work | days | blocked by |
|---|---|---|---|
| 0 | build configuration | 0.5 | — |
| 1 | phoenix: mechanical actuals (14) | 12 | — |
| 2 | phoenix: drivers + network (4) | 23 | — |
| 3 | phoenix: key storage (4) | **decision** | — |
| 4 | mantra actuals (16) | 12 | 1, 2, 3 |
| 5 | desktop entry point + shakeout | 12 | 4 |
**Roughly one focused week to a launchable desktop build**, assuming Phase 3
takes the passphrase-derived KEK and the build is marked dev-only. Real desktop
key storage is separate work that should not be folded into this estimate, and
should land before anyone holds funds on a desktop Mantra.
Phases 1, 2 and 3 are independent of each other and can go in parallel if more
than one person is on it. Phase 4 cannot start until all three are done, because
it is where the compiler finally checks the whole thing.
## Out of scope
- **`linuxX64()`** — commented out in the phoenix library at line 53. A native
Linux target is a different problem from a JVM one and buys nothing here.
- **iOS on a Linux host** — still impossible, for the reasons already documented
in both build files. The JVM target does not change that.
- **Publishing desktop distributables** — the `compose.desktop` block already
declares Dmg/Msi/Deb formats, but signing, notarisation and update channels are
untouched by this plan.
## Appendix: Room DAO tests do not need this
Worth stating plainly, because the two questions arrived together and the answer
to one is not the answer to the other.
Room's own [testing guidance](https://developer.android.com/training/data-storage/room/testing-db)
recommends host-machine tests over instrumented ones. We can have those today,
without a JVM target, because `room3-runtime-android:3.0.1` exposes the
no-`Context` builder overload:
```
inMemoryDatabaseBuilder(kotlin.jvm.functions.Function0<? extends T>)
```
and [MantraDatabaseConstructor.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/MantraDatabaseConstructor.kt)
already supplies what it needs. So `Room.inMemoryDatabaseBuilder<MantraDatabase>()`
compiles in `commonTest` and runs under `testDebugUnitTest`.
The one trap is native, and it is the same shape as the secp256k1 problem
documented in the build file — in the opposite direction:
| artifact | ships |
|---|---|
| `sqlite-bundled-android` | `jni/{arm64-v8a,armeabi-v7a,x86,x86_64}/libsqliteJni.so` |
| `sqlite-bundled-jvm` | `natives/{linux_x64,linux_arm64,osx_*,windows_x64}/` |
A local unit test resolves the **android** variant, whose `.so` files the host JVM
cannot load, so `BundledSQLiteDriver()` fails at construction. Naming
`sqlite-bundled-jvm` on the `androidUnitTest` classpath fixes it.
Robolectric does not help and is not needed — it cannot load android `.so` on the
host either, and Room's guidance advises against it regardless.