Cross compile Rust Cargo projects for Android targets.
This is a fork of the original mozilla/rust-android-gradle with a focus on up-to-date Gradle plugin authoring practices. Support for older AGP versions is sacrificed to focus on the upcoming Gradle 9.0 release, which removes several APIs that were relied upon by the upstream plugin.
Ensure that your settings.gradle.kts includes the gradlePluginPortal() repository:
pluginManagement {
repositories {
/* other repos */
gradlePluginPortal()
}
}In your project's build.gradle.kts, declare the rust-android-gradle plugin in your plugins block and include the cargo block:
plugins {
id("me.sigptr.rust-android") version("1.0.0")
}
cargo {
module = "../rust"
libname = "rust"
targets = listOf("x86_64", "arm64")
}Install the Rust targets corresponding to your cargo.targets, e.g. in this case:
rustup target add x86_64-linux-android
rustup target add aarch64-linux-androidNow you need to make sure that the cargoBuild task is a dependency of your generate*Assets tasks through the following segment in your build.gradle.kts:
afterEvaluate {
fun CharSequence.capitalized() =
toString().replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
/* replace applicationVariants with libraryVariants if compiling an Android library */
android.applicationVariants.forEach { variant ->
val productFlavor = variant.productFlavors.joinToString("") { it.name.capitalized() }
val buildType = variant.buildType.name.capitalized()
tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"])
}
}There are two kinds of targets, desktop targets and Android targets. Android targets are designed for inclusion in an Android app at runtime, whereas desktop targets are useful for running unit tests on a local machine. Better support for desktop targets in unit tests is planned. In the meantime, see the unittest example for more guidance on using a desktop target to run Java unit tests with Rust code.
| OS | Arch | Rust target | build.gradle target |
|---|---|---|---|
| Android | x86_64 | x86_64-linux-android |
x86_64 |
| arm64 | aarch64-linux-android |
arm64 |
|
| x86 | i686-linux-android |
x86 |
|
| armv7 | armv7-linux-androideabi |
arm |
|
| Linux | x86_64 | x86_64-unknown-linux-gnu |
linux-x86-64 |
| MacOS | arm64 | aarch64-apple-darwin |
darwin-aarch64 |
| x86_64 | x86_64-apple-darwin |
darwin-x86-64 |
|
| Windows | x86_64 | x86_64-pc-windows-msvc |
win32-x86-64-msvc |
x86_64-pc-windows-gnu |
win32-x86-64-gnu |
The cargo Gradle configuration accepts many options.
Generated libraries will be added to the Android jniLibs source-sets, when correctly referenced in
the cargo configuration through the libname and/or targetIncludes options. The latter
defaults to ["lib${libname}.so", "lib${libname}.dylib", "{$libname}.dll"], so the following configuration will
include all libbackend libraries generated in the Rust project in ../rust:
cargo {
module = "../rust"
libname = "backend"
}
Now, Java code can reference the native library using, e.g.,
static {
System.loadLibrary("backend");
}The Android NDK also fixes an API level,
which can be specified using the apiLevel option. This option defaults to the minimum SDK API
level. As of API level 21, 64-bit builds are possible; and conversely, the arm64 and x86_64
targets require apiLevel >= 21.
The profile option selects between the --debug and --release profiles in cargo. Defaults
to debug!
The path to the Rust library to build with Cargo; required. module can be absolute; if it is not,
it is interpreted as a path relative to the Gradle projectDir.
cargo {
// Note: path is either absolute, or relative to the gradle project's `projectDir`.
module = "../rust"
}The library name produced by Cargo; required.
libname is used to determine which native libraries to include in the produced AARs and/or APKs.
See also targetIncludes.
libname is also used to determine the ELF SONAME to declare in the Android libraries produced by
Cargo. Different versions of the Android system linker
depend on the ELF SONAME.
In Cargo.toml:
[lib]
name = "test"In build.gradle:
cargo {
libname = "test"
}A list of Android targets to build with Cargo; required. See Supported Targets for a list of supported values.
cargo {
/* groovy */
targets = ['arm', 'x86', 'linux-x86-64']
/* kotlin */
targets = listOf("arm", "x86", "linux-x86-64")
}When set to true (which requires NDK version 19+), use the prebuilt toolchains bundled with the
NDK. When set to false, generate per-target architecture standalone NDK toolchains using
make_standalone_toolchain.py. When unset, use the prebuilt toolchains if the NDK version is 19+,
and fall back to generated toolchains for older NDK versions.
Defaults to null.
cargo {
prebuiltToolchains = true
}When set, execute cargo build with or without the --verbose flag. When unset, respect the
Gradle log level: execute cargo build with or without the --verbose flag according to whether
the log level is at least INFO. In practice, this makes ./gradlew ... --info (and ./gradlew ... --debug) execute cargo build --verbose ....
Defaults to null.
cargo {
verbose = true
}The Cargo release profile to build.
Defaults to "debug".
cargo {
profile = 'release'
}Set the Cargo features.
Defaults to passing no flags to cargo.
To pass --all-features, use
cargo {
features {
all()
}
}To pass an optional list of --features, use
cargo {
features {
defaultAnd("x")
defaultAnd("x", "y")
}
}To pass --no-default-features, and an optional list of replacement --features, use
cargo {
features {
noDefaultBut()
noDefaultBut("x")
noDefaultBut("x", "y")
}
}The target directory into which Cargo writes built outputs. You will likely need to specify this if you are using a cargo virtual workspace, as our default will likely fail to locate the correct target directory.
Defaults to ${module}/target. targetDirectory can be absolute; if it is not, it is interpreted
as a path relative to the Gradle projectDir.
Note that if CARGO_TARGET_DIR (see https://doc.rust-lang.org/cargo/reference/environment-variables.html)
is specified in the environment, it takes precedence over targetDirectory, as cargo will output
all build artifacts to it, regardless of what is being built, or where it was invoked.
You may also override CARGO_TARGET_DIR variable by setting rust.cargoTargetDir in
local.properties, however it seems very unlikely that this will be useful, as we don't pass this
information to cargo itself. That said, it can be used to control where we search for the built
library on a per-machine basis.
cargo {
// Note: path is either absolute, or relative to the gradle project's `projectDir`.
targetDirectory = "path/to/workspace/root/target"
}Which Cargo outputs to consider JNI libraries.
Defaults to ["lib${libname}.so", "lib${libname}.dylib", "{$libname}.dll"].
cargo {
targetIncludes = ["libnotlibname.so"]
}The Android NDK API level to target. NDK API levels are not the same as SDK API versions; they are updated less frequently. For example, SDK API versions 18, 19, and 20 all target NDK API level 18.
Defaults to the minimum SDK version of the Android project's default configuration.
cargo {
apiLevel = 21
}You may specify the API level per target in targets using the apiLevels option. At most one of
apiLevel and apiLevels may be specified. apiLevels must have an entry for each target in
targets.
cargo {
/* groovy */
targets = ["arm", "x86_64"]
apiLevels = [
"arm": 16,
"x86_64": 21,
]
/* kotlin */
targets = listOf("arm", "x86_64")
apiLevels = mapOf(
"arm" to 16,
"x86_64" to 21
)
}Sometimes, you need to do things that the plugin doesn't anticipate. Use extraCargoBuildArguments
to append a list of additional arguments to each cargo build invocation.
cargo {
extraCargoBuildArguments = ["a", "list", "of", "strings"]
}Generate a build-id for the shared library during the link phase.
This is a callback taking the ExecSpec we're going to use to invoke cargo build, and
the relevant toolchain. It's called for each invocation of cargo build. This generally
is useful for the following scenarios:
- Specifying target-specific environment variables.
- Adding target-specific flags to the command line.
- Removing/modifying environment variables or command line options the rust-android-gradle plugin would provide by default.
cargo {
exec { spec, toolchain ->
if (toolchain.target != "x86_64-apple-darwin") {
// Don't statically link on macOS desktop builds, for some
// entirely hypothetical reason.
spec.environment("EXAMPLELIB_STATIC", "1")
}
}
}The plugin can either use prebuilt NDK toolchain binaries, or search for (and if missing, build)
NDK toolchains as generated by make_standalone_toolchain.py.
A prebuilt NDK toolchain will be used if:
rust.prebuiltToolchain=truein the per-(multi-)project${rootDir}/local.propertiesprebuiltToolchain=truein thecargo { ... }block (if not overridden bylocal.properties)- The discovered NDK is version 19 or higher (if not overridden per above)
The toolchains are rooted in a single Android NDK toolchain directory. In order of preference, the toolchain root directory is determined by:
rust.androidNdkToolchainDirin the per-(multi-)project${rootDir}/local.properties- the environment variable
ANDROID_NDK_TOOLCHAIN_DIR ${System.getProperty(java.io.tmpdir)}/rust-android-ndk-toolchains
Note that the Java system property java.io.tmpdir is not necessarily /tmp, including on macOS hosts.
Each target architecture toolchain is named like $arch-$apiLevel: for example, arm-16 or arm64-21.
When developing a project that consumes rust-android-gradle locally, it's often convenient to
temporarily change the set of Rust target architectures. In order of preference, the plugin
determines the per-project targets by:
rust.targets.${project.Name}for each project in${rootDir}/local.propertiesrust.targetsin${rootDir}/local.properties- the
cargo { targets ... }block in the per-projectbuild.gradle
The targets are split on ','. For example:
rust.targets.library=linux-x86-64
rust.targets=arm,linux-x86-64,darwin
The plugin invokes Python, Cargo and Rustc. In order of preference, the plugin determines what command to invoke for Python by:
- the value of
cargo { pythonCommand = "..." }, if non-empty rust.pythonCommandin${rootDir}/local.properties- the environment variable
RUST_ANDROID_GRADLE_PYTHON_COMMAND - the default,
python
In order of preference, the plugin determines what command to invoke for Cargo by:
- the value of
cargo { cargoCommand = "..." }, if non-empty rust.cargoCommandin${rootDir}/local.properties- the environment variable
RUST_ANDROID_GRADLE_CARGO_COMMAND - the default,
cargo
In order of preference, the plugin determines what command to invoke for rustc by:
- the value of
cargo { rustcCommand = "..." }, if non-empty rust.rustcCommandin${rootDir}/local.properties- the environment variable
RUST_ANDROID_GRADLE_RUSTC_COMMAND - the default,
rustc
(Note that failure to locate rustc is not fatal, however it may result in rebuilding the code more often than is necessary).
Paths must be host operating system specific. For example, on Windows:
rust.pythonCommand=c:\Python27\bin\pythonOn Linux,
env RUST_ANDROID_GRADLE_CARGO_COMMAND=$HOME/.cargo/bin/cargo ./gradlew ...Rust is released to three different "channels": stable, beta, and nightly (see
https://rust-lang.github.io/rustup/concepts/channels.html). The rustup tool, which is how most
people install Rust, allows multiple channels to be installed simultaneously and to specify which
channel to use by invoking cargo +channel ....
In order of preference, the plugin determines what channel to invoke cargo with by:
- the value of
cargo { rustupChannel = "..." }, if non-empty rust.rustupChannelin${rootDir}/local.properties- the environment variable
RUST_ANDROID_GRADLE_RUSTUP_CHANNEL - the default, no channel specified (which
cargoinstalled viarustupgenerally defaults to thestablechannel)
The channel should be recognized by cargo installed via rustup, i.e.:
"stable""beta""nightly"
A single leading '+' will be stripped, if present.
(Note that Cargo installed by a method other than rustup will generally not understand +channel
and builds will likely fail.)
The plugin passes project properties named like RUST_ANDROID_GRADLE_target_..._KEY=VALUE through
to the Cargo invocation for the given Rust target as KEY=VALUE. Target should be upper-case
with "-" replaced by "_". (See the links from this Cargo issue.) So, for example,
project.RUST_ANDROID_GRADLE_I686_LINUX_ANDROID_FOO=BARand
./gradlew -PRUST_ANDROID_GRADLE_ARMV7_LINUX_ANDROIDEABI_FOO=BAR ...and
env ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_ARMV7_LINUX_ANDROIDEABI_FOO=BAR ./gradlew ...
all set FOO=BAR in the cargo execution environment (for the "armv7-linux-androideabi` Rust
target, corresponding to the "x86" target in the plugin).
At top-level, the publish Gradle task updates the Maven repository
under build/local-repo:
$ ./gradlew publish
...
$ ls -al build/local-repo/org/mozilla/rust-android-gradle/org.mozilla.rust-android-gradle.gradle.plugin/0.4.0/org.mozilla.rust-android-gradle.gradle.plugin-0.4.0.pom
-rw-r--r-- 1 nalexander staff 670 18 Sep 10:09
build/local-repo/org/mozilla/rust-android-gradle/org.mozilla.rust-android-gradle.gradle.plugin/0.4.0/org.mozilla.rust-android-gradle.gradle.plugin-0.4.0.pom
The easiest way to get started is to run the sample projects. The sample projects have dependency
substitutions configured so that changes made to plugin/ are reflected in the sample projects
immediately.
$ ./gradlew -p samples/library :assembleDebug
...
$ file samples/library/build/outputs/aar/library-debug.aar
samples/library/build/outputs/aar/library-debug.aar: Zip archive data, at least v1.0 to extract
$ ./gradlew -p samples/app :assembleDebug
...
$ file samples/app/build/outputs/apk/debug/app-debug.apk
samples/app/build/outputs/apk/debug/app-debug.apk: Zip archive data, at least v?[0] to extract
An easy way to locally test changes made in this plugin is to simply add this to your project's settings.gradle:
// Switch this to point to your local plugin dir
includeBuild('../rust-android-gradle') {
dependencySubstitution {
// As required.
substitute module('gradle.plugin.org.mozilla.rust-android-gradle:plugin') with project(':plugin')
}
}You will need to be a collaborator. First, manually invoke the Bump version Github Actions
workflow. Specify a
version (like "x.y.z", without quotes) and a single line changelog entry. (This entry will have a
dash prepended, so that it would look normal in a list. This is working around the lack of a
multi-line input in Github
Actions.) This will push
a preparatory commit updating version numbers and the changelog like this
one,
and make a draft Github Release with a name like vx.y.z. After verifying that tests pass,
navigate to the releases panel and edit
the release, finally pressing "Publish release". The release Github workflow will build and publish
the plugin, although it may take some days for it to be reflected on the Gradle plugin portal.
You will need credentials to publish to the Gradle plugin portal in
the appropriate place for the plugin-publish to
find them. Usually, that's in ~/.gradle/gradle.properties.
At top-level, the publishPlugins Gradle task publishes the plugin for consumption:
$ ./gradlew publishPlugins
...
Publishing plugin org.mozilla.rust-android-gradle.rust-android version 0.8.1
Publishing artifact build/libs/plugin-0.8.1.jar
Publishing artifact build/libs/plugin-0.8.1-sources.jar
Publishing artifact build/libs/plugin-0.8.1-javadoc.jar
Publishing artifact build/publish-generated-resources/pom.xml
Activating plugin org.mozilla.rust-android-gradle.rust-android version 0.8.1
To test in a real project, use the local Maven repository in your build.gradle, like:
buildscript {
repositories {
maven {
url "file:///Users/nalexander/Mozilla/rust-android-gradle/build/local-repo"
}
}
dependencies {
classpath 'me.sigptr.rust-android:plugin:0.10.0'
}
}