Using the Android Native Development Kit with Bazel
If you’re new to Bazel, please start with the Building Android with Bazel tutorial.
Overview
Bazel can run in many different build configurations, including several that use
the Android Native Development Kit (NDK) toolchain. This means that normal
cc_library
and cc_binary
rules can be compiled for Android directly within
Bazel. Bazel accomplishes this by using the android_ndk_repository
repository
rule.
Prerequisites
Please ensure that you have installed the Android SDK and NDK.
To set up the SDK and NDK, add the following snippet to your WORKSPACE
:
android_sdk_repository(
name = "androidsdk", # Required. Name *must* be "androidsdk".
path = "/path/to/sdk", # Optional. Can be omitted if `ANDROID_HOME` environment variable is set.
)
android_ndk_repository(
name = "androidndk", # Required. Name *must* be "androidndk".
path = "/path/to/ndk", # Optional. Can be omitted if `ANDROID_NDK_HOME` environment variable is set.
)
For more information on the android_ndk_repository
rule, see the Build
Encyclopedia entry.
Quick start
To build C++ for Android, simply add cc_library
dependencies to your
android_binary
or android_library
rules.
For example, given the following BUILD
file for an Android app:
# In <project>/app/src/main/BUILD.bazel
cc_library(
name = "jni_lib",
srcs = ["cpp/native-lib.cpp"],
)
android_library(
name = "lib",
srcs = ["java/com/example/android/bazel/MainActivity.java"],
resource_files = glob(["res/**/*"]),
custom_package = "com.example.android.bazel",
manifest = "LibraryManifest.xml",
deps = [":jni_lib"],
)
android_binary(
name = "app",
deps = [":lib"],
manifest = "AndroidManifest.xml",
)
This BUILD
file results in the following target graph:
To build the app, simply run:
$ bazel build //app/src/main:app
The bazel build
command compiles the Java files, Android resource files, and
cc_library
rules, and packages everything into an APK:
$ zipinfo -1 bazel-bin/app/src/main/app.apk
nativedeps
lib/armeabi-v7a/libapp.so
classes.dex
AndroidManifest.xml
...
res/...
...
META-INF/CERT.SF
META-INF/CERT.RSA
META-INF/MANIFEST.MF
Bazel compiles all of the cc_libraries into a single shared object (.so
) file,
targeted for the armeabi-v7a
ABI by default. To change this or build for
multiple ABIs at the same time, see the section on configuring the target
ABI.
Example setup
This example is available in the Bazel examples repository.
In the BUILD.bazel
file, three targets are defined with the android_binary
,
android_library
, and cc_library
rules.
The android_binary
top-level target builds the APK.
The cc_library
target contains a single C++ source file with a JNI function
implementation:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring
JNICALL
Java_com_example_android_bazel_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
The android_library
target specifies the Java sources, resource files, and the
dependency on a cc_library
target. For this example, MainActivity.java
loads
the shared object file libapp.so
, and defines the method signature for the JNI
function:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("app");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
}
public native String stringFromJNI();
}
Note: The name of the native library is derived from the name of the top
level android_binary
target. In this example, it is app
.
Configuring the STL
To configure the C++ STL, use the flag --android_crosstool_top
.
bazel build //:app --android_crosstool_top=<target label>
The available STLs in @androidndk
are:
STL | Target label |
---|---|
STLport | @androidndk//:toolchain-stlport |
libc++ | @androidndk//:toolchain-libcpp |
gnustl | @androidndk//:toolchain-gnu-libstdcpp |
For r16 and below, the default STL is gnustl
. For r17 and above, it is
libc++
. For convenience, the target @androidndk//:default_crosstool
is
aliased to the respective default STLs.
Please note that from r18 onwards, STLport and gnustl will be
removed,
making libc++
the only STL in the NDK.
See the NDK documentation for more information on these STLs.
Configuring the target ABI
To configure the target ABI, use the --fat_apk_cpu
flag as follows:
bazel build //:app --fat_apk_cpu=<comma-separated list of ABIs>
By default, Bazel builds native Android code for armeabi-v7a
. To build for x86
(e.g. for emulators), pass --fat_apk_cpu=x86
. To create a fat APK for multiple
architectures, you can specify multiple CPUs: --fat_apk_cpu=armeabi-v7a,x86
.
If more than one ABI is specified, Bazel will build an APK containing a shared object for each ABI.
Depending on the NDK revision and Android API level, the following ABIs are available:
NDK revision | ABIs |
---|---|
16 and lower | armeabi, armeabi-v7a, arm64-v8a, mips, mips64, x86, x86_64 |
17 and above | armeabi-v7a, arm64-v8a, x86, x86_64 |
See the NDK docs for more information on these ABIs.
Multi-ABI Fat APKs are not recommended for release builds since they increase the size of the APK, but can be useful for development and QA builds.
Selecting a C++ standard
Use the following flags to build according to a C++ standard:
C++ Standard | Flag |
---|---|
C++98 | Default, no flag needed |
C++11 | --cxxopt=-std=c++11 |
C++14 | --cxxopt=-std=c++14 |
For example:
bazel build //:app --cxxopt=-std=c++11
Read more about passing compiler and linker flags with --cxxopt
, --copt
, and
--linkopt
in the User Manual.
Compiler and linker flags can also be specified as attributes in cc_library
using copts
and linkopts
. For example:
cc_library(
name = "jni_lib",
srcs = ["cpp/native-lib.cpp"],
copts = ["-std=c++11"],
linkopts = ["-ldl"], # link against libdl
)
Integration with platforms and toolchains
Bazel’s configuration model is moving towards
platforms and
toolchains. If your
build uses the --platforms
flag to select for the architecture or operating system
to build for, you will need to pass the --extra_toolchains
flag to Bazel in
order to use the NDK.
For example, to integrate with the android_arm64_cgo
toolchain provided by
the Go rules, pass --extra_toolchains=@androidndk//:all
in addition to the
--platforms
flag.
bazel build //my/cc:lib \
--platforms=@io_bazel_rules_go//go/toolchain:android_arm64_cgo \
--extra_toolchains=@androidndk//:all
You can also register them directly in the WORKSPACE
file:
android_ndk_repository(name = "androidndk")
register_toolchains("@androidndk//:all")
Registering these toolchains tells Bazel to look for them in the NDK BUILD
file (for NDK 20) when resolving architecture and operating system constraints:
toolchain(
name = "x86-clang8.0.7-libcpp_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
target_compatible_with = [
"@platforms//os:android",
"@platforms//cpu:x86_32",
],
toolchain = "@androidndk//:x86-clang8.0.7-libcpp",
)
toolchain(
name = "x86_64-clang8.0.7-libcpp_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
target_compatible_with = [
"@platforms//os:android",
"@platforms//cpu:x86_64",
],
toolchain = "@androidndk//:x86_64-clang8.0.7-libcpp",
)
toolchain(
name = "arm-linux-androideabi-clang8.0.7-v7a-libcpp_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
target_compatible_with = [
"@platforms//os:android",
"@platforms//cpu:arm",
],
toolchain = "@androidndk//:arm-linux-androideabi-clang8.0.7-v7a-libcpp",
)
toolchain(
name = "aarch64-linux-android-clang8.0.7-libcpp_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
target_compatible_with = [
"@bazel_tools//platforms:android",
"@bazel_tools//platforms:aarch64"
],
toolchain = "@androidndk//:aarch64-linux-android-clang8.0.7-libcpp",
)
How it works: introducing Android configuration transitions
The android_binary
rule can explicitly ask Bazel to build its dependencies in
an Android-compatible configuration so that the Bazel build just works without
any special flags, except for --fat_apk_cpu
and --android_crosstool_top
for
ABI and STL configuration.
Behind the scenes, this automatic configuration uses Android configuration transitions.
A compatible rule, like android_binary
, automatically changes the
configuration of its dependencies to an Android configuration, so only
Android-specific subtrees of the build are affected. Other parts of the build
graph are processed using the top-level target configuration. It may even
process a single target in both configurations, if there are paths through the
build graph to support that.
Once Bazel is in an Android-compatible configuration, either specified at the top level or due to a higher-level transition point, additional transition points encountered do not further modify the configuration.
The only built-in location that triggers the transition to the Android
configuration is android_binary
’s deps
attribute.
Note: The data
attribute of android_binary
intentionally does not
trigger the transition. Additionally, android_local_test
and android_library
intentionally do not trigger the transition at all.
For example, if you try to build an android_library
target with a cc_library
dependency without any flags, you may encounter an error about a missing JNI
header:
ERROR: <project>/app/src/main/BUILD.bazel:16:1: C++ compilation of rule '//app/src/main:jni_lib' failed (Exit 1)
app/src/main/cpp/native-lib.cpp:1:10: fatal error: 'jni.h' file not found
#include <jni.h>
^~~~~~~
1 error generated.
Target //app/src/main:lib failed to build
Use --verbose_failures to see the command lines of failed build steps.
Ideally, these automatic transitions should make Bazel do the right thing in the
majority of cases. However, if the target on the Bazel command-line is already
below any of these transition rules, such as C++ developers testing a specific
cc_library
, then a custom --crosstool_top
must be used.
Building a cc_library
for Android without using android_binary
To build a standalone cc_binary
or cc_library
for Android without using an
android_binary
, use the --crosstool_top
, --cpu
and --host_crosstool_top
flags.
For example:
bazel build //my/cc/jni:target \
--crosstool_top=@androidndk//:default_crosstool \
--cpu=<abi> \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain
In this example, the top-level cc_library
and cc_binary
targets are built
using the NDK toolchain. However, this causes Bazel’s own host tools to be built
with the NDK toolchain (and thus for Android), because the host toolchain is
copied from the target toolchain. To work around this, specify the value of
--host_crosstool_top
to be @bazel_tools//tools/cpp:toolchain
to
explicitly set the host’s C++ toolchain.
With this approach, the entire build tree is affected.
Note that all of the targets on the command line must be compatible with
building for Android when specifying these flags, which may make it difficult to
use Bazel wild-cards like
/...
and :all
.
These flags can be put into a bazelrc
config (one for each ABI), in
<project>/.bazelrc
:
common:android_x86 --crosstool_top=@androidndk//:default_crosstool
common:android_x86 --cpu=x86
common:android_x86 --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
common:android_armeabi-v7a --crosstool_top=@androidndk//:default_crosstool
common:android_armeabi-v7a --cpu=armeabi-v7a
common:android_armeabi-v7a --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
# In general
common:android_<abi> --crosstool_top=@androidndk//:default_crosstool
common:android_<abi> --cpu=<abi>
common:android_<abi> --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
Then, to build a cc_library
for x86
for example, run:
bazel build //my/cc/jni:target --config=android_x86
In general, use this method for low-level targets (like cc_library
) or when
you know exactly what you’re building; rely on the automatic configuration
transitions from android_binary
for high-level targets where you’re expecting
to build a lot of targets you don’t control.