ビルドツール「Bazel」について

ソフトウェアエンジニアのshinyaです。

2019年10月に、ソフトウェアのビルド・テストツールである「Bazel」のバージョン1.0がリリースされました(Bazel公式ブログ)。

Bazelは使いやすいのですが、現時点では情報がまとまっているサイトが少ないです。そのため、この記事では私が調べたことをまとめることにしました。

目次

適宜 NOTE:という項目で、補足的な情報を追加しています。

以下は2019年11月24日時点(Bazel 1.1.0)での情報です。コードはUbuntu 18.04とmacOSでテストしました。

Bazelとは

Bazel (/ˈbeɪzˌəl/1) はGoogleにより開発されているソフトウェアのビルド・テストツールです。

特徴

  • ビルド結果をキャッシュし、必要な箇所のみビルドすることで高速動作する。
  • 多くのプログラミング言語に対応している。
  • Starlark2というPythonライクな言語で拡張機能を追加できる。

よく比較対象として挙げられるビルドツール

Bazelが使われている有名なOSS

Bazelのインストール

Bazelのインストール手順は公式サイトに各OSごとにまとめられています。

ただ、私はBazelを直接インストールするよりもBazeliskをインストールしたほうが良いと考えています。BazeliskはBazelのラッパーで、プロジェクトごとに適切なバージョンのBazel使い分ける事ができます(Rubyのrbenvや、Pythonのpyenvに似ています)。例えば、TensorFlowはBazel 1.1.0、KubernetesはBazel 0.23.2を使用しており、各プロジェクトごとに対応するバージョンのBazelをインストールするのは面倒です。Bazeliskは設定ファイル(.bazelversion)や環境変数(USE_BAZEL_VERSION)などから、各プロジェクトごとに適切なバージョンのBazelをダウンロードし、使用してくれます。

その他のBazelのバージョン管理方法として、asdfbazelプラグインを使う方法もあります。

この記事では今後、Bazeliskを使う前提で話を進めますが、Bazelを直接インストールした場合でもほとんど違いはありません。

Bazeliskのインストール

詳細なインストール手順はBazelisk公式サイトを参照してください。

Ubuntu 18.04

A) リリースファイルをダウンロードするパターン

B) Go getでインストールするパターン

Go1.11以降がインストールされているなら、go getでインストールするのが楽です。

macOS

Homebrewからインストールするパターン

Homebrew経由が楽です。


NOTE: コマンドラインの補完機能について

Bazelはbash用とzsh用のコマンドライン補完用スクリプトを提供しています(公式サイト)。Bazelを直接インストールした場合、コマンドライン補完スクリプトも同時にインストールされるのですが、Bazeliskではインストールされません。以下、Bazeliskでコマンドライン補完用スクリプトを取得する方法をいくつか考えましたが、どれもあまり良い方法には思えていません。

Ubuntu 18.04

A) APTのBazelからスクリプトを取得するパターン

Bazelを一度APTでインストールし、補完スクリプトをコピー後、Bazelを消します。

B) Bazelのソースコードからスクリプトを作成する

macOS

HomebrewのBazelからスクリプトを取得するパターン

Bazelを一度Homebrewでインストールし、補完スクリプトをコピー後、Bazelを消します。



NOTE: 統合開発環境(IDE)のサポートについて

プラグインを追加することで、IDEでBazelに対応できます。


Bazelのチュートリアル

空のBazelワークスペース

最終的なコードはこちらに置いてあります。

とりあえず空のBazelワークスペース ~/first_bazel を作ってみます。

これで何もビルド対象が無い、空のBazelワークスペースが完成しました。

Bazelがちゃんとインストールされていれば、bazel info workspaceでワークスペースの情報が表示されます。

簡単なJavaパッケージ

最終的なコードはこちら

JDKが必要なので、あらかじめインストールしておきます。

Javaライブラリ

ワークスペースにsrc/main/java/jp/flywheel/ディレクトリを作り、例としてMyLib.javaファイルを作ります。

