Skip to content
Open
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
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,142 @@
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...")

// Fetch the expected SHA256 checksum from the provided URL, with error handling for network issues and invalid responses
val expectedHash = try {
val shaConnection = URL(shaUrl).openConnection() as HttpURLConnection
shaConnection.connectTimeout = 30_000
shaConnection.readTimeout = 30_000

val responseCode = shaConnection.responseCode
if (responseCode !in 200..299) {
error("Failed to fetch SHA256 checksum from $shaUrl - HTTP $responseCode. Ensure checksum files are published to the release.")
}

shaConnection.inputStream.bufferedReader().use { it.readText() }
.trim()
.split("\\s+".toRegex())
.first()
} catch (e: Exception) {
error("Error fetching SHA256 checksum: ${e.message}")
}

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

val responseCode = connection.responseCode
if (responseCode !in 200..299) {
error("Download failed for $url — HTTP $responseCode")
}

// 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
if (!calculatedHash.equals(expectedHash, ignoreCase = true)) {
tempFile.delete()
error("Security failure: Checksum mismatch for $url!\nExpected: $expectedHash\nGot: $calculatedHash")
}

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

// Extract the archive, but only include the allowed .so files to prevent zip slip vulnerabilities. Use Gradle's built-in archive handling for safety.
try {
fsOps.copy {
from(archiveOps.zipTree(tempFile))
into(destDir)

// Fix: Explicitly include only the valid files to avoid eachFile exclude() bugs
validFiles.forEach { fileName ->
include("**/$fileName")
}

eachFile {
// Flatten the directory structure
relativePath = RelativePath(true, name)
}
includeEmptyDirs = false
}
} finally {
tempFile.delete()
}

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

class NativeLibraryDownloadPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Plugin registration logic if required
}
}
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