Sử dụng Bộ phát triển gốc Android với Bazel

Báo cáo vấn đề Xem nguồn Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Nếu bạn mới sử dụng Bazel, vui lòng bắt đầu với hướng dẫn Tạo Android bằng Bazel.

Tổng quan

Bazel có thể chạy trong nhiều cấu hình bản dựng, bao gồm cả một số cấu hình sử dụng chuỗi công cụ Bộ phát triển mã gốc của Android (NDK). Điều này có nghĩa là bạn có thể biên dịch các quy tắc cc_librarycc_binary thông thường cho Android ngay trong Bazel. Bazel thực hiện việc này bằng cách sử dụng quy tắc kho lưu trữ android_ndk_repository.

Điều kiện tiên quyết

Vui lòng đảm bảo bạn đã cài đặt SDK Android và NDK.

Để thiết lập SDK và NDK, hãy thêm đoạn mã sau vào 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.
)

Để biết thêm thông tin về quy tắc android_ndk_repository, hãy xem mục Build Encyclopedia (Bách khoa toàn thư về bản dựng).

Bắt đầu nhanh

Để tạo C++ cho Android, bạn chỉ cần thêm các phần phụ thuộc cc_library vào quy tắc android_binary hoặc android_library.

Ví dụ: giả sử tệp BUILD sau đây cho một ứng dụng 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",
)

Tệp BUILD này dẫn đến biểu đồ mục tiêu sau:

Kết quả mẫu

Hình 1. Biểu đồ bản dựng của dự án Android có các phần phụ thuộc cc_library.

Để tạo ứng dụng, bạn chỉ cần chạy:

bazel build //app/src/main:app

Lệnh bazel build biên dịch các tệp Java, tệp tài nguyên Android và các quy tắc cc_library, đồng thời đóng gói mọi thứ vào một tệp 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

Theo mặc định, Bazel biên dịch tất cả cc_libraries thành một tệp đối tượng dùng chung (.so) duy nhất, nhắm đến ABI armeabi-v7a. Để thay đổi hoặc tạo cho nhiều ABI cùng một lúc, hãy xem phần định cấu hình ABI mục tiêu.

Ví dụ về cách thiết lập

Bạn có thể xem ví dụ này trong kho lưu trữ ví dụ về Bazel.

Trong tệp BUILD.bazel, ba mục tiêu được xác định bằng các quy tắc android_binary, android_librarycc_library.

Mục tiêu cấp cao nhất android_binary sẽ tạo tệp APK.

Mục tiêu cc_library chứa một tệp nguồn C++ duy nhất với hoạt động triển khai hàm 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());
}

Mục tiêu android_library chỉ định các nguồn Java, tệp tài nguyên và phần phụ thuộc trên mục tiêu cc_library. Đối với ví dụ này, MainActivity.java sẽ tải tệp đối tượng dùng chung libapp.so và xác định chữ ký phương thức cho hàm JNI:

public class MainActivity extends AppCompatActivity {

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

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

    public native String stringFromJNI();

}

Định cấu hình STL

Để định cấu hình STL C++, hãy sử dụng cờ --android_crosstool_top.

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

Các STL có sẵn trong @androidndk là:

STL Nhãn mục tiêu
STLport @androidndk//:toolchain-stlport
libc++ @androidndk//:toolchain-libcpp
gnustl @androidndk//:toolchain-gnu-libstdcpp

Đối với phiên bản r16 trở xuống, STL mặc định là gnustl. Đối với phiên bản r17 trở lên, giá trị này là libc++. Để thuận tiện, @androidndk//:default_crosstool mục tiêu được gán bí danh cho các STL mặc định tương ứng.

Xin lưu ý rằng từ phiên bản r18 trở đi, STLport và gnustl sẽ bị xoá, khiến libc++ trở thành STL duy nhất trong NDK.

Hãy xem tài liệu về NDK để biết thêm thông tin về các STL này.

Định cấu hình ABI mục tiêu

Để định cấu hình ABI mục tiêu, hãy sử dụng cờ --fat_apk_cpu như sau:

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

Theo mặc định, Bazel sẽ tạo mã Android gốc cho armeabi-v7a. Để tạo bản dựng cho x86 (chẳng hạn như cho trình mô phỏng), hãy truyền --fat_apk_cpu=x86. Để tạo một tệp APK lớn cho nhiều cấu trúc, bạn có thể chỉ định nhiều CPU: --fat_apk_cpu=armeabi-v7a,x86.

Nếu bạn chỉ định nhiều ABI, Bazel sẽ tạo một APK chứa một đối tượng dùng chung cho mỗi ABI.

Tuỳ thuộc vào bản sửa đổi NDK và cấp độ API Android, bạn có thể sử dụng các ABI sau:

Bản sửa đổi NDK ABI
16 trở xuống armeabi, armeabi-v7a, arm64-v8a, mips, mips64, x86, x86_64
Từ 17 tuổi trở lên armeabi-v7a, arm64-v8a, x86, x86_64

Hãy xem tài liệu về NDK để biết thêm thông tin về các ABI này.

Bạn không nên sử dụng APK lớn nhiều ABI cho các bản phát hành vì các tệp này làm tăng kích thước APK, nhưng có thể hữu ích cho các bản phát triển và bản QA.

Chọn một tiêu chuẩn C++

Sử dụng các cờ sau để tạo theo tiêu chuẩn C++:

