diff --git a/apps/flipcash/features/messenger/build.gradle.kts b/apps/flipcash/features/messenger/build.gradle.kts index 1c21aec922..db75e306bb 100644 --- a/apps/flipcash/features/messenger/build.gradle.kts +++ b/apps/flipcash/features/messenger/build.gradle.kts @@ -9,7 +9,7 @@ android { dependencies { implementation(project(":apps:flipcash:shared:analytics")) testImplementation(testFixtures(project(":apps:flipcash:shared:analytics"))) - testImplementation(testFixtures(project(":libs:coroutines"))) + testImplementation(project(":libs:coroutines:test-fixtures")) implementation(project(":apps:flipcash:shared:blob")) implementation(project(":apps:flipcash:shared:bills")) implementation(project(":apps:flipcash:shared:blocklist")) diff --git a/apps/flipcash/shared/appupdates/build.gradle.kts b/apps/flipcash/shared/appupdates/build.gradle.kts index c6677cd406..0c30c7d864 100644 --- a/apps/flipcash/shared/appupdates/build.gradle.kts +++ b/apps/flipcash/shared/appupdates/build.gradle.kts @@ -20,5 +20,5 @@ dependencies { testImplementation(kotlin("test")) testImplementation(libs.bundles.unit.testing) testImplementation(libs.robolectric) - testImplementation(testFixtures(project(":libs:coroutines"))) + testImplementation(project(":libs:coroutines:test-fixtures")) } diff --git a/apps/flipcash/shared/authentication/build.gradle.kts b/apps/flipcash/shared/authentication/build.gradle.kts index d0da4fab23..76ffc14e35 100644 --- a/apps/flipcash/shared/authentication/build.gradle.kts +++ b/apps/flipcash/shared/authentication/build.gradle.kts @@ -26,5 +26,5 @@ dependencies { testImplementation(kotlin("test")) testImplementation(libs.bundles.unit.testing) testImplementation(libs.robolectric) - testImplementation(testFixtures(project(":libs:coroutines"))) + testImplementation(project(":libs:coroutines:test-fixtures")) } diff --git a/apps/flipcash/shared/funding/build.gradle.kts b/apps/flipcash/shared/funding/build.gradle.kts index 178401e7f9..03fc03ffca 100644 --- a/apps/flipcash/shared/funding/build.gradle.kts +++ b/apps/flipcash/shared/funding/build.gradle.kts @@ -1,16 +1,44 @@ plugins { - alias(libs.plugins.flipcash.android.feature) + alias(libs.plugins.flipcash.kmp.library) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.ksp) } -android { - namespace = "${Gradle.flipcashNamespace}.shared.funding" +kotlin { + android { + namespace = "${Gradle.flipcashNamespace}.shared.funding" + withHostTest {} + androidResources { enable = true } + } + sourceSets { + androidMain.dependencies { + implementation(project.dependencies.platform(libs.compose.bom)) + implementation(libs.bundles.compose) + implementation(libs.bundles.hilt) + implementation(project(":libs:coroutines")) + implementation(project(":libs:logging")) + implementation(project(":ui:core")) + implementation(project(":ui:components")) + implementation(project(":ui:navigation")) + implementation(project(":ui:resources")) + implementation(project(":ui:theme")) + implementation(project(":apps:flipcash:core")) + implementation(project(":apps:flipcash:core-ui")) + implementation(project(":apps:flipcash:shared:analytics")) + implementation(project(":apps:flipcash:shared:featureflags")) + implementation(project(":apps:flipcash:shared:tokens:core")) + implementation(project(":apps:flipcash:shared:userflags")) + implementation(project(":libs:messaging")) + implementation(project(":services:flipcash")) + } + getByName("androidHostTest").dependencies { + implementation(kotlin("test")) + implementation(libs.bundles.unit.testing) + } + } } dependencies { - implementation(project(":apps:flipcash:shared:analytics")) - implementation(project(":apps:flipcash:shared:featureflags")) - implementation(project(":apps:flipcash:shared:tokens:core")) - implementation(project(":apps:flipcash:shared:userflags")) - implementation(project(":libs:messaging")) - implementation(project(":services:flipcash")) + add("kspAndroid", libs.hilt.android.compiler) + add("kspAndroid", libs.hilt.compiler) } diff --git a/apps/flipcash/shared/funding/src/test/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt b/apps/flipcash/shared/funding/src/androidHostTest/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt similarity index 100% rename from apps/flipcash/shared/funding/src/test/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt rename to apps/flipcash/shared/funding/src/androidHostTest/kotlin/com/flipcash/app/funding/PurchaseMethodStateTest.kt diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethod.kt b/apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethod.kt similarity index 100% rename from apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethod.kt rename to apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethod.kt diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt b/apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt similarity index 100% rename from apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt rename to apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethodController.kt diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt b/apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt similarity index 100% rename from apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt rename to apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/PurchaseMethodState.kt diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt b/apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/internal/Buttons.kt similarity index 100% rename from apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt rename to apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/internal/Buttons.kt diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt similarity index 100% rename from apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt rename to apps/flipcash/shared/funding/src/androidMain/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt diff --git a/apps/flipcash/shared/invite/build.gradle.kts b/apps/flipcash/shared/invite/build.gradle.kts index ab06609168..0a43002570 100644 --- a/apps/flipcash/shared/invite/build.gradle.kts +++ b/apps/flipcash/shared/invite/build.gradle.kts @@ -12,5 +12,5 @@ dependencies { testImplementation(kotlin("test")) testImplementation(libs.bundles.unit.testing) testImplementation(libs.robolectric) - testImplementation(testFixtures(project(":libs:coroutines"))) + testImplementation(project(":libs:coroutines:test-fixtures")) } diff --git a/apps/flipcash/shared/session/build.gradle.kts b/apps/flipcash/shared/session/build.gradle.kts index cbd954cd22..d89ecb4b3d 100644 --- a/apps/flipcash/shared/session/build.gradle.kts +++ b/apps/flipcash/shared/session/build.gradle.kts @@ -9,7 +9,7 @@ android { dependencies { testImplementation(kotlin("test")) testImplementation(libs.bundles.unit.testing) - testImplementation(testFixtures(project(":libs:coroutines"))) + testImplementation(project(":libs:coroutines:test-fixtures")) testImplementation(testFixtures(project(":ui:resources"))) implementation(project(":apps:flipcash:shared:blob")) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 256dd56494..2d16a1a8e8 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -49,6 +49,10 @@ gradlePlugin { id = "flipcash.kmp.library" implementationClass = "KmpLibraryConventionPlugin" } + register("kmpAppLibrary") { + id = "flipcash.kmp.app.library" + implementationClass = "KmpAppLibraryConventionPlugin" + } register("kmpTestFixtures") { id = "flipcash.kmp.test.fixtures" implementationClass = "KmpTestFixturesConventionPlugin" diff --git a/build-logic/convention/src/main/kotlin/KmpAppLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpAppLibraryConventionPlugin.kt new file mode 100644 index 0000000000..dd47da8950 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/KmpAppLibraryConventionPlugin.kt @@ -0,0 +1,129 @@ +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.tasks.testing.Test +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +/** + * Convention plugin for app-layer KMP modules that build for Android and the desktop JVM only. + * + * Unlike `flipcash.kmp.library` it declares no Kotlin/Native targets: these modules sit on Android + * and JVM libraries (gRPC, Room, DataStore, OkHttp) with no native counterpart. Because every target + * is a JVM, `commonMain` sees the JDK, so code that only needs `java.*` moves there unchanged and + * only `android.*` uses need an `expect`/`actual` or an interface. + * + * Applies: + * - `org.jetbrains.kotlin.multiplatform` + * - `com.android.kotlin.multiplatform.library` + * - `org.jetbrains.kotlin.plugin.serialization` + * + * and declares: + * - `android` -- compileSdk and minSdk from the version catalog, host tests enabled with the same + * options `flipcash.android.library` sets on unit tests + * - `jvm` -- the desktop client + * + * The AGP KMP library target is single-variant: it has no `buildTypes` and no `BuildConfig`, so a + * value that differed between debug and release must come from the app at runtime. + * + * Usage in a module's `build.gradle.kts`: + * ``` + * plugins { + * alias(libs.plugins.flipcash.kmp.app.library) + * } + * + * kotlin { + * android { + * namespace = "com.example" + * } + * } + * ``` + */ +class KmpAppLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("org.jetbrains.kotlin.multiplatform") + apply("com.android.kotlin.multiplatform.library") + apply("org.jetbrains.kotlin.plugin.serialization") + } + + val libs = extensions.getByType().named("libs") + val compileSdkVersion = libs.findVersion("android-compileSdk").get().requiredVersion.toInt() + val compileSdkMinorVersion = libs.findVersion("android-compileSdkMinor").get().requiredVersion.toInt() + val minSdkVersion = libs.findVersion("android-minSdk").get().requiredVersion.toInt() + val javaVersion = libs.findVersion("android-java").get().requiredVersion + + extensions.configure { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(javaVersion)) + } + + (this as ExtensionAware).extensions.configure { + compileSdk { + version = release(compileSdkVersion) { + minorApiLevel = compileSdkMinorVersion + } + } + minSdk = minSdkVersion + withHostTestBuilder {}.configure { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(javaVersion)) + } + } + + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(javaVersion)) + } + } + + compilerOptions { + optIn.addAll( + "kotlin.time.ExperimentalTime", + "kotlin.ExperimentalUnsignedTypes", + "kotlin.RequiresOptIn" + ) + freeCompilerArgs.add("-Xexpect-actual-classes") + } + + sourceSets.named("commonMain") { + dependencies { + implementation(libs.findLibrary("kotlinx-coroutines-core").get()) + } + } + sourceSets.named("androidMain") { + dependencies { + implementation(libs.findLibrary("timber").get()) + } + } + sourceSets.named("androidHostTest") { + dependencies { + implementation(libs.findLibrary("kotlin-test-junit").get()) + // Provides robolectric.properties, which pins the Robolectric SDK level. + implementation(project(":libs:test-utils")) + } + } + sourceSets.named("jvmTest") { + dependencies { + implementation(libs.findLibrary("kotlin-test-junit").get()) + } + } + } + + tasks.withType { + failOnNoDiscoveredTests.set(false) + maxHeapSize = "2g" + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt index 4d394bce85..94efac5553 100644 --- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -1,5 +1,12 @@ +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.plugins.ExtensionAware +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension /** * Convention plugin for KMP library modules. @@ -8,14 +15,26 @@ import org.gradle.api.Project * - `org.jetbrains.kotlin.multiplatform` * - `com.android.kotlin.multiplatform.library` * - * After applying this plugin the `kotlin {}` DSL block is available for configuring targets, - * source sets, and dependencies. + * and declares the targets every shared library builds for: + * - `android` -- compileSdk and minSdk from the version catalog + * - `iosArm64`, `iosSimulatorArm64`, `iosX64`, `macosArm64`, `macosX64` -- the Kotlin/Native + * slices of the `SharedCore` framework iOS consumes + * - `jvm` -- the desktop client + * + * A module still sets its own `namespace`, and opts into host/device tests, inside `kotlin { android {} }`. * * Usage in a module's `build.gradle.kts`: * ``` * plugins { * alias(libs.plugins.flipcash.kmp.library) * } + * + * kotlin { + * android { + * namespace = "com.example" + * withHostTest {} + * } + * } * ``` */ class KmpLibraryConventionPlugin : Plugin { @@ -25,6 +44,35 @@ class KmpLibraryConventionPlugin : Plugin { apply("org.jetbrains.kotlin.multiplatform") apply("com.android.kotlin.multiplatform.library") } + + val libs = extensions.getByType().named("libs") + val compileSdkVersion = libs.findVersion("android-compileSdk").get().requiredVersion.toInt() + val compileSdkMinorVersion = libs.findVersion("android-compileSdkMinor").get().requiredVersion.toInt() + val minSdkVersion = libs.findVersion("android-minSdk").get().requiredVersion.toInt() + val javaVersion = libs.findVersion("android-java").get().requiredVersion + + extensions.configure { + (this as ExtensionAware).extensions.configure { + compileSdk { + version = release(compileSdkVersion) { + minorApiLevel = compileSdkMinorVersion + } + } + minSdk = minSdkVersion + } + + iosArm64() + iosSimulatorArm64() + iosX64() + macosArm64() + macosX64() + + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(javaVersion)) + } + } + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff83cf456c..55493d0055 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -349,6 +349,7 @@ flipcash-android-library-compose = { id = "flipcash.android.library.compose" } flipcash-android-feature = { id = "flipcash.android.feature" } flipcash-android-ed25519-shadow = { id = "flipcash.android.ed25519.shadow" } flipcash-kmp-library = { id = "flipcash.kmp.library" } +flipcash-kmp-app-library = { id = "flipcash.kmp.app.library" } flipcash-kmp-test-fixtures = { id = "flipcash.kmp.test.fixtures" } flipcash-analytics-catalogue = { id = "flipcash.analytics.catalogue" } android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/kmp/shared-core/build.gradle.kts b/kmp/shared-core/build.gradle.kts index a9bfafc66e..3bf85b67e9 100644 --- a/kmp/shared-core/build.gradle.kts +++ b/kmp/shared-core/build.gradle.kts @@ -1,9 +1,9 @@ import co.touchlab.kmmbridge.KmmBridgeExtension import org.gradle.api.tasks.bundling.Zip +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) id("co.touchlab.kmmbridge.github") version "1.2.1" `maven-publish` } @@ -24,17 +24,11 @@ val spmPackageDir = findProperty("spmRepoDir") as String? kotlin { android { namespace = "com.flipcash.shared" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 } - - val appleTargets = listOf(iosArm64(), iosSimulatorArm64(), iosX64(), macosArm64(), macosX64()) - appleTargets.forEach { - it.binaries.framework { + + // The Apple targets come from `flipcash.kmp.library`; each one ships a `SharedCore` slice. + targets.withType().configureEach { + binaries.framework { baseName = "SharedCore" isStatic = true export(project(":libs:codes:kikcode")) diff --git a/libs/analytics-events/build.gradle.kts b/libs/analytics-events/build.gradle.kts index 194f8071e1..48eae940ca 100644 --- a/libs/analytics-events/build.gradle.kts +++ b/libs/analytics-events/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) // Generates the builders and value enums from events.toml into commonMain. alias(libs.plugins.flipcash.analytics.catalogue) } @@ -8,21 +7,9 @@ plugins { kotlin { android { namespace = "com.flipcash.analytics" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { // Pure Kotlin -- no external dependencies needed. diff --git a/libs/codes/kikcode/build.gradle.kts b/libs/codes/kikcode/build.gradle.kts index b2d926844a..fafb2aaca7 100644 --- a/libs/codes/kikcode/build.gradle.kts +++ b/libs/codes/kikcode/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -13,21 +12,9 @@ testFixtures { kotlin { android { namespace = "com.getcode.codes.kikcode" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { // Pure Kotlin -- geometry + string building, no platform APIs. diff --git a/libs/coroutines/build.gradle.kts b/libs/coroutines/build.gradle.kts index 0be0cc0f8c..3d32a19f24 100644 --- a/libs/coroutines/build.gradle.kts +++ b/libs/coroutines/build.gradle.kts @@ -1,15 +1,9 @@ plugins { - alias(libs.plugins.flipcash.android.library) - id("org.jetbrains.kotlin.plugin.parcelize") + alias(libs.plugins.flipcash.kmp.app.library) } -android { - namespace = "${Gradle.codeNamespace}.libs.coroutines" - testFixtures { - enable = true +kotlin { + android { + namespace = "${Gradle.codeNamespace}.libs.coroutines" } } - -dependencies { - testFixturesImplementation(libs.kotlinx.coroutines.test) -} diff --git a/libs/coroutines/src/main/kotlin/com/flipcash/libs/coroutines/DispatcherProvider.kt b/libs/coroutines/src/commonMain/kotlin/com/flipcash/libs/coroutines/DispatcherProvider.kt similarity index 100% rename from libs/coroutines/src/main/kotlin/com/flipcash/libs/coroutines/DispatcherProvider.kt rename to libs/coroutines/src/commonMain/kotlin/com/flipcash/libs/coroutines/DispatcherProvider.kt diff --git a/libs/coroutines/test-fixtures/build.gradle.kts b/libs/coroutines/test-fixtures/build.gradle.kts new file mode 100644 index 0000000000..bdc3f43d24 --- /dev/null +++ b/libs/coroutines/test-fixtures/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.flipcash.kmp.app.library) +} + +kotlin { + android { + namespace = "${Gradle.codeNamespace}.libs.coroutines.testfixtures" + } + + sourceSets { + commonMain.dependencies { + api(project(":libs:coroutines")) + implementation(libs.kotlinx.coroutines.test) + } + } +} diff --git a/libs/coroutines/src/testFixtures/kotlin/com/flipcash/libs/coroutines/TestDispatcherProvider.kt b/libs/coroutines/test-fixtures/src/commonMain/kotlin/com/flipcash/libs/coroutines/TestDispatcherProvider.kt similarity index 100% rename from libs/coroutines/src/testFixtures/kotlin/com/flipcash/libs/coroutines/TestDispatcherProvider.kt rename to libs/coroutines/test-fixtures/src/commonMain/kotlin/com/flipcash/libs/coroutines/TestDispatcherProvider.kt diff --git a/libs/currency-math/discrete-curve/build.gradle.kts b/libs/currency-math/discrete-curve/build.gradle.kts index 6ba6a5cf34..5cdde2fe86 100644 --- a/libs/currency-math/discrete-curve/build.gradle.kts +++ b/libs/currency-math/discrete-curve/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -13,8 +12,6 @@ testFixtures { kotlin { android { namespace = "com.flipcash.libs.currency.math.curve" - compileSdk = 37 - minSdk = 29 withHostTest {} // `flipcash.kmp.test.fixtures` compiles this module's fixtures -- @@ -31,12 +28,6 @@ kotlin { } } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/base58/build.gradle.kts b/libs/encryption/base58/build.gradle.kts index 143d07f8f0..a4ca3469bc 100644 --- a/libs/encryption/base58/build.gradle.kts +++ b/libs/encryption/base58/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -13,21 +12,9 @@ testFixtures { kotlin { android { namespace = "com.getcode.encryption.base58" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { // Pure Kotlin -- no external dependencies needed. diff --git a/libs/encryption/chat-cipher/build.gradle.kts b/libs/encryption/chat-cipher/build.gradle.kts index 3491f546a8..d61bc38456 100644 --- a/libs/encryption/chat-cipher/build.gradle.kts +++ b/libs/encryption/chat-cipher/build.gradle.kts @@ -1,8 +1,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -15,12 +14,6 @@ testFixtures { kotlin { android { namespace = "com.getcode.encryption.chatcipher" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 // No host test: libsodium's Android binding loads its .so from the APK and the Ed25519 // actual is JNI, so neither loads on a JVM host. The device test runs commonTest instead, // and the Apple targets run it natively. @@ -31,12 +24,6 @@ kotlin { } } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/ed25519/build.gradle.kts b/libs/encryption/ed25519/build.gradle.kts index b77ecbce3f..28dac3e0e8 100644 --- a/libs/encryption/ed25519/build.gradle.kts +++ b/libs/encryption/ed25519/build.gradle.kts @@ -1,8 +1,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -79,21 +78,9 @@ appleTargetDefs.forEach { target -> kotlin { android { namespace = "com.getcode.encryption.ed25519" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - // ── Cinterop + linker wiring for each Apple target ──────────────────────── targets.withType().configureEach { val targetDef = appleTargetDefs.first { it.kotlinName == name } @@ -138,6 +125,12 @@ kotlin { api(project(":libs:encryption:ed25519-native")) } } + jvmMain { + dependencies { + // Pure-Java Ed25519 for desktop -- no per-OS native library to build or ship. + implementation(libs.eddsa) + } + } commonTest { dependencies { implementation(kotlin("test")) diff --git a/libs/encryption/ed25519/src/jvmMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.jvm.kt b/libs/encryption/ed25519/src/jvmMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.jvm.kt new file mode 100644 index 0000000000..2b7ddbbad5 --- /dev/null +++ b/libs/encryption/ed25519/src/jvmMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.jvm.kt @@ -0,0 +1,61 @@ +package com.getcode.ed25519kmp + +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.math.GroupElement +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec +import java.security.MessageDigest + +/** + * JVM (desktop) actual: pure Java, on `net.i2p.crypto:eddsa`, so there is no native library to + * ship per OS. + * + * The 64-byte private key keeps the orlp layout the Android and Apple actuals produce: the clamped + * SHA-512 expansion of the seed (`a || prefix`). eddsa calls that same value `h`, and accepts it + * directly through [EdDSAPrivateKeySpec]'s `(spec, h)` constructor, so a key created on any + * platform signs identically here. The JDK's own Ed25519 provider can't do this: its private key + * spec only takes the 32-byte seed, which the orlp key doesn't contain. + */ +actual object Ed25519Kmp { + + private val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519) + + actual fun createKeyPair(seed: ByteArray): KeyPair { + val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(seed, spec)) + return KeyPair(publicKey = privateKey.abyte, privateKey = privateKey.h) + } + + actual fun sign( + message: ByteArray, + publicKey: ByteArray, + privateKey: ByteArray, + ): ByteArray { + val key = EdDSAPrivateKey(EdDSAPrivateKeySpec(spec, privateKey)) + return EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm)).run { + initSign(key) + update(message) + sign() + } + } + + actual fun verify( + signature: ByteArray, + message: ByteArray, + publicKey: ByteArray, + ): Boolean = runCatching { + val key = EdDSAPublicKey(EdDSAPublicKeySpec(publicKey, spec)) + EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm)).run { + initVerify(key) + update(message) + verify(signature) + } + }.getOrDefault(false) + + actual fun onCurve(publicKey: ByteArray): Boolean = runCatching { + GroupElement(spec.curve, publicKey) + true + }.getOrDefault(false) +} diff --git a/libs/encryption/hmac/build.gradle.kts b/libs/encryption/hmac/build.gradle.kts index 258a914a10..93776d856c 100644 --- a/libs/encryption/hmac/build.gradle.kts +++ b/libs/encryption/hmac/build.gradle.kts @@ -1,26 +1,13 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) } kotlin { android { namespace = "com.getcode.encryption.hmac" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/mnemonic/build.gradle.kts b/libs/encryption/mnemonic/build.gradle.kts index fa0c0dd626..6752d8968c 100644 --- a/libs/encryption/mnemonic/build.gradle.kts +++ b/libs/encryption/mnemonic/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) alias(libs.plugins.flipcash.kmp.test.fixtures) } @@ -13,22 +12,10 @@ testFixtures { kotlin { android { namespace = "com.getcode.encryption.mnemonic" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} withDeviceTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/sha256/build.gradle.kts b/libs/encryption/sha256/build.gradle.kts index adfcd4938f..b2dc650c44 100644 --- a/libs/encryption/sha256/build.gradle.kts +++ b/libs/encryption/sha256/build.gradle.kts @@ -1,26 +1,13 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) } kotlin { android { namespace = "com.getcode.encryption.sha256" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/sha512/build.gradle.kts b/libs/encryption/sha512/build.gradle.kts index 038470ad6b..e8557c31cc 100644 --- a/libs/encryption/sha512/build.gradle.kts +++ b/libs/encryption/sha512/build.gradle.kts @@ -1,26 +1,13 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) } kotlin { android { namespace = "com.getcode.encryption.sha512" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/encryption/sha512/src/jvmMain/kotlin/com/getcode/crypt/PBKDF2SHA512.jvm.kt b/libs/encryption/sha512/src/jvmMain/kotlin/com/getcode/crypt/PBKDF2SHA512.jvm.kt new file mode 100644 index 0000000000..eb2a0f2aa5 --- /dev/null +++ b/libs/encryption/sha512/src/jvmMain/kotlin/com/getcode/crypt/PBKDF2SHA512.jvm.kt @@ -0,0 +1,27 @@ +package com.getcode.crypt + +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + +private const val ALGORITHM = "PBKDF2WithHmacSHA512" + +/** + * The JDK's SunJCE provider implements PBKDF2WithHmacSHA512, so the desktop JVM takes the same JCA + * route as Android (Conscrypt there). Same ASCII-only restriction and fallback as the Android + * actual -- see `PBKDF2SHA512.android.kt` for why: providers disagree about encoding a non-ASCII + * `char[]` password, and ASCII is where every encoding coincides. + */ +internal actual fun derivePlatform(P: String, S: String, c: Int, dkLen: Int): ByteArray? { + if (!P.isAscii()) return null + + return runCatching { + val spec = PBEKeySpec(P.toCharArray(), S.encodeToByteArray(), c, dkLen * 8) + try { + SecretKeyFactory.getInstance(ALGORITHM).generateSecret(spec).encoded + } finally { + spec.clearPassword() + } + }.getOrNull() +} + +private fun String.isAscii(): Boolean = all { it.code < 0x80 } diff --git a/libs/encryption/utils/build.gradle.kts b/libs/encryption/utils/build.gradle.kts index 6b5bc632f3..79e57d881d 100644 --- a/libs/encryption/utils/build.gradle.kts +++ b/libs/encryption/utils/build.gradle.kts @@ -1,26 +1,13 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) } kotlin { android { namespace = "com.getcode.encryption.utils" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { dependencies { diff --git a/libs/reporting/build.gradle.kts b/libs/reporting/build.gradle.kts index a03de2ded7..70b77cc4e6 100644 --- a/libs/reporting/build.gradle.kts +++ b/libs/reporting/build.gradle.kts @@ -1,26 +1,13 @@ plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.library) } kotlin { android { namespace = "com.flipcash.reporting" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { // Pure Kotlin -- no external dependencies needed. diff --git a/libs/text-format/build.gradle.kts b/libs/text-format/build.gradle.kts index 16de7b7f18..bf08b14d1a 100644 --- a/libs/text-format/build.gradle.kts +++ b/libs/text-format/build.gradle.kts @@ -12,21 +12,9 @@ testFixtures { kotlin { android { namespace = "com.flipcash.libs.textformat" - compileSdk { - version = release(libs.versions.android.compileSdk.get().toInt()) { - minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() - } - } - minSdk = 29 withHostTest {} } - iosArm64() - iosSimulatorArm64() - iosX64() - macosArm64() - macosX64() - sourceSets { commonMain { // Pure Kotlin -- no external dependencies needed. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a7f302c17..ce47d2b015 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -200,6 +200,7 @@ include( ":libs:code-detection", ":libs:codes:kikcode", ":libs:coroutines", + ":libs:coroutines:test-fixtures", ":libs:crypto:kin", ":libs:crypto:solana", ":libs:currency",