Cómo usar el kit de desarrollo nativo de Android con Bazel

Informar un problema Ver fuente Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Si es la primera vez que usas Bazel, comienza con el instructivo Cómo compilar Android con Bazel.

Descripción general

Bazel se puede ejecutar en muchas configuraciones de compilación diferentes, incluidas varias que usan la cadena de herramientas del kit de desarrollo nativo de Android (NDK). Esto significa que las reglas normales de cc_library y cc_binary se pueden compilar para Android directamente en Bazel. Para lograrlo, Bazel usa la regla del repositorio android_ndk_repository.

Requisitos previos

Asegúrate de haber instalado el SDK y el NDK de Android.

Para configurar el SDK y el NDK, agrega el siguiente fragmento a tu 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.
)

Para obtener más información sobre la regla android_ndk_repository, consulta la entrada de la enciclopedia de Build.

Si usas una versión reciente del NDK de Android (r22 y versiones posteriores), usa la implementación de Starlark de android_ndk_repository. Sigue las instrucciones en su archivo README.

Inicio rápido

Para compilar C++ para Android, simplemente agrega dependencias de cc_library a tus reglas android_binary o android_library.

Por ejemplo, dado el siguiente archivo BUILD para una app para Android:

# 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",
)

Este archivo BUILD genera el siguiente grafo de destino:

Resultados de ejemplo

Figura 1: Compila el grafo del proyecto de Android con dependencias de cc_library.

Para compilar la app, simplemente ejecuta lo siguiente:

bazel build //app/src/main:app

El comando bazel build compila los archivos Java, los archivos de recursos de Android y las reglas cc_library, y empaqueta todo en un 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 compila todas las cc_libraries en un solo archivo de objeto compartido (.so), que se orienta a la ABI armeabi-v7a de forma predeterminada. Para cambiar esto o compilar para varios ABI al mismo tiempo, consulta la sección sobre cómo configurar la ABI de destino.

Configuración de ejemplo

Este ejemplo está disponible en el repositorio de ejemplos de Bazel.

En el archivo BUILD.bazel, se definen tres objetivos con las reglas android_binary, android_library y cc_library.

El objetivo de nivel superior android_binary compila el APK.

El destino cc_library contiene un solo archivo de origen C++ con una implementación de función JNI:

#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());
}

El destino android_library especifica las fuentes de Java, los archivos de recursos y la dependencia en un destino cc_library. En este ejemplo, MainActivity.java carga el archivo de objeto compartido libapp.so y define la firma del método para la función JNI:

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("app");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
       // ...
    }

    public native String stringFromJNI();

}

Cómo configurar el STL

Para configurar la STL de C++, usa la marca --android_crosstool_top.

bazel build //:app --android_crosstool_top=target label

Los STL disponibles en @androidndk son los siguientes:

STL Etiqueta de destino
STLport @androidndk//:toolchain-stlport
libc++ @androidndk//:toolchain-libcpp
gnustl @androidndk//:toolchain-gnu-libstdcpp

Para r16 y versiones anteriores, el STL predeterminado es gnustl. Para r17 y versiones posteriores, es libc++. Para mayor comodidad, el @androidndk//:default_crosstool de destino tiene un alias a las STL predeterminadas correspondientes.

Ten en cuenta que, a partir de la versión r18, se quitarán STLport y gnustl, lo que hará que libc++ sea la única STL en el NDK.

Consulta la documentación del NDK para obtener más información sobre estas STL.

Cómo configurar la ABI de destino

Para configurar la ABI de destino, usa la marca --fat_apk_cpu de la siguiente manera:

bazel build //:app --fat_apk_cpu=comma-separated list of ABIs

De forma predeterminada, Bazel compila código nativo de Android para armeabi-v7a. Para compilar para x86 (como para emuladores), pasa --fat_apk_cpu=x86. Para crear un APK grueso para varias arquitecturas, puedes especificar varias CPUs: --fat_apk_cpu=armeabi-v7a,x86.

Si se especifica más de una ABI, Bazel compilará un APK que contiene un objeto compartido para cada ABI.

Según la revisión del NDK y el nivel de API de Android, están disponibles las siguientes ABI:

Revisión del NDK ABI
16 y anteriores armeabi, armeabi-v7a, arm64-v8a, mips, mips64, x86 y x86_64
17 años en adelante armeabi-v7a, arm64-v8a, x86, x86_64

Consulta la documentación del NDK para obtener más información sobre estas ABI.

No se recomiendan los APKs multiarquitectura para las compilaciones de lanzamiento, ya que aumentan el tamaño del APK, pero pueden ser útiles para las compilaciones de desarrollo y QA.

Cómo seleccionar un estándar C++

Usa las siguientes marcas para compilar según un estándar de C++:

C++ estándar Marcar
C++98 Es el valor predeterminado, no se necesita ninguna marca.
C++11 --cxxopt=-std=c++11
C++14 --cxxopt=-std=c++14

