استفاده از کیت توسعه بومی اندروید با Bazel

اگر تازه وارد Bazel هستید، لطفا با آموزش ساخت اندروید با بازل شروع کنید.

بررسی اجمالی

Bazel می تواند در بسیاری از پیکربندی های ساخت مختلف اجرا شود، از جمله چندین مورد که از زنجیره ابزار Android Native Development Kit (NDK) استفاده می کنند. این بدان معناست که قوانین عادی cc_library و cc_binary را می توان مستقیماً در Bazel برای اندروید کامپایل کرد. Bazel این کار را با استفاده از قانون مخزن android_ndk_repository انجام می دهد.

پیش نیازها

لطفاً مطمئن شوید که Android SDK و 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 ، به مدخل Build Encyclopedia مراجعه کنید.

شروع سریع

برای ساختن ++C برای اندروید، به سادگی وابستگی cc_library را به قوانین android_binary یا android_library خود اضافه کنید.

به عنوان مثال، با توجه به فایل BUILD زیر برای یک برنامه اندروید:

# 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 منجر به نمودار هدف زیر می شود:

Example results

شکل 1. ساخت نمودار پروژه اندروید با وابستگی های cc_library.

برای ساختن اپلیکیشن، به سادگی اجرا کنید:

bazel build //app/src/main:app

دستور bazel build bazel فایل های جاوا، فایل های منبع اندروید و قوانین 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_libraries را در یک فایل شی مشترک ( .so ) کامپایل می کند که به طور پیش فرض برای armeabi-v7a ABI هدف گذاری شده است. برای تغییر این یا ساخت چندین 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 منابع جاوا، فایل‌های منبع و وابستگی به هدف 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

برای پیکربندی C++ STL، از پرچم --android_crosstool_top استفاده کنید.

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

STL های موجود در @androidndk عبارتند از:

STL برچسب هدف
STLport @androidndk//:toolchain-stlport
libc++ @androidndk//:toolchain-libcpp
gnustl @androidndk//:toolchain-gnu-libstdcpp

برای r16 و پایین تر، STL پیش فرض gnustl است. برای r17 و بالاتر، libc++ است. برای راحتی، هدف @androidndk//:default_crosstool به STL های پیش فرض مربوطه نامیده می شود.

لطفاً توجه داشته باشید که از r18 به بعد، STLport و gnustl حذف خواهند شد و libc++ تنها STL در NDK خواهد بود.

برای اطلاعات بیشتر در مورد این STLها به مستندات NDK مراجعه کنید.

پیکربندی ABI هدف

برای پیکربندی ABI هدف، از پرچم --fat_apk_cpu به صورت زیر استفاده کنید:

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

به طور پیش فرض، Bazel کد بومی اندروید را برای armeabi-v7a می سازد. برای ساخت x86 (مانند شبیه سازها)، --fat_apk_cpu=x86 را پاس کنید. برای ایجاد یک APK چربی برای چندین معماری، می توانید چندین CPU را مشخص کنید: --fat_apk_cpu=armeabi-v7a,x86 .

اگر بیش از یک ABI مشخص شده باشد، Bazel یک APK حاوی یک شی مشترک برای هر ABI می‌سازد.

بسته به ویرایش NDK و سطح API Android، ABI های زیر در دسترس هستند:

ویرایش NDK ABI ها
16 و پایین تر armeabi، armeabi-v7a، arm64-v8a، mips، mips64، x86، x86_64
17 و بالاتر armeabi-v7a، arm64-v8a، x86، x86_64

برای اطلاعات بیشتر در مورد این ABI ها به اسناد NDK مراجعه کنید.

فایل‌های APK Fat Multi-ABI برای ساخت‌های منتشر شده توصیه نمی‌شوند زیرا اندازه APK را افزایش می‌دهند، اما می‌توانند برای توسعه و ساخت‌های QA مفید باشند.

انتخاب استاندارد 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 را در راهنمای کاربر بخوانید .

پرچم های کامپایلر و پیوند دهنده را نیز می توان به عنوان ویژگی در cc_library با استفاده از copts و linkopts کرد. مثلا:

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

ادغام با پلتفرم ها و زنجیره های ابزار

مدل پیکربندی Bazel به سمت پلتفرم‌ها و زنجیره‌های ابزار حرکت می‌کند. اگر ساخت شما از پرچم --platforms برای انتخاب معماری یا سیستم عامل برای ساخت استفاده می کند، برای استفاده از NDK باید پرچم --extra_toolchains را به Bazel ارسال کنید.

به عنوان مثال، برای ادغام با زنجیره ابزار 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_binary می‌تواند صراحتاً از Bazel بخواهد که وابستگی‌های خود را در یک پیکربندی سازگار با Android بسازد تا بیلد Bazel فقط بدون هیچ پرچم خاصی کار کند، به جز --fat_apk_cpu و --android_crosstool_top برای پیکربندی ABI و STL.

در پشت صحنه، این پیکربندی خودکار از انتقال پیکربندی اندروید استفاده می کند.

یک قانون سازگار، مانند android_binary ، به‌طور خودکار پیکربندی وابستگی‌های خود را به پیکربندی Android تغییر می‌دهد، بنابراین فقط زیردرخت‌های ساخت مربوط به اندروید تحت تأثیر قرار می‌گیرند. سایر بخش‌های نمودار ساخت با استفاده از پیکربندی هدف سطح بالا پردازش می‌شوند. حتی ممکن است یک هدف واحد را در هر دو پیکربندی پردازش کند، اگر مسیرهایی از طریق نمودار ساخت برای پشتیبانی از آن وجود داشته باشد.

هنگامی که Bazel در یک پیکربندی سازگار با 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_binary

برای ساختن یک cc_binary یا cc_library مستقل برای اندروید بدون استفاده از 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 (و در نتیجه برای اندروید) ساخته شود، زیرا زنجیره ابزار میزبان از زنجیره ابزار هدف کپی می‌شود. برای حل این مشکل، مقدار --host_crosstool_top را به @bazel_tools//tools/cpp:toolchain تعیین کنید تا به صراحت زنجیره ابزار C++ میزبان را تنظیم کنید.

با این رویکرد، کل درخت ساخت تحت تأثیر قرار می گیرد.

این پرچم ها را می توان در یک پیکربندی 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 تکیه کنید، جایی که شما انتظار دارید اهداف زیادی را بسازید که کنترل آنها را ندارید.