Bazelのrules_kotlinについてのメモ

Bazelのrules_kotlinにkotlin_android_library rule 追加され、Kotlinで書かれたAndroidアプリがビルド可能になったので調べたメモ。

BazelでのAndroidアプリのビルド方法の復習

BazelでAndroidアプリをビルドするのは久しぶりなので、ビルド方法を復習してみた。対象はJavaで書かれたアプリとしてIntroduction to Bazel: Build an Android Appを参考に進める。

Bazelのインストール

brewでインストールする。

$ brew update
$ brew install bazel

2017年9月18日時点でBazelのバージョンは0.5.4。Release notesはここから確認できる。

$ bazel version
Build label: 0.5.4-homebrew
Build target: bazel-out/darwin_x86_64-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
Build time: Fri Aug 25 16:55:29 2017 (1503680129)
Build timestamp: 1503680129
Build timestamp as int: 1503680129

サンプルプロジェクトの用意

Bazelリポジトリからサンプルプロジェクトをcloneする。

$ git clone -b source-only https://github.com/bazelbuild/examples

examples以下のtutorial/androidAndroidのサンプルプロジェクトになる。

workspaceのセットアップ

workspaceディレクトリでソフトウェアのソースコードや、WORKSPACEBUILDファイルといったBazelがソフトウェアをビルドするために必要な情報が含まれる。また、workspaceにはoutput先のシンボリックリンクが貼られる。

workspaceディレクトリはファイルシステム上のどこに置いてもいいが、WORKSPACEがrootに存在する必要がある。Introduction to Bazel: Build an Android Appチュートリアルでは、workspaceディレクトリをcloneしてきたサンプルプロジェクトの<YOUR_DIRECTORY_PATH>/examples/tutorial/に指定する。

環境変数$WORKSPACEにworkspaceディレクトリを指定すると以後便利なので設定しておく。

$ export WORKSPACE=<YOUR_DIRECTORY_PATH>/examples/tutorial

WORKSPACEファイルの作成

全てのworkspaceディレクトリはrootにWORKSPACEファイルを持つ必要がある。WORKSPACEファイルは空であるかソフトウェアのビルドに必要なexternal dependenciesへの参照を持つ。依存関係は後で追記するのでまずは空のWORKSPACEファイルを作成する。

$ touch $WORKSPACE/WORKSPACE

これで空のWORKSPACEファイルが作られる。

WORKSPACEファイルの更新

BazelがAndroidアプリをビルドするためには、 Android SDK build toolsとSDK librariesを使う必要がある。そのため、WORKSPACEファイルにAndroid SDKへの参照を追記する必要がある。

WORKSPACEファイルに以下の記述を追加することで自動的に環境変数ANDROID_HOMEを参照してAndroid SDKを使用するようになる。この時、インスール済みの最も高いAPIレベルのbuild toolsを自動的に使用する。

android_sdk_repository(
    name = "androidsdk"
)

または、明示的にAndroid SDKのロケーションやAPIレベルbuild toolsバージョンをpath, api_level, build_tools_versionといったattributesで指定できる。

android_sdk_repository(
    name = "androidsdk",
    path = "/path/to/Android/sdk",
    api_level = 25,
    build_tools_version = "26.0.1"
)

BUILDファイルの作成

BUILDファイルには、ビルド成果物とworkspaceディレクトリ中のソースコードや他の成果物といったdependenciesとの関係を記述する。BUILDファイルはBazel build languageによって書かれる。

BUILDファイルはpackage hierarchyと呼ばれるBazelのコンセプトの一部で、package hierarchyはworkspaceのディレクトリ構造を覆う論理的な構造?らしい(workspaceのディレクトリ構造がそのままpackage hierarchyとして表されるということかもしれない)。各packageはディレクトリで、関連するソースファイルとBUILDファイルが含まれる。packageには独自のBUILDファイルを持たないサブディレクトリも含む。package nameはBUILDファイルが位置するディレクトリ名となる。

Bazelのpacage階層は、AndroidアプリのJavaのパッケージ階層と異なるが、この2つは共存できる。

サンプルアプリは$WORKSPACE/android/に全てのソースファイルがあるシンプルなBazel packageを構成している。より複雑なプロジェクトの場合よりネストが深いpackage構成となる。

android_library ruleの追加

BUILDファイルを編集する。

$ vi $WORKSPACE/android/BUILD

BUILDファイルにはBazelへのいくつかの異なるタイプの命令を書ける。その中で最も重要なのがbuild ruleでBazelにソースコードや依存から最終的な成果物や中間成果物をどうビルドするかを伝える。

