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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,27 @@ rustJni{
}
```

### How to define what Rust version is acceptable to compile your project ?

You can define the Rust version that is acceptable to compile your project.
This is useful if you want to ensure that your project is always compiled with a specific version of Rust.

```kotlin
rustJni{
//...
rustVersion = "1.86.0"
//...
}
```

#### Supported `rustVersion` patterns

| Feature | Pattern Example | Description |
|------------------|-------------------------------|-----------------------------------------------------|
| Exact version | `1.86.0` | Only this exact Rust version is accepted |
| Minimum version | `>=1.64.0` | Accepts any version greater than or equal to this |
| Wildcard version | `1.86.*`, `1.*.*` | Allows flexibility within minor and/or patch versions |

### How can I take a look at some samples?

- [Java](./sample/java) - A java sample with 1 method
Expand Down
2 changes: 1 addition & 1 deletion gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repositories {
google()
}

version = "0.0.23"
version = "0.0.24"
group = "io.github.andrefigas.rustjni"

gradlePlugin {
Expand Down
152 changes: 125 additions & 27 deletions gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.andrefigas.rustjni.reflection.ReflectionNative
import io.github.andrefigas.rustjni.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.util.Properties
Expand All @@ -24,10 +25,13 @@ class RustJNI : Plugin<Project> {
}
}

val helper = Helper(project, extension)
helper.registerCompileTask()
helper.registerInitTask()
helper.configureAndroidSettings()
project.afterEvaluate {
val helper = Helper(project, extension)
helper.validateRustVersion()
helper.registerCompileTask()
helper.registerInitTask()
helper.configureAndroidSettings()
}
}

private class Helper(
Expand All @@ -38,30 +42,42 @@ class RustJNI : Plugin<Project> {
/** The directory where the rust project lives. See [RustJniExtension.rustPath]. */
val rustDir: File by lazy { FileUtils.getRustDir(project, extension) }

private fun runCargoCommand(arguments: List<String>, dir: File = rustDir) {
runCommand("cargo", arguments, dir)
private fun runCargoCommand(arguments: List<String>,
dir: File = rustDir,
outputProcessor: ((String) -> Unit)? = null) {
runCommand("cargo", arguments, dir, outputProcessor)
}

private fun runRustupCommand(arguments: List<String>,
dir: File = rustDir,
outputProcessor: ((String) -> Unit)? = null) {
runCommand("rustup", arguments, dir, outputProcessor)
}

private fun runRustupCommand(arguments: List<String>, dir: File = rustDir) {
runCommand("rustup", arguments, dir)
private fun runRustcCommand(arguments: List<String>,
dir: File = rustDir,
outputProcessor: ((String) -> Unit)? = null) {
runCommand("rustc", arguments, dir, outputProcessor)
}

/** Run a command like you would in a terminal.
*
* The [executable] can be one of the executables named in the **PATH** environment variable.
*
* Sets [dir] as the *current working directory (cwd)* of the command. */
private fun runCommand(executable: String, arguments: List<String>, dir: File = rustDir) {
private fun runCommand(
executable: String,
arguments: List<String>,
dir: File = rustDir,
outputProcessor: ((String) -> Unit)? = null
) {
val userHome = System.getProperty("user.home")
val isWindows = System.getProperty("os.name").toLowerCase().contains("win")
val exec_ext = if (isWindows) ".exe" else ""

val cargoBinDir = "$userHome${File.separator}.cargo${File.separator}bin"
val executablePath = "$cargoBinDir${File.separator}$executable$exec_ext"
val isWindows = System.getProperty("os.name").lowercase().contains("win")
val execExt = if (isWindows) ".exe" else ""
val executablePath = "$userHome${File.separator}.cargo${File.separator}bin${File.separator}$executable$execExt"

val executableFile = File(executablePath)
if (!executableFile.exists()) {
println("Executable not found at: $executablePath")
throw IllegalStateException("Executable not found at: $executablePath")
}

Expand All @@ -70,30 +86,112 @@ class RustJNI : Plugin<Project> {
try {
println("Running $executable command: ${fullCommand.joinToString(" ")} in $dir")

val output = if (outputProcessor != null) ByteArrayOutputStream() else null

val result = project.exec {
workingDir = dir
commandLine = fullCommand
isIgnoreExitValue = true
standardOutput = System.out
output?.let { standardOutput = it }
errorOutput = System.err
}

// Check the exit code to determine success or failure
if (result.exitValue != 0) {
println("$executable command failed with exit code ${result.exitValue}")
throw IllegalStateException("$executable command failed: ${fullCommand.joinToString(" ")} in $dir")
} else {
println("$executable command succeeded.")
throw IllegalStateException("$executable command failed with exit code ${result.exitValue}")
}

outputProcessor?.let { callback ->
output?.toString()
?.lineSequence()
?.forEach { callback(it) }
}

} catch (e: IOException) {
// Log specific message for I/O issues
println("IOException occurred while attempting to execute $executable command: ${e.message}")
throw IllegalStateException("Failed to execute $executable command due to an IOException in $dir", e)
throw IllegalStateException("IOException while executing $executable command in $dir", e)
} catch (e: Exception) {
// General exception logging
println("An error occurred while executing $executable command: ${e.message}")
throw IllegalStateException("Failed to execute $executable command: ${fullCommand.joinToString(" ")} in $dir", e)
throw IllegalStateException("Failed to execute $executable command: ${fullCommand.joinToString(" ")}", e)
}
}

fun validateRustVersion() {
val versionRequired = extension.rustVersion
if (versionRequired.isEmpty()) {
project.logger.lifecycle("No rustVersion specified. Skipping validation.")
return
}

runRustcCommand(listOf("--version")) { output ->
val tokens = output.trim().split(" ")
if (tokens.size >= 2 && tokens[0] == "rustc") {
val versionFound = tokens[1] // Ex: "1.86.0"
val (foundMajor, foundMinor, foundPatch) = parseVersion(versionFound)

when {
versionRequired.startsWith(">=") -> {
val required = parseVersion(versionRequired.removePrefix(">="))
if (!isVersionGreaterOrEqual(
found = Triple(foundMajor, foundMinor, foundPatch),
required = required
)
) {
throw IllegalStateException("Rust version $versionFound is lower than required version $versionRequired")
}
}

versionRequired.contains("*") -> {
val requiredParts = versionRequired.split(".")
if (requiredParts.size != 3) {
throw IllegalArgumentException("Wildcard version must be in format 'x.y.z' where any of them may be '*'")
}

val match = listOf(
requiredParts[0] == "*" || requiredParts[0].toInt() == foundMajor,
requiredParts[1] == "*" || requiredParts[1].toInt() == foundMinor,
requiredParts[2] == "*" || requiredParts[2].toInt() == foundPatch
).all { it }

if (!match) {
throw IllegalStateException("Rust version $versionFound does not match wildcard version $versionRequired")
}
}

versionRequired.matches(Regex("""\d+\.\d+\.\d+""")) -> {
if (versionFound != versionRequired) {
throw IllegalStateException("Rust version $versionFound does not match required version $versionRequired")
}
}

else -> {
throw IllegalArgumentException("Unsupported rustVersion format: $versionRequired")
}
}

}
}
}

private fun parseVersion(version: String): Triple<Int, Int, Int> {
val parts = version.split(".")
if (parts.size != 3) throw IllegalArgumentException("Version must be in format x.y.z")
return Triple(
parts[0].toIntOrNull() ?: throw IllegalArgumentException("Invalid major version: ${parts[0]}"),
parts[1].toIntOrNull() ?: throw IllegalArgumentException("Invalid minor version: ${parts[1]}"),
parts[2].toIntOrNull() ?: throw IllegalArgumentException("Invalid patch version: ${parts[2]}")
)
}

private fun isVersionGreaterOrEqual(
found: Triple<Int, Int, Int>,
required: Triple<Int, Int, Int>
): Boolean {
val (fMaj, fMin, fPatch) = found
val (rMaj, rMin, rPatch) = required
return when {
fMaj > rMaj -> true
fMaj < rMaj -> false
fMin > rMin -> true
fMin < rMin -> false
else -> fPatch >= rPatch
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ open class RustJniExtension {
* Default is `true`. */
var exportFunctions = true
var applyAsCompileDependency = true

/**
* Specifies the required Rust version for this project.
*
* This field accepts the following formats:
*
* 1. Exact version:
* - Example: "1.76.0"
* - The build will require exactly this Rust version.
*
* 2. Minimum version (range):
* - Example: ">=1.64.0"
* - Indicates that the project requires at least version 1.64.0 of Rust or newer.
*
* 3. Wildcard version:
* - Example: "1.76.*"
* - Allows any patch version within the specified minor version (e.g., 1.76.0, 1.76.1, etc).
*/
var rustVersion: String = ""

private var architectures: (ArchitectureListScope.() -> Unit)? = null

fun architectures(architectures: ArchitectureListScope.() -> Unit) {
Expand Down
3 changes: 2 additions & 1 deletion sample/java/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import io.github.andrefigas.rustjni.reflection.Visibility
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
id("io.github.andrefigas.rustjni") version "0.0.23"
id("io.github.andrefigas.rustjni") version "0.0.24"
}

rustJni{
Expand All @@ -17,6 +17,7 @@ rustJni{
i686_linux_android("i686-linux-android21-clang")
x86_64_linux_android("x86_64-linux-android21-clang")
}
rustVersion = ">=1.0.0"
}

android {
Expand Down
3 changes: 2 additions & 1 deletion sample/kotlin/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import io.github.andrefigas.rustjni.reflection.Visibility
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
id("io.github.andrefigas.rustjni") version "0.0.23"
id("io.github.andrefigas.rustjni") version "0.0.24"
}

rustJni{
Expand All @@ -17,6 +17,7 @@ rustJni{
i686_linux_android("i686-linux-android21-clang")
x86_64_linux_android("x86_64-linux-android21-clang")
}
rustVersion = ">=1.0.0"
}

android {
Expand Down