Por ejemplo:

bazel build //:app --cxxopt=-std=c++11

Obtén más información sobre cómo pasar marcas del compilador y del vinculador con --cxxopt, --copt y --linkopt en el Manual del usuario.

Las marcas del compilador y del vinculador también se pueden especificar como atributos en cc_library con copts y linkopts. Por ejemplo:

cc_library(
    name = "jni_lib",
    srcs = ["cpp/native-lib.cpp"],
    copts = ["-std=c++11"],
    linkopts = ["-ldl"], # link against libdl
)

Integración con plataformas y cadenas de herramientas

El modelo de configuración de Bazel se está orientando hacia las plataformas y las cadenas de herramientas. Si tu compilación usa la marca --platforms para seleccionar la arquitectura o el sistema operativo para el que se compilará, deberás pasar la marca --extra_toolchains a Bazel para poder usar el NDK.

Por ejemplo, para integrarte con la cadena de herramientas android_arm64_cgo que proporcionan las reglas de Go, pasa --extra_toolchains=@androidndk//:all además de la marca --platforms.

bazel build //my/cc:lib \
  --platforms=@io_bazel_rules_go//go/toolchain:android_arm64_cgo \
  --extra_toolchains=@androidndk//:all

También puedes registrarlos directamente en el archivo WORKSPACE:

android_ndk_repository(name = "androidndk")
register_toolchains("@androidndk//:all")

El registro de estas cadenas de herramientas le indica a Bazel que las busque en el archivo BUILD del NDK (para el NDK 20) cuando resuelva las restricciones de arquitectura y sistema operativo:

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 = [
      "@platforms//os:android",
      "@platforms//cpu:aarch64",
  ],
  toolchain = "@androidndk//:aarch64-linux-android-clang8.0.7-libcpp",
)

Cómo funciona: presentamos las transiciones de configuración de Android

La regla android_binary puede pedirle a Bazel de forma explícita que compile sus dependencias en una configuración compatible con Android para que la compilación de Bazel simplemente funcione sin marcas especiales, excepto --fat_apk_cpu y --android_crosstool_top para la configuración de ABI y STL.

En segundo plano, esta configuración automática usa las transiciones de configuración de Android.

Una regla compatible, como android_binary, cambia automáticamente la configuración de sus dependencias a una configuración de Android, de modo que solo se vean afectados los subárboles específicos de Android de la compilación. Otras partes del gráfico de compilación se procesan con la configuración de destino de nivel superior. Incluso puede procesar un solo objetivo en ambas configuraciones, si hay rutas a través del gráfico de compilación para admitirlo.

Una vez que Bazel está en una configuración compatible con Android, ya sea especificada en el nivel superior o debido a un punto de transición de nivel superior, los puntos de transición adicionales que se encuentren no modificarán más la configuración.

La única ubicación integrada que activa la transición a la configuración de Android es el atributo deps de android_binary.

Por ejemplo, si intentas compilar un destino android_library con una dependencia cc_library sin ninguna marca, es posible que encuentres un error sobre un encabezado JNI faltante:

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.

Idealmente, estas transiciones automáticas deberían hacer que Bazel haga lo correcto en la mayoría de los casos. Sin embargo, si el destino de la línea de comandos de Bazel ya está debajo de cualquiera de estas reglas de transición, como los desarrolladores de C++ que prueban un cc_library específico, se debe usar un --crosstool_top personalizado.

Cómo compilar un cc_library para Android sin usar android_binary

Para compilar un cc_binary o cc_library independiente para Android sin usar un android_binary, usa las marcas --crosstool_top, --cpu y --host_crosstool_top.

Por ejemplo:

bazel build //my/cc/jni:target \
      --crosstool_top=@androidndk//:default_crosstool \
      --cpu=<abi> \
      --host_crosstool_top=@bazel_tools//tools/cpp:toolchain

En este ejemplo, los destinos cc_library y cc_binary de nivel superior se compilan con la cadena de herramientas del NDK. Sin embargo, esto hace que las propias herramientas de host de Bazel se compilen con la cadena de herramientas del NDK (y, por lo tanto, para Android), ya que la cadena de herramientas del host se copia de la cadena de herramientas de destino. Para solucionar esto, especifica el valor de --host_crosstool_top de modo que sea @bazel_tools//tools/cpp:toolchain para configurar explícitamente la cadena de herramientas de C++ del host.

Con este enfoque, se ve afectado todo el árbol de compilación.

Estas marcas se pueden colocar en una configuración bazelrc (una para cada ABI), en 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

Luego, para compilar un cc_library para x86, por ejemplo, ejecuta lo siguiente:

bazel build //my/cc/jni:target --config=android_x86

En general, usa este método para objetivos de bajo nivel (como cc_library) o cuando sabes exactamente lo que estás compilando; depende de las transiciones de configuración automáticas de android_binary para objetivos de alto nivel en los que esperas compilar muchos objetivos que no controlas.