diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 42dc330..303e61e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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' @@ -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' @@ -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 diff --git a/build-logic/src/main/kotlin/-KmpConfigurationExtension.kt b/build-logic/src/main/kotlin/-KmpConfigurationExtension.kt index dbe0d03..d520d98 100644 --- a/build-logic/src/main/kotlin/-KmpConfigurationExtension.kt +++ b/build-logic/src/main/kotlin/-KmpConfigurationExtension.kt @@ -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, @@ -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 { diff --git a/build.gradle.kts b/build.gradle.kts index c9bb8d9..50f73f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) } diff --git a/gradle.properties b/gradle.properties index 56059ac..a431110 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99989e7..5a06706 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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] @@ -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" } diff --git a/library/crypto-rand/build.gradle.kts b/library/crypto-rand/build.gradle.kts index ac31ec5..66edf89 100644 --- a/library/crypto-rand/build.gradle.kts +++ b/library/crypto-rand/build.gradle.kts @@ -13,6 +13,19 @@ * 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") } @@ -20,6 +33,8 @@ plugins { kmpConfiguration { configureShared(java9ModuleName = "org.kotlincrypto.random", publish = true) { common { + pluginIds(libs.plugins.cklib.get().pluginId) + sourceSetMain { dependencies { api(libs.kotlincrypto.error) @@ -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 { @@ -52,5 +63,121 @@ kmpConfiguration { } } } + + kotlin { + val cInteropDir = projectDir + .resolve("src") + .resolve("nativeInterop") + .resolve("cinterop") + + val interopTaskInfo = targets.filterIsInstance().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("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() +} diff --git a/library/crypto-rand/src/androidNativeMain/kotlin/org/kotlincrypto/random/internal/AndroidNativePlatform.kt b/library/crypto-rand/src/androidNativeMain/kotlin/org/kotlincrypto/random/internal/AndroidNativePlatform.kt index b665724..4e8845e 100644 --- a/library/crypto-rand/src/androidNativeMain/kotlin/org/kotlincrypto/random/internal/AndroidNativePlatform.kt +++ b/library/crypto-rand/src/androidNativeMain/kotlin/org/kotlincrypto/random/internal/AndroidNativePlatform.kt @@ -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() diff --git a/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/AndroidNativeTestPlatform.kt b/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/AndroidNativeTestPlatform.kt new file mode 100644 index 0000000..a0c8b15 --- /dev/null +++ b/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/AndroidNativeTestPlatform.kt @@ -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 diff --git a/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/internal/CryptoRandAndroidNativeUnitTest.kt b/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/internal/CryptoRandAndroidNativeUnitTest.kt new file mode 100644 index 0000000..72b5571 --- /dev/null +++ b/library/crypto-rand/src/androidNativeTest/kotlin/org/kotlincrypto/random/internal/CryptoRandAndroidNativeUnitTest.kt @@ -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()}]", + ) + } +} diff --git a/library/crypto-rand/src/commonMain/kotlin/org/kotlincrypto/random/CryptoRand.kt b/library/crypto-rand/src/commonMain/kotlin/org/kotlincrypto/random/CryptoRand.kt index 405a070..a5ec153 100644 --- a/library/crypto-rand/src/commonMain/kotlin/org/kotlincrypto/random/CryptoRand.kt +++ b/library/crypto-rand/src/commonMain/kotlin/org/kotlincrypto/random/CryptoRand.kt @@ -55,8 +55,8 @@ public abstract class CryptoRand @DelicateCryptoRandApi protected constructor() * - WasmWasi: [random_get](https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#random_get) * - Native: * - Linux & Android Native targets: [getrandom(2)](https://www.man7.org/linux/man-pages/man2/getrandom.2.html) - * when available (GLIBC 2.25+ & Android API 23+), with a fallback to reading from `/dev/urandom` after polling - * `/dev/random` once (per process lifetime) to ensure appropriate levels of system entropy are had. + * when available (Linux Kernel 3.17+ & Android API 26+), with a fallback to reading from `/dev/urandom` after + * polling `/dev/random` once (per process lifetime) to ensure appropriate levels of system entropy are had. * - Apple targets: [CCRandomGenerateBytes](https://github.com/apple-oss-distributions/CommonCrypto/blob/main/include/CommonRandom.h) * - Windows targets: [BCryptGenRandom](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom) * */ diff --git a/library/crypto-rand/src/linuxAndroidMain/kotlin/org/kotlincrypto/random/internal/LinuxAndroidPlatform.kt b/library/crypto-rand/src/linuxAndroidMain/kotlin/org/kotlincrypto/random/internal/LinuxAndroidPlatform.kt index 75b7e66..ba18938 100644 --- a/library/crypto-rand/src/linuxAndroidMain/kotlin/org/kotlincrypto/random/internal/LinuxAndroidPlatform.kt +++ b/library/crypto-rand/src/linuxAndroidMain/kotlin/org/kotlincrypto/random/internal/LinuxAndroidPlatform.kt @@ -32,7 +32,7 @@ private inline fun getrandom2(buf: CPointer, buflen: size_t, flags: u_i return syscall(_SYS_getrandom().convert(), buf, buflen, flags).convert() } -// getrandom(2) available for Linux Kernel 3.17+ (Android API 23+) +// getrandom(2) available for Linux Kernel 3.17+ (Android API 26+) @OptIn(ExperimentalForeignApi::class, UnsafeNumber::class) internal val HAS_GET_RANDOM: Boolean by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { val buf = ByteArray(1) diff --git a/library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt b/library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt similarity index 69% rename from library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt rename to library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt index a29b471..fcba5bf 100644 --- a/library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt +++ b/library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/CryptoRandURandomUnitTest.kt @@ -18,22 +18,15 @@ package org.kotlincrypto.random import org.kotlincrypto.random.internal.HAS_GET_RANDOM import org.kotlincrypto.random.internal.cryptoRandFillURandom import kotlin.test.Test -import kotlin.test.assertTrue +import kotlin.test.assertEquals class CryptoRandURandomUnitTest: CryptoRandUnitTest() { override val cryptoRand: CryptoRand = URandom @Test - fun givenSystem_whenHasGetRandom_thenIsTrue() { - // Should always be true, unless linux box running this test - // is rocking GLIBC 2.24 or below... Which I don't even think - // Kotlin would run on? - // - // This simply confirms that the CryptoRand.Default.nextBytes - // is working as expected using getrandom(2) to source them - // when CryptoRandUnitTest is run from commonTest for Linux. - assertTrue(HAS_GET_RANDOM) + fun givenPlatform_whenHasGetRandom_thenIsAsExpected() { + assertEquals(SHOULD_HAVE_GET_RANDOM, HAS_GET_RANDOM) } @OptIn(DelicateCryptoRandApi::class) diff --git a/library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/LinuxAndroidTestPlatform.kt b/library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/LinuxAndroidTestPlatform.kt new file mode 100644 index 0000000..09df8ff --- /dev/null +++ b/library/crypto-rand/src/linuxAndroidTest/kotlin/org/kotlincrypto/random/LinuxAndroidTestPlatform.kt @@ -0,0 +1,18 @@ +/* + * 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 expect val SHOULD_HAVE_GET_RANDOM: Boolean diff --git a/library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/LinuxTestPlatform.kt b/library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/LinuxTestPlatform.kt new file mode 100644 index 0000000..7162169 --- /dev/null +++ b/library/crypto-rand/src/linuxTest/kotlin/org/kotlincrypto/random/LinuxTestPlatform.kt @@ -0,0 +1,25 @@ +/* + * 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 + +// Should always be true, unless linux box running this test +// is rocking GLIBC 2.24 or below... Which I don't even think +// Kotlin would run on? +// +// This simply confirms that the CryptoRand.Default.nextBytes +// is working as expected using getrandom(2) to source them +// when CryptoRandUnitTest is run from commonTest for Linux. +internal actual val SHOULD_HAVE_GET_RANDOM: Boolean = true diff --git a/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.c b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.c new file mode 100644 index 0000000..9bc3037 --- /dev/null +++ b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.c @@ -0,0 +1,27 @@ +/* + * 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. + **/ + +#include "crypto_rand_sys.h" + +#ifdef __ANDROID__ +#include + +int +__SYS_getrandom() +{ + return SYS_getrandom; +} +#endif /* !defined(__ANDROID__) */ diff --git a/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.def b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.def new file mode 100644 index 0000000..da08ffe --- /dev/null +++ b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.def @@ -0,0 +1,3 @@ +package = org.kotlincrypto.random.internal +headers = crypto_rand_sys.h +headerFilter = crypto_rand_sys.h diff --git a/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.h b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.h new file mode 100644 index 0000000..2c9dbbf --- /dev/null +++ b/library/crypto-rand/src/nativeInterop/cinterop/crypto_rand_sys.h @@ -0,0 +1,25 @@ +/* + * 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. + **/ + +/* https://youtrack.jetbrains.com/issue/KT-75722 */ +#ifndef CRYPTO_RAND_SYS_H +#define CRYPTO_RAND_SYS_H + +#ifdef __ANDROID__ +int __SYS_getrandom(); +#endif /* !defined(__ANDROID__) */ + +#endif /* !defined(CRYPTO_RAND_SYS_H) */ diff --git a/library/crypto-rand/src/nativeInterop/cinterop/syscall.def b/library/crypto-rand/src/nativeInterop/cinterop/syscall.def new file mode 100644 index 0000000..a806b30 --- /dev/null +++ b/library/crypto-rand/src/nativeInterop/cinterop/syscall.def @@ -0,0 +1,6 @@ +# SYS_getrandom for AndroidNative testing +package = org.kotlincrypto.random.internal.testing +--- +#ifdef __ANDROID__ +#include +#endif diff --git a/library/internal-cinterop/.gitignore b/library/internal-cinterop/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/library/internal-cinterop/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/library/internal-cinterop/README.md b/library/internal-cinterop/README.md deleted file mode 100644 index f6975d1..0000000 --- a/library/internal-cinterop/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Module internal-cinterop - -An internal module for which `crypto-rand` depends on. This is to transparently "hide" cinterop from -`crypto-rand` consumers because Kotlin's generated `.knm` files are all `public` visibility. - -This publication SHOULD NOT BE USED and will go away when [KT-75722](https://youtrack.jetbrains.com/issue/KT-75722) -is resolved. diff --git a/library/internal-cinterop/api/internal-cinterop.klib.api b/library/internal-cinterop/api/internal-cinterop.klib.api deleted file mode 100644 index 9f3a608..0000000 --- a/library/internal-cinterop/api/internal-cinterop.klib.api +++ /dev/null @@ -1,8 +0,0 @@ -// Klib ABI Dump -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Rendering settings: -// - Signature version: 2 -// - Show manifest properties: true -// - Show declarations: true - -// Library unique name: diff --git a/library/internal-cinterop/build.gradle.kts b/library/internal-cinterop/build.gradle.kts deleted file mode 100644 index 150ee00..0000000 --- a/library/internal-cinterop/build.gradle.kts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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. - **/ -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.konan.target.Family - -plugins { - id("configuration") -} - -kmpConfiguration { - configure { - options { - useUniqueModuleNames = true - } - - androidNativeAll() - - common { pluginIds("publication") } - - kotlin { explicitApi() } - - kotlin { - val cInteropDir = projectDir - .resolve("src") - .resolve("nativeInterop") - .resolve("cinterop") - - targets.filterIsInstance().forEach target@ { target -> - if (target.konanTarget.family != Family.ANDROID) return@target - target.compilations["main"].cinterops.create("syscall") { - definitionFile.set(cInteropDir.resolve("$name.def")) - } - } - } - } -} diff --git a/library/internal-cinterop/gradle.properties b/library/internal-cinterop/gradle.properties deleted file mode 100644 index 048dacb..0000000 --- a/library/internal-cinterop/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2023 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. -POM_ARTIFACT_ID=internal-cinterop -POM_NAME=Internal Module. Do not use. -POM_DESCRIPTION=Module for crypto-rand to consume in order to not expose cinterop declarations publicly diff --git a/library/internal-cinterop/src/nativeInterop/cinterop/syscall.def b/library/internal-cinterop/src/nativeInterop/cinterop/syscall.def deleted file mode 100644 index 6f1449d..0000000 --- a/library/internal-cinterop/src/nativeInterop/cinterop/syscall.def +++ /dev/null @@ -1,3 +0,0 @@ -# SYS_getrandom for AndroidNative targets -package = org.kotlincrypto.random.internal.cinterop -headers = sys/syscall.h diff --git a/settings.gradle.kts b/settings.gradle.kts index e291db1..561cb7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ rootProject.name = "random" pluginManagement { repositories { mavenCentral() + google() gradlePluginPortal() } } @@ -17,11 +18,11 @@ if (CHECK_PUBLICATION != null) { } else { listOf( "crypto-rand", - "internal-cinterop", ).forEach { name -> include(":library:$name") } include(":benchmarks") include(":sample") + include(":test-android") } diff --git a/test-android/.gitignore b/test-android/.gitignore new file mode 100644 index 0000000..8deec77 --- /dev/null +++ b/test-android/.gitignore @@ -0,0 +1,2 @@ +build/ +src/androidInstrumentedTest/jniLibs/ diff --git a/test-android/api/test-android.api b/test-android/api/test-android.api new file mode 100644 index 0000000..e69de29 diff --git a/test-android/api/test-android.klib.api b/test-android/api/test-android.klib.api new file mode 100644 index 0000000..e69de29 diff --git a/test-android/build.gradle.kts b/test-android/build.gradle.kts new file mode 100644 index 0000000..6a8fbd1 --- /dev/null +++ b/test-android/build.gradle.kts @@ -0,0 +1,122 @@ +/* + * 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. + **/ +import com.android.build.gradle.tasks.MergeSourceSetFolders + +plugins { + id("configuration") +} + +repositories { google() } + +kmpConfiguration { + configure { + val jniLibsDir = projectDir + .resolve("src") + .resolve("androidInstrumentedTest") + .resolve("jniLibs") + + project.tasks.all { + if (name != "clean") return@all + doLast { jniLibsDir.deleteRecursively() } + } + + androidLibrary { + android { + buildToolsVersion = "34.0.0" + compileSdk = 34 + namespace = "org.kotlincrypto.random.test.android" + + defaultConfig { + minSdk = 21 + + testInstrumentationRunnerArguments["disableAnalytics"] = true.toString() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + packaging.jniLibs.useLegacyPackaging = true + + sourceSets["androidTest"].jniLibs.srcDir(jniLibsDir) + } + + sourceSetTestInstrumented { + dependencies { + implementation(libs.androidx.test.core) + implementation(libs.androidx.test.runner) + implementation(libs.kmp.process) + } + } + + kotlinJvmTarget = JavaVersion.VERSION_1_8 + compileSourceCompatibility = JavaVersion.VERSION_1_8 + compileTargetCompatibility = JavaVersion.VERSION_1_8 + } + + common { + sourceSetTest { + dependencies { + implementation(kotlin("test")) + } + } + } + + kotlin { + if (!project.plugins.hasPlugin("com.android.base")) return@kotlin + + try { + project.evaluationDependsOn(":library:crypto-rand") + } catch (_: Throwable) {} + + val cryptoRandProject = project(":library:crypto-rand") + + val cryptoRandBuildDir = cryptoRandProject + .layout + .buildDirectory + .asFile.get() + + val nativeTestBinariesTasks = listOf( + "Arm32" to "armeabi-v7a", + "Arm64" to "arm64-v8a", + "X64" to "x86_64", + "X86" to "x86", + ).mapNotNull { (arch, abi) -> + val nativeTestBinariesTask = cryptoRandProject + .tasks + .findByName("androidNative${arch}TestBinaries") + ?: return@mapNotNull null + + val abiDir = jniLibsDir.resolve(abi) + if (!abiDir.exists() && !abiDir.mkdirs()) throw RuntimeException("mkdirs[$abiDir]") + + val testExecutable = cryptoRandBuildDir + .resolve("bin") + .resolve("androidNative$arch") + .resolve("debugTest") + .resolve("test.kexe") + + nativeTestBinariesTask.doLast { + testExecutable.copyTo(abiDir.resolve("libTestExec.so"), overwrite = true) + } + + nativeTestBinariesTask + } + + project.tasks.withType(MergeSourceSetFolders::class.java) { + if (name != "mergeDebugAndroidTestJniLibFolders") return@withType + nativeTestBinariesTasks.forEach { task -> this.dependsOn(task) } + } + } + } +} diff --git a/test-android/src/androidInstrumentedTest/kotlin/org/kotlincrypto/random/test/android/AndroidNativeTest.kt b/test-android/src/androidInstrumentedTest/kotlin/org/kotlincrypto/random/test/android/AndroidNativeTest.kt new file mode 100644 index 0000000..d3b79b7 --- /dev/null +++ b/test-android/src/androidInstrumentedTest/kotlin/org/kotlincrypto/random/test/android/AndroidNativeTest.kt @@ -0,0 +1,42 @@ +/* + * 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.test.android + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import io.matthewnelson.kmp.file.toFile +import io.matthewnelson.kmp.process.Process +import kotlin.test.Test +import kotlin.test.assertEquals + +class AndroidNativeTest { + + private val ctx = ApplicationProvider.getApplicationContext().applicationContext + private val nativeLibraryDir = ctx.applicationInfo.nativeLibraryDir.toFile().absoluteFile + + @Test + fun givenAndroidNative_whenExecuteTestBinary_thenIsSuccessful() { + val out = Process.Builder(executable = nativeLibraryDir.resolve("libTestExec.so")) + .output { + timeoutMillis = 5_000 + maxBuffer = Int.MAX_VALUE / 2 + } + + assertEquals(0, out.processInfo.exitCode, out.stdout) + println(out.stdout) + println(out.stderr) + } +} diff --git a/test-android/src/androidMain/AndroidManifest.xml b/test-android/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..696bcc7 --- /dev/null +++ b/test-android/src/androidMain/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/library/internal-cinterop/src/commonMain/kotlin/org/kotlincrypto/random/internal/cinterop/Stub.kt b/test-android/src/androidMain/kotlin/org/kotlincrypto/random/test/android/-Stub.kt similarity index 90% rename from library/internal-cinterop/src/commonMain/kotlin/org/kotlincrypto/random/internal/cinterop/Stub.kt rename to test-android/src/androidMain/kotlin/org/kotlincrypto/random/test/android/-Stub.kt index 87c1bbe..305e384 100644 --- a/library/internal-cinterop/src/commonMain/kotlin/org/kotlincrypto/random/internal/cinterop/Stub.kt +++ b/test-android/src/androidMain/kotlin/org/kotlincrypto/random/test/android/-Stub.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -package org.kotlincrypto.random.internal.cinterop +package org.kotlincrypto.random.test.android -@Suppress("unused") +@Suppress("UNUSED") internal fun stub() { /* no-op */ }