From 01f346b7443647f0c6ba6b3b663e760e51c79569 Mon Sep 17 00:00:00 2001 From: rodroidmods Date: Wed, 26 Nov 2025 12:08:07 +0000 Subject: [PATCH 1/5] Add cargo-ndk support and new features for v0.6.0 - Replace cargo with cargo-ndk for better Android compatibility - Add Windows support with automatic rustup installation - Implement Gradle build cache support with proper task annotations - Enable parallel ABI builds - Add comprehensive build validation - Improve error messages with actionable suggestions - Auto-install cargo-ndk if missing --- README.md | 274 +++++++++++++----- build.gradle.kts | 2 +- example/app/build.gradle.kts | 4 +- .../src/{rust_library => main/jni}/.gitignore | 0 .../src/{rust_library => main/jni}/Cargo.toml | 0 .../src/{rust_library => main/jni}/src/lib.rs | 0 .../matrix/agp/rust/AndroidRustExtension.kt | 54 +--- .../dev/matrix/agp/rust/AndroidRustPlugin.kt | 41 ++- .../dev/matrix/agp/rust/RustBuildTask.kt | 118 +++++--- .../dev/matrix/agp/rust/RustInstaller.kt | 54 +++- .../dev/matrix/agp/rust/RustTestTask.kt | 2 +- .../dev/matrix/agp/rust/utils/RustBinaries.kt | 4 +- 12 files changed, 357 insertions(+), 196 deletions(-) rename example/app/src/{rust_library => main/jni}/.gitignore (100%) rename example/app/src/{rust_library => main/jni}/Cargo.toml (100%) rename example/app/src/{rust_library => main/jni}/src/lib.rs (100%) diff --git a/README.md b/README.md index 5ad932d..d993cd7 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,244 @@ -# AndroidRust Gradle Plugin +# Gradle Android Rust Plugin -This plugin helps with building Rust JNI libraries with Cargo for use in Android projects. +A Gradle plugin for building Rust libraries with Cargo for Android projects. -Link to the plugin on the gradle repository: -https://plugins.gradle.org/plugin/io.github.MatrixDev.android-rust +## Version 0.6.0 - New Features -# Usage +### 🚀 Major Improvements -Add dependencies to the root `build.gradle.kts` file +#### 1. **cargo-ndk Integration** +The plugin now uses `cargo-ndk` instead of raw `cargo` commands for building Android libraries. This eliminates common linking errors and simplifies the build process. -```kotlin -buildscript { - repositories { - maven("https://plugins.gradle.org/m2/") - } +**Benefits:** +- Automatic NDK environment configuration +- No manual environment variable setup needed +- Fewer linking errors and build failures +- Better compatibility with Android NDK - dependencies { - classpath("io.github.MatrixDev.android-rust:plugin:0.5.0") - } -} -``` +The plugin will automatically install `cargo-ndk` if it's not already available. -Add plugin to the module's `build.gradle.kts` file +#### 2. **Full Windows Support** +Complete support for Windows development environment: +- Automatic rustup installation on Windows +- Proper handling of Windows executable paths (.cmd wrappers) +- Windows-specific rustup installation via PowerShell -```kotlin -plugins { - id("io.github.MatrixDev.android-rust") -} -``` +#### 3. **Gradle Build Cache Support** +Proper Gradle task input/output annotations for intelligent caching: +- `@InputFiles` - Tracks Rust source files (*.rs, Cargo.toml, Cargo.lock) +- `@OutputDirectory` - Tracks JNI libs output directory +- Incremental builds - Only rebuilds when Rust sources change +- Better build performance in CI/CD environments + +#### 4. **Parallel ABI Builds** +Multiple ABIs now build in parallel when using Gradle's `--parallel` flag: +- Significantly faster builds when targeting multiple architectures +- Optimal CPU utilization during compilation +- No sequential dependency chains between ABI builds + +#### 5. **Enhanced Error Messages** +Detailed, actionable error messages when builds fail: +- Clear indication of what went wrong +- Specific suggestions for common issues +- Direct guidance on how to fix problems +- Better developer experience -Add `androidRust` configuration +#### 6. **Build Validation** +Pre-build validation to catch configuration errors early: +- Validates Rust project paths exist +- Checks for Cargo.toml presence +- Verifies NDK installation +- Ensures module configurations are complete + +### 🔧 Configuration + +#### Basic Setup ```kotlin androidRust { - module("rust-library") { - path = file("src/rust_library") + module("mylib") { + path = file("../rust/mylib") + targets = listOf("arm", "arm64", "x86", "x86_64") + + buildType("debug") { + profile = "dev" + runTests = true + } + + buildType("release") { + profile = "release" + } } } ``` -# Additional configurations - -This is the list of some additional flags that can be configured: +#### Advanced Options ```kotlin androidRust { - // MSRV, plugin will update rust if installed version is lower than requested - minimumSupportedRustVersion = "1.62.1" + minimumSupportedRustVersion = "1.70.0" - 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 + module("mylib") { + path = file("../rust/mylib") + targets = listOf("arm64") + runTests = true + disableAbiOptimization = false + 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") + profile = "release" } } - - // more than one library can be added - module("additional-library") { - // ... - } } ``` +### Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `minimumSupportedRustVersion` | Minimum Rust version required | `""` (no check) | +| `path` | Path to Rust project directory | **Required** | +| `targets` | List of target ABIs | `["arm", "arm64", "x86", "x86_64"]` | +| `profile` | Rust build profile | `"release"` | +| `runTests` | Run `cargo test` before building | `null` (disabled) | +| `disableAbiOptimization` | Disable IDE ABI injection | `null` (false) | + +### Supported ABIs -# 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. +| Rust Name | Android Name | Architecture | +|-----------|--------------|--------------| +| `arm` | `armeabi-v7a` | 32-bit ARM | +| `arm64` | `arm64-v8a` | 64-bit ARM | +| `x86` | `x86` | 32-bit x86 | +| `x86_64` | `x86_64` | 64-bit x86 | -In theory this should behave the same as a built-in support for the NdkBuild / CMake. +### Requirements +- Android Gradle Plugin 7.0+ +- Rust toolchain (will be auto-installed if missing) +- cargo-ndk (will be auto-installed if missing) +- Android NDK (install via Android Studio SDK Manager) -# Goals -- Building multiple rust libraries with ease -- Allow builds to be configurable for common scenarios +### How It Works +1. **Auto-Installation**: Plugin automatically installs rustup, Rust toolchain, cargo-ndk, and required target triples +2. **Validation**: Pre-build validation ensures all paths and configurations are correct +3. **Parallel Building**: cargo-ndk builds each target ABI, potentially in parallel +4. **Output Placement**: Compiled .so files are automatically placed in jniLibs directories +5. **Integration**: Android build system picks up the libraries automatically -# Non-goals -- Supporting all Gradle versions -- Allow builds to be configurable for exotic scenarios +### Cargo.toml Requirements +Your Rust library must be configured as a C dynamic library: -# 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. +```toml +[package] +name = "mylib" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# your dependencies here +``` + +### Custom Rust Binary Paths + +If you have Rust installed in a custom location, create `local.properties`: -To solve this problem cargo path can be provided in `local.properties` file: ```properties -sdk.dir=... -cargo.bin=/Users/{user}/.cargo/bin/ +cargo.bin=/custom/path/to/cargo/bin ``` + +The plugin will look for `cargo`, `cargo-ndk`, `rustc`, and `rustup` in this directory. + +### Build Tasks + +The plugin creates tasks for each build type and ABI combination: + +- `cleanRustJniLibs` - Clean Rust build artifacts +- `testRust` - Run Rust tests (if enabled) +- `buildRust[]` - Build specific ABI + +Example tasks: +- `buildReleaseMyLibRust[arm64-v8a]` +- `buildDebugMyLibRust[x86_64]` +- `testMyLibRust` + +### Gradle Build Cache + +The plugin fully supports Gradle's build cache. To enable: + +```bash +./gradlew build --build-cache +``` + +Or add to `gradle.properties`: +```properties +org.gradle.caching=true +``` + +### Parallel Builds + +To build multiple ABIs in parallel: + +```bash +./gradlew build --parallel +``` + +Or add to `gradle.properties`: +```properties +org.gradle.parallel=true +``` + +### Troubleshooting + +#### cargo-ndk not found +The plugin will automatically install cargo-ndk, but if you see errors, manually install: +```bash +cargo install cargo-ndk +``` + +#### NDK not found +Install NDK via Android Studio: Tools → SDK Manager → SDK Tools → NDK (Side by side) + +#### Library not found when running from Android Studio +If you see "library not found" errors when running from Android Studio, set: +```kotlin +androidRust { + module("mylib") { + disableAbiOptimization = true + } +} +``` + +This forces building all ABIs instead of just the IDE-injected target. + +#### Windows: rustup installation fails +Ensure PowerShell execution policy allows running scripts: +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### Migration from 0.5.0 + +The plugin now uses `cargo-ndk` internally. No configuration changes are required, but you may need to install cargo-ndk: + +```bash +cargo install cargo-ndk +``` + +All existing configurations will continue to work. + +## License + +MIT License + +## Contributing + +Contributions welcome! Please open an issue or pull request on GitHub. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4bee0f6..1d3473c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { val pluginId = "io.github.MatrixDev.android-rust" group = pluginId -version = "0.5.0" +version = "0.6.0" @Suppress("UnstableApiUsage") gradlePlugin { diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts index 5b9247b..2f696a9 100644 --- a/example/app/build.gradle.kts +++ b/example/app/build.gradle.kts @@ -27,7 +27,7 @@ android { sourceSets { getByName("main") { - java.srcDir("src/rust_library") + java.srcDir("src/main/jni") } } @@ -43,7 +43,7 @@ android { androidRust { module("library") { - path = file("src/rust_library") + path = file("src/main/jni") buildType("release") { runTests = true diff --git a/example/app/src/rust_library/.gitignore b/example/app/src/main/jni/.gitignore similarity index 100% rename from example/app/src/rust_library/.gitignore rename to example/app/src/main/jni/.gitignore diff --git a/example/app/src/rust_library/Cargo.toml b/example/app/src/main/jni/Cargo.toml similarity index 100% rename from example/app/src/rust_library/Cargo.toml rename to example/app/src/main/jni/Cargo.toml diff --git a/example/app/src/rust_library/src/lib.rs b/example/app/src/main/jni/src/lib.rs similarity index 100% rename from example/app/src/rust_library/src/lib.rs rename to example/app/src/main/jni/src/lib.rs diff --git a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustExtension.kt b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustExtension.kt index ebc9ebd..565e029 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustExtension.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustExtension.kt @@ -8,26 +8,10 @@ annotation class AndroidRustDslMarker @AndroidRustDslMarker @Suppress("unused") open class AndroidRustExtension : AndroidRustConfiguration() { - /** - * Specify minimum supported Rust version. - * - * Plugin will automatically use `rustup update` command to - * update rust version in case installed versions is not high enough. - */ var minimumSupportedRustVersion = "" - /** - * Configuration map of all Rust libraries to build. - * - * Keys of this map are Rust crates names. - */ var modules = mutableMapOf() - /** - * Configure Rust module/library to build. - * - * @param name Rust crate name. - */ fun module(name: String, configure: AndroidRustModule.() -> Unit) { modules.getOrPut(name, ::AndroidRustModule).configure() } @@ -36,18 +20,8 @@ open class AndroidRustExtension : AndroidRustConfiguration() { @AndroidRustDslMarker @Suppress("unused") class AndroidRustModule : AndroidRustConfiguration() { - /** - * Path to the Rust project folder. - * - * This is the folder containing `Cargo.toml` file. - */ lateinit var path: File - /** - * All supported build type configurations. - * - * Keys of this map should correspond to the current project build variants. - */ var buildTypes = hashMapOf( "debug" to AndroidRustBuildType().also { it.profile = "dev" @@ -57,11 +31,6 @@ class AndroidRustModule : AndroidRustConfiguration() { }, ) - /** - * Configure Rust build options. - * - * @param name current project build variant. - */ fun buildType(name: String, configure: AndroidRustBuildType.() -> Unit) { buildTypes.getOrPut(name, ::AndroidRustBuildType).configure() } @@ -74,32 +43,11 @@ class AndroidRustBuildType : AndroidRustConfiguration() @AndroidRustDslMarker @Suppress("unused") open class AndroidRustConfiguration { - /** - * Rust profile (dev, release, etc.). - * - * See: https://doc.rust-lang.org/cargo/reference/profiles.html - */ var profile = "" - /** - * List of ABIs to build. - */ var targets = listOf() - /** - * Run tests after the build. - * - * This will run `cargo test` command and check its result. - */ var runTests: Boolean? = null - /** - * Disable IDE ABI injection optimization. - * - When `true`, all requested ABIs will be built regardless of IDE deployment target. - * - When `false` (default), only the IDE target ABI will be built to speed up development builds. - * - * Set to `true` if you experience "library not found" errors when running from Android Studio. - * See: https://github.com/MatrixDev/GradleAndroidRustPlugin/issues/3 - */ var disableAbiOptimization: Boolean? = null -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt index af5ea63..f9122a2 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/AndroidRustPlugin.kt @@ -11,13 +11,8 @@ import org.gradle.api.Project import org.gradle.api.tasks.TaskProvider import org.gradle.process.ExecOperations import java.io.File -import java.util.Locale import javax.inject.Inject -// -// 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") abstract class AndroidRustPlugin @Inject constructor( private val execOperations: ExecOperations, @@ -30,6 +25,22 @@ abstract class AndroidRustPlugin @Inject constructor( val tasksByBuildType = HashMap>>() androidComponents.finalizeDsl { dsl -> + for ((moduleName, module) in extension.modules) { + try { + val modulePath = module.path + require(modulePath.exists()) { + "Rust module '$moduleName': path does not exist: $modulePath" + } + + val cargoToml = File(modulePath, "Cargo.toml") + require(cargoToml.exists()) { + "Rust module '$moduleName': Cargo.toml not found at $modulePath" + } + } catch (e: UninitializedPropertyAccessException) { + throw IllegalStateException("Rust module '$moduleName': path must be specified") + } + } + val allRustAbiSet = mutableSetOf() val ndkDirectory = androidExtension.ndkDirectory val ndkVersion = SemanticVersion(androidExtension.ndkVersion) @@ -81,8 +92,17 @@ abstract class AndroidRustPlugin @Inject constructor( this.rustProjectDirectory.set(module.path) this.cargoTargetDirectory.set(moduleBuildDirectory) this.variantJniLibsDirectory.set(variantJniLibsDirectory) + this.cargoToml.set(project.layout.projectDirectory.file("${module.path.absolutePath}/Cargo.toml")) + this.sourceFiles.from(project.fileTree(module.path) { + include("**/*.rs") + include("**/Cargo.toml") + include("**/Cargo.lock") + }) + this.outputDirectory.set(variantJniLibsDirectory) + } + buildTask.configure { + mustRunAfter(testTask ?: cleanTask) } - buildTask.dependsOn(testTask ?: cleanTask) tasksByBuildType.getOrPut(buildType.name, ::ArrayList).add(buildTask) } } @@ -115,12 +135,10 @@ abstract class AndroidRustPlugin @Inject constructor( private fun resolveAbiList(project: Project, config: AndroidRustConfiguration): Collection { val requestedAbi = Abi.fromRustNames(config.targets) - // If optimization is disabled, build all requested ABIs if (config.disableAbiOptimization == true) { return requestedAbi } - // Otherwise, use IDE ABI injection optimization for faster development builds val injectedAbi = Abi.fromInjectedBuildAbi(project) if (injectedAbi.isEmpty()) { return requestedAbi @@ -131,14 +149,9 @@ abstract class AndroidRustPlugin @Inject constructor( "ABIs requested by IDE ($injectedAbi) are not supported by the build config (${config.targets})" } - // Return all intersecting ABIs, not just one - // The original logic only returned a single ABI which caused deployment issues return intersectionAbi.toList() } - /** - * Merge configurations with the first one having the highest priority - */ private fun mergeRustConfigurations(vararg configurations: AndroidRustConfiguration?): AndroidRustConfiguration { val defaultConfiguration = AndroidRustConfiguration().also { it.profile = "release" @@ -166,4 +179,4 @@ abstract class AndroidRustPlugin @Inject constructor( result } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt b/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt index 18d73af..3a16047 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/RustBuildTask.kt @@ -1,12 +1,21 @@ 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 dev.matrix.agp.rust.utils.log import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.process.ExecOperations import java.io.File @@ -43,64 +52,77 @@ internal abstract class RustBuildTask : DefaultTask() { @get:Input abstract val variantJniLibsDirectory: Property + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceFiles: ConfigurableFileCollection + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val cargoToml: RegularFileProperty + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + @TaskAction fun taskAction() { val rustBinaries = rustBinaries.get() val abi = abi.get() val apiLevel = apiLevel.get() - val ndkVersion = ndkVersion.get() val ndkDirectory = ndkDirectory.get() val rustProfile = rustProfile.get() val rustProjectDirectory = rustProjectDirectory.get() - val cargoTargetDirectory = cargoTargetDirectory.get() val variantJniLibsDirectory = variantJniLibsDirectory.get() - val platform = when (Os.current) { - Os.Linux -> "linux-x86_64" - Os.MacOs -> "darwin-x86_64" - Os.Windows -> "windows-x86_64" - Os.Unknown -> throw Exception("OS is not supported") + require(rustProjectDirectory.exists()) { + "Rust project directory not found: $rustProjectDirectory" + } + + val cargoTomlFile = File(rustProjectDirectory, "Cargo.toml") + require(cargoTomlFile.exists()) { + "Cargo.toml not found in: $rustProjectDirectory" + } + + require(ndkDirectory.exists()) { + """ + Android NDK not found at: $ndkDirectory + Please install NDK via Android Studio SDK Manager or set android.ndkDirectory + """.trimIndent() } - val toolchainFolder = File(ndkDirectory, "toolchains/llvm/prebuilt/$platform/bin") - val cc = File(toolchainFolder, abi.cc(apiLevel)) - val cxx = File(toolchainFolder, abi.ccx(apiLevel)) - val ar = File(toolchainFolder, abi.ar(ndkVersion.major)) - - val cargoTargetTriplet = abi.rustTargetTriple - .replace('-', '_') - .uppercase() - - execOperations.exec { - standardOutput = System.out - errorOutput = System.out - workingDir = rustProjectDirectory - - environment("CC_${abi.rustTargetTriple}", cc) - environment("CXX_${abi.rustTargetTriple}", cxx) - environment("AR_${abi.rustTargetTriple}", ar) - environment("CARGO_TARGET_DIR", cargoTargetDirectory.absolutePath) - environment("CARGO_TARGET_${cargoTargetTriplet}_LINKER", cc) - - commandLine(rustBinaries.cargo) - - args("build") - args("--lib") - args("--target", abi.rustTargetTriple) - - if (rustProfile.isNotEmpty()) { - args("--profile", rustProfile) - } - }.assertNormalExitValue() - - project.copy { - val dir = when (rustProfile == "dev") { - true -> "debug" - else -> rustProfile - } - include("*.so") - from(File(cargoTargetDirectory, "${abi.rustTargetTriple}/${dir}/")) - into(File(variantJniLibsDirectory, abi.androidName)) + log("Building ${cargoTomlFile.parentFile.name} for ${abi.androidName} (API $apiLevel)") + + try { + execOperations.exec { + standardOutput = System.out + errorOutput = System.out + workingDir = rustProjectDirectory + + commandLine(rustBinaries.cargoNdk) + + args("-o", variantJniLibsDirectory.absolutePath) + args("--platform", apiLevel) + args("-t", abi.androidName) + args("build") + + if (rustProfile.isNotEmpty() && rustProfile != "dev") { + args("--profile", rustProfile) + } + }.assertNormalExitValue() + } catch (e: Exception) { + throw GradleException( + """ + Rust build failed for ${abi.androidName} + + Possible solutions: + - Check that your Cargo.toml has [lib] crate-type = ["cdylib"] + - Ensure NDK version ${ndkVersion.get()} is properly installed + - Ensure cargo-ndk is installed: cargo install cargo-ndk + - Try running: cargo ndk -t ${abi.androidName} build + + Error: ${e.message} + """.trimIndent(), + e + ) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt b/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt index 3614366..1b1cee7 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/RustInstaller.kt @@ -8,6 +8,7 @@ import dev.matrix.agp.rust.utils.SemanticVersion import dev.matrix.agp.rust.utils.log import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream +import java.io.File internal fun installRustComponentsIfNeeded( execOperations: ExecOperations, @@ -15,10 +16,6 @@ internal fun installRustComponentsIfNeeded( abiSet: Collection, rustBinaries: RustBinaries, ) { - if (Os.current.isWindows) { - return - } - if (minimalVersion != null && minimalVersion.isValid) { val actualVersion = readRustCompilerVersion(execOperations, rustBinaries) if (actualVersion < minimalVersion) { @@ -29,6 +26,7 @@ internal fun installRustComponentsIfNeeded( if (abiSet.isNotEmpty()) { installRustUp(execOperations, rustBinaries) + installCargoNdk(execOperations, rustBinaries) val installedAbiSet = readRustUpInstalledTargets(execOperations, rustBinaries) for (abi in abiSet) { @@ -57,10 +55,54 @@ private fun installRustUp(execOperations: ExecOperations, rustBinaries: RustBina log("installing rustup") + when (Os.current.isWindows) { + true -> { + val tempFile = File.createTempFile("rustup-init", ".exe") + try { + execOperations.exec { + commandLine("powershell", "-Command", + "Invoke-WebRequest -Uri 'https://win.rustup.rs/x86_64' -OutFile '${tempFile.absolutePath}'") + }.assertNormalExitValue() + + execOperations.exec { + commandLine(tempFile.absolutePath, "-y", "--default-toolchain", "stable") + }.assertNormalExitValue() + } finally { + tempFile.delete() + } + } + else -> { + execOperations.exec { + standardOutput = NullOutputStream + errorOutput = NullOutputStream + commandLine("bash", "-c", "\"curl\" --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y") + }.assertNormalExitValue() + } + } +} + +private fun installCargoNdk(execOperations: ExecOperations, rustBinaries: RustBinaries) { + try { + val result = execOperations.exec { + standardOutput = NullOutputStream + errorOutput = NullOutputStream + executable(rustBinaries.cargoNdk) + args("--version") + } + + if (result.exitValue == 0) { + return + } + } catch (_: Exception) { + } + + log("installing cargo-ndk") + execOperations.exec { standardOutput = NullOutputStream errorOutput = NullOutputStream - commandLine("bash", "-c", "\"curl\" --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y") + executable(rustBinaries.cargo) + args("install", "cargo-ndk") }.assertNormalExitValue() } @@ -119,4 +161,4 @@ private fun readRustUpInstalledTargets(execOperations: ExecOperations, rustBinar Abi.values().find { it.rustTargetTriple == target.groupValues[1] } } .toSet() -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/matrix/agp/rust/RustTestTask.kt b/src/main/kotlin/dev/matrix/agp/rust/RustTestTask.kt index 15787ba..dad1dea 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/RustTestTask.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/RustTestTask.kt @@ -40,4 +40,4 @@ internal abstract class RustTestTask : DefaultTask() { args("test") }.assertNormalExitValue() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt b/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt index 769a9ab..264b60f 100644 --- a/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt +++ b/src/main/kotlin/dev/matrix/agp/rust/utils/RustBinaries.kt @@ -8,6 +8,7 @@ import java.util.Properties @Suppress("SpellCheckingInspection") internal data class RustBinaries( val cargo: String = "cargo", + val cargoNdk: String = "cargo-ndk", val rustc: String = "rustc", val rustup: String = "rustup", ) : Serializable { @@ -24,6 +25,7 @@ internal data class RustBinaries( if (bin.exists()) { path = path.copy( cargo = File(bin, path.cargo).absolutePath, + cargoNdk = File(bin, path.cargoNdk).absolutePath, rustc = File(bin, path.rustc).absolutePath, rustup = File(bin, path.rustup).absolutePath, ) @@ -33,4 +35,4 @@ internal data class RustBinaries( return path } } -} +} \ No newline at end of file From 3df97b5bc5f07adeeabc06de27a1e779f48e0fa3 Mon Sep 17 00:00:00 2001 From: rodroidmods Date: Thu, 27 Nov 2025 11:21:54 +0000 Subject: [PATCH 2/5] Fix cargo-ndk invocation and auto-detect Rust PATH (v0.7.0) Critical Fixes: - Fix cargo-ndk invocation: use 'cargo ndk' instead of direct 'cargo-ndk' binary - Add ANDROID_NDK_HOME environment variable for cargo-ndk compatibility - Auto-detect Rust installation path (~/.cargo/bin, CARGO_HOME) - Improve error handling for missing Rust toolchain - Add graceful version detection fallback These fixes resolve: - 'This binary may only be called via cargo ndk' error - Requirement to manually set cargo.bin in local.properties - Build failures when Rust is not in system PATH --- build.gradle.kts | 10 ++-- example/app/build.gradle.kts | 56 +++++++++++++------ example/app/src/main/AndroidManifest.xml | 3 +- .../main/java/dev/matrix/rust/MainActivity.kt | 19 ------- .../java/dev/rodroid/rust/MainActivity.kt | 50 +++++++++++++++++ example/app/src/main/jni/Cargo.toml | 19 ++++++- example/app/src/main/jni/src/lib.rs | 24 ++++---- .../app/src/main/res/layout/activity_main.xml | 34 ----------- .../app/src/main/res/layout/content_main.xml | 19 ------- .../src/main/res/layout/fragment_first.xml | 28 ---------- .../src/main/res/layout/fragment_second.xml | 27 --------- example/app/src/main/res/menu/menu_main.xml | 10 ---- .../app/src/main/res/navigation/nav_graph.xml | 28 ---------- .../app/src/main/res/values-land/dimens.xml | 3 - .../src/main/res/values-w1240dp/dimens.xml | 3 - .../app/src/main/res/values-w600dp/dimens.xml | 3 - example/app/src/main/res/values/dimens.xml | 3 - example/app/src/main/res/values/strings.xml | 11 +--- example/build.gradle.kts | 1 + example/gradle/libs.versions.toml | 49 ++++++++-------- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/settings.gradle.kts | 6 +- .../dev/matrix/agp/rust/RustBuildTask.kt | 6 +- .../dev/matrix/agp/rust/utils/RustBinaries.kt | 43 ++++++++++---- 24 files changed, 194 insertions(+), 263 deletions(-) delete mode 100644 example/app/src/main/java/dev/matrix/rust/MainActivity.kt create mode 100644 example/app/src/main/java/dev/rodroid/rust/MainActivity.kt delete mode 100644 example/app/src/main/res/layout/activity_main.xml delete mode 100644 example/app/src/main/res/layout/content_main.xml delete mode 100644 example/app/src/main/res/layout/fragment_first.xml delete mode 100644 example/app/src/main/res/layout/fragment_second.xml delete mode 100644 example/app/src/main/res/menu/menu_main.xml delete mode 100644 example/app/src/main/res/navigation/nav_graph.xml delete mode 100644 example/app/src/main/res/values-land/dimens.xml delete mode 100644 example/app/src/main/res/values-w1240dp/dimens.xml delete mode 100644 example/app/src/main/res/values-w600dp/dimens.xml delete mode 100644 example/app/src/main/res/values/dimens.xml diff --git a/build.gradle.kts b/build.gradle.kts index 1d3473c..617a553 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,15 +4,15 @@ plugins { id("com.gradle.plugin-publish") version "1.3.0" } -val pluginId = "io.github.MatrixDev.android-rust" +val pluginId = "io.github.rodroidmods.android-rust" group = pluginId -version = "0.6.0" +version = "0.7.0" @Suppress("UnstableApiUsage") gradlePlugin { - website = "https://github.com/MatrixDev/GradleAndroidRustPlugin" - vcsUrl = "https://github.com/MatrixDev/GradleAndroidRustPlugin.git" + website = "https://github.com/rodroidmods/GradleAndroidRustPlugin" + vcsUrl = "https://github.com/rodroidmods/GradleAndroidRustPlugin.git" plugins { create("AndroidRust") { @@ -44,4 +44,4 @@ dependencies { implementation(libs.agp) implementation(libs.agp.api) -} +} \ No newline at end of file diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts index 2f696a9..1b6e7f3 100644 --- a/example/app/build.gradle.kts +++ b/example/app/build.gradle.kts @@ -1,25 +1,34 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) alias(libs.plugins.android.rust) } android { - namespace = "dev.matrix.rust" - compileSdk = 33 + namespace = "dev.rodroid.rust" + compileSdk = 36 ndkVersion = "25.2.9519653" defaultConfig { - applicationId = "dev.matrix.rust" - minSdk = 21 - targetSdk = 33 + applicationId = "dev.rodroid.rust" + minSdk = 26 + targetSdk = 36 versionCode = 1 versionName = "1.0" } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.15" + } + buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) proguardFiles("proguard-rules.pro") } @@ -32,19 +41,20 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } - kotlinOptions { - jvmTarget = "1.8" + kotlin { + jvmToolchain(21) } } androidRust { module("library") { path = file("src/main/jni") - + //targets = listOf("arm64", "x86_64", "arm", "x86") + targets = listOf("x86_64") buildType("release") { runTests = true } @@ -53,10 +63,22 @@ androidRust { } dependencies { - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") - implementation("androidx.navigation:navigation-ui-ktx:2.5.3") - implementation("com.google.android.material:material:1.8.0") + implementation(platform(libs.androidx.compose.bom)) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.google.material) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml index 1493687..1868b0d 100644 --- a/example/app/src/main/AndroidManifest.xml +++ b/example/app/src/main/AndroidManifest.xml @@ -4,7 +4,6 @@ diff --git a/example/app/src/main/java/dev/matrix/rust/MainActivity.kt b/example/app/src/main/java/dev/matrix/rust/MainActivity.kt deleted file mode 100644 index 94a5867..0000000 --- a/example/app/src/main/java/dev/matrix/rust/MainActivity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.matrix.rust - -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - private external fun callRustCode(): String - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - System.loadLibrary("rust_library") - - Toast.makeText(this, callRustCode(), Toast.LENGTH_LONG).show() - - finish() - } -} diff --git a/example/app/src/main/java/dev/rodroid/rust/MainActivity.kt b/example/app/src/main/java/dev/rodroid/rust/MainActivity.kt new file mode 100644 index 0000000..badca4e --- /dev/null +++ b/example/app/src/main/java/dev/rodroid/rust/MainActivity.kt @@ -0,0 +1,50 @@ +package dev.rodroid.rust + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.delay +import androidx.compose.ui.text.style.TextAlign + +class MainActivity : ComponentActivity() { + + private external fun callRustCode(): String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + System.loadLibrary("rust_library") + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = Color.Black + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + contentAlignment = Alignment.Center + ) { + Text( + text = callRustCode(), + color = Color.Red, + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/example/app/src/main/jni/Cargo.toml b/example/app/src/main/jni/Cargo.toml index 81a227e..0dc9dac 100644 --- a/example/app/src/main/jni/Cargo.toml +++ b/example/app/src/main/jni/Cargo.toml @@ -1,10 +1,27 @@ [package] name = "rust_library" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] crate-type = ["cdylib"] [dependencies] jni = "0.19.0" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = "symbols" +debug = false +overflow-checks = false +incremental = false + +[profile.release.package."*"] +opt-level = "z" +codegen-units = 1 +strip = true +debug = false +overflow-checks = false \ No newline at end of file diff --git a/example/app/src/main/jni/src/lib.rs b/example/app/src/main/jni/src/lib.rs index bad34cb..fb07cc5 100644 --- a/example/app/src/main/jni/src/lib.rs +++ b/example/app/src/main/jni/src/lib.rs @@ -1,16 +1,12 @@ -use jni::JNIEnv; -use jni::objects::JObject; +use jni::{JNIEnv, objects::JObject}; use jni::sys::jstring; -#[no_mangle] -extern "C" fn Java_dev_matrix_rust_MainActivity_callRustCode(env: JNIEnv, _: JObject) -> jstring { - env.new_string("Hello from rust!").unwrap().into_inner() -} - -#[cfg(test)] -mod test { - #[test] - fn test() { - println!("Hello from rust test!") - } -} +#[unsafe(no_mangle)] +pub extern "C" fn Java_dev_rodroid_rust_MainActivity_callRustCode( + env: JNIEnv, + _: JObject, +) -> jstring { + let message = "Hello from Rust"; + let java_string = env.new_string(message).expect("Couldn't create java string!"); + java_string.into_inner() +} \ No newline at end of file diff --git a/example/app/src/main/res/layout/activity_main.xml b/example/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 1968bd8..0000000 --- a/example/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/app/src/main/res/layout/content_main.xml b/example/app/src/main/res/layout/content_main.xml deleted file mode 100644 index e416e1c..0000000 --- a/example/app/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/example/app/src/main/res/layout/fragment_first.xml b/example/app/src/main/res/layout/fragment_first.xml deleted file mode 100644 index fb44a3d..0000000 --- a/example/app/src/main/res/layout/fragment_first.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - -