Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 11
java-version: 17

- name: Check API Compatibility
if: matrix.os == 'macos-latest'
Expand All @@ -40,7 +40,7 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: >
./gradlew check --stacktrace
-PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"
-PKMP_TARGETS="ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM,JS,LINUX_ARM64,LINUX_X64,WASM_JS,WASM_WASI"

- name: Run Windows Tests
if: matrix.os == 'windows-latest'
Expand Down Expand Up @@ -85,3 +85,74 @@ jobs:
with:
name: benchmark-report-${{ matrix.os }}
path: '**/build/reports/benchmarks/**'

android-check:
strategy:
fail-fast: false
matrix:
include:
- api-level: 21
arch: x86_64
- api-level: 22
arch: x86_64
- api-level: 23
arch: x86_64
- api-level: 24
arch: x86_64
- api-level: 25
arch: x86
- api-level: 26
arch: x86_64
- api-level: 27
arch: x86_64
# - api-level: 28
# arch: x86_64
- api-level: 29
arch: x86
- api-level: 30
arch: x86_64
- api-level: 31
arch: x86_64
- api-level: 32
arch: x86_64
- api-level: 33
arch: x86_64
- api-level: 34
arch: x86_64
- api-level: 35
arch: x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v3

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17

- name: Run Android Instrumented Tests
uses: reactivecircus/android-emulator-runner@v2
with:
emulator-boot-timeout: 300 # 5 minutes
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.arch }}
script: ./gradlew connectedCheck -PKMP_TARGETS="ANDROID,ANDROID_ARM32,ANDROID_ARM64,ANDROID_X64,ANDROID_X86,JVM"

- name: Upload Test Reports
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-report-android-${{ matrix.api-level }}-${{ matrix.arch }}
path: '**/build/reports/androidTests/**'
retention-days: 1
6 changes: 5 additions & 1 deletion build-logic/src/main/kotlin/-KmpConfigurationExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.matthewnelson.kmp.configuration.extension.container.target.KmpConfigur
import org.gradle.api.Action
import org.gradle.api.JavaVersion
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.konan.target.HostManager

fun KmpConfigurationExtension.configureShared(
java9ModuleName: String? = null,
Expand All @@ -38,7 +39,10 @@ fun KmpConfigurationExtension.configureShared(
compileSourceCompatibility = JavaVersion.VERSION_1_8
compileTargetCompatibility = JavaVersion.VERSION_1_8

java9ModuleInfoName = java9ModuleName
// Windows cries if Java 11 is not installed...
if (!HostManager.hostIsMingw) {
java9ModuleInfoName = java9ModuleName
}
}

js {
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension

plugins {
alias(libs.plugins.android.library) apply(false)
alias(libs.plugins.benchmark) apply(false)
alias(libs.plugins.binary.compat)
alias(libs.plugins.cklib) apply(false)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.multiplatform) apply(false)
}
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true

android.useAndroidX=true
android.enableJetifier=true

kotlin.code.style=official
kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.mpp.enableCInteropCommonization=true
Expand Down
11 changes: 11 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
[versions]
androidx-test-core = "1.6.1"
androidx-test-runner = "1.6.2"

gradle-android = "8.7.3"
gradle-benchmark = "0.4.13"
gradle-binary-compat = "0.17.0"
gradle-cklib = "0.3.3"
gradle-dokka = "2.0.0"
gradle-kmp-configuration = "0.4.0"
gradle-kotlin = "2.1.10"
gradle-publish-maven = "0.30.0"

kmp-process = "0.2.1"
kotlincrypto-error = "0.3.0"

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

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

[plugins]
android-library = { id = "com.android.library", version.ref = "gradle-android" }
benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "gradle-benchmark" }
binary-compat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "gradle-binary-compat" }
cklib = { id = "co.touchlab.cklib", version.ref = "gradle-cklib" }
dokka = { id = "org.jetbrains.dokka", version.ref = "gradle-dokka" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "gradle-kotlin" }
137 changes: 132 additions & 5 deletions library/crypto-rand/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
import co.touchlab.cklib.gradle.CKlibGradleExtension
import co.touchlab.cklib.gradle.CompileToBitcode
import co.touchlab.cklib.gradle.CompileToBitcodeExtension
import org.gradle.accessors.dm.LibrariesForLibs
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.TargetSupportException
import org.jetbrains.kotlin.konan.util.ArchiveType
import org.jetbrains.kotlin.konan.util.DependencyProcessor
import org.jetbrains.kotlin.konan.util.DependencySource

plugins {
id("configuration")
}

kmpConfiguration {
configureShared(java9ModuleName = "org.kotlincrypto.random", publish = true) {
common {
pluginIds(libs.plugins.cklib.get().pluginId)

sourceSetMain {
dependencies {
api(libs.kotlincrypto.error)
Expand All @@ -30,11 +45,7 @@ kmpConfiguration {
kotlin {
with(sourceSets) {
val linuxMain = findByName("linuxMain")
val androidNativeMain = findByName("androidNativeMain")?.apply {
dependencies {
implementation(project(":library:internal-cinterop"))
}
}
val androidNativeMain = findByName("androidNativeMain")

if (linuxMain != null || androidNativeMain != null) {
val linuxAndroidMain = maybeCreate("linuxAndroidMain").apply {
Expand All @@ -52,5 +63,121 @@ kmpConfiguration {
}
}
}

kotlin {
val cInteropDir = projectDir
.resolve("src")
.resolve("nativeInterop")
.resolve("cinterop")

val interopTaskInfo = targets.filterIsInstance<KotlinNativeTarget>().map { target ->
if (target.konanTarget.family == Family.ANDROID) {
target.compilations["main"].cinterops.create("crypto_rand_sys") {
definitionFile.set(cInteropDir.resolve("$name.def"))
includeDirs(cInteropDir)
}
}

target.compilations["test"].cinterops.create("syscall") {
definitionFile.set(cInteropDir.resolve("$name.def"))
}.interopProcessingTaskName to target.konanTarget
}

project.extensions.configure<CompileToBitcodeExtension>("cklib") {
config.configure(libs)

create("crypto_rand_sys") {
language = CompileToBitcode.Language.C
srcDirs = project.files(cInteropDir)
includeFiles = listOf("$compileName.c")

listOf(
"-Wno-unused-command-line-argument",
).let { compilerArgs.addAll(it) }

val kt = KonanTarget.predefinedTargets[target]!!

// Must add dependency on the test cinterop task to ensure
// that Kotlin/Native dependencies get downloaded beforehand
interopTaskInfo.forEach { (interopTaskName, konanTarget) ->
if (kt != konanTarget) return@forEach
this.dependsOn(interopTaskName)
}
}
}
}
}
}

// CKLib uses too old of a version of LLVM for current version of Kotlin which produces errors for android
// native due to unsupported link arguments. Below is a supplemental implementation to download and use
// the -dev llvm compiler for the current kotlin version.
//
// The following info can be found in ~/.konan/kotlin-native-prebuild-{os}-{arch}-{kotlin version}/konan/konan.properties
private object LLVM {
const val URL: String = "https://download.jetbrains.com/kotlin/native/resources/llvm"
const val VERSION: String = "16.0.0"

// llvm-{llvm version}-{arch}-{host}-dev-{id}
object DevID {
object Linux {
const val x86_64: Int = 80
}
object MacOS {
const val aarch64: Int = 63
const val x86_64: Int = 50
}
object MinGW {
const val x86_64: Int = 56
}
}
}

private fun CKlibGradleExtension.configure(libs: LibrariesForLibs) {
kotlinVersion = libs.versions.gradle.kotlin.get()
check(kotlinVersion == "2.1.10") {
"Kotlin version out of date! Download URLs for LLVM need to be updated for ${project.path}"
}

val host = HostManager.simpleOsName()
val arch = HostManager.hostArch()
val (id, archive) = when (host) {
"linux" -> when (arch) {
"x86_64" -> LLVM.DevID.Linux.x86_64 to ArchiveType.TAR_GZ
else -> null
}
"macos" -> when (arch) {
"aarch64" -> LLVM.DevID.MacOS.aarch64 to ArchiveType.TAR_GZ
"x86_64" -> LLVM.DevID.MacOS.x86_64 to ArchiveType.TAR_GZ
else -> null
}
"windows" -> when (arch) {
"x86_64" -> LLVM.DevID.MinGW.x86_64 to ArchiveType.ZIP
else -> null
}
else -> null
} ?: throw TargetSupportException("Unsupported host[$host] or arch[$arch]")

val llvmDev = "llvm-${LLVM.VERSION}-${arch}-${host}-dev-${id}"
val cklibDir = File(System.getProperty("user.home")).resolve(".cklib")
llvmHome = cklibDir.resolve(llvmDev).path

val source = DependencySource.Remote.Public(subDirectory = "${LLVM.VERSION}-${arch}-${host}")

DependencyProcessor(
dependenciesRoot = cklibDir,
dependenciesUrl = LLVM.URL,
dependencyToCandidates = mapOf(llvmDev to listOf(source)),
homeDependencyCache = cklibDir.resolve("cache"),
customProgressCallback = { _, currentBytes, totalBytes ->
val total = totalBytes.toString()
var current = currentBytes.toString()
while (current.length < 15 && current.length < total.length) {
current = " $current"
}

println("Downloading[$llvmDev] - $current / $total")
},
archiveType = archive,
).run()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
package org.kotlincrypto.random.internal

import kotlinx.cinterop.ExperimentalForeignApi
import org.kotlincrypto.random.internal.cinterop.SYS_getrandom

@OptIn(ExperimentalForeignApi::class)
internal actual inline fun _SYS_getrandom(): Int = SYS_getrandom
// https://youtrack.jetbrains.com/issue/KT-75722
@ExperimentalForeignApi
internal actual inline fun _SYS_getrandom(): Int = __SYS_getrandom()
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package org.kotlincrypto.random

import platform.posix.android_get_device_api_level

internal actual val SHOULD_HAVE_GET_RANDOM: Boolean = android_get_device_api_level() >= 26
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package org.kotlincrypto.random.internal

import kotlinx.cinterop.ExperimentalForeignApi
import org.kotlincrypto.random.internal.testing.SYS_getrandom
import kotlin.test.Test
import kotlin.test.assertEquals

@OptIn(ExperimentalForeignApi::class)
class CryptoRandAndroidNativeUnitTest {

@Test
fun givenSYSgetrandom_whenCheckedAgainstHeaderDefinition_thenMatches() {
assertEquals(
SYS_getrandom,
_SYS_getrandom(),
"expected[${SYS_getrandom}] vs actual[${_SYS_getrandom()}]",
)
}
}
Loading