Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 22 additions & 0 deletions sdk/runanywhere-kotlin/build-logic/build.gradle.kts
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 {

}
1 change: 1 addition & 0 deletions sdk/runanywhere-kotlin/build-logic/settings.gradle.kts
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()
Copy link
Copy Markdown
Contributor

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 FileNotFoundException when .sha256 files don't exist in releases. Wrap in try-catch and provide a clear error message about missing checksums

Suggested change
// Grab the first sequence of non-whitespace characters (the hash itself)
val expectedHash = URL(shaUrl).readText().trim().split("\\s+".toRegex()).first()
// Grab the first sequence of non-whitespace characters (the hash itself)
val expectedHash = try {
URL(shaUrl).readText().trim().split("\\s+".toRegex()).first()
} catch (e: Exception) {
throw IllegalStateException("Failed to fetch SHA256 checksum from $shaUrl. Ensure release assets include .sha256 files.", e)
}
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: 55-56

Comment:
No error handling for the SHA256 file download. As noted in the PR description, this will throw `FileNotFoundException` when `.sha256` files don't exist in releases. Wrap in try-catch and provide a clear error message about missing checksums

```suggestion
        // Grab the first sequence of non-whitespace characters (the hash itself)
        val expectedHash = try {
            URL(shaUrl).readText().trim().split("\\s+".toRegex()).first()
        } catch (e: Exception) {
            throw IllegalStateException("Failed to fetch SHA256 checksum from $shaUrl. Ensure release assets include .sha256 files.", e)
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHA256 URL fetch has no timeout configured. If the checksum file fetch hangs, the entire build will stall indefinitely. Set connection and read timeouts similar to lines 64-65

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: 56

Comment:
SHA256 URL fetch has no timeout configured. If the checksum file fetch hangs, the entire build will stall indefinitely. Set connection and read timeouts similar to lines 64-65

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "NativeLibraryDownloadPlugin.kt" | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 157


🏁 Script executed:

# Get the file size first to ensure we can read it
wc -l sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 159


🏁 Script executed:

# Read the entire file with line numbers
cat -n sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 5252


🏁 Script executed:

# Search for PR `#210` or issue references in git history
git log --oneline --all --grep="210" | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 176


🏁 Script executed:

# Search for any related commits mentioning timeout, URL.readText, or similar
git log --oneline --all --grep="timeout" | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 867


🏁 Script executed:

# Check for issue tracker references in recent commits
git log --oneline -30 | grep -i "timeout\|hash\|download\|sha"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 176


🏁 Script executed:

# Show the commit details for PR `#210`
git show 1c542182 --stat | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 3059


🏁 Script executed:

# Check the diff for the specific file to understand the intent
git show 1c542182:sdk/runanywhere-kotlin/build-logic/src/main/kotlin/NativeLibraryDownloadPlugin.kt | head -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4011


Apply HttpURLConnection with connection and read timeouts to SHA256 fetch (line 56) — currently unprotected.

Line 56 uses URL(shaUrl).readText(), which lacks timeout configuration and contradicts the explicit goal of PR #210 ("implement secure native library downloading with SHA256 and timeouts"). The main download (lines 63–65) correctly enforces 30s connection and 120s read timeouts via HttpURLConnection, but the SHA256 fetch remains vulnerable to indefinite hangs from stalled servers.

Proposed fix — fetch SHA256 via HttpURLConnection with matching timeouts
-        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()
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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).


logger.lifecycle("Downloading $url...")
var connection: HttpURLConnection? = null
val digest = MessageDigest.getInstance("SHA-256")

try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connectTimeout = 30_000 // 30 seconds connect timeout
connection.readTimeout = 120_000 // 2 minutes read timeout for large files

// Stream the download directly to a temp file while updating the digest
connection.inputStream.use { input ->
tempFile.outputStream().use { output ->
val buffer = ByteArray(8192) // 8 KB buffer for efficient reading
var bytesRead = input.read(buffer)
while (bytesRead != -1) {
output.write(buffer, 0, bytesRead)
digest.update(buffer, 0, bytesRead)
bytesRead = input.read(buffer)
}
}
}
} finally {
connection?.disconnect() // Clean up hanging sockets
}

// Verify Checksum
val calculatedHash = digest.digest().joinToString("") { "%02x".format(it) } // Convert to hex string
check(calculatedHash.equals(expectedHash, ignoreCase = true)) {
tempFile.delete() // Nuke the bad file
Copy link
Copy Markdown
Contributor

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.

"Security failure: Checksum mismatch for $url!\nExpected: $expectedHash\nGot: $calculatedHash"
}

logger.lifecycle("Checksum verified! Extracting...")

// Extract only the allowed .so files, flattening the directory structure
fsOps.copy {
from(archiveOps.zipTree(tempFile))
into(destDir)

include("**/*.so")
eachFile {
if (validFiles.contains(name)) {
// Flatten the directory structure
relativePath = RelativePath(true, name)
} else {
exclude()
Copy link
Copy Markdown
Contributor

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

Suggested change
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.

}
}
Copy link
Copy Markdown
Contributor

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

Suggested change
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.

includeEmptyDirs = false
}

tempFile.delete()
logger.lifecycle("Successfully extracted to $destDir")
}
}

class NativeLibraryDownloadPlugin : Plugin<Project> {
override fun apply(project: Project) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.kts

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 853


targetAbis includes armeabi-v7a but abiFilters does not — unnecessary download in both modules.

Both runanywhere-core-llamacpp and runanywhere-core-onnx have the same mismatch: targetAbis lists arm64-v8a, armeabi-v7a, and x86_64 (3 ABIs), but ndk { abiFilters } only packages arm64-v8a and x86_64. The armeabi-v7a native library (~34 MB per download) is fetched on every non-testLocal build but never included in the output, wasting CI bandwidth. Either add armeabi-v7a to abiFilters to align with the download list, or remove it from targetAbis if it is not required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/build.gradle.kts` at
line 163, The build config lists targetAbis = listOf("arm64-v8a", "armeabi-v7a",
"x86_64") but ndk { abiFilters } only packages "arm64-v8a" and "x86_64", causing
unnecessary downloads of armeabi-v7a; fix by either adding "armeabi-v7a" to the
abiFilters entry so it matches targetAbis or remove "armeabi-v7a" from
targetAbis in both runanywhere-core-llamacpp and runanywhere-core-onnx so the
download list aligns with the packaged ABIs (update the targetAbis and/or
abiFilters usages accordingly).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Prompt To Fix With AI
This 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 {
Expand Down
Loading