mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
Address bugs and gaps surfaced by an audit of the prior 14 commits.
JVM tests passed because of typealias / platform-type lenience that
won't hold on Native; these are real iOS compile / behavior issues.
BUG fixes (iOS compile failures):
- commons/.../Note.kt:899 — Iterable.sumOf { -> BigDecimal } is a
JVM-stdlib-only overload. Common stdlib ships sumOf only for
Int/Long/Double/Float/UInt/ULong. Replaced with fold(BigDecimal(0)).
- commons/.../Note.kt:889 — BigDecimal(it.event?.content): the quartz
expect-class constructor takes String non-null; JVM accepted nullable
via platform-type lenience and threw NPE caught downstream. Switched
to ?.let { content -> BigDecimal(content) }.
- commons/.../Note.kt:838 — `catch (e: java.lang.Exception)` -> `Exception`.
- commons/.../feeds/custom/FeedDefinitionBuilder.kt + FeedBuilderState.kt:
inline FQN `java.util.UUID.randomUUID().toString()` -> kotlin.uuid.Uuid.
random().toString() (Kotlin 2.0+, @OptIn ExperimentalUuidApi).
inline `System.currentTimeMillis() / 1000` -> TimeUtils.now() (already
used elsewhere in the codebase).
- commons/.../viewmodels/NestViewModelTest.kt: moved from commonTest to
jvmTest. The test imports NestViewModel + nestsclient, both of which
the prior PR moved to jvmAndroid. commonTest depends on commonMain
only, so the test would fail to compile for iosSimulatorArm64Test.
SUBTLE fixes:
- commons/.../UserRelaysCache.kt: the flow field used double-checked
locking on a non-volatile var. JMM hazard on Native (ARM weak memory
model) — outer fast-path could observe a partially-published
WeakReference. Added @kotlin.concurrent.Volatile.
- commons/.../util/UrlValidation.ios.kt: NSURL.URLWithString("http:")
returns non-null with scheme="http" and no host; JVM's URI.toURL()
rejects with MalformedURLException. Reject scheme-only network URLs
(http/https/ws/wss/ftp without a host) to match JVM behavior.
- commons/.../util/KmpLock.kt commonMain doc: corrected "NSLock" ->
"NSRecursiveLock" to match the actual iOS implementation.
verifyKmpPurity gate extended (commons + quartz):
- Adds patterns: System.currentTimeMillis, Thread.sleep, java.util.UUID,
kotlin.jvm.Synchronized, kotlin.jvm.Volatile.
- Each pattern paired with a hint pointing at the canonical KMP
replacement; the error message surfaces both.
- Skips lines that start with //, *, or /* to avoid false positives on
KDoc / migration notes.
475 lines
16 KiB
Kotlin
475 lines
16 KiB
Kotlin
import com.vanniktech.maven.publish.KotlinMultiplatform
|
||
import com.vanniktech.maven.publish.SourcesJar
|
||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
|
||
|
||
|
||
plugins {
|
||
alias(libs.plugins.kotlinMultiplatform)
|
||
alias(libs.plugins.androidKotlinMultiplatformLibrary)
|
||
alias(libs.plugins.serialization)
|
||
alias(libs.plugins.vanniktech.mavenPublish)
|
||
}
|
||
|
||
kotlin {
|
||
compilerOptions {
|
||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||
}
|
||
jvm {
|
||
compilerOptions {
|
||
jvmTarget.set(JvmTarget.JVM_21)
|
||
}
|
||
}
|
||
|
||
android {
|
||
namespace = "com.vitorpamplona.quartz"
|
||
compileSdk =
|
||
libs.versions.android.compileSdk
|
||
.get()
|
||
.toInt()
|
||
minSdk =
|
||
libs.versions.android.minSdk
|
||
.get()
|
||
.toInt()
|
||
|
||
compilerOptions {
|
||
jvmTarget.set(JvmTarget.JVM_21)
|
||
}
|
||
|
||
optimization {
|
||
consumerKeepRules.apply {
|
||
publish = true
|
||
file("consumer-rules.pro")
|
||
}
|
||
}
|
||
|
||
withHostTest {}
|
||
|
||
withDeviceTest {
|
||
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||
}
|
||
}
|
||
|
||
// For iOS targets, this is also where you should
|
||
// configure native binary output. For more information, see:
|
||
// https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks
|
||
|
||
// A step-by-step guide on how to include this library in an XCode
|
||
// project can be found here:
|
||
// https://developer.android.com/kotlin/multiplatform/migrate
|
||
val xcfName = "quartz-kmpKit"
|
||
|
||
iosArm64 {
|
||
binaries.framework {
|
||
baseName = xcfName
|
||
isStatic = true
|
||
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
||
}
|
||
}
|
||
|
||
iosSimulatorArm64 {
|
||
binaries.framework {
|
||
baseName = xcfName
|
||
isStatic = true
|
||
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
||
}
|
||
}
|
||
|
||
linuxX64()
|
||
|
||
macosArm64()
|
||
|
||
// This makes sure that the resource file directory is visible for iOS tests.
|
||
val rootDir = "${rootProject.rootDir.path}/quartz/src/commonTest/resources"
|
||
|
||
tasks.withType<Test>().configureEach {
|
||
maxHeapSize = "4g"
|
||
environment("TEST_RESOURCES_ROOT", rootDir)
|
||
}
|
||
|
||
tasks.withType<KotlinNativeTest>().configureEach {
|
||
environment("TEST_RESOURCES_ROOT", rootDir)
|
||
// This is necessary to have the variable propagated on iOS
|
||
environment("SIMCTL_CHILD_TEST_RESOURCES_ROOT", rootDir)
|
||
}
|
||
|
||
// Enable LLVM optimizations for native test binaries (benchmark accuracy).
|
||
// Without opt, K/N test binaries compile in debug mode (~20× slower).
|
||
// Use the binary-level API instead of -opt freeCompilerArg to avoid the
|
||
// "Unsupported combination of flags: -opt and -g" conflict in Kotlin/Native 2.1+.
|
||
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
|
||
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable>().configureEach {
|
||
optimized = true
|
||
debuggable = false
|
||
}
|
||
}
|
||
|
||
// Source set declarations.
|
||
// Declaring a target automatically creates a source set with the same name. By default, the
|
||
// Kotlin Gradle Plugin creates additional source sets that depend on each other, since it is
|
||
// common to share sources between related targets.
|
||
// See: https://kotlinlang.org/docs/multiplatform-hierarchy.html
|
||
sourceSets {
|
||
commonMain {
|
||
dependencies {
|
||
implementation(libs.kotlin.stdlib)
|
||
implementation(libs.kotlinx.coroutines.core)
|
||
|
||
// For LruCache
|
||
implementation(libs.androidx.collection)
|
||
|
||
// @Immutable and @Stable
|
||
implementation(libs.androidx.compose.runtime.annotation)
|
||
|
||
// Bitcoin secp256k1 bindings
|
||
api(libs.secp256k1.kmp.common)
|
||
|
||
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
|
||
implementation(libs.kotlinx.serialization.json)
|
||
|
||
// immutable collections to avoid recomposition
|
||
implementation(libs.kotlinx.collections.immutable)
|
||
|
||
// SQLite KMP driver for event store
|
||
api(libs.androidx.sqlite)
|
||
implementation(libs.androidx.sqlite.bundled)
|
||
|
||
// Negentropy set reconciliation (NIP-77)
|
||
api(libs.negentropy.kmp)
|
||
|
||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||
}
|
||
}
|
||
|
||
commonTest {
|
||
dependencies {
|
||
implementation(libs.kotlin.test)
|
||
implementation(libs.kotlinx.coroutines.test)
|
||
|
||
// SQLite bundled driver for tests
|
||
api(libs.androidx.sqlite)
|
||
implementation(libs.androidx.sqlite.bundled)
|
||
}
|
||
}
|
||
|
||
// Must be defined before androidMain and jvmMain
|
||
val jvmAndroid =
|
||
create("jvmAndroid") {
|
||
dependsOn(commonMain.get())
|
||
|
||
dependencies {
|
||
// Performant Parser of JSONs into Events
|
||
api(libs.jackson.module.kotlin)
|
||
|
||
// Websockets API
|
||
implementation(libs.okhttp)
|
||
implementation(libs.okhttpCoroutines)
|
||
|
||
// Chess engine for move validation and legal move generation
|
||
// NOTE: 1.0.4+ uses Java 21's removeLast() which crashes on Android API < 34
|
||
// TODO: Test if 1.0.0 works, or fork library to fix
|
||
implementation(libs.kchesslib)
|
||
}
|
||
}
|
||
|
||
// Must be defined before androidMain and jvmMain
|
||
val jvmAndroidTest =
|
||
create("jvmAndroidTest") {
|
||
dependsOn(commonTest.get())
|
||
dependencies {
|
||
implementation(libs.kotlin.test)
|
||
implementation(libs.kotlinx.coroutines.test)
|
||
|
||
// In-process Nostr relay (geode) so JVM/Android host
|
||
// tests don't need network access or a Rust toolchain.
|
||
// testFixtures (RelayClientTest base, fixtures,
|
||
// collectUntilEose) are wired below at the top-level
|
||
// `dependencies` block — the KMP source-set DSL
|
||
// doesn't expose the `testFixtures(...)` consumer.
|
||
implementation(project(":geode"))
|
||
}
|
||
}
|
||
|
||
jvmMain {
|
||
dependsOn(jvmAndroid)
|
||
dependencies {
|
||
// Bitcoin secp256k1 bindings
|
||
implementation(libs.secp256k1.kmp.jni.jvm)
|
||
}
|
||
}
|
||
|
||
jvmTest {
|
||
dependsOn(jvmAndroidTest)
|
||
dependencies {
|
||
// Bitcoin secp256k1 bindings
|
||
implementation(libs.secp256k1.kmp.jni.jvm)
|
||
|
||
// Custom C secp256k1 (libschnorr256k1) for cross-validation + 3-way benchmark
|
||
implementation(libs.schnorr256k1.kmp)
|
||
|
||
// SQLite bundled driver for JVM tests
|
||
implementation(libs.androidx.sqlite.bundled.jvm)
|
||
}
|
||
}
|
||
|
||
androidMain {
|
||
dependsOn(jvmAndroid)
|
||
dependencies {
|
||
implementation(libs.androidx.core.ktx)
|
||
|
||
// Bitcoin secp256k1 bindings to Android
|
||
api(libs.secp256k1.kmp.jni.android)
|
||
}
|
||
}
|
||
|
||
getByName("androidHostTest") {
|
||
dependencies {
|
||
implementation(libs.kotlin.test)
|
||
implementation(libs.kotlinx.coroutines.test)
|
||
|
||
// Bitcoin secp256k1 bindings
|
||
implementation(libs.secp256k1.kmp.jni.jvm)
|
||
|
||
// SQLite bundled driver for Host tests
|
||
implementation(libs.androidx.sqlite.bundled.jvm)
|
||
}
|
||
}
|
||
|
||
getByName("androidDeviceTest") {
|
||
dependencies {
|
||
implementation(libs.androidx.runner)
|
||
implementation(libs.androidx.core)
|
||
implementation(libs.androidx.junit)
|
||
implementation(libs.androidx.espresso.core)
|
||
|
||
implementation(libs.kotlin.test)
|
||
implementation(libs.kotlinx.coroutines.test)
|
||
|
||
// Bitcoin secp256k1 bindings to Android
|
||
api(libs.secp256k1.kmp.jni.android)
|
||
|
||
// Custom C secp256k1 (libschnorr256k1) for cross-validation + 3-way benchmark
|
||
implementation(libs.schnorr256k1.kmp)
|
||
}
|
||
}
|
||
|
||
// Must be defined before appleMain, linuxMain, etc.
|
||
val nativeMain =
|
||
create("nativeMain") {
|
||
dependsOn(commonMain.get())
|
||
}
|
||
|
||
val nativeTest =
|
||
create("nativeTest") {
|
||
dependsOn(commonTest.get())
|
||
}
|
||
|
||
// Must be defined before iosMain and macosMain
|
||
val appleMain =
|
||
create("appleMain") {
|
||
dependsOn(nativeMain)
|
||
dependencies {
|
||
implementation(libs.charlietap.cachemap)
|
||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||
}
|
||
}
|
||
|
||
val appleTest =
|
||
create("appleTest") {
|
||
dependsOn(nativeTest)
|
||
}
|
||
|
||
iosMain {
|
||
dependsOn(appleMain)
|
||
}
|
||
|
||
val iosArm64Main by getting {
|
||
dependsOn(iosMain.get())
|
||
}
|
||
|
||
val iosSimulatorArm64Main by getting {
|
||
dependsOn(iosMain.get())
|
||
}
|
||
|
||
iosTest {
|
||
dependsOn(appleTest)
|
||
}
|
||
|
||
val iosArm64Test by getting {
|
||
dependsOn(iosTest.get())
|
||
}
|
||
|
||
val iosSimulatorArm64Test by getting {
|
||
dependsOn(iosTest.get())
|
||
}
|
||
|
||
val macosMain =
|
||
create("macosMain") {
|
||
dependsOn(appleMain)
|
||
}
|
||
|
||
val macosTest =
|
||
create("macosTest") {
|
||
dependsOn(appleTest)
|
||
}
|
||
|
||
val macosArm64Main by getting {
|
||
dependsOn(macosMain)
|
||
}
|
||
|
||
val macosArm64Test by getting {
|
||
dependsOn(macosTest)
|
||
}
|
||
|
||
val linuxMain =
|
||
create("linuxMain") {
|
||
dependsOn(nativeMain)
|
||
dependencies {
|
||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||
}
|
||
}
|
||
|
||
val linuxTest =
|
||
create("linuxTest") {
|
||
dependsOn(nativeTest)
|
||
}
|
||
|
||
val linuxX64Main by getting {
|
||
dependsOn(linuxMain)
|
||
}
|
||
|
||
val linuxX64Test by getting {
|
||
dependsOn(linuxTest)
|
||
}
|
||
}
|
||
}
|
||
|
||
// testFixtures(...) consumer lives outside the KMP source-set DSL —
|
||
// the KMP source-set `dependencies { }` block uses
|
||
// `KotlinDependencyHandler`, which does not expose the
|
||
// `testFixtures(...)` projection. The standard Gradle dependency
|
||
// configuration name (`jvmAndroidTestImplementation`) does work here.
|
||
dependencies {
|
||
"jvmAndroidTestImplementation"(testFixtures(project(":geode")))
|
||
}
|
||
|
||
// iOS purity gate. Jackson and OkHttp are JVM-only and must never appear
|
||
// in source sets that compile for iOS / native / common targets. The KMP
|
||
// build would eventually fail with an "unresolved reference" if these
|
||
// leaked in, but that requires a macOS runner. This task is a fast Linux
|
||
// linter that fails with a clear message before iOS CI ever spins up.
|
||
//
|
||
// Scope: source sets whose code is compiled for at least one non-JVM
|
||
// target. Excludes jvmAndroid, jvmMain, androidMain (and their tests),
|
||
// where Jackson and OkHttp are legitimately used.
|
||
val verifyKmpPurity by tasks.registering {
|
||
group = "verification"
|
||
description = "Fails if iOS-targeted source sets import JVM-only deps."
|
||
val checkedDirs =
|
||
listOf(
|
||
"src/commonMain", "src/commonTest",
|
||
"src/appleMain", "src/appleTest",
|
||
"src/nativeMain", "src/nativeTest",
|
||
"src/iosMain", "src/iosTest",
|
||
"src/iosArm64Main", "src/iosArm64Test",
|
||
"src/iosSimulatorArm64Main", "src/iosSimulatorArm64Test",
|
||
"src/linuxMain", "src/linuxTest",
|
||
"src/linuxX64Main", "src/linuxX64Test",
|
||
"src/macosMain", "src/macosTest",
|
||
"src/macosArm64Main", "src/macosArm64Test",
|
||
).map { layout.projectDirectory.dir(it).asFile }
|
||
.filter { it.exists() }
|
||
inputs.files(checkedDirs)
|
||
doLast {
|
||
// Each pattern is paired with a short hint so the failure message
|
||
// points at the canonical KMP replacement.
|
||
val forbidden =
|
||
listOf(
|
||
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
|
||
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
|
||
"System.currentTimeMillis" to "use TimeUtils.now()",
|
||
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
|
||
"java.util.UUID" to "use kotlin.uuid.Uuid",
|
||
"kotlin.jvm.Synchronized" to "use a KMP lock primitive",
|
||
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
|
||
)
|
||
val offenders =
|
||
checkedDirs.flatMap { dir ->
|
||
dir.walkTopDown()
|
||
.filter { it.isFile && it.extension == "kt" }
|
||
.flatMap { file ->
|
||
file.readLines().withIndex().mapNotNull { (idx, line) ->
|
||
val trimmed = line.trimStart()
|
||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||
return@mapNotNull null
|
||
}
|
||
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
|
||
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (offenders.isNotEmpty()) {
|
||
throw GradleException(
|
||
"iOS-targeted source sets must not reference JVM-only APIs. " +
|
||
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
|
||
offenders.joinToString("\n "),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
tasks.named("check").configure { dependsOn(verifyKmpPurity) }
|
||
|
||
mavenPublishing {
|
||
// sources publishing is always enabled by the Kotlin Multiplatform plugin
|
||
configure(
|
||
KotlinMultiplatform(
|
||
// whether to publish a sources jar
|
||
sourcesJar = SourcesJar.Sources(),
|
||
),
|
||
)
|
||
|
||
coordinates(
|
||
groupId = "com.vitorpamplona.quartz",
|
||
artifactId = "quartz",
|
||
version = "1.11.0",
|
||
)
|
||
|
||
// Configure publishing to Maven Central
|
||
publishToMavenCentral(automaticRelease = true)
|
||
|
||
// Enable GPG signing for all publications
|
||
signAllPublications()
|
||
|
||
pom {
|
||
name = "Quartz: Nostr Library for Kotlin Multiplatform"
|
||
description = "Nostr library ported to Kotlin/Multiplatform for JVM, Android, iOS & Linux"
|
||
inceptionYear = "2025"
|
||
url = "https://github.com/vitorpamplona/amethyst/"
|
||
licenses {
|
||
license {
|
||
name = "MIT License"
|
||
url = "https://github.com/vitorpamplona/amethyst/blob/main/LICENSE"
|
||
}
|
||
}
|
||
developers {
|
||
developer {
|
||
id = "vitorpamplona"
|
||
name = "Vitor Pamplona"
|
||
url = "http://vitorpamplona.com"
|
||
email = "vitor@vitorpamplona.com"
|
||
}
|
||
}
|
||
scm {
|
||
url = "https://github.com/vitorpamplona/amethyst/"
|
||
connection = "https://github.com/vitorpamplona/amethyst/.git"
|
||
}
|
||
}
|
||
}
|