diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..631fa5e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Rostyslav Lesovyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..01708f4 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# AndroidRust Gradle Plugin + +This plugin helps with building Rust JNI libraries with Cargo for use in Android projects. + +Link to the plugin on the gradle repository: +https://plugins.gradle.org/plugin/io.github.MatrixDev.android-rust + +# Usage + +Add dependencies to the root `build.gradle.kts` file + +```kotlin +buildscript { + repositories { + maven("https://plugins.gradle.org/m2/") + } + + dependencies { + classpath("io.github.MatrixDev.android-rust:plugin:0.3.2") + } +} +``` + +Add plugin to the module's `build.gradle.kts` file + +```kotlin +plugins { + id("io.github.MatrixDev.android-rust") +} +``` + +Add `androidRust` configuration + +```kotlin +androidRust { + module("rust-library") { + path = file("src/rust_library") + } +} +``` + +# Additional configurations + +This is the list of some additional flags that can be configured: + +```kotlin +androidRust { + // MSRV, plugin will update rust if installed version is lower than requested + minimumSupportedRustVersion = "1.62.1" + + module("rust-library") { + // path to your rust library + path = file("src/rust_library") + + // default rust profile + profile = "release" + + // default abi targets + targets = listOf("arm", "arm64") + + // "debug" build type specific configuration + buildType("debug") { + // use "dev" profile in rust + profile = "dev" + } + + // "release" build type specific configuration + buildType("release") { + // run rust tests before build + runTests = true + + // build all supported abi versions + targets = listOf("arm", "arm64", "x86", "x86_64") + } + } + + // more than one library can be added + module("additional-library") { + // ... + } +} +``` + + +# Development support +Plugin will check for a magic property `android.injected.build.abi` set by Android Studio when +running application on device. This will limit ABI targets to only required by the device and +should speedup development quite a bit. + +In theory this should behave the same as a built-in support for the NdkBuild / CMake. + + +# Goals +- Building multiple rust libraries with ease +- Allow builds to be configurable for common scenarios + + +# Non-goals +- Supporting all Gradle versions +- Allow builds to be configurable for exotic scenarios + + +# IDE Enviroment PATH Workaround +On some systems (notably MacOS) gradle task might fail to locate rust binaries. At this moment there are multiple issues/discussions for both gradle and IntelliJ IDEs. + +To solve this problem cargo path can be provided in `local.properties` file: +```properties +sdk.dir=... +cargo.bin=/Users/{user}/.cargo/bin/ +``` diff --git a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt index 57fa200..6bfba4e 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt @@ -2,6 +2,7 @@ package dev.matrix.agp.rust import com.android.build.gradle.internal.tasks.factory.dependsOn import dev.matrix.agp.rust.utils.Abi +import dev.matrix.agp.rust.utils.RustBinaries import dev.matrix.agp.rust.utils.SemanticVersion import dev.matrix.agp.rust.utils.getAndroidComponentsExtension import dev.matrix.agp.rust.utils.getAndroidExtension @@ -15,9 +16,10 @@ import java.util.Locale // TODO: migrate to variant API with artifacts when JNI will be supported // https://developer.android.com/studio/build/extend-agp#access-modify-artifacts // -@Suppress("unused", "UnstableApiUsage") +@Suppress("unused") class AndroidRustPlugin : Plugin { override fun apply(project: Project) { + val rustBinaries = RustBinaries(project) val extension = project.extensions.create("androidRust", AndroidRustExtension::class.java) val androidExtension = project.getAndroidExtension() val androidComponents = project.getAndroidComponentsExtension() @@ -65,6 +67,7 @@ class AndroidRustPlugin : Plugin { for (rustAbi in rustAbiSet) { val buildTaskName = "build${buildTypeNameCap}${moduleNameCap}Rust[${rustAbi.androidName}]" val buildTask = project.tasks.register(buildTaskName, RustBuildTask::class.java) { + this.rustBinaries.set(rustBinaries) this.abi.set(rustAbi) this.apiLevel.set(dsl.defaultConfig.minSdk ?: 21) this.ndkVersion.set(ndkVersion) @@ -83,7 +86,7 @@ class AndroidRustPlugin : Plugin { } val minimumSupportedRustVersion = SemanticVersion(extension.minimumSupportedRustVersion) - installRustComponentsIfNeeded(project, minimumSupportedRustVersion, allRustAbiSet) + installRustComponentsIfNeeded(project, minimumSupportedRustVersion, allRustAbiSet, rustBinaries) } androidComponents.onVariants { variant -> diff --git a/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt b/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt index f4b25f7..cac1dd5 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt @@ -2,6 +2,7 @@ package dev.matrix.agp.rust import dev.matrix.agp.rust.utils.Abi import dev.matrix.agp.rust.utils.Os +import dev.matrix.agp.rust.utils.RustBinaries import dev.matrix.agp.rust.utils.SemanticVersion import org.gradle.api.DefaultTask import org.gradle.api.provider.Property @@ -10,6 +11,9 @@ import org.gradle.api.tasks.TaskAction import java.io.File internal abstract class RustBuildTask : DefaultTask() { + @get:Input + abstract val rustBinaries: Property + @get:Input abstract val abi: Property @@ -36,6 +40,7 @@ internal abstract class RustBuildTask : DefaultTask() { @TaskAction fun taskAction() { + val rustBinaries = rustBinaries.get() val abi = abi.get() val apiLevel = apiLevel.get() val ndkVersion = ndkVersion.get() @@ -72,7 +77,7 @@ internal abstract class RustBuildTask : DefaultTask() { environment("CARGO_TARGET_DIR", cargoTargetDirectory.absolutePath) environment("CARGO_TARGET_${cargoTargetTriplet}_LINKER", cc) - commandLine("cargo") + commandLine(rustBinaries.cargo) args("build") args("--lib") diff --git a/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt b/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt index deb49dc..7dbf782 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt @@ -3,6 +3,7 @@ package dev.matrix.agp.rust import dev.matrix.agp.rust.utils.Abi import dev.matrix.agp.rust.utils.NullOutputStream import dev.matrix.agp.rust.utils.Os +import dev.matrix.agp.rust.utils.RustBinaries import dev.matrix.agp.rust.utils.SemanticVersion import dev.matrix.agp.rust.utils.log import org.gradle.api.Project @@ -12,38 +13,39 @@ internal fun installRustComponentsIfNeeded( project: Project, minimalVersion: SemanticVersion?, abiSet: Collection, + rustBinaries: RustBinaries, ) { if (Os.current.isWindows) { return } if (minimalVersion != null && minimalVersion.isValid) { - val actualVersion = readRustCompilerVersion(project) + val actualVersion = readRustCompilerVersion(project, rustBinaries) if (actualVersion < minimalVersion) { - installRustUp(project) - updateRust(project) + installRustUp(project, rustBinaries) + updateRust(project, rustBinaries) } } if (abiSet.isNotEmpty()) { - installRustUp(project) + installRustUp(project, rustBinaries) - val installedAbiSet = readRustUpInstalledTargets(project) + val installedAbiSet = readRustUpInstalledTargets(project, rustBinaries) for (abi in abiSet) { if (installedAbiSet.contains(abi)) { continue } - installRustTarget(project, abi) + installRustTarget(project, abi, rustBinaries) } } } -private fun installRustUp(project: Project) { +private fun installRustUp(project: Project, rustBinaries: RustBinaries) { try { val result = project.exec { standardOutput = NullOutputStream errorOutput = NullOutputStream - executable("rustup") + executable(rustBinaries.rustup) args("-V") } @@ -62,34 +64,34 @@ private fun installRustUp(project: Project) { }.assertNormalExitValue() } -private fun updateRust(project: Project) { +private fun updateRust(project: Project, rustBinaries: RustBinaries) { log("updating rust version") project.exec { standardOutput = NullOutputStream errorOutput = NullOutputStream - executable("rustup") + executable(rustBinaries.rustup) args("update") }.assertNormalExitValue() } -private fun installRustTarget(project: Project, abi: Abi) { +private fun installRustTarget(project: Project, abi: Abi, rustBinaries: RustBinaries) { log("installing rust target $abi (${abi.rustTargetTriple})") project.exec { standardOutput = NullOutputStream errorOutput = NullOutputStream - executable("rustup") + executable(rustBinaries.rustup) args("target", "add", abi.rustTargetTriple) }.assertNormalExitValue() } -private fun readRustCompilerVersion(project: Project): SemanticVersion { +private fun readRustCompilerVersion(project: Project, rustBinaries: RustBinaries): SemanticVersion { val output = ByteArrayOutputStream() project.exec { standardOutput = output errorOutput = NullOutputStream - executable("rustc") + executable(rustBinaries.rustc) args("--version") }.assertNormalExitValue() @@ -102,12 +104,12 @@ private fun readRustCompilerVersion(project: Project): SemanticVersion { return SemanticVersion(match.groupValues[1]) } -private fun readRustUpInstalledTargets(project: Project): Set { +private fun readRustUpInstalledTargets(project: Project, rustBinaries: RustBinaries): Set { val output = ByteArrayOutputStream() project.exec { standardOutput = output errorOutput = NullOutputStream - executable("rustup") + executable(rustBinaries.rustup) args("target", "list") }.assertNormalExitValue() diff --git a/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt b/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt new file mode 100644 index 0000000..769a9ab --- /dev/null +++ b/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt @@ -0,0 +1,36 @@ +package dev.matrix.agp.rust.utils + +import org.gradle.api.Project +import java.io.File +import java.io.Serializable +import java.util.Properties + +@Suppress("SpellCheckingInspection") +internal data class RustBinaries( + val cargo: String = "cargo", + val rustc: String = "rustc", + val rustup: String = "rustup", +) : Serializable { + companion object { + operator fun invoke(project: Project): RustBinaries { + var path = RustBinaries() + try { + val file = project.rootProject.file("local.properties") + val properties = Properties().also { + it.load(file.inputStream()) + } + + val bin = File(properties.getProperty("cargo.bin").orEmpty()) + if (bin.exists()) { + path = path.copy( + cargo = File(bin, path.cargo).absolutePath, + rustc = File(bin, path.rustc).absolutePath, + rustup = File(bin, path.rustup).absolutePath, + ) + } + } catch (ignore: Exception) { + } + return path + } + } +}