src/main/java/jp/flywheel/に以下のBUILD.bazelというファイルを作成します。

これでsrc/main/java/jp/flywheel/パッケージが出来ました。パッケージは、BUILDもしくはBUILD.bazelファイルを含むディレクトリです。
上記のBUILD.bazelでは、java_libraryというビルドルールで、MyLib.javaというソースファイルから、mylibというJavaライブラリのターゲットを作成しています。

パッケージが出来上がったので、ビルドしてみます。

//src/main/java/jp/flywheel:mylibの部分は//パッケージ名:ターゲット名を表していて、ラベルと呼ばれます。

ビルドの生成物は bazel-bin/src/main/java/jp/flywheel/ に出力されます。

Javaバイナリ

続いて、このMyLibを使用する、MyBin.javaというファイルを作成し、java_binaryルールをBUILD.bazelに追加します。

java_binaryルールは、Javaの実行ファイルを作成します。

ビルドの生成物は bazel-bin/src/main/java/jp/flywheel/ に出力されます。

java_binaryルールでの生成物はbazel runで直接実行できます。

bazel runからmain関数に引数を渡すには、-- のあとに追加します。

java_binaryルールは、暗黙的(implicit)にいくつかのターゲットを追加します。例えば、ターゲット名に_deploy.jarを追加すると、デプロイ用のuber JAR (fat JARやall-in-one JARとも呼ばれます)を作る事ができます。

bazel-bin/src/main/java/jp/flywheel/mybin_deploy.jarというuber JARが作成され、java -jarなどで実行できます。

Javaテスト

MyLib.javaのユニットテストを作るため、ワークスペースにsrc/test/java/jp/flywheel/ディレクトリを作り、MyLibTest.javaファイルを作ります。

src/test/java/jp/flywheel/に以下のBUILD.bazelというファイルを作成します。

