-
Notifications
You must be signed in to change notification settings - Fork 352
feat(kotlin-sdk): implement secure native library downloading with SH… #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| plugins { | ||
| `kotlin-dsl` | ||
| } | ||
|
|
||
| repositories { | ||
| mavenCentral() | ||
| google() | ||
| gradlePluginPortal() | ||
| } | ||
|
|
||
| gradlePlugin { | ||
| plugins { | ||
| create("nativeDownloader") { | ||
| id = "com.runanywhere.native-downloader" | ||
| implementationClass = "com.runanywhere.buildlogic.NativeLibraryDownloadPlugin" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| dependencies { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| rootProject.name = "build-logic" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||||||||||||||||||||||||
| package com.runanywhere.buildlogic | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import org.gradle.api.DefaultTask | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.Plugin | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.Project | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.file.ArchiveOperations | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.file.DirectoryProperty | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.file.FileSystemOperations | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.file.RelativePath | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.provider.ListProperty | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.provider.Property | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.tasks.Input | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.tasks.OutputDirectory | ||||||||||||||||||||||||||||||||||
| import org.gradle.api.tasks.TaskAction | ||||||||||||||||||||||||||||||||||
| import java.io.File | ||||||||||||||||||||||||||||||||||
| import java.net.HttpURLConnection | ||||||||||||||||||||||||||||||||||
| import java.net.URL | ||||||||||||||||||||||||||||||||||
| import java.security.MessageDigest | ||||||||||||||||||||||||||||||||||
| import javax.inject.Inject | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // A Gradle plugin that provides a task to securely download and verify native libraries using SHA-256 checksums. | ||||||||||||||||||||||||||||||||||
| abstract class DownloadAndVerifyNativeLibTask @Inject constructor( | ||||||||||||||||||||||||||||||||||
| private val fsOps: FileSystemOperations, | ||||||||||||||||||||||||||||||||||
| private val archiveOps: ArchiveOperations | ||||||||||||||||||||||||||||||||||
| ) : DefaultTask() { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Inputs for the task: download URL, expected SHA256 URL, allowed .so files, and output directory | ||||||||||||||||||||||||||||||||||
| @get:Input | ||||||||||||||||||||||||||||||||||
| abstract val downloadUrl: Property<String> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @get:Input | ||||||||||||||||||||||||||||||||||
| abstract val expectedSha256Url: Property<String> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @get:Input | ||||||||||||||||||||||||||||||||||
| abstract val allowedSoFiles: ListProperty<String> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @get:OutputDirectory | ||||||||||||||||||||||||||||||||||
| abstract val outputDir: DirectoryProperty | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // The main action of the task: download the file, verify its checksum, and extract it if valid | ||||||||||||||||||||||||||||||||||
| @TaskAction | ||||||||||||||||||||||||||||||||||
| fun execute() { | ||||||||||||||||||||||||||||||||||
| val url = downloadUrl.get() | ||||||||||||||||||||||||||||||||||
| val shaUrl = expectedSha256Url.get() | ||||||||||||||||||||||||||||||||||
| val validFiles = allowedSoFiles.get() | ||||||||||||||||||||||||||||||||||
| val destDir = outputDir.get().asFile | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Ensure output directory exists | ||||||||||||||||||||||||||||||||||
| destDir.mkdirs() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| val tempFile = File(temporaryDir, "downloaded.zip") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| logger.lifecycle("Fetching expected SHA256 from $shaUrl...") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Grab the first sequence of non-whitespace characters (the hash itself) | ||||||||||||||||||||||||||||||||||
| val expectedHash = URL(shaUrl).readText().trim().split("\\s+".toRegex()).first() | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| val expectedHash = URL(shaUrl).readText().trim().split("\\s+".toRegex()).first() | |
| var shaConnection: HttpURLConnection? = null | |
| val expectedHash: String | |
| try { | |
| shaConnection = URL(shaUrl).openConnection() as HttpURLConnection | |
| shaConnection.connectTimeout = 30_000 | |
| shaConnection.readTimeout = 30_000 | |
| expectedHash = shaConnection.inputStream.bufferedReader() | |
| .use { it.readText() } | |
| .trim() | |
| .split("\\s+".toRegex()) | |
| .first() | |
| } finally { | |
| shaConnection?.disconnect() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt`
at line 56, Replace the unprotected URL(shaUrl).readText() call used to compute
expectedHash with an HttpURLConnection-based fetch that sets connectTimeout to
30_000 and readTimeout to 120_000, reads the response body (respecting charset),
trims and splits on whitespace to get the first token; ensure you open the
connection from URL(shaUrl).openConnection() as HttpURLConnection, call
connect(), read the stream into a String, and disconnect/close streams when done
so the SHA256 fetch uses the same timeouts as the main download (reference
expectedHash variable and the same timeout values used in the main download
logic).
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Temp file cleanup happens inside the error message construction, which only executes if the check fails. If verification succeeds but extraction fails later, tempFile won't be deleted until line 109. Consider using try-finally or tempFile.deleteOnExit() for guaranteed cleanup
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt
Line: 85-86
Comment:
Temp file cleanup happens inside the error message construction, which only executes if the check fails. If verification succeeds but extraction fails later, `tempFile` won't be deleted until line 109. Consider using try-finally or `tempFile.deleteOnExit()` for guaranteed cleanup
How can I resolve this? If you propose a fix, please make it concise.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exclude() is not valid inside eachFile. To skip files, set relativePath to null or omit the file
| exclude() | |
| exclude() |
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt
Line: 103
Comment:
`exclude()` is not valid inside `eachFile`. To skip files, set `relativePath` to `null` or omit the file
```suggestion
exclude()
```
How can I resolve this? If you propose a fix, please make it concise.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File filtering logic will include all .so files first (line 97), then try to filter in eachFile. Files not in validFiles list will still be included because exclude() inside eachFile doesn't work as expected. Use include with explicit patterns or filter differently
| include("**/*.so") | |
| eachFile { | |
| if (validFiles.contains(name)) { | |
| // Flatten the directory structure | |
| relativePath = RelativePath(true, name) | |
| } else { | |
| exclude() | |
| } | |
| } | |
| include { element -> | |
| element.isDirectory || (element.name.endsWith(".so") && validFiles.contains(element.name)) | |
| } | |
| eachFile { | |
| // Flatten the directory structure | |
| relativePath = RelativePath(true, name) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt
Line: 97-105
Comment:
File filtering logic will include all `.so` files first (line 97), then try to filter in `eachFile`. Files not in `validFiles` list will still be included because `exclude()` inside `eachFile` doesn't work as expected. Use `include` with explicit patterns or filter differently
```suggestion
include { element ->
element.isDirectory || (element.name.endsWith(".so") && validFiles.contains(element.name))
}
eachFile {
// Flatten the directory structure
relativePath = RelativePath(true, name)
}
```
How can I resolve this? If you propose a fix, please make it concise.
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| */ | ||
|
|
||
| plugins { | ||
| id("com.runanywhere.native-downloader") | ||
| alias(libs.plugins.kotlin.multiplatform) | ||
| alias(libs.plugins.android.library) | ||
| alias(libs.plugins.kotlin.serialization) | ||
|
|
@@ -152,89 +153,40 @@ android { | |
| // Downloaded from RABackendLLAMACPP-android GitHub release assets, or built locally. | ||
| } | ||
|
|
||
| // Native lib version for downloads | ||
| // Securely download native libraries from GitHub releases, with checksum verification and safe extraction | ||
| val nativeLibVersion: String = | ||
| rootProject.findProperty("runanywhere.nativeLibVersion")?.toString() | ||
| ?: project.findProperty("runanywhere.nativeLibVersion")?.toString() | ||
| ?: (System.getenv("SDK_VERSION")?.removePrefix("v") ?: "0.1.5-SNAPSHOT") | ||
|
|
||
| // Download LlamaCPP backend libs from GitHub releases (testLocal=false) | ||
| val releaseBaseUrl = "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v$nativeLibVersion" | ||
| val targetAbis = listOf("arm64-v8a", "armeabi-v7a", "x86_64") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm abiFilters vs targetAbis across both affected modules
rg -n "abiFilters|targetAbis" \
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/build.gradle.kts \
sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/build.gradle.ktsRepository: RunanywhereAI/runanywhere-sdks Length of output: 853
Both 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/build.gradle.kts
Line: 163
Comment:
`targetAbis` includes `"armeabi-v7a"` but the `ndk.abiFilters` on line 127 only includes `"arm64-v8a"` and `"x86_64"`. This mismatch means the `armeabi-v7a` libraries will be downloaded but never packaged into the AAR
How can I resolve this? If you propose a fix, please make it concise. |
||
| val packageType = "RABackendLLAMACPP-android" | ||
| val llamacppLibs = listOf("librac_backend_llamacpp.so", "librac_backend_llamacpp_jni.so") | ||
|
|
||
| // Create a secure download task for EACH architecture | ||
| val downloadTasks = targetAbis.map { abi -> | ||
| val sanitizedAbiName = abi.replace("-", "_") | ||
|
|
||
| tasks.register<com.runanywhere.buildlogic.DownloadAndVerifyNativeLibTask>("downloadJniLibs_$sanitizedAbiName") { | ||
| val packageName = "$packageType-$abi-v$nativeLibVersion.zip" | ||
|
|
||
| downloadUrl.set("$releaseBaseUrl/$packageName") | ||
| expectedSha256Url.set("$releaseBaseUrl/$packageName.sha256") | ||
| outputDir.set(file("src/androidMain/jniLibs/$abi")) | ||
| allowedSoFiles.set(llamacppLibs) | ||
|
|
||
| onlyIf { !testLocal } | ||
| } | ||
| } | ||
|
|
||
| // Aggregate task to download all ABIs | ||
| tasks.register("downloadJniLibs") { | ||
| group = "runanywhere" | ||
| description = "Download LlamaCPP backend JNI libraries from GitHub releases" | ||
| description = "Securely download and verify LlamaCPP backend JNI libraries from GitHub releases" | ||
|
|
||
| onlyIf { !testLocal } | ||
|
|
||
| val outputDir = file("src/androidMain/jniLibs") | ||
| val tempDir = file("${layout.buildDirectory.get()}/jni-temp") | ||
|
|
||
| val releaseBaseUrl = "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v$nativeLibVersion" | ||
| val targetAbis = listOf("arm64-v8a", "armeabi-v7a", "x86_64") | ||
| val packageType = "RABackendLLAMACPP-android" | ||
|
|
||
| val llamacppLibs = setOf( | ||
| "librac_backend_llamacpp.so", | ||
| "librac_backend_llamacpp_jni.so", | ||
| ) | ||
|
|
||
| outputs.dir(outputDir) | ||
|
|
||
| doLast { | ||
| val existingLibs = outputDir.walkTopDown().filter { it.extension == "so" }.count() | ||
| if (existingLibs > 0) { | ||
| logger.lifecycle("LlamaCPP: Skipping download, $existingLibs .so files already present") | ||
| return@doLast | ||
| } | ||
|
|
||
| outputDir.deleteRecursively() | ||
| tempDir.deleteRecursively() | ||
| outputDir.mkdirs() | ||
| tempDir.mkdirs() | ||
|
|
||
| logger.lifecycle("LlamaCPP Module: Downloading backend JNI libraries") | ||
|
|
||
| var totalDownloaded = 0 | ||
|
|
||
| targetAbis.forEach { abi -> | ||
| val abiOutputDir = file("$outputDir/$abi") | ||
| abiOutputDir.mkdirs() | ||
|
|
||
| val packageName = "$packageType-$abi-v$nativeLibVersion.zip" | ||
| val zipUrl = "$releaseBaseUrl/$packageName" | ||
| val tempZip = file("$tempDir/$packageName") | ||
|
|
||
| logger.lifecycle(" Downloading: $packageName") | ||
|
|
||
| try { | ||
| ant.withGroovyBuilder { | ||
| "get"("src" to zipUrl, "dest" to tempZip, "verbose" to false) | ||
| } | ||
|
|
||
| val extractDir = file("$tempDir/extracted-${packageName.replace(".zip", "")}") | ||
| extractDir.mkdirs() | ||
| ant.withGroovyBuilder { | ||
| "unzip"("src" to tempZip, "dest" to extractDir) | ||
| } | ||
|
|
||
| extractDir | ||
| .walkTopDown() | ||
| .filter { it.extension == "so" && it.name in llamacppLibs } | ||
| .forEach { soFile -> | ||
| val targetFile = file("$abiOutputDir/${soFile.name}") | ||
| soFile.copyTo(targetFile, overwrite = true) | ||
| logger.lifecycle(" ${soFile.name}") | ||
| totalDownloaded++ | ||
| } | ||
|
|
||
| tempZip.delete() | ||
| } catch (e: Exception) { | ||
| logger.warn(" Failed to download $packageName: ${e.message}") | ||
| } | ||
| } | ||
|
|
||
| tempDir.deleteRecursively() | ||
| logger.lifecycle("LlamaCPP: $totalDownloaded .so files downloaded") | ||
| } | ||
| dependsOn(downloadTasks) | ||
| } | ||
|
|
||
| tasks.matching { it.name.contains("merge") && it.name.contains("JniLibFolders") }.configureEach { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No error handling for the SHA256 file download. As noted in the PR description, this will throw
FileNotFoundExceptionwhen.sha256files don't exist in releases. Wrap in try-catch and provide a clear error message about missing checksumsPrompt To Fix With AI