Quartz moves to its own repo, the way the crypto primitives moved to crypto-kmp. Every module depends on com.vitorpamplona.quartz:quartz 2.0.0 (libs.quartz) instead of :quartz, and -PquartzDir=../quartz builds a local checkout instead of the artifact, for every module and for quic's own quartz dependency (which replaces the root build's substitution of the published quartz with :quartz). What moved with quartz: its CI jobs (lint tasks, test-quartz-linux-native, test-quartz-ios), the Maven Central publish step of the release workflow, jitpack.yml, tools/unicode-nfkc, the nostr-expert and event-store-semantics skills, the agent-skill check, and the README's library usage docs. What stays here: - The four quartz client tests that need a real Ktor socket relay (TrustNetworkSync*, GiantReqStreamTest, BulkDownloadBenchmark) move to geode's tests; geode forwards -PprodRelayBench for the latter. quartz's other relay-client tests now use a test relay of its own, which ends quartz's test dependency on geode. - The benchmark module's resources (previously read from quartz's androidDeviceTest resources) move into benchmark/; relayBench --real reads the corpus from there. - NoteComposeKindCoverageTest reads EventFactory.kt from quartz's JVM sources jar, or from the -PquartzDir checkout, passed by commonsUI's build. - commons and commonsUI declare crypto-kmp (and commons mls-kmp), which they import directly. Through quartz's api alone, a -PcryptoKmpDir checkout never reached the common metadata compile. - The catalog drops what only quartz used (kchesslib, negentropy, jackson-databind, sqlite-bundled, secp256k1-kmp common, compose runtime-annotation, the maven-publish plugin). agent_config_check checks a skill's quartz/... paths against a ../quartz checkout when there is one and skips them otherwise. AGENTS.md, the skills, CONTRIBUTING, BUILDING, RELEASE_OPS, PLANS and the READMEs point at the new repo. Needs quartz 2.0.0 (and crypto-kmp 1.0.0) on Maven Central before CI can resolve it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEkJaDWYrcTbGvT8JLQCrG
5.7 KiB
name: kotlin-multiplatform description: KMP placement and Gradle in Amethyst: which module or source set, expect/actual vs ports, verifyKmpPurity, extracting Android composables to commonsUI, build.gradle.kts/libs.versions.toml edits, secp256k1 JNI variants, R8.
Kotlin Multiplatform and Gradle in Amethyst
Module roles, the "share vs keep native" rule and the CLI-safe boundary are in
AGENTS.md (Architecture, Feature Workflow). Package placement inside commons /
commonsUI is in commons/ARCHITECTURE.md ("Where does my code go?", "Source sets")
and commonsUI/ARCHITECTURE.md ("Platform slots"). Read those first; this skill
adds the source-set mechanics and the build traps.
Modules
settings.gradle.kts includes 13: amethyst, nappletHost, benchmark,
baselineprofile, geode, commons, commonsUI, nestsClient,
marmotQuic, desktopApp, cli, relayBench, marmotBench. Quartz is the
external libs.quartz (vitorpamplona/quartz) and QUIC the external libs.quic
(vitorpamplona/quic-kmp); -PquartzDir=../quartz / -PquicKmpDir=../quic-kmp build a
local checkout instead (an included build, so quic's own quartz dependency follows).
Source sets
| Source set | Holds | Targets |
|---|---|---|
commonMain |
Pure Kotlin. Compiled for iOS/native too, so no JVM APIs | all |
jvmAndroid |
JVM libraries shared by Android + Desktop: Jackson (jackson.databind), OkHttp |
android, jvm |
androidMain / jvmMain |
Platform actuals (Keystore/DataStore vs keyring, Coil bridges) | one each |
iosMain, appleMain, nativeMain, linuxMain, macosMain |
quartz only (plus iosMain in commons/commonsUI) |
Apple / native |
skikoMain |
commonsUI code shared by Desktop and iOS | jvm, ios |
Targets: quartz builds android, jvm, iosArm64, iosSimulatorArm64, linuxX64,
macosArm64. commons and commonsUI build android, jvm, iosArm64,
iosSimulatorArm64. nestsClient, marmotQuic are android + jvm. There is
no iosX64.
jvmAndroid is a custom intermediate set created with create("jvmAndroid")
before androidMain/jvmMain depend on it (see quartz/build.gradle.kts).
Prefer the most common set that compiles: commonMain → jvmAndroid → platform.
Jackson and OkHttp are the house JSON and HTTP stack (AGENTS.md › Tech Stack).
Don't migrate JVM code to kotlinx.serialization or Ktor. They are only banned in
iOS-compiled sets, which the purity gate enforces.
expect/actual vs ports and slots
- expect/actual for a small platform verb or value with no app state:
KmpLock,ConcurrentSet,SecureKeyStorage,isDebug,formattedDateTime(commons),rememberTextSharer,rememberShortNotice(commonsUI). Put the actual injvmAndroidwhen Android and Desktop share it. - A port (interface the shell implements) when the platform side needs app
state or services:
AccountViewModelHost,UserNoticeSink,LocalCacheHost. - A slot (CompositionLocal the front end installs) for things only a front end
can draw:
LocalNotePlatform,LocalRichTextPlatform. - Not every quartz singleton is expect:
Secp256k1Instance,LogandLibSodiumInstanceare plainobjects inquartz/.../utils/. - Don't
expecta whole screen or navigation; share it and put only the leaf behind a port or slot.
Purity gate: verifyKmpPurity
gradle/kmp-purity.gradle.kts, applied by commons and commonsUI (quartz keeps a copy) and
wired into check, scans every iOS/native-compiled source set (commonMain,
iosMain, skikoMain, …, and their tests) and fails on com.fasterxml.jackson,
okhttp3, System.currentTimeMillis (use TimeUtils.now()), Thread.sleep,
java.util.UUID (use kotlin.uuid.Uuid), kotlin.jvm.Synchronized or bare
synchronized( (use KmpLock), kotlin.jvm.Volatile (use
kotlin.concurrent.Volatile). CI runs it on Linux in the lint job; the macOS
test-commons-ios / test-commonsui-ios jobs are authoritative. Run
./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity
after touching commonMain. Quartz has its own copy of the task and its own iOS CI.
Gradle traps
- Versions live only in
gradle/libs.versions.toml. Add the library there and referencelibs.*; never hardcode a version or copy one into docs. Check the license first (AGENTS.md› Dependency Licensing). testis an alias for each KMP module'sjvmTest. Other targets (:commonsUI:testAndroidHostTest, the native ones) run only when named; seeAGENTS.md› Build Commands.- testFixtures in KMP: the source-set
dependencies { }block (KotlinDependencyHandler) has notestFixtures(...). Use a top-level block with the configuration name as a string:dependencies { "jvmAndroidTestImplementation"(testFixtures(project(":geode"))) }. - secp256k1 variants:
secp256k1-kmp-commonincommonMain,secp256k1-kmp-jni-androidinandroidMain,secp256k1-kmp-jni-jvminjvmMainand JVM tests. All three share oneversion.ref. A JVM classpath with the android variant fails at load withUnsatisfiedLinkError. - R8: library keep rules ship in
quartz/consumer-rules.pro. Never add a blanket-keep class com.vitorpamplona.quartz.** { *; }. R8 can't see reflection, so a moved/renamed reflective class is a silent release break: before tagging, runpython3 tools/r8-verify/verify_reflection_contract.py amethyst/build/outputs/mapping/<variant>/(the release workflow checksplayReleaseandfdroidRelease) and updatetools/r8-verify/reflection-contract.txtwhen you move a listed class.
Extracting Android code to shared modules
Steps and the Android → multiplatform replacement table: references/extract-composable.md.