これでテストパッケージ(//src/test/java/jp/flywheel/)の準備は整いました。

しかしこのままでは、テストパッケージ(//src/test/java/jp/flywheel/)に、ライブラリのパッケージ(//src/main/java/jp/flywheel/)の参照権限(visibility)が無いため、ビルドが失敗します。

デフォルトでは、各ターゲットは同じパッケージ内のターゲットからしか参照できません。

テストパッケージ(//src/test/java/jp/flywheel/)が、ライブラリのパッケージ(//src/test/java/jp/flywheel/)のターゲットを参照できるように、src/test/java/jp/flywheel/BUILD.bazeldefault_visibilityを追加します。

//src/test/java/jp/flywheel:__subpackages__は、テストパッケージ(//src/test/java/jp/flywheel)と、それ以下のサブパッケージを意味します。

Visibilityの詳しい設定方法は、common attributesのvisibilityの項目を見てください。

これでテストパッケージ(//src/test/java/jp/flywheel)からライブラリのパッケージが見られるようになったので、bazel testでユニットテストを実行してみます。

これで無事にJavaのテストを書くことができました。


NOTE: BUILDBUILD.bazel

ビルドターゲットは BUILDBUILD.bazel のどちらかに記入します。公式サイトBazel本体BUILDを、rules_gobuildtoolsではBUILD.bazelを使用しています。

BUILDBUILD.bazelのどちらを使用しても良いのですが、私は以下の理由から、今後新しくプロジェクトを作る場合はBUILD.bazelを使用したほうが良いと考えています。

  • Bazel本体や、(ちゃんと更新されている)周辺ツールはBUILDBUILD.bazelのどちらにも対応しているため。(BUILDBUILD.bazelが同じディレクトリにある場合、BUILD.bazelが優先される。)
  • 大文字小文字を区別しない(case-insensitive)ファイルシステムの場合、buildBUILDで名前の衝突が起きるため。

WORKSPACEも同様にWORKSPACE.bazelを代わりに使用することができます。しかし、BUILDと比較して名前の衝突が起きにくためか、対応しているツールが少ない印象です。例えば、GitHubのシンタックスハイライトにはBUILD.bazelは登録されていますが、WORKSPACE.bazelは登録されていません。

参考



NOTE: Bazelプロジェクト用の参考になる.gitignore



NOTE: ErrorProne

BazelではErrorProneがデフォルトで有効3になっています。

そのため、以下のようなIllegalFormatConversion("%d"の型と"hello"という文字列の型が合わない)はエラーとなりビルドに失敗します。

コードはこちら

ErrorProneを無効にするメリットは特に思いつきませんが、--javacopt='-XepDisableAllChecks'というフラグを追加するとErrorProneを無効にできます。

.bazelrcファイルに書いておくと、フラグを常に適用できます。

いずれにしても、ErrorProneを無効にするメリットは無いように思います。



NOTE: Java 11

BazelのJavaコードはデフォルトでJava8でビルドされます。--java_toolchain(と場合によっては--javabase)をJava11にするとJava11でビルドされます。

コードはこちら

常にJava11でビルドするなら、.bazelrcファイルに書いておくと楽だと思います。

参考



NOTE: testonly属性

先述の通り、Bazelではパッケージやターゲットごとにvisibilityを設定でき、これによりターゲットの影響範囲を制御することができます。

ターゲットの影響範囲を制限する別の方法として、testonly属性が便利です。testonlyTrueに設定されているターゲットは、testonlyのターゲットもしくはテストターゲット(*_testルール)からしか参照できません。testonlyを使用することで、テスト用のライブラリ・コードをテストでしか使用できないように制限できます。



NOTE: パッケージレイアウト

前述の例では、パッケージのレイアウトはMavenのディレクトリレイアウトと同様に、src/{main, test}/java/jp/flywheel/...というレイアウトでした。

コードはこちら

しかし、BazelではMavenのレイアウトに揃える必要はありません。以下のように、ライブラリ、バイナリ、テストのコードを1つのパッケージにすることもできます。さらに言えば、C++のソースコードやCSVなどのデータも同じパッケージに入れる事もできます。

コードはこちら

ただ、以下の理由から、私はMavenのレイアウトを使用したほうが良いと考えています。

  • Visibilityの管理や、テストコードの分離が容易であるため。
  • 統合開発環境(IDE)や周辺ツールがMavenのレイアウトを仮定している可能性があるため。
  • Bazel本体のGitHubリポジトリの構造がsrc/{main, test}/java/com/google/...となっているため。
  • MavenやGradleなどから移行したユーザが容易に学習できるため。

より実践的な例

Apache Spark Scalaパイプライン

Scala 2.12.8 で書かれたApache Spark 2.4.4のパイプラインをBazel上で作ります。

最終的なコードはこちらに置いてあります。

WORKSPACEに外部依存を追加する

ScalaをBazelで使うにはrules_scalaからScala用のルールをインポートする必要があります。

また、SparkのartifactをMavenリポジトリから取得するため、rules_jvm_externalもインポートします。

rules_scalarules_jvm_externalにかかれている内容を参考に、WORKSPACEを更新します。バージョンやSHA256の値は適宜変更します。SkylibProtocol Buffersはrules_scalaのために必要なので、これらも追加します。

WORKSPACEの中身が大きくなっていますが、やっていることは単純です。http_archiveルールを使用して、Protocol BuffersなどのソースコードをGitHubからダウンロードし、それらの中に定義されているルールを読み込み、実行しているだけです。


NOTE: maven_installで取得するartifactを固定する

rules_jvm_externalmaven_installルールは、推移的な依存関係を解決し、必要なJARファイルをダウンロードします。

例えば、org.apache.spark:spark-sql_2.12:2.4.4org.apache.parquet:parquet-hadoop:1.10.1に依存しています。maven_installartifactsorg.apache.spark:spark-sql_2.12:2.4.4を追加すると、org.apache.parquet:parquet-hadoop:1.10.1も自動的にダウンロードされ、@maven//:org_apache_parquet_parquet_hadoopラベルとして参照できるようになります。この依存関係の解決に時間がかかります。rules_jvm_externalでは解決済みの依存関係をファイルに出力し、2回目以降はその解決済みの依存関係を利用することで、処理の高速化ができます4。処理の高速化だけでなく、Yarnyarn.lockPipenvのPipfile.lockのように、推移的な依存関係を固定することで再現性を上げる効果もありそうです。

Bazelのquery機能を使うと、各ターゲットの依存関係などを調べることができます。Graphviz (dot)がインストールされていれば、依存関係グラフを画像として出力できます。

例えば、@maven//:org_apache_spark_spark_sql_2_12の依存関係は、以下のコマンドでPNG画像に出力できます。(出力PNG画像: けっこう大きいので注意)。


パイプラインのコードを追加する

テキストファイル中の単語をカウントする単純なSparkパイプラインを
src/main/scala/jp/flywheel/WordCount.scalaに作成します。

BUILD.bazelも追加します。

java_binaryルールと同様に、scala_binaryルールでもターゲット名に_deploy.jarを追加することで、uber JARを作成できます。

生成したuber JARはMavenやGradleで作成した場合と同様に、spark-submitなどで実行できます。

テストコードを追加する

WordCount.scalaをテストするためのコードを、src/test/scala/jp/flywheel/WordCountTest.scalaに作成します。

SharedSparkSessionを使いたいため、Sparkのテスト用のartifact (@maven//:org_apache_spark_spark_sql_2_12_tests)をtestonly属性付きで追加しています。

以上で簡単なSparkパイプラインが出来上がりました。

参考サイトなど

公式サイト

https://bazel.build/

Bazelのビジョンなどがまとまっています。

複数のバージョンについての情報が書かれているため、今見ているページがどのバージョンのことについて書かれているかをしっかり確認した方が良いと思います。サイト内検索を使用する際は、過去のバージョンのページに移動してしまう場合があるため特に注意。

リリースについてのblog記事(例えばBazel 1.1)のCommunityの項目には、Bazelのチュートリアル記事などのリンクがあります。

Awesome Bazel

https://awesomebazel.com/

Bazelの拡張ルールや、周辺ツール、ブログ記事などのリソースへのリンクがまとめられています。

Buildifier

BuildifierはBazel関連ファイル(WORKSPACE, BUILD, BUILD.bazel, *.bzlなど)のフォーマッタです。Buildifierに--lint=warnフラグを追加することで、Bazel関連ファイルをチェックすることができます(Warning一覧)。Bazelの情報はサイトやソースコードが書かれた時期によりバラバラで、廃止予定(deprecated)の機能が書かれたサイトやソースコードがあります。現時点で何が廃止予定の機能なのかはBuildifierでチェックするのが確実な上、楽です。

bazelbuild内のソースコード

https://github.com/bazelbuild

結局のところ、Bazel本体や拡張ルールのソースコードやissueを読むのが手っ取り早くて正確だと思います。

まとめ・感想

Bazelは使いやすいのですが、まとまっている情報が少ないため、本記事では調べた事や実際に使ってみた感想などをまとめました。

Bazelや周辺ツールの進化が速いため、本記事の有効期間は短いかもしれませんが、誰かのお役に立てば幸いです。

Notes


  1. Bazel FAQ: How do you pronounce “Bazel”?
    カタカナ表記だと、ベイゼルでしょうか?
    Javaビルドツール入門 Maven/Gradle/SBT/Bazel対応」では、バゼルと表記されています。 

  2. Starlarkは以前はSkylarkという名前であったため、今でもSkylarkという表現をときどき見かけます。
    https://blog.bazel.build/2018/08/17/starlark.html 

  3. https://blog.bazel.build/2015/06/25/ErrorProne.html 

  4. https://github.com/bazelbuild/rules_jvm_external#pinning-artifacts-and-integration-with-bazels-downloader