Skip to content

Commit c945db4

Browse files
authored
Replace module internal-cinterop with CKLib compilations (#40)
1 parent 9336430 commit c945db4

32 files changed

Lines changed: 580 additions & 111 deletions

File tree

.github/workflows/CI.yml

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
uses: actions/setup-java@v4
2424
with:
2525
distribution: 'zulu'
26-
java-version: 11
26+
java-version: 17
2727

2828
- name: Check API Compatibility
2929
if: matrix.os == 'macos-latest'
@@ -40,7 +40,7 @@ jobs:
4040
if: matrix.os == 'ubuntu-latest'
4141
run: >
4242
./gradlew check --stacktrace
43-
-PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"
43+
-PKMP_TARGETS="ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"
4444
4545
- name: Run Windows Tests
4646
if: matrix.os == 'windows-latest'
@@ -85,3 +85,74 @@ jobs:
8585
with:
8686
name: benchmark-report-${{ matrix.os }}
8787
path: '**/build/reports/benchmarks/**'
88+
89+
android-check:
90+
strategy:
91+
fail-fast: false
92+
matrix:
93+
include:
94+
- api-level: 21
95+
arch: x86_64
96+
- api-level: 22
97+
arch: x86_64
98+
- api-level: 23
99+
arch: x86_64
100+
- api-level: 24
101+
arch: x86_64
102+
- api-level: 25
103+
arch: x86
104+
- api-level: 26
105+
arch: x86_64
106+
- api-level: 27
107+
arch: x86_64
108+
# - api-level: 28
109+
# arch: x86_64
110+
- api-level: 29
111+
arch: x86
112+
- api-level: 30
113+
arch: x86_64
114+
- api-level: 31
115+
arch: x86_64
116+
- api-level: 32
117+
arch: x86_64
118+
- api-level: 33
119+
arch: x86_64
120+
- api-level: 34
121+
arch: x86_64
122+
- api-level: 35
123+
arch: x86_64
124+
runs-on: ubuntu-latest
125+
steps:
126+
- name: Checkout Repository
127+
uses: actions/checkout@v4
128+
129+
- name: Enable KVM
130+
run: |
131+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
132+
sudo udevadm control --reload-rules
133+
sudo udevadm trigger --name-match=kvm
134+
135+
- name: Validate Gradle Wrapper
136+
uses: gradle/actions/wrapper-validation@v3
137+
138+
- name: Setup JDK
139+
uses: actions/setup-java@v4
140+
with:
141+
distribution: 'zulu'
142+
java-version: 17
143+
144+
- name: Run Android Instrumented Tests
145+
uses: reactivecircus/android-emulator-runner@v2
146+
with:
147+
emulator-boot-timeout: 300 # 5 minutes
148+
api-level: ${{ matrix.api-level }}
149+
arch: ${{ matrix.arch }}
150+
script: ./gradlew connectedCheck -PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM"
151+
152+
- name: Upload Test Reports
153+
uses: actions/upload-artifact@v4
154+
if: ${{ always() }}
155+
with:
156+
name: test-report-android-${{ matrix.api-level }}-${{ matrix.arch }}
157+
path: '**/build/reports/androidTests/**'
158+
retention-days: 1

build-logic/src/main/kotlin/-KmpConfigurationExtension.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.matthewnelson.kmp.configuration.extension.container.target.KmpConfigur
1818
import org.gradle.api.Action
1919
import org.gradle.api.JavaVersion
2020
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
21+
import org.jetbrains.kotlin.konan.target.HostManager
2122

2223
fun KmpConfigurationExtension.configureShared(
2324
java9ModuleName: String? = null,
@@ -38,7 +39,10 @@ fun KmpConfigurationExtension.configureShared(
3839
compileSourceCompatibility = JavaVersion.VERSION_1_8
3940
compileTargetCompatibility = JavaVersion.VERSION_1_8
4041

41-
java9ModuleInfoName = java9ModuleName
42+
// Windows cries if Java 11 is not installed...
43+
if (!HostManager.hostIsMingw) {
44+
java9ModuleInfoName = java9ModuleName
45+
}
4246
}
4347

4448
js {

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
1818
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
1919

2020
plugins {
21+
alias(libs.plugins.android.library) apply(false)
2122
alias(libs.plugins.benchmark) apply(false)
2223
alias(libs.plugins.binary.compat)
24+
alias(libs.plugins.cklib) apply(false)
2325
alias(libs.plugins.dokka)
2426
alias(libs.plugins.kotlin.multiplatform) apply(false)
2527
}

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
22
org.gradle.parallel=true
33
org.gradle.caching=true
44

5+
android.useAndroidX=true
6+
android.enableJetifier=true
7+
58
kotlin.code.style=official
69
kotlin.mpp.applyDefaultHierarchyTemplate=false
710
kotlin.mpp.enableCInteropCommonization=true

gradle/libs.versions.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
[versions]
2+
androidx-test-core = "1.6.1"
3+
androidx-test-runner = "1.6.2"
4+
5+
gradle-android = "8.7.3"
26
gradle-benchmark = "0.4.13"
37
gradle-binary-compat = "0.17.0"
8+
gradle-cklib = "0.3.3"
49
gradle-dokka = "2.0.0"
510
gradle-kmp-configuration = "0.4.0"
611
gradle-kotlin = "2.1.10"
712
gradle-publish-maven = "0.30.0"
813

14+
kmp-process = "0.2.1"
915
kotlincrypto-error = "0.3.0"
1016

1117
[libraries]
@@ -17,10 +23,15 @@ gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-pl
1723
kotlincrypto-error = { module = "org.kotlincrypto:error", version.ref = "kotlincrypto-error" }
1824

1925
# tests & tooling
26+
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
27+
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
2028
benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "gradle-benchmark" }
29+
kmp-process = { module = "io.matthewnelson.kmp-process:process", version.ref = "kmp-process" }
2130

2231
[plugins]
32+
android-library = { id = "com.android.library", version.ref = "gradle-android" }
2333
benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "gradle-benchmark" }
2434
binary-compat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "gradle-binary-compat" }
35+
cklib = { id = "co.touchlab.cklib", version.ref = "gradle-cklib" }
2536
dokka = { id = "org.jetbrains.dokka", version.ref = "gradle-dokka" }
2637
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "gradle-kotlin" }

library/crypto-rand/build.gradle.kts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,28 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
**/
16+
import co.touchlab.cklib.gradle.CKlibGradleExtension
17+
import co.touchlab.cklib.gradle.CompileToBitcode
18+
import co.touchlab.cklib.gradle.CompileToBitcodeExtension
19+
import org.gradle.accessors.dm.LibrariesForLibs
20+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
21+
import org.jetbrains.kotlin.konan.target.Family
22+
import org.jetbrains.kotlin.konan.target.HostManager
23+
import org.jetbrains.kotlin.konan.target.KonanTarget
24+
import org.jetbrains.kotlin.konan.target.TargetSupportException
25+
import org.jetbrains.kotlin.konan.util.ArchiveType
26+
import org.jetbrains.kotlin.konan.util.DependencyProcessor
27+
import org.jetbrains.kotlin.konan.util.DependencySource
28+
1629
plugins {
1730
id("configuration")
1831
}
1932

2033
kmpConfiguration {
2134
configureShared(java9ModuleName = "org.kotlincrypto.random", publish = true) {
2235
common {
36+
pluginIds(libs.plugins.cklib.get().pluginId)
37+
2338
sourceSetMain {
2439
dependencies {
2540
api(libs.kotlincrypto.error)
@@ -30,11 +45,7 @@ kmpConfiguration {
3045
kotlin {
3146
with(sourceSets) {
3247
val linuxMain = findByName("linuxMain")
33-
val androidNativeMain = findByName("androidNativeMain")?.apply {
34-
dependencies {
35-
implementation(project(":library:internal-cinterop"))
36-
}
37-
}
48+
val androidNativeMain = findByName("androidNativeMain")
3849

3950
if (linuxMain != null || androidNativeMain != null) {
4051
val linuxAndroidMain = maybeCreate("linuxAndroidMain").apply {
@@ -52,5 +63,121 @@ kmpConfiguration {
5263
}
5364
}
5465
}
66+
67+
kotlin {
68+
val cInteropDir = projectDir
69+
.resolve("src")
70+
.resolve("nativeInterop")
71+
.resolve("cinterop")
72+
73+
val interopTaskInfo = targets.filterIsInstance<KotlinNativeTarget>().map { target ->
74+
if (target.konanTarget.family == Family.ANDROID) {
75+
target.compilations["main"].cinterops.create("crypto_rand_sys") {
76+
definitionFile.set(cInteropDir.resolve("$name.def"))
77+
includeDirs(cInteropDir)
78+
}
79+
}
80+
81+
target.compilations["test"].cinterops.create("syscall") {
82+
definitionFile.set(cInteropDir.resolve("$name.def"))
83+
}.interopProcessingTaskName to target.konanTarget
84+
}
85+
86+
project.extensions.configure<CompileToBitcodeExtension>("cklib") {
87+
config.configure(libs)
88+
89+
create("crypto_rand_sys") {
90+
language = CompileToBitcode.Language.C
91+
srcDirs = project.files(cInteropDir)
92+
includeFiles = listOf("$compileName.c")
93+
94+
listOf(
95+
"-Wno-unused-command-line-argument",
96+
).let { compilerArgs.addAll(it) }
97+
98+
val kt = KonanTarget.predefinedTargets[target]!!
99+
100+
// Must add dependency on the test cinterop task to ensure
101+
// that Kotlin/Native dependencies get downloaded beforehand
102+
interopTaskInfo.forEach { (interopTaskName, konanTarget) ->
103+
if (kt != konanTarget) return@forEach
104+
this.dependsOn(interopTaskName)
105+
}
106+
}
107+
}
108+
}
55109
}
56110
}
111+
112+
// CKLib uses too old of a version of LLVM for current version of Kotlin which produces errors for android
113+
// native due to unsupported link arguments. Below is a supplemental implementation to download and use
114+
// the -dev llvm compiler for the current kotlin version.
115+
//
116+
// The following info can be found in ~/.konan/kotlin-native-prebuild-{os}-{arch}-{kotlin version}/konan/konan.properties
117+
private object LLVM {
118+
const val URL: String = "https://download.jetbrains.com/kotlin/native/resources/llvm"
119+
const val VERSION: String = "16.0.0"
120+
121+
// llvm-{llvm version}-{arch}-{host}-dev-{id}
122+
object DevID {
123+
object Linux {
124+
const val x86_64: Int = 80
125+
}
126+
object MacOS {
127+
const val aarch64: Int = 63
128+
const val x86_64: Int = 50
129+
}
130+
object MinGW {
131+
const val x86_64: Int = 56
132+
}
133+
}
134+
}
135+
136+
private fun CKlibGradleExtension.configure(libs: LibrariesForLibs) {
137+
kotlinVersion = libs.versions.gradle.kotlin.get()
138+
check(kotlinVersion == "2.1.10") {
139+
"Kotlin version out of date! Download URLs for LLVM need to be updated for ${project.path}"
140+
}
141+
142+
val host = HostManager.simpleOsName()
143+
val arch = HostManager.hostArch()
144+
val (id, archive) = when (host) {
145+
"linux" -> when (arch) {
146+
"x86_64" -> LLVM.DevID.Linux.x86_64 to ArchiveType.TAR_GZ
147+
else -> null
148+
}
149+
"macos" -> when (arch) {
150+
"aarch64" -> LLVM.DevID.MacOS.aarch64 to ArchiveType.TAR_GZ
151+
"x86_64" -> LLVM.DevID.MacOS.x86_64 to ArchiveType.TAR_GZ
152+
else -> null
153+
}
154+
"windows" -> when (arch) {
155+
"x86_64" -> LLVM.DevID.MinGW.x86_64 to ArchiveType.ZIP
156+
else -> null
157+
}
158+
else -> null
159+
} ?: throw TargetSupportException("Unsupported host[$host] or arch[$arch]")
160+
161+
val llvmDev = "llvm-${LLVM.VERSION}-${arch}-${host}-dev-${id}"
162+
val cklibDir = File(System.getProperty("user.home")).resolve(".cklib")
163+
llvmHome = cklibDir.resolve(llvmDev).path
164+
165+
val source = DependencySource.Remote.Public(subDirectory = "${LLVM.VERSION}-${arch}-${host}")
166+
167+
DependencyProcessor(
168+
dependenciesRoot = cklibDir,
169+
dependenciesUrl = LLVM.URL,
170+
dependencyToCandidates = mapOf(llvmDev to listOf(source)),
171+
homeDependencyCache = cklibDir.resolve("cache"),
172+
customProgressCallback = { _, currentBytes, totalBytes ->
173+
val total = totalBytes.toString()
174+
var current = currentBytes.toString()
175+
while (current.length < 15 && current.length < total.length) {
176+
current = " $current"
177+
}
178+
179+
println("Downloading[$llvmDev] - $current / $total")
180+
},
181+
archiveType = archive,
182+
).run()
183+
}

library/crypto-rand/src/androidNativeMain/kotlin/org/kotlincrypto/random/internal/AndroidNativePlatform.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
package org.kotlincrypto.random.internal
1919

2020
import kotlinx.cinterop.ExperimentalForeignApi
21-
import org.kotlincrypto.random.internal.cinterop.SYS_getrandom
2221

23-
@OptIn(ExperimentalForeignApi::class)
24-
internal actual inline fun _SYS_getrandom(): Int = SYS_getrandom
22+
// https://youtrack.jetbrains.com/issue/KT-75722
23+
@ExperimentalForeignApi
24+
internal actual inline fun _SYS_getrandom(): Int = __SYS_getrandom()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (c) 2025 KotlinCrypto
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
package org.kotlincrypto.random
17+
18+
import platform.posix.android_get_device_api_level
19+
20+
internal actual val SHOULD_HAVE_GET_RANDOM: Boolean = android_get_device_api_level() >= 26
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025 KotlinCrypto
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
package org.kotlincrypto.random.internal
17+
18+
import kotlinx.cinterop.ExperimentalForeignApi
19+
import org.kotlincrypto.random.internal.testing.SYS_getrandom
20+
import kotlin.test.Test
21+
import kotlin.test.assertEquals
22+
23+
@OptIn(ExperimentalForeignApi::class)
24+
class CryptoRandAndroidNativeUnitTest {
25+
26+
@Test
27+
fun givenSYSgetrandom_whenCheckedAgainstHeaderDefinition_thenMatches() {
28+
assertEquals(
29+
SYS_getrandom,
30+
_SYS_getrandom(),
31+
"expected[${SYS_getrandom}] vs actual[${_SYS_getrandom()}]",
32+
)
33+
}
34+
}

0 commit comments

Comments
 (0)