Chuẩn C++ Cờ
C++98 Mặc định, không cần cờ
C++11 --cxxopt=-std=c++11
C++14 --cxxopt=-std=c++14

Ví dụ:

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

Đọc thêm về cách truyền cờ trình biên dịch và trình liên kết bằng --cxxopt, --copt--linkopt trong Hướng dẫn sử dụng.

Bạn cũng có thể chỉ định cờ trình biên dịch và trình liên kết dưới dạng thuộc tính trong cc_library bằng cách sử dụng coptslinkopts. Ví dụ:

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

Tích hợp với các nền tảng và chuỗi công cụ

Mô hình cấu hình của Bazel đang chuyển hướng sang nền tảngchuỗi công cụ. Nếu bản dựng của bạn sử dụng cờ --platforms để chọn kiến trúc hoặc hệ điều hành cần tạo bản dựng, bạn sẽ cần truyền cờ --extra_toolchains đến Bazel để sử dụng NDK.

Ví dụ: để tích hợp với chuỗi công cụ android_arm64_cgo do các quy tắc Go cung cấp, hãy truyền --extra_toolchains=@androidndk//:all cùng với cờ --platforms.

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

Bạn cũng có thể đăng ký trực tiếp các lớp này trong tệp WORKSPACE:

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

Việc đăng ký các chuỗi công cụ này sẽ yêu cầu Bazel tìm kiếm các chuỗi công cụ đó trong tệp BUILD của NDK (đối với NDK 20) khi phân giải các quy tắc ràng buộc về cấu trúc và hệ điều hành:

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ách hoạt động: giới thiệu các chuyển đổi cấu hình Android

Quy tắc android_binary có thể yêu cầu rõ ràng Bazel tạo các phần phụ thuộc trong một cấu hình tương thích với Android để bản dựng Bazel chỉ hoạt động mà không cần bất kỳ cờ đặc biệt nào, ngoại trừ --fat_apk_cpu--android_crosstool_top cho cấu hình ABI và STL.

Ở chế độ nền, cấu hình tự động này sử dụng các chuyển đổi cấu hình của Android.

Một quy tắc tương thích, chẳng hạn như android_binary, sẽ tự động thay đổi cấu hình của các phần phụ thuộc thành cấu hình Android, vì vậy, chỉ các cây con dành riêng cho Android của bản dựng mới bị ảnh hưởng. Các phần khác của biểu đồ bản dựng được xử lý bằng cấu hình mục tiêu cấp cao nhất. Công cụ này thậm chí có thể xử lý một mục tiêu duy nhất trong cả hai cấu hình, nếu có các đường dẫn thông qua biểu đồ bản dựng để hỗ trợ mục tiêu đó.

Khi Bazel ở trong một cấu hình tương thích với Android, được chỉ định ở cấp cao nhất hoặc do một điểm chuyển đổi cấp cao hơn, các điểm chuyển đổi bổ sung gặp phải sẽ không sửa đổi thêm cấu hình.

Vị trí tích hợp duy nhất kích hoạt quá trình chuyển đổi sang cấu hình Android là thuộc tính deps của android_binary.

Ví dụ: nếu cố gắng tạo một mục tiêu android_library có phần phụ thuộc cc_library mà không có cờ nào, bạn có thể gặp lỗi về việc thiếu tiêu đề JNI:

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.

Lý tưởng nhất là các quá trình chuyển đổi tự động này sẽ giúp Bazel thực hiện đúng việc trong hầu hết các trường hợp. Tuy nhiên, nếu mục tiêu trên dòng lệnh Bazel đã nằm bên dưới bất kỳ quy tắc chuyển đổi nào trong số này, chẳng hạn như nhà phát triển C++ kiểm thử một cc_library cụ thể, thì bạn phải sử dụng --crosstool_top tuỳ chỉnh.

Tạo cc_library cho Android mà không cần sử dụng android_binary

Để tạo cc_binary hoặc cc_library độc lập cho Android mà không cần sử dụng android_binary, hãy sử dụng cờ --crosstool_top, --cpu--host_crosstool_top.

Ví dụ:

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

Trong ví dụ này, các mục tiêu cc_librarycc_binary cấp cao nhất được tạo bằng chuỗi công cụ NDK. Tuy nhiên, điều này khiến các công cụ lưu trữ của riêng Bazel được tạo bằng chuỗi công cụ NDK (và do đó là cho Android), vì chuỗi công cụ lưu trữ được sao chép từ chuỗi công cụ mục tiêu. Để khắc phục vấn đề này, hãy chỉ định giá trị của --host_crosstool_top@bazel_tools//tools/cpp:toolchain để đặt rõ ràng chuỗi công cụ C++ của máy chủ.

Với phương pháp này, toàn bộ cây bản dựng sẽ bị ảnh hưởng.

Bạn có thể đưa các cờ này vào cấu hình bazelrc (một cờ cho mỗi ABI) trong 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

Sau đó, để tạo cc_library cho x86, hãy chạy:

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

Nhìn chung, hãy sử dụng phương thức này cho các mục tiêu cấp thấp (như cc_library) hoặc khi bạn biết chính xác những gì mình đang xây dựng; dựa vào các chuyển đổi cấu hình tự động từ android_binary cho các mục tiêu cấp cao mà bạn dự kiến sẽ tạo ra nhiều mục tiêu mà bạn không kiểm soát.