From 0d7c04ededebb5f89b4795fcf9326cbf1f8dc8f5 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 19 Dec 2025 20:09:07 +0100 Subject: [PATCH 1/2] feat: Add shared KMP and JVM module --- crypto-ffi/bindings/android/build.gradle.kts | 7 ++++--- crypto-ffi/bindings/jvm/build.gradle.kts | 4 +++- .../main/kotlin/com/wire/crypto/ByteUtils.kt | 5 ----- crypto-ffi/bindings/settings.gradle.kts | 2 +- crypto-ffi/bindings/shared/build.gradle.kts | 21 +++++++++++++++++++ .../kotlin/com/wire/crypto/ByteUtils.kt | 7 +++++++ .../kotlin/com/wire/crypto/CoreCrypto.kt | 0 .../kotlin/com/wire/crypto/MlsModel.kt | 0 8 files changed, 36 insertions(+), 10 deletions(-) delete mode 100644 crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/ByteUtils.kt create mode 100644 crypto-ffi/bindings/shared/build.gradle.kts create mode 100644 crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/ByteUtils.kt rename crypto-ffi/bindings/{jvm/src/main => shared/src/commonMain}/kotlin/com/wire/crypto/CoreCrypto.kt (100%) rename crypto-ffi/bindings/{jvm/src/main => shared/src/commonMain}/kotlin/com/wire/crypto/MlsModel.kt (100%) diff --git a/crypto-ffi/bindings/android/build.gradle.kts b/crypto-ffi/bindings/android/build.gradle.kts index f359a2d125..b80079ae6b 100644 --- a/crypto-ffi/bindings/android/build.gradle.kts +++ b/crypto-ffi/bindings/android/build.gradle.kts @@ -6,7 +6,8 @@ plugins { id("com.vanniktech.maven.publish.base") } -val jvmSources = projectDir.resolve("../jvm/src") +val sharedSources = projectDir.resolve("../shared/src/commonMain") +val jvmTestSources = projectDir.resolve("../jvm/src") val dokkaHtmlJar = tasks.register("dokkaHtmlJar") { dependsOn(tasks.dokkaGeneratePublicationHtml) @@ -87,12 +88,12 @@ android { main { kotlin { srcDir(projectDir.resolve("src/main/uniffi")) - srcDir(jvmSources.resolve("main/kotlin")) + srcDir(sharedSources.resolve("kotlin")) } } androidTest { kotlin { - srcDir(jvmSources.resolve("test")) + srcDir(jvmTestSources.resolve("test")) } } } diff --git a/crypto-ffi/bindings/jvm/build.gradle.kts b/crypto-ffi/bindings/jvm/build.gradle.kts index 181b1d56e3..225f09b6f1 100644 --- a/crypto-ffi/bindings/jvm/build.gradle.kts +++ b/crypto-ffi/bindings/jvm/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { jvmToolchain(17) } +val sharedSources = projectDir.resolve("../shared/src/commonMain") + dependencies { implementation(platform(kotlin("bom"))) implementation(platform(libs.coroutines.bom)) @@ -61,7 +63,7 @@ tasks.named("compileKotlin") { sourceSets { main { kotlin { - srcDir(projectDir.resolve("src/main/kotlin")) + srcDir(sharedSources.resolve("kotlin")) srcDir(projectDir.resolve("src/main/uniffi")) } resources { diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/ByteUtils.kt b/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/ByteUtils.kt deleted file mode 100644 index 8f072e6fdf..0000000000 --- a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/ByteUtils.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wire.crypto - -internal fun String.toByteArray() = encodeToByteArray() - -internal fun ByteArray.toHex(): String = joinToString(separator = "") { b -> "%02x".format(b) } diff --git a/crypto-ffi/bindings/settings.gradle.kts b/crypto-ffi/bindings/settings.gradle.kts index 45d9cfc660..10385709c8 100644 --- a/crypto-ffi/bindings/settings.gradle.kts +++ b/crypto-ffi/bindings/settings.gradle.kts @@ -7,4 +7,4 @@ pluginManagement { } } -include(":jvm", ":android") +include(":shared", ":jvm", ":android") diff --git a/crypto-ffi/bindings/shared/build.gradle.kts b/crypto-ffi/bindings/shared/build.gradle.kts new file mode 100644 index 0000000000..4028a845b4 --- /dev/null +++ b/crypto-ffi/bindings/shared/build.gradle.kts @@ -0,0 +1,21 @@ +// This module holds shared Kotlin source files used by both JVM and Android modules. +// The sources are included directly by those modules rather than compiled as a separate library, +// because they depend on Uniffi-generated types that are platform-specific. + +plugins { + kotlin("multiplatform") +} + +kotlin { + jvmToolchain(17) + + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(kotlin("stdlib")) + } + } + } +} diff --git a/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/ByteUtils.kt b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/ByteUtils.kt new file mode 100644 index 0000000000..7010ef599f --- /dev/null +++ b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/ByteUtils.kt @@ -0,0 +1,7 @@ +package com.wire.crypto + +internal fun String.toByteArray() = encodeToByteArray() + +// this opt-in can be removed once the project updates to Kotlin 2.2 or higher +@OptIn(ExperimentalStdlibApi::class) +internal fun ByteArray.toHex(): String = toHexString() diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/CoreCrypto.kt b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/CoreCrypto.kt similarity index 100% rename from crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/CoreCrypto.kt rename to crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/CoreCrypto.kt diff --git a/crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/MlsModel.kt b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt similarity index 100% rename from crypto-ffi/bindings/jvm/src/main/kotlin/com/wire/crypto/MlsModel.kt rename to crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt From 7a9f95d50ed805caffc035d8a59f171429cf4da1 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 19 Dec 2025 23:05:12 +0100 Subject: [PATCH 2/2] feat: Add KMP module --- crypto-ffi/bindings/gradle/libs.versions.toml | 5 + crypto-ffi/bindings/kmp/build.gradle.kts | 177 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 crypto-ffi/bindings/kmp/build.gradle.kts diff --git a/crypto-ffi/bindings/gradle/libs.versions.toml b/crypto-ffi/bindings/gradle/libs.versions.toml index 4e34270a17..76995cb819 100644 --- a/crypto-ffi/bindings/gradle/libs.versions.toml +++ b/crypto-ffi/bindings/gradle/libs.versions.toml @@ -15,12 +15,17 @@ kotlin-gradle = "1.9.21" dokka = "2.0.0" detekt = "1.23.8" agp = "8.12.3" +gobley = "0.3.7" [plugins] android-library = { id = "com.android.library", version.ref = "agp" } vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +gobley-cargo = { id = "dev.gobley.cargo", version.ref = "gobley" } +gobley-uniffi = { id = "dev.gobley.uniffi", version.ref = "gobley" } +gobley-rust = { id = "dev.gobley.rust", version.ref = "gobley" } +kotlin-atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } [libraries] coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "coroutines" } diff --git a/crypto-ffi/bindings/kmp/build.gradle.kts b/crypto-ffi/bindings/kmp/build.gradle.kts new file mode 100644 index 0000000000..fea9a904ae --- /dev/null +++ b/crypto-ffi/bindings/kmp/build.gradle.kts @@ -0,0 +1,177 @@ +import gobley.gradle.GobleyHost +import gobley.gradle.cargo.dsl.* +import org.gradle.api.tasks.bundling.Jar + +plugins { + kotlin("multiplatform") + id("com.android.library") + alias(libs.plugins.gobley.cargo) + alias(libs.plugins.gobley.uniffi) + id("com.vanniktech.maven.publish.base") + alias(libs.plugins.kotlin.atomicfu) +} + +val dokkaHtmlJar = tasks.register("dokkaHtmlJar") { + dependsOn(tasks.dokkaGeneratePublicationHtml) + from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory }) + archiveClassifier.set("html-docs") +} + +kotlin { + jvmToolchain(17) + + // Android target + androidTarget { + publishLibraryVariants("release") + } + + // JVM target + jvm() + + // iOS targets + iosArm64() + iosSimulatorArm64() + + // macOS ARM64 target + macosArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.coroutines.core) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.coroutines.test) + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.jna) + } + } + + val jvmTest by getting { + dependencies { + implementation(libs.assertj.core) + } + } + + val androidMain by getting { + dependencies { + implementation("${libs.jna.get()}@aar") + implementation("androidx.annotation:annotation:1.9.1") + } + } + + val androidInstrumentedTest by getting { + dependencies { + implementation(libs.android.junit) + implementation(libs.espresso) + implementation(libs.assertj.core) + } + } + + // Native targets share sources + val nativeMain by creating { + dependsOn(commonMain) + } + + val nativeTest by creating { + dependsOn(commonTest) + } + + val iosArm64Main by getting { + dependsOn(nativeMain) + } + + val iosArm64Test by getting { + dependsOn(nativeTest) + } + + val iosSimulatorArm64Main by getting { + dependsOn(nativeMain) + } + + val iosSimulatorArm64Test by getting { + dependsOn(nativeTest) + } + + val macosArm64Main by getting { + dependsOn(nativeMain) + } + + val macosArm64Test by getting { + dependsOn(nativeTest) + } + } +} + +android { + namespace = "com.wire.crypto" + compileSdk = libs.versions.sdk.compile.get().toInt() + + defaultConfig { + minSdk = libs.versions.sdk.min.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters += setOf("arm64-v8a", "armeabi-v7a", "x86_64") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +cargo { + // Point to the crypto-ffi crate directory + packageDirectory = layout.projectDirectory.dir("../..") + + // Only build JVM native libraries for the current host platform + // This disables cross-compilation for other JVM targets (e.g., Linux ARM64 on macOS) + builds.jvm { + embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget) + } +} + +// Configure iOS build tasks with the required environment +afterEvaluate { + tasks.matching { it.name.contains("cargoBuildIos") }.configureEach { + // Set environment via the task's additionalEnvironment if available + if (this is gobley.gradle.cargo.tasks.CargoBuildTask) { + val target = if (name.contains("Simulator")) { + "IPHONESIMULATOR_DEPLOYMENT_TARGET" + } else { + "IPHONEOS_DEPLOYMENT_TARGET" + } + additionalEnvironment.put(target, "16.0") + } + } +} + +uniffi { + generateFromLibrary { + namespace = "core_crypto_ffi" + packageName = "com.wire.crypto" + } +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + pomFromGradleProperties() + signAllPublications() +} + +// Allows skipping signing jars published to 'MavenLocal' repository +tasks.withType().configureEach { + if (System.getenv("CI") == null) { + enabled = false + } +}