Bazel과 함께 Android Native Development Kit 사용

문제 신고 소스 보기 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Bazel을 처음 사용하는 경우 Bazel을 사용하여 Android 빌드 튜토리얼로 시작하세요.

개요

Bazel은 Android Native Development Kit (NDK) 도구 모음을 사용하는 여러 빌드 구성을 비롯한 다양한 빌드 구성에서 실행할 수 있습니다. 즉, 일반 cc_librarycc_binary 규칙을 Bazel 내에서 직접 Android용으로 컴파일할 수 있습니다. 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 규칙에 관한 자세한 내용은 빌드 백과사전 항목을 참고하세요.

빠른 시작

Android용 C++를 빌드하려면 android_binary 또는 android_library 규칙에 cc_library 종속 항목을 추가하기만 하면 됩니다.

예를 들어 Android 앱에 다음과 같은 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 파일은 다음과 같은 타겟 그래프를 생성합니다.

결과 예시

그림 1. cc_library 종속 항목이 있는 Android 프로젝트의 그래프를 빌드합니다.

앱을 빌드하려면 다음을 실행하면 됩니다.

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_libraries를 기본적으로 armeabi-v7a ABI를 타겟팅하는 단일 공유 객체 (.so) 파일로 컴파일합니다. 이를 변경하거나 여러 ABI용으로 동시에 빌드하려면 타겟 ABI 구성 섹션을 참고하세요.

설정 예시

이 예는 Bazel 예시 저장소에서 확인할 수 있습니다.

BUILD.bazel 파일에서 android_binary, android_library, cc_library 규칙으로 3개의 대상이 정의됩니다.

android_binary 최상위 타겟은 APK를 빌드합니다.

cc_library 타겟에는 JNI 함수 구현이 포함된 단일 C++ 소스 파일이 포함되어 있습니다.

#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 구성

C++ STL을 구성하려면 --android_crosstool_top 플래그를 사용합니다.

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

@androidndk에서 사용할 수 있는 STL은 다음과 같습니다.

STL 대상 라벨
STLport @androidndk//:toolchain-stlport
libc++ @androidndk//:toolchain-libcpp
Ginustl @androidndk//:toolchain-gnu-libstdcpp

r16 이하의 경우 기본 STL은 gnustl입니다. r17 이상에서는 libc++입니다. 편의를 위해 타겟 @androidndk//:default_crosstool는 각 기본 STL로 별칭이 지정됩니다.

r18부터 STLport 및 gnustl이 삭제되므로 NDK에서 libc++이 유일한 STL이 됩니다.

이러한 STL에 관한 자세한 내용은 NDK 문서를 참고하세요.

타겟 ABI 구성

타겟 ABI를 구성하려면 다음과 같이 --fat_apk_cpu 플래그를 사용합니다.

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

기본적으로 Bazel은 armeabi-v7a용 네이티브 Android 코드를 빌드합니다. x86용으로 빌드하려면(예: 에뮬레이터용) --fat_apk_cpu=x86를 전달합니다. 여러 아키텍처용 팻 APK를 만들려면 여러 CPU(--fat_apk_cpu=armeabi-v7a,x86)를 지정할 수 있습니다.

ABI가 두 개 이상 지정되면 Bazel은 각 ABI의 공유 객체가 포함된 APK를 빌드합니다.

NDK 버전 및 Android API 수준에 따라 다음 ABI를 사용할 수 있습니다.

NDK 버전 ABI
16 이하 armeabi, armeabi-v7a, arm64-v8a, mips, mips64, x86, x86_64
17세 이상 armeabi-v7a, arm64-v8a, x86, x86_64

이러한 ABI에 관한 자세한 내용은 NDK 문서를 참조하세요.

멀티 ABI fat APK는 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를 사용하여 컴파일러 및 링커 플래그를 전달하는 방법에 관한 자세한 내용은 사용자 설명서를 참고하세요.

컴파일러 및 링커 플래그는 coptslinkopts를 사용하여 cc_library에서 속성으로 지정할 수도 있습니다. 예를 들면 다음과 같습니다.

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에 전달해야 합니다.

예를 들어 Go 규칙에서 제공하는 android_arm64_cgo 도구 모음을 통합하려면 --platforms 플래그 외에 --extra_toolchains=@androidndk//:all를 전달합니다.

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 빌드가 ABI 및 STL 구성의 --fat_apk_cpu--android_crosstool_top를 제외한 특수 플래그 없이 작동하도록 Bazel에 Android 호환 구성으로 종속 항목을 빌드하도록 명시적으로 요청할 수 있습니다.

이 자동 구성은 백그라운드에서 Android 구성 전환을 사용합니다.

android_binary와 같은 호환 규칙은 종속 항목의 구성을 Android 구성으로 자동 변경하므로 빌드의 Android 전용 하위 트리만 영향을 받습니다. 빌드 그래프의 다른 부분은 최상위 타겟 구성을 사용하여 처리됩니다. 빌드 그래프를 통해 이를 지원하는 경로가 있는 경우 두 구성에서 단일 타겟을 처리할 수도 있습니다.

Bazel이 최상위 수준에서 지정되었거나 상위 수준 전환 지점으로 인해 Android 호환 구성이 되면 추가로 발생하는 전환 지점은 구성을 더 이상 수정하지 않습니다.

Android 구성으로의 전환을 트리거하는 유일한 내장 위치는 android_binarydeps 속성입니다.

예를 들어 플래그 없이 cc_library 종속 항목이 있는 android_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 명령줄의 타겟이 이미 이러한 전환 규칙 아래에 있는 경우(예: 특정 cc_library를 테스트하는 C++ 개발자) 맞춤 --crosstool_top를 사용해야 합니다.

android_binary를 사용하지 않고 Android용 cc_library 빌드

android_binary를 사용하지 않고 Android용 독립형 cc_binary 또는 cc_library를 빌드하려면 --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_librarycc_binary 타겟이 NDK 도구 모음을 사용하여 빌드됩니다. 그러나 이로 인해 Bazel의 자체 호스트 도구가 NDK 도구 모음으로 빌드됩니다 (따라서 Android의 경우에도). 호스트 도구 모음이 대상 도구 모음에서 복사되기 때문입니다. 이 문제를 해결하려면 --host_crosstool_top의 값을 @bazel_tools//tools/cpp:toolchain로 지정하여 호스트의 C++ 도구 모음을 명시적으로 설정합니다.

이 접근 방식을 사용하면 전체 빌드 트리가 영향을 받습니다.

이러한 플래그는 project/.bazelrcbazelrc 구성 (ABI마다 하나)에 배치할 수 있습니다.

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

그런 다음 예를 들어 x86cc_library를 빌드하려면 다음을 실행합니다.

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

일반적으로 이 메서드는 하위 수준 타겟 (예: cc_library) 또는 빌드 중인 항목을 정확히 알고 있는 경우에 사용합니다. 제어하지 않는 많은 타겟을 빌드할 것으로 예상되는 상위 수준 타겟의 경우 android_binary의 자동 구성 전환을 사용합니다.