mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-30 19:46:17 +00:00
Reverts the Kover coverage aggregation (introduced indca345212b) from every module, the root aggregation block, the Sonar coverage import, the version catalog, and BUILDING.md. With the plugin present the 16 GB CI runner OOM-killed during the Android test+build job even with Kover opt-in disabled; removing it entirely clears the OOM. No other CI changes — the diff versus main is exactly the inverse ofdca345212b. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
504 lines
17 KiB
Kotlin
504 lines
17 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)
|
||
// Opt-in gate for ProductionReceiverBenchmark (opens sockets to live
|
||
// relays). `-PprodRelayBench=1` reaches the daemon even when an env
|
||
// var would not survive daemon reuse.
|
||
(project.findProperty("prodRelayBench") as? String)?.let {
|
||
environment("PROD_RELAY_BENCH", it)
|
||
}
|
||
// Forward the negentropy-benchmark corpus size to the test JVM.
|
||
System.getProperty("negBenchN")?.let { systemProperty("negBenchN", it) }
|
||
System.getProperty("followBenchScale")?.let { systemProperty("followBenchScale", it) }
|
||
// Opt-in JFR profiling of a benchmark run (-PnegProfile=/tmp/neg.jfr).
|
||
(project.findProperty("negProfile") as? String)?.let {
|
||
jvmArgs("-XX:+FlightRecorder", "-XX:StartFlightRecording=filename=$it,settings=profile,dumponexit=true")
|
||
}
|
||
}
|
||
|
||
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)
|
||
|
||
// CBOR serialization for the NUT-00 v4 (cashuB) token wire format.
|
||
implementation(libs.kotlinx.serialization.cbor)
|
||
|
||
// 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",
|
||
// Library version tracks the app version in gradle/libs.versions.toml,
|
||
// bumped in lockstep with each release.
|
||
version = libs.versions.app.get(),
|
||
)
|
||
|
||
// 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"
|
||
}
|
||
}
|
||
}
|