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().configureEach { maxHeapSize = "4g" environment("TEST_RESOURCES_ROOT", rootDir) } tasks.withType().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().configureEach { binaries.withType().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", // 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" } } }