שימוש ב-Android Native Development Kit עם Bazel

אם עוד לא השתמשתם ב-Bazel, כדאי להתחיל לקרוא את המדריך Building Android with Bazel.

סקירה כללית

Bazel יכולה לפעול בתצורות שונות שונות של Build, כולל מספר הגדרות שמשתמשות בערכת הכלים למפתחים של Android מקורי (NDK). פירוש הדבר הוא שניתן לכתוב את הכללים הרגילים cc_library ו-cc_binary ישירות ב-Android ב-Bazel. ב-Bazel מצליחה לעשות זאת באמצעות כלל המאגר של android_ndk_repository.

דרישות מוקדמות

יש לוודא שהתקנת את ה-SDK של Android ואת ה-NDK.

כדי להגדיר את ה-SDK ואת ה-NDK, יש להוסיף את קטע הקוד הבא ל-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.
)

למידע נוסף על כלל android_ndk_repository, עיינו בבניין האנציקלופדיה.

התחלה מהירה

כדי לבנות את C++ ל-Android, עליך להוסיף יחסי תלות של cc_library לכללים android_binary או android_library שלך.

לדוגמה, בהתאם לקובץ BUILD הבא לאפליקציה ל-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",
)

קובץ BUILD זה מופיע בתרשים היעד הבא:

תוצאות לדוגמה

איור 1. יצירת תרשים לפרויקט Android עם יחסי תלות של cc_library.

כדי לבנות את האפליקציה, מריצים את האפליקציה:

bazel build //app/src/main:app

הפקודה bazel build מקבצים את קובצי ה-Java, קובצי המשאבים של Android ואת cc_library הכללים, ומתאימה את הכול ל-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 מכינה את כל ה-cc_librarys לתוך אובייקט משותף (.so) אחד, ומטרגט כברירת מחדל ל-ABI של armeabi-v7a. כדי לשנות את ההגדרה או לבנות כמה ממשקי ABI בו-זמנית, יש לעיין בקטע הגדרת היעד ב-ABI.

הגדרה לדוגמה

דוגמה זו זמינה במאגר התבניות של Bazel.

בקובץ BUILD.bazel, שלושה יעדים מוגדרים עם הכללים android_binary, android_library ו-cc_library.

היעד ברמה העליונה android_binary בונה את ה-APK.

היעד cc_library מכיל קובץ מקור יחיד מסוג C++ עם הטמעה של פונקציית 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());
}

היעד android_library מציין את מקורות ה-Java, קובצי המשאבים ואת התלות ביעד cc_library. בדוגמה הבאה, MainActivity.java טוען את קובץ האובייקט המשותף libapp.so, ומגדיר את חתימת השיטה לפונקציה של JNI:

public class MainActivity extends AppCompatActivity {

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

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

    public native String stringFromJNI();

}

הגדרת ה-STL

כדי להגדיר את ה-STL + C+ יש להשתמש בסימון --android_crosstool_top.

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

ה-STLs הזמינים ב@androidndk הם:

STL תווית יעד
STLport @androidndk//:toolchain-stlport
Libc++ @androidndk//:toolchain-libcpp
גבורה @androidndk//:toolchain-gnu-libstdcpp

בגרסה r16 ומטה, ברירת המחדל של STL היא gnustl. בגרסה r17 ואילך, הערך הוא libc++. לנוחיותך, היעד @androidndk//:default_crosstool דומה לברירות המחדל של STL.

הערה: החל מגרסה 18 ואילך, STLport and gnustl יוסרו , libc++ה-STL היחיד ב-NDK.

מומלץ לעיין בתיעוד של NDK למידע נוסף על ה-STL האלה.

הגדרת ממשק ABI של היעד

כדי להגדיר את ממשק ה-ABI של היעד, יש להשתמש בסימון --fat_apk_cpu באופן הבא:

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

כברירת מחדל, Bazel בונה קוד Android מקורי עבור armeabi-v7a. בשביל לבנות ב-x86 (למשל לאמולטורים), יש להעביר את --fat_apk_cpu=x86. כדי ליצור APK לשומן עבור מספר ארכיטקטורה, אפשר לציין כמה מעבדים שונים: --fat_apk_cpu=armeabi-v7a,x86.

אם מציינים יותר מ-ABI אחד, Bazel תבנה APK עם אובייקט משותף לכל ממשק ABI.

בהתאם לגרסה הקודמת של NDK ולרמת Android API, ממשקי ה-ABI הבאים זמינים:

גרסת NDK ממשקי ABI
16 ומטה ארמביבי, ארמאו-v7a, Arm64-v8a, mips, mips64, x86, x86_64
17 ומעלה Aremabi-v7a, Arm64-v8a, x86, x86_64

ניתן לעיין במסמכי NDK למידע נוסף על ממשקי ה-ABI האלה.

לא מומלץ להשתמש בחבילות APK עם מספר שומן רב-שכבתי לבנייה של גרסאות טרום-השקה, כי הן מגדילות את גודל ה-APK, אבל הן שימושיות לפיתוח ולפיתוחי בקרת איכות.

בחירת תקן C++

יש להשתמש בסימונים הבאים כדי ליצור תוכן בהתאם לתקן C++:

C++ רגיל סמן כלא הולם
C++98 ברירת מחדל, אין צורך בסימון
C++11 --cxxopt=-std=c++11
C++14 --cxxopt=-std=c++14

למשל:

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

מידע נוסף על העברת סימונים של מהדר ומקשרת עם --cxxopt, --copt ו --linkopt זמין במדריך למשתמש.

אפשר לציין התרעות מסוג Compiler ו-Linker כמאפיינים ב-cc_library באמצעות copts ו-linkopts. למשל:

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

שילוב עם פלטפורמות וארגזי כלים

מודל התצורה של Bazel'זז לשימוש בפלטפורמות וב כלי תקשורת. אם גרסת ה-build משתמשת בסימון --platforms כדי לבחור את הארכיטקטורה או מערכת ההפעלה לבנייה, יהיה עליך להעביר את הדגל --extra_toolchains ל-Bazel כדי להשתמש ב-NDK.

לדוגמה, כדי לשלב את android_arm64_cgo בערכת הכלים שמספקת כללי ה-Go, עליך להעביר את --extra_toolchains=@androidndk//:all בנוסף לסימון --platforms.

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

אפשר גם לרשום אותם ישירות בקובץ WORKSPACE:

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

רישום מחרוזות הכלים האלה מורה ל-Bazel לחפש אותן בקובץ ה-NDK BUILD (ל-NDK 20) בפתרון בעיות הארכיטקטורה ומערכת ההפעלה:

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

איך זה עובד: מציגים את המעברים של תצורת Android

הכלל android_binary יכול לבקש במפורש מ-Bazel לבנות את תלויותיו בתצורה שתואמת ל-Android, כדי שהמבנה של Bazel יפעל ללא סימונים מיוחדים, פרט ל---fat_apk_cpu ול---android_crosstool_top לתצורה של ABI ו-STL.

מאחורי הקלעים של התצורה האוטומטית הזו, המערכת משתמשת במעברים של תצורה ב-Android.

כלל תואם, כמו android_binary, משנה באופן אוטומטי את ההגדרות התלויות שלו בהתאם לתצורה של Android, כך שרק עבודות משנה ספציפיות של Android מושפעות. חלקים אחרים ב-build מעובדים באמצעות הגדרת היעד ברמה העליונה. הוא עשוי אפילו לעבד יעד יחיד בשתי התצורות, אם יש נתיבים בתרשים שיתמכו בו.

לאחר הצירוף של Bazel בתצורה התואמת ל-Android, שצוינה ברמה העליונה או בגלל נקודת מעבר ברמה גבוהה יותר, נקודות המעבר הנוספות לא ישנו את התצורה.

המיקום המובנה היחיד שמפעיל את המעבר לתצורה של Android הוא המאפיין deps של android_binary.

לדוגמה, אם אתם מנסים ליצור יעד מסוג android_library עם תלות מסוג cc_library ללא סימונים, ייתכן שתופיע שגיאה לגבי כותרת 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.

באופן אידיאלי, המעברים האוטומטיים האלה צריכים לבצע את הפעולה הנכונה ב-Bazel ברוב המקרים. עם זאת, אם היעד בשורת הפקודה של Bazel כבר נמוך מכללי המעבר האלה, כמו מפתחי C++ שבודקים cc_library ספציפי, יש להשתמש ב---crosstool_top מותאם אישית.

מתבצעת יצירה של cc_library ל-Android ללא שימוש ב-android_binary

כדי ליצור cc_binary או cc_library עצמאים ל-Android בלי להשתמש ב android_binary, יש להשתמש בסימונים --crosstool_top, --cpu ו---host_crosstool_top.

למשל:

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

בדוגמה הזו, היעדים ברמה העליונה cc_library ו-cc_binary נוצרים באמצעות ארגז הכלים של NDK. כתוצאה מכך, הכלים של Bazel מתבצע לשימוש באמצעות כלי הפיתוח NDK (ולכן גם עבור Android), כי כלי הכלים למארחים הועתק מכלי הכלים של היעד. כדי לעקוף את הבעיה, צריך לציין את הערך של --host_crosstool_top כ-@bazel_tools//tools/cpp:toolchain כדי להגדיר במפורש את כלי הכלים של המארח (++39) ++39.

בגישה הזו, כל עץ ה-build מושפע.

אפשר להכניס את הסימונים האלה להגדרה מסוג bazelrc (אחת לכל ממשק ABI), וכך 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

לדוגמה, כדי לבנות cc_library עבור x86, לדוגמה, יש להריץ:

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

באופן כללי, מומלץ להשתמש בשיטה הזו ליעדים ברמה נמוכה (כמו cc_library) או כשמדובר בדיוק במה שהעסק שלך מגדיר; אפשר להסתמך על המעברים האוטומטיים של android_binary ליעדים ברמה גבוהה שבהם אתה מצפה ליצור יעדים רבים שאינם בשליטתך.