Announcing cargo-apk

The Rust compiler has been supporting Android for a long time now. However when you ask rustc to compile for Android, what it produces is an executable that you can only run through debugging tools. When we talk about developing for Android, what we usually want is an apk or Android package. An apk is a file that contains the Android application in addition to various resource files and that can be installed then run by an average user.

To automate building an apk, you may have heard of the android-rs-glue repository, which Servo uses. Through a weird trick, it would capture the arguments that rustc passed to the linker and instead manually invoke the linker itself, then setup a build directory and run the normal Android package building process.

One of the difficult part of setting up an Android environment was compiling Rust for Android. This would require some steps are not necessarily easy for everyone. But now that the Rustup utility appeared, you can obtain an Android-compatible with just rustup target add arm-linux-androideabi. Now only the only difficult part left was setting up android-rs-glue.

Announcing cargo-apk

A few days ago, I started reworking the content of android-rs-glue to make it much easier to use. The initial part of the rework has just landed.

Now, all you need to do is to compile for Android is:

  • Download the Android SDK and NDK, unzip them somewhere, and export the NDK_HOME and ANDROID_HOME environment variables to point to their content.
  • Obtain an Android-compatible rustc. If you installed rustup, this is done with rustup target add arm-linux-androideabi.
  • Run cargo install cargo-apk.

In the future this may be even more simplified by the fact that rustup could automatically distribute the NDK.

Now go to your project's path and run cargo apk install. Provided that all the libraries you use support Android, you won't need to make any modification to your source code.

This should build your crate just like any other crate, except that the final output is an Android package that runs the main function of your program at startup. Once this is done, it automatically runs adb install -r <path to apk> to install the app on your device so you can test it. If you don't want to install the package, just run cargo apk.

Android_glue package

If you run cargo apk on a hello world program, starting the application will write hello world to the logs and show a black screen. This is obviously not very useful.

In order to interface with the Android environment, you will need to use the android_glue crate. This crate provides the functions required to capture user input and draw to the window through OpenGL or Vulkan.

It is important to note, however, that no function will allow you to get a native Android UI. This build system and this library are meant for applications that handle everything themselves, like video games. If you want a regular app you should write in Java.

Customizing the apk

In order to customize the apk, you can put additional elements in your Cargo.toml, like the basic example shows. Only a few values exist yet. This remains to be documented.

A few notes

  • Contributors are welcome! Your contributions is even more valuable if you are familiar with Android apps development in general. Even if you are afraid of the technical complexity there are a lot of areas where work is needed, like being able to costumize the app's icon, or handling non-debug builds.
  • Apps that use glutin should work out of the box, as glutin supports android_glue. EDIT: glutin hasn't been updated yet, so if you try now you will get the old version of android_glue and likely a panic.
  • For the moment cargo-apk only runs on Linux 64bits because of this precise line of code. PRs welcome!
  • Cargo-apk supports Android packages that provide multiple binaries for multiple platforms. Unfortunately this isn't customizable yet and it defaults to just arm-linux-androideabi.
46 Likes

The Rust cross-compile story is improving fast! This is going to be a really important tool. Thanks @tomaka.

5 Likes

This sounds awesome, but I'm having trouble getting it to work. The examples/basic example needs a cargo update in a fresh checkout, and even after that I get linker errors when running cargo apk install:

note: /home/huon/tmp/android-rs-glue/glue/src/lib.rs:111: error: undefined reference to 'cargo_apk_injected_glue_write_log'

If I remove the write_log call (leaving just fn main() { loop {} }) it compiles successfully, but crashes on start-up.