AndroidについてはBazelはandroid_libraryandroid_binaryの2つのruleを提供していて、Androidアプリのビルドに利用できる。android_library ruleは、アプリのソースコードやリソースファイルからAndroid library moduleのビルド方法をBazelに伝える。そして、android_binary rule はAndroid application package(APK)のビルド方法をBazelに伝える。

android_library rule を追加したBUILDファイルは以下のようになる。

android_library(
    name = "activities",
    srcs = glob(["src/main/java/com/google/bazel/example/android/activities/*.java"]),
    custom_package = "com.google.bazel.example.android.activities",
    manifest = "src/main/java/com/google/bazel/example/android/activities/AndroidManifest.xml",
    resource_files = glob(["src/main/java/com/google/bazel/example/android/activities/res/**"]),
)

android_library rule はBazelがソースファイルからlibrary moduleをビルドするために必要な情報がattributesとして指定する。また、name属性にはactivitiesが指定されていて、android_binary rule からこの名前で成果物を参照することができる。

android_binary rule はAPKをビルドする。BUILDファイルには以下を追記する。

android_binary(
    name = "android",
    custom_package = "com.google.bazel.example.android",
    manifest = "src/main/java/com/google/bazel/example/android/AndroidManifest.xml",
    resource_files = glob(["src/main/java/com/google/bazel/example/android/res/**"]),
    visibility = ["//visibility:public"],
    deps = [":activities"],
)

deps属性は、activities rule の成果物への参照を持つ。これはBazelがandroid_binary rule でビルドする際にactivities library rule でビルドされた成果物が最新かを最初にチェックすることを意味している。もし最新でなければactivities rule でのビルドを実行し、その成果物をAPKのビルドに使用する。

最終的なBUILDファイルは以下のようになる。

github.com

アプリのビルド

workspaceディレクトリに移動し、bazel command-line toolでビルドを開始し、BazelでのUnitテストやその他の操作を実行する。

$ cd $WORKSPACE
$ bazel build //android:android

buildコマンドは、Bazelにそれ以降で指定するtargetをビルドすることを指示する。targetはworkspaceディレクトリからのpackageの相対パスBUILDファイルのbuild ruleの名前(target name)で指定する。

target nameやコマンドを実行するディレクトリによってはpackageパスやtarget nameを省略できる。詳細はLabelsで解説されている。

bazel buildを実行すると以下のようにビルドプロセスの進捗が表示され、成果物が出力される。

..............................................................
INFO: Found 1 target...
Target //android:android up-to-date:
  bazel-bin/android/android_deploy.jar
  bazel-bin/android/android_unsigned.apk
  bazel-bin/android/android.apk
INFO: Elapsed time: 47.792s, Critical Path: 4.48s

成果物の確認

Bazelはユーザーやworkspace毎に中間成果物と最終的な成果物の両方をディレクトリに保存していて、それらのディレクトリへの以下のようなシムリンクがworkspaceディレクリに追加される。

  • $WORKSPACE/bazel-bin, which stores binary executables and other runnable build outputs
  • $WORKSPACE/bazel-genfiles, which stores intermediary source files that are generated by Bazel rules
  • $WORKSPACE/bazel-out, which stores other types of build outputs

android_binary rule で生成されたAPKは、bazel-bin/androidに保存されていて、サブディレクトandroidはBazelのpakcage構成と一致している。

bazel-bin/androidディレクトリにandroid.apkファイルがあればビルドは成功している。

$ ls $WORKSPACE/bazel-bin/android

rules_kotlin

rules_kotlinでのAndroidアプリのビルドについて試していく。

github.com

rules_kotlinで使用できるrule

以下の5種類を使用できる。

Rule Description
kotlin_repositories workspaceディレクトリへの依存関係の読み込み
kotlin_library KotlinソースコードからのJavaライブラリのビルド
kotlin_binary KotlinソースコードからのJavaバイナリのビルド
kotlin_android_library Kotlinソースコードからのandroidライブラリのビルド
kotlin_test テストの実行

WORKSPACEの設定

rules_kotlinを利用するにはWORKSPACEファイルに以下を追加する。

git_repository(
    name = "org_pubref_rules_kotlin",
    remote = "https://github.com/pubref/rules_kotlin.git",
    tag = "v0.4.0", # update as needed
)

load("@org_pubref_rules_kotlin//kotlin:rules.bzl", "kotlin_repositories")

kotlin_repositories()

kotlin_repositories()が実行されるとkotlin releaseからコンパイラを取得しdaggerに関連する依存ライブラリを読み込む。(daggerはKotlinCompilerのBazel workerをビルドするために使われるらしい。)

BUILD rules

BUILDファイルに以下を追加することでkotlin_library ruleが使えるようになる。

load("@org_pubref_rules_kotlin//kotlin:rules.bzl", "kotlin_library")

kotlin_library

