diff --git a/README.md b/README.md index 7d95bde..5fefee8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index 81d8994..34eb82e 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -9,7 +9,7 @@ repositories { google() } -version = "0.0.23" +version = "0.0.24" group = "io.github.andrefigas.rustjni" gradlePlugin { diff --git a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt index 2991943..6729209 100644 --- a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt +++ b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt @@ -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 @@ -24,10 +25,13 @@ class RustJNI : Plugin { } } - 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( @@ -38,12 +42,22 @@ class RustJNI : Plugin { /** The directory where the rust project lives. See [RustJniExtension.rustPath]. */ val rustDir: File by lazy { FileUtils.getRustDir(project, extension) } - private fun runCargoCommand(arguments: List, dir: File = rustDir) { - runCommand("cargo", arguments, dir) + private fun runCargoCommand(arguments: List, + dir: File = rustDir, + outputProcessor: ((String) -> Unit)? = null) { + runCommand("cargo", arguments, dir, outputProcessor) + } + + private fun runRustupCommand(arguments: List, + dir: File = rustDir, + outputProcessor: ((String) -> Unit)? = null) { + runCommand("rustup", arguments, dir, outputProcessor) } - private fun runRustupCommand(arguments: List, dir: File = rustDir) { - runCommand("rustup", arguments, dir) + private fun runRustcCommand(arguments: List, + dir: File = rustDir, + outputProcessor: ((String) -> Unit)? = null) { + runCommand("rustc", arguments, dir, outputProcessor) } /** Run a command like you would in a terminal. @@ -51,17 +65,19 @@ class RustJNI : Plugin { * 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, dir: File = rustDir) { + private fun runCommand( + executable: String, + arguments: List, + 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") } @@ -70,30 +86,112 @@ class RustJNI : Plugin { 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 { + 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, + required: Triple + ): 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 } } diff --git a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJniExtension.kt b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJniExtension.kt index 82374b3..f8e9299 100644 --- a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJniExtension.kt +++ b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJniExtension.kt @@ -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) { diff --git a/sample/java/app/build.gradle.kts b/sample/java/app/build.gradle.kts index 6d9a7da..d98ffd3 100644 --- a/sample/java/app/build.gradle.kts +++ b/sample/java/app/build.gradle.kts @@ -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{ @@ -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 { diff --git a/sample/kotlin/app/build.gradle.kts b/sample/kotlin/app/build.gradle.kts index 6d9a7da..d98ffd3 100644 --- a/sample/kotlin/app/build.gradle.kts +++ b/sample/kotlin/app/build.gradle.kts @@ -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{ @@ -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 {