(I believe I'm using a slightly old version of the NDK: r10e-rc4.)

The examples/basic example needs a cargo update in a fresh checkout

I'm going to fix this.

and even after that I get linker errors when running cargo apk install:

Is that a linker error? This looks rather like a compilation error. But in that case it's weird, as the function is declared at the top of that same file:

If I remove the write_log call (leaving just fn main() { loop {} }) it compiles successfully, but crashes on start-up.

Hmm, could you run adb logcat after running the app and try find any sort of error or exception that could help?

Ah nevermind, if it's a linking error then the problem is that the glue is not correctly injected.
This would be the cause for the startup crash as well.

Yes, full message:

$ ANDROID_SDK_HOME=~/android/android-sdk-linux/ NDK_HOME=~/android/android-ndk-r10e/ cargo apk install --verbose
/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/injected-glue/lib.rs:9:55: 9:62 warning: unused import, #[warn(unused_imports)] on by default
/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/injected-glue/lib.rs:9 use std::sync::mpsc::{Sender, Receiver, TryRecvError, channel};
                                                                                                                                                    ^~~~~~~
[... more warnings ...]
/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/injected-glue/lib.rs:499:9: 499:25 warning: use of deprecated item: replaced by `std::thread::sleep`, #[warn(deprecated)] on by default
/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/injected-glue/lib.rs:499         thread::sleep_ms(10);
                                                                                                        ^~~~~~~~~~~~~~~~
unused manifest key: package.metadata.android.label
       Fresh android_glue v0.2.0 (file:///home/huon/tmp/android-rs-glue/examples/basic)
   Compiling android_glue_example v0.1.0 (file:///home/huon/tmp/android-rs-glue/examples/basic)
     Running `rustc src/basic.rs --crate-name example --crate-type bin -g -C linker=/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/linker_exe --extern cargo_apk_injected_glue=/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/arm-linux-androideabi/libcargo_apk_injected_glue.rlib --out-dir /home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug --emit=dep-info,link --target arm-linux-androideabi -C ar=/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-ar -C linker=/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-gcc -L dependency=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug -L dependency=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps --extern android_glue=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps/libandroid_glue-3b1e2d39e5994b6e.rlib`
error: linking with `/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-gcc` failed: exit code: 1
note: "/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-gcc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-Wl,--allow-multiple-definition" "-L" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib" "/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/example.0.o" "-o" "/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/example" "-Wl,--gc-sections" "-pie" "-nodefaultlibs" "-L" "/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug" "-L" "/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps" "-L" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib" "-Wl,-Bstatic" "-Wl,-Bdynamic" "/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps/libandroid_glue-3b1e2d39e5994b6e.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/libstd-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/libcollections-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/librustc_unicode-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/librand-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/liballoc-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/liballoc_jemalloc-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/liblibc-4fda350b.rlib" "/home/huon/.multirust/toolchains/1.8.0-x86_64-unknown-linux-gnu/lib/rustlib/arm-linux-androideabi/lib/libcore-4fda350b.rlib" "-l" "dl" "-l" "log" "-l" "gcc" "-l" "gcc" "-l" "c" "-l" "m" "-l" "compiler-rt"
note: /home/huon/tmp/android-rs-glue/glue/src/lib.rs:111: error: undefined reference to 'cargo_apk_injected_glue_write_log'
collect2: error: ld returned 1 exit status

error: aborting due to previous error
Could not compile `android_glue_example`.

Caused by:
  Process didn't exit successfully: `rustc src/basic.rs --crate-name example --crate-type bin -g -C linker=/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/linker_exe --extern cargo_apk_injected_glue=/home/huon/tmp/android-rs-glue/examples/basic/target/android-artifacts/arm-linux-androideabi/libcargo_apk_injected_glue.rlib --out-dir /home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug --emit=dep-info,link --target arm-linux-androideabi -C ar=/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-ar -C linker=/home/huon/android/ndk-standalone-21-arm/bin/arm-linux-androideabi-gcc -L dependency=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug -L dependency=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps --extern android_glue=/home/huon/tmp/android-rs-glue/examples/basic/target/arm-linux-androideabi/debug/deps/libandroid_glue-3b1e2d39e5994b6e.rlib` (exit code: 101)
third party subcommand `cargo-apk` exited unsuccessfully

To learn more, run the command again with --verbose.

I can't see any of the custom linker arguments that cargo-apk is trying to pass in that command line, so maybe the linker interception isn't firing properly? (I'm using rust 1.8.0, but it seems to fail in a similar way with a recent nightly.)

I see from the log that you passed your own linker (my guess is that you put it in a .cargo/config and forgot about it).
Unfortunately this overrides the linker that is set by cargo-apk.

I wonder if cargo-apk can detect that problem.

1 Like

Ah, yes, I'd had a custom linker from when I was using my android devices as ARM test hardware with command line apps. Thanks for the help!

This is really excellent to see. With just a little bit more work on the Java side (setting up communication with Rust code via channels), embedding Rust in Android apps could be as much of a breeze as building them is now. Great work as usual, @tomaka!

1 Like