kotlin_library(
    name = "my_kotlin_lib",
    srcs = ["kotlin_source_file.kt"],
    deps = [":some_other_kotlin_library_rule"],
    java_deps = [":some_other_java_library_rule", "@another_maven_jar//jar"],
)

deps属性には、使用したい成果物を提供する他のkotlin_library targets のname属性の値を指定する。また、java_deps属性には、他のjava_librarytargets かjava_import targetsのname属性の値を指定する。

Kotlinのソースをkotlincでコンパイルし、対応するjarファイルを出力するには以下のようにする。

$ bazel build :my_kotlin_lib
Target :my_kotlin_lib up-to-date:
  bazel-bin/.../my_kotlin_lib.jar

kotlin_library targets の成果物をjava_library targets のインプットとして使用するには、deps属性に追加したいkotlin_library targets のname属性に_ktを加えた値を指定する。これは、他のjava_library targets についても同様になる。例えば、:my_kotlin_libの成果物を使用したいならば以下のように指定する。

android_binary(
   name = "foo",
   deps = [
       ":my_kotlin_lib_kt`,
   ]
)

kotlin_libraryの属性一覧

Name Type Description
srcs label_list 拡張子が*.ktであるKotlinのソースファイル
deps label_list kotlin_library targetsのリスト
java_deps label_list Java provider targets (java_library, java_import, …)のリスト
android_deps label_list Android provider targets (android_library)のリスト
jars label_list jar file targets (*.jar)のリスト
x_opts string_list Additional -X options to kotlincのリスト
plugin_opts string_dict Additional -P options to kotlincのリスト

kotlin_binary

kotlin_binarykotlin_libraryと似ているが、main_class属性(コンパイル済みのKotlinクラス名をJavaのpackage宣言で指定)が追加されている。main_class属性で指定するクラスはエントリーポイントとしてfun main()が宣言されていなければならない。

例としては以下のようになる。

kotlin_binary(
    name = "main_kt",
    main_class = "my.project.MainKt",
    srcs = ["main.kt"],
    deps = [":my_kotlin_lib"]
    java_deps = [":javalib"]
)

自分自身を含む実行な脳なjarファイルを作成するには、暗黙的に宣言されている_deploy.jar targetを実行する。rules_kotlin/examples/helloworldディレクトリでビルドを実行すると以下のようになる。

$ bazel build :main_kt_deploy.jar
INFO: Found 1 target...
Target //examples/helloworld:main_kt_deploy.jar up-to-date:
  bazel-bin/examples/helloworld/main_kt_deploy.jar
INFO: Elapsed time: 54.341s, Critical Path: 6.97s

main_kt_deploy.jar$WORKSPACE/bazel-bin/examples/helloworld以下に生成されるのでそれを実行すると以下のようになる。

$ java -jar main_kt_deploy.jar
I am Kotlin! ......
... But what is soy milk?
What if soy milk is just regular milk introducing itself in Spanish?

kotlin_binaryの属性一覧

kotlin_libraryの属性を全て含み、それに加えてmain_class属性が追加される。

Name Type Description
main_class string Main class to run with the kotlin_binary rule

kotlin_android_library

kotlin_android_libraryは、kotlin_libraryの属性に加えて、Androidに特化した属性が追加されている。aar_depsresource_filescustom_packagemanifestなどがそれにあたる。

ソースコードとリソースからAPKをビルドする例は以下のようになる。RクラスをKotlinのコードで使用したい場合、kotlin_android_library rule のresource_files属性を指定する必要がある。

PACKAGE = "com.company.app"
MANIFEST = "AndroidManifest.xml"

kotlin_android_library(
    name = "src",
    srcs = glob(["src/**/*.kt"]),
    custom_package = PACKAGE,
    manifest = MANIFEST,
    resource_files = glob(["res/**/*"]),
    java_deps = [
        "@com_squareup_okhttp3_okhttp//jar",
        "@com_squareup_okio_okio//jar",
    ],
    aar_deps = [
        "@androidsdk//com.android.support:appcompat-v7-25.3.1",
        "@androidsdk//com.android.support:cardview-v7-25.3.1",
        "@androidsdk//com.android.support:recyclerview-v7-25.3.1",
    ],
)

android_binary(
    name = "app",
    custom_package = PACKAGE,
    manifest = MANIFEST,
    deps = [
        ":src",
    ],
)

ソースコードとリソースを別々のBazel ruleで分ける場合には、android_library rule を使い以下のようにする。name属性resでリソースファイルを読み込み、kotlin_android_library rule のandroid_deps属性でそれを指定している。

PACKAGE = "com.company.app"
MANIFEST = "AndroidManifest.xml"

android_library(
    name = "res",
    custom_package = PACKAGE,
    manifest = MANIFEST,
    resource_files = glob(["res/**/*"]),
    aar_deps = [
        "@androidsdk//com.android.support:appcompat-v7-25.3.1",
        "@androidsdk//com.android.support:cardview-v7-25.3.1",
        "@androidsdk//com.android.support:recyclerview-v7-25.3.1",
    ],
)

android_library(
    name = "java",
    srcs = glob(["src/**/*.java"]),
    deps = [
        ":res",
        # And other depedencies
    ]
)

kotlin_android_library(
    name = "kt",
    srcs = glob(["src/**/*.kt"]),
    aar_deps = [
        "@androidsdk//com.android.support:appcompat-v7-25.3.1",
        "@androidsdk//com.android.support:cardview-v7-25.3.1",
        "@androidsdk//com.android.support:recyclerview-v7-25.3.1",
    ],
    android_deps = [
        ":res",
    ]
)

android_binary(
    name = "app",
    custom_package = PACKAGE,
    manifest = MANIFEST,
    deps = [
        ":java",
        ":kt",
        ":res",
    ],
)

↑の例だとandroid_library rule でaar_deps属性を指定しているが、このまま実行するとエラーになるので削除する必要がある。また、kotlin_android_library ruleのandroid_deps属性に:resを指定しても怒られる。これはkotlin_android_library ruleの定義中kotlin_compile rule のandroid_deps属性と名前が重複するから。

kotlin_android_libraryの属性一覧

android_librarykotlin_libraryの属性を全て含み、それに加えてaar_deps属性が追加される。

Name Type Description
aar_deps label_list AAR library targetsのリスト

kotlin_test

kotlin_test rule はkotlin_binary rule とほとんと同じruleになっている(内部的にjava_binaryではなくjava_testを呼び出すこと以外同じ)。

kotlin_test(
    name = "main_kt_test",
    test_class = "examples.helloworld.MainKtTest",
    srcs = ["MainKtTest.kt"],
    size = "small",
    deps = [
        ":rules",
    ],
    java_deps = [
        "@junit4//jar",
    ],
)

bazel testでテストを実行することができる。

$ bazel test :main_kt_test.jar
............................................................
INFO: Found 1 target and 0 test targets...
Target //examples/helloworld:main_kt_test.jar up-to-date:
  bazel-bin/examples/helloworld/main_kt_test.jar
INFO: Elapsed time: 31.513s, Critical Path: 0.30s
ERROR: No test targets were found, yet testing was requested.

サンプルではテストが書かれていないのでERRORとなる。

kotlin_compile

kotlin_compile rule は、Kotlinコンパイラを実行し.jarファイルをKotlinのソースコードから生成する。kotlin_library rule では内部的にkotlin_compile rule を実行している。そして、java_import rule を介して他のJava rule で使用できるようにしている。

kotlin_compile rule は、通常では直接使用することは無い。

Annotation Processing

現状サポートしていない。TODOにはkapt supportとあるのと、issuesでのやり取りを見る限るBazel 0.6.0でサポートされそう。Incremental compilationなんかもTODOに入ってるので、ビルドのパフォーマンスはもっと良くなりそう。

サンプルコードの実行

rules_kotlinのリポジトリをcloneすることでローカル環境で実行することができる。

$ git clone https://github.com/pubref/rules_kotlin
$ cd rules_kotlin

bazel queryで実行可能なtargetの一覧を取得できる。

$ bazel query //... --output label_kind
java_binary rule //kotlin:kotlinc
java_test rule //examples/helloworld:main_test
kotlin_compile rule //examples/helloworld:main_kt_test_kt
java_test rule //examples/helloworld:main_kt_test
kotlin_compile rule //examples/helloworld:main_kt_kt
kotlin_compile rule //examples/helloworld:rules
java_binary rule //java/org/pubref/rules/kotlin:worker
java_library rule //java/org/pubref/rules/kotlin:preloader
java_library rule //java/org/pubref/rules/kotlin:compiler
java_library rule //java/io/bazel/rules/closure:BazelWorker
java_library rule //java/io/bazel/rules/closure/program:program
java_library rule //java/com/google/devtools/build/lib/worker:worker
java_binary rule //examples/helloworld:main_kt
java_binary rule //examples/helloworld:main_java
java_import rule //examples/helloworld:rules_kt
java_library rule //examples/helloworld:milk
java_library rule //examples/helloworld:guava

以下のようにtargetを指定して実行することができる。bazel runコマンドはビルドと実行をまとめてやってくれるので便利。

$ bazel run examples/helloworld:main_kt
$ bazel run examples/helloworld:main_java
$ bazel test examples/helloworld:main_test
$ bazel test examples/helloworld:main_kt_test

参考