ビルドツール「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により開発されているソフトウェアのビルド・テストツールです。
特徴
よく比較対象として挙げられるビルドツール
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のバージョン管理方法として、asdfのbazelプラグインを使う方法もあります。
この記事では今後、Bazeliskを使う前提で話を進めますが、Bazelを直接インストールした場合でもほとんど違いはありません。
Bazeliskのインストール
詳細なインストール手順はBazelisk公式サイトを参照してください。
Ubuntu 18.04
A) リリースファイルをダウンロードするパターン
$ sudo apt update
$ sudo apt install -y curl build-essential
$ curl -L -o bazel https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64
$ chmod +x bazel
$ export PATH="${PATH}:${PWD}"
$ bazel version
B) Go getでインストールするパターン
Go1.11以降がインストールされているなら、 go get でインストールするのが楽です。
$ go get github.com/bazelbuild/bazelisk
$ export PATH="${PATH}:$(go env GOPATH)/bin"
$ alias bazel=bazelisk
$ bazel version
macOS
Homebrewからインストールするパターン
Homebrew経由が楽です。
$ brew tap bazelbuild/tap
$ brew install bazelbuild/tap/bazelisk
$ bazel version
NOTE: コマンドラインの補完機能について
Bazelはbash用とzsh用のコマンドライン補完用スクリプトを提供しています(公式サイト)。Bazelを直接インストールした場合、コマンドライン補完スクリプトも同時にインストールされるのですが、Bazeliskではインストールされません。以下、Bazeliskでコマンドライン補完用スクリプトを取得する方法をいくつか考えましたが、どれもあまり良い方法には思えていません。
Ubuntu 18.04
A) APTのBazelからスクリプトを取得するパターン
Bazelを一度APTでインストールし、補完スクリプトをコピー後、Bazelを消します。
$ sudo apt sudo apt update
$ sudo apt install -y curl build-essential
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -
$ echo 'deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8' | sudo tee /etc/apt/sources.list.d/bazel.list
$ sudo apt update
$ sudo apt install -y bazel
# bash用スクリプトは /etc/bash_completion.d/bazel に出力されます。
# 適宜必要なパスにコピーしてください。
$ cp /etc/bash_completion.d/bazel PATH_TO_BASH_COMPLETIONS
# zsh用スクリプトはGitHubからダウンロードします。
$ curl https://raw.githubusercontent.com/bazelbuild/bazel/master/scripts/zsh_completion/_bazel -o PATH_TO_ZSH_COMPLETIONS
# Bazelをアンインストールする。
$ apt --purge remove bazel
B) Bazelのソースコードからスクリプトを作成する
$ sudo apt install -y build-essential openjdk-11-jdk
$ curl -L -o 1.1.0.tar.gz https://github.com/bazelbuild/bazel/archive/1.1.0.tar.gz
$ tar xf 1.1.0.tar.gz
$ cd bazel-1.1.0/
# bash用スクリプトは bazel-bin/scripts/bazel-complete.bash に出力されます。
# 適宜必要なパスにコピーしてください。
$ bazel build //scripts:bash_completion
$ cp bazel-bin/scripts/bazel-complete.bash PATH_TO_BASH_COMPLETIONS
# zsh用スクリプトはビルド不要で、 scripts/zsh_completion/_bazel にあります。
$ cp scripts/zsh_completion/_bazel PATH_TO_ZSH_COMPLETIONS
# ソースコードが不要なら、消します。
$ cd ..
$ rm -rf 1.1.0.tar.gz bazel-1.1.0/
macOS
HomebrewのBazelからスクリプトを取得するパターン
Bazelを一度Homebrewでインストールし、補完スクリプトをコピー後、Bazelを消します。
$ brew tap bazelbuild/tap
# Bazeliskが入っていれば一度削除する。$ brew uninstall bazelbuild/tap/bazelisk
# Bazelをインストールする。
$ brew install bazelbuild/tap/bazel
# bash用スクリプトを適宜必要なパスにコピーする。
$ cp $(brew --prefix)/etc/bash_completion.d/bazel-complete.bash PATH_TO_BASH_COMPLETIONS
# zsh用スクリプトを適宜必要なパスにコピーする。
$ cp $(brew --prefix)/share/zsh-completions/_bazel PATH_TO_ZSH_COMPLETIONS
# Bazelをアンインストールする。
$ brew uninstall bazelbuild/tap/bazel
# Bazeliskを再度インストールする。
$ brew install bazelbuild/tap/bazelisk
NOTE: 統合開発環境(IDE)のサポートについて
プラグインを追加することで、IDEでBazelに対応できます。
- IntelliJ: Bazel
- Visual Studio Code: vscode-bazel
Bazelのチュートリアル
空のBazelワークスペース
最終的なコードはこちらに置いてあります。
とりあえず空のBazelワークスペース ~/first_bazel を作ってみます。
$ mkdir ~/first_bazel
$ cd ~/first_bazel/
$ echo '1.1.0' > .bazelversion
$ touch WORKSPACE
これで何もビルド対象が無い、空のBazelワークスペースが完成しました。
~/first_bazel
├── .bazelversion # Bazelのバージョンを指定
└── WORKSPACE # 外部依存などを追加する際にはこのファイルを変更する。
Bazelがちゃんとインストールされていれば、 bazel info workspace でワークスペースの情報が表示されます。
$ bazel info workspace
~/first_bazel/
簡単なJavaパッケージ
最終的なコードはこちら。
JDKが必要なので、あらかじめインストールしておきます。
Javaライブラリ
ワークスペースに src/main/java/jp/flywheel/ ディレクトリを作り、例として MyLib.java ファイルを作ります。
$ mkdir -p src/main/java/jp/flywheel/
// src/main/java/jp/flywheel/MyLib.java
package jp.flywheel;
public class MyLib {
public static int fibonacci(int n) {
if (n < 0) {
throw new IllegalArgumentException();
} else if (n == 0 || n == 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
src/main/java/jp/flywheel/ に以下の BUILD.bazel というファイルを作成します。
# src/main/java/jp/flywheel/BUILD.bazel
load("@rules_java//java:defs.bzl", "java_library")
java_library(
name = "mylib",
srcs = ["MyLib.java"],
)
これで src/main/java/jp/flywheel/ パッケージが出来ました。パッケージは、 BUILD もしくは BUILD.bazel ファイルを含むディレクトリです。
上記の BUILD.bazel では、 java_library というビルドルールで、 MyLib.java というソースファイルから、 mylib というJavaライブラリのターゲットを作成しています。
パッケージが出来上がったので、ビルドしてみます。
$ bazel build //src/main/java/jp/flywheel:mylib
//src/main/java/jp/flywheel:mylib の部分は //パッケージ名:ターゲット名 を表していて、ラベルと呼ばれます。
ビルドの生成物は bazel-bin/src/main/java/jp/flywheel/ に出力されます。
Javaバイナリ
続いて、この MyLib を使用する、 MyBin.java というファイルを作成し、 java_binary ルールを BUILD.bazel に追加します。
// src/main/java/jp/flywheel/MyBin.java
package jp.flywheel;
public class MyBin {
public static void main(String[] args) {
int n = Integer.parseInt(args[0]);
System.out.println("Hello Bazel!");
System.out.println("Fib(" + n + ") = " + MyLib.fibonacci(n));
}
}
# src/main/java/jp/flywheel/BUILD.bazel
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
java_library(
name = "mylib",
srcs = ["MyLib.java"],
)
java_binary(
name = "mybin",
srcs = ["MyBin.java"],
main_class = "jp.flywheel.MyBin",
deps = [
":mylib", # or "//src/main/java/jp/flywheel:mylib",
],
)
java_binary ルールは、Javaの実行ファイルを作成します。
$ bazel build //src/main/java/jp/flywheel:mybin
ビルドの生成物は bazel-bin/src/main/java/jp/flywheel/ に出力されます。
java_binary ルールでの生成物は bazel run で直接実行できます。
$ bazel run //src/main/java/jp/flywheel:mybin
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at jp.flywheel.MyBin.main(MyBin.java:6)
bazel run から main 関数に引数を渡すには、 — のあとに追加します。
$ bazel run //src/main/java/jp/flywheel:mybin -- 10
Hello Bazel!
Fib(10) = 55
java_binary ルールは、暗黙的(implicit)にいくつかのターゲットを追加します。例えば、ターゲット名に _deploy.jar を追加すると、デプロイ用のuber JAR (fat JARやall-in-one JARとも呼ばれます)を作る事ができます。
$ bazel build //src/main/java/jp/flywheel:mybin_deploy.jar
# 実際にデプロイするなら、"-c opt"などの最適化オプションの追加を検討します。
# $ bazel build -c opt //src/main/java/jp/flywheel:mybin_deploy.jar
bazel-bin/src/main/java/jp/flywheel/mybin_deploy.jar というuber JARが作成され、 java -jar などで実行できます。
$ java -jar bazel-bin/src/main/java/jp/flywheel/mybin_deploy.jar 11
Hello Bazel!
Fib(11) = 89
Javaテスト
MyLib.java のユニットテストを作るため、ワークスペースに src/test/java/jp/flywheel/ ディレクトリを作り、 MyLibTest.java ファイルを作ります。
$ mkdir -p src/test/java/jp/flywheel/
// src/test/java/jp/flywheel/MyLibTest.java
package jp.flywheel;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class MyLibTest {
@Test
public void testFibonacci() {
assertEquals(144, MyLib.fibonacci(12));
}
}
src/test/java/jp/flywheel/ に以下の BUILD.bazel というファイルを作成します。
# src/test/java/jp/flywheel/BUILD.bazel
load("@rules_java//java:defs.bzl", "java_test")
java_test(
name = "mylib_test",
srcs = [
"MyLibTest.java",
],
test_class = "jp.flywheel.MyLibTest",
deps = [
"//src/main/java/jp/flywheel:mylib",
],
)
これでテストパッケージ( //src/test/java/jp/flywheel/ )の準備は整いました。
しかしこのままでは、テストパッケージ( //src/test/java/jp/flywheel/ )に、ライブラリのパッケージ( //src/main/java/jp/flywheel/ )の参照権限(visibility)が無いため、ビルドが失敗します。
$ bazel build src/test/java/jp/flywheel:mylib_test
target '//src/main/java/jp/flywheel:mylib' is not visible from target '//src/test/java/jp/flywheel:mylib_test'.
Check the visibility declaration of the former target if you think the dependency is legitimate
フォルトでは、各ターゲットは同じパッケージ内のターゲットからしか参照できません。
テストパッケージ( //src/test/java/jp/flywheel/ )が、ライブラリのパッケージ( //src/main/java/jp/flywheel/ )のターゲットを参照できるように、 src/main/java/jp/flywheel/BUILD.bazel に default_visibility を追加します。
# src/main/java/jp/flywheel/BUILD.bazel
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
package(default_visibility = ["//src/test/java/jp/flywheel:__subpackages__"])
java_library(
name = "mylib",
srcs = ["MyLib.java"],
)
java_binary(
name = "mybin",
srcs = ["MyBin.java"],
main_class = "jp.flywheel.MyBin",
deps = [
":mylib", # or "//src/main/java/jp/flywheel:mylib",
],
)
//src/test/java/jp/flywheel:__subpackages__ は、テストパッケージ( //src/test/java/jp/flywheel )と、それ以下のサブパッケージを意味します。
Visibilityの詳しい設定方法は、common attributesのvisibilityの項目を見てください。
これでテストパッケージ( //src/test/java/jp/flywheel )からライブラリのパッケージが見られるようになったので、 bazel test でユニットテストを実行してみます。
$ bazel test //src/test/java/jp/flywheel:mylib_test
//src/test/java/jp/flywheel:mylib_test PASSED in 0.5s
これで無事にJavaのテストを書くことができました。
NOTE: BUILD と BUILD.bazel
ビルドターゲットは BUILD か BUILD.bazel のどちらかに記入します。公式サイトやBazel本体は BUILD を、rules_goやbuildtoolsでは BUILD.bazel を使用しています。
BUILD と BUILD.bazel のどちらを使用しても良いのですが、私は以下の理由から、今後新しくプロジェクトを作る場合は BUILD.bazel を使用したほうが良いと考えています。
- Bazel本体や、(ちゃんと更新されている)周辺ツールは BUILD と BUILD.bazel のどちらにも対応しているため。( BUILD と BUILD.bazel が同じディレクトリにある場合、 BUILD.bazel が優先される。)
- 大文字小文字を区別しない(case-insensitive)ファイルシステムの場合、 build と BUILD で名前の衝突が起きるため。
WORKSPACE も同様に WORKSPACE.bazelを代わりに使用することができます。しかし、 BUILD と比較して名前の衝突が起きにくいためか、対応しているツールが少ない印象です。例えば、GitHubのシンタックスハイライトには BUILD.bazel は登録されていますが、 WORKSPACE.bazel は登録されていません。
参考
- bazelbuild/rules_go: To BUILD or to BUILD.bazel, that is the question
NOTE: Bazelプロジェクト用の参考になる .gitignore
- gitignore.ioのBazelテンプレート
- Bazel本体の.gitignore
NOTE: ErrorProne
BazelではErrorProneがデフォルトで有効3になっています。
そのため、以下のような IllegalFormatConversion (“%d”の型と“hello”という文字列の型が合わない)はエラーとなりビルドに失敗します。
コードはこちら。
package jp.flywheel;
public class MyBin {
public static void main(String[] args) {
System.out.println(String.format("%d", "hello"));
}
}
$ bazel build //src/main/java/jp/flywheel:mybin
src/main/java/jp/flywheel/MyBin.java:5:
error: [FormatString] illegal format conversion: 'java.lang.String' cannot be formatted using '%d'
System.out.println(String.format("%d", "hello"));
^
(see https://errorprone.info/bugpattern/FormatString)
Target //src/main/java/jp/flywheel:mybin failed to build
ErrorProneを無効にするメリットは特に思いつきませんが、 –javacopt=’-XepDisableAllChecks’ というフラグを追加するとErrorProneを無効にできます。
$ bazel build --javacopt='-XepDisableAllChecks' //src/main/java/jp/flywheel:mybin
.bazelrc ファイルに書いておくと、フラグを常に適用できます。
$ cat .bazelrc
build --javacopt='-XepDisableAllChecks'
いずれにしても、ErrorProneを無効にするメリットは無いように思います。
NOTE: Java 11
BazelのJavaコードはデフォルトでJava8でビルドされます。 –java_toolchain (と場合によっては –javabase )をJava11にするとJava11でビルドされます。
コードはこちら。
$ bazel build --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 \
--javabase=@bazel_tools//tools/jdk:remote_jdk11 \
//src/main/java/jp/flywheel:mybin
常にJava11でビルドするなら、 .bazelrc ファイルに書いておくと楽だと思います。
$ cat .bazelrc
build --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11
build --javabase=@bazel_tools//tools/jdk:remote_jdk11
参考
- bazelbuild/bazel: Support for JDK 11
NOTE: testonly 属性
先述の通り、Bazelではパッケージやターゲットごとに visibility を設定でき、これによりターゲットの影響範囲を制御することができます。
ターゲットの影響範囲を制限する別の方法として、 testonly 属性が便利です。 testonly が True に設定されているターゲットは、 testonly ターゲットもしくはテストターゲット( *_test ルール)からしか参照できません。 testonly を使用することで、テスト用のライブラリ・コードをテストでしか使用できないように制限できます。
NOTE: パッケージレイアウト
前述の例では、パッケージのレイアウトはMavenのディレクトリレイアウトと同様に、 src/{main, test}/java/jp/flywheel/… というレイアウトでした。
コードはこちら。
simple_java
├── .bazelversion
├── WORKSPACE
└── src
├── main
│ └── java
│ └── jp
│ └── flywheel
│ ├── BUILD.bazel
│ ├── MyBin.java
│ └── MyLib.java
└── test
└── java
└── jp
└── flywheel
├── BUILD.bazel
└── MyLibTest.java
しかし、BazelではMavenのレイアウトに揃える必要はありません。以下のように、ライブラリ、バイナリ、テストのコードを1つのパッケージにすることもできます。さらに言えば、C++のソースコードやCSVなどのデータも同じパッケージに入れる事もできます。
コードはこちら。
simple_java_flat
├── .bazelversion
├── WORKSPACE
└── src
├── BUILD.bazel
├── MyBin.java
├── MyLib.java
└── MyLibTest.java
ただ、以下の理由から、私は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_scalaとrules_jvm_externalにかかれている内容を参考に、 WORKSPACE を更新します。バージョンやSHA256の値は適宜変更します。SkylibとProtocol Buffersはrules_scalaのために必要なので、これらも追加します。
WORKSPACE の中身が大きくなっていますが、やっていることは単純です。 http_archive ルールを使用して、Protocol BuffersなどのソースコードをGitHubからダウンロードし、それらの中に定義されているルールを読み込み、実行しているだけです。
workspace(name = "simple_spark")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Skylib
SKYLIB_TAG = "0.9.0"
SKYLIB_SHA = "9245b0549e88e356cd6a25bf79f97aa19332083890b7ac6481a2affb6ada9752"
http_archive(
name = "bazel_skylib",
sha256 = SKYLIB_SHA,
strip_prefix = "bazel-skylib-%s" % SKYLIB_TAG,
url = "https://github.com/bazelbuild/bazel-skylib/archive/%s.tar.gz" % SKYLIB_TAG,
)
# Protocol Buffers
PROTOBUF_TAG = "3.10.1"
PROTOBUF_SHA = "6adf73fd7f90409e479d6ac86529ade2d45f50494c5c10f539226693cb8fe4f7"
http_archive(
name = "com_google_protobuf",
sha256 = PROTOBUF_SHA,
strip_prefix = "protobuf-%s" % PROTOBUF_TAG,
url = "https://github.com/protocolbuffers/protobuf/archive/v%s.tar.gz" % PROTOBUF_TAG,
)
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()
# Scala
RULES_SCALA_VERSION = "ff57530cc6796cdcd4ab0405c5404fad2d2e8923" # Latest commit as of 2019-11-27.
RULES_SCALA_SHA = "3712768d345917b9a94557a4ab008a89a9488031662ec5ab3d8fb2efa0ed5ec6"
http_archive(
name = "io_bazel_rules_scala",
sha256 = RULES_SCALA_SHA,
strip_prefix = "rules_scala-%s" % RULES_SCALA_VERSION,
url = "https://github.com/bazelbuild/rules_scala/archive/%s.zip" % RULES_SCALA_VERSION,
)
load("@io_bazel_rules_scala//scala:toolchains.bzl", "scala_register_toolchains")
scala_register_toolchains()
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_repositories")
scala_repositories(
scala_version_shas = (
"2.12.8",
{
"scala_compiler": "f34e9119f45abd41e85b9e121ba19dd9288b3b4af7f7047e86dc70236708d170",
"scala_library": "321fb55685635c931eba4bc0d7668349da3f2c09aee2de93a70566066ff25c28",
"scala_reflect": "4d6405395c4599ce04cea08ba082339e3e42135de9aae2923c9f5367e957315a",
},
),
)
# Maven
RULES_JVM_EXTERNAL_TAG = "2.10"
RULES_JVM_EXTERNAL_SHA = "1bbf2e48d07686707dd85357e9a94da775e1dbd7c464272b3664283c9c716d26"
http_archive(
name = "rules_jvm_external",
sha256 = RULES_JVM_EXTERNAL_SHA,
strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)
load("@rules_jvm_external//:defs.bzl", "maven_install")
maven_install(
artifacts = [
"org.apache.spark:spark-catalyst_2.12:2.4.4",
"org.apache.spark:spark-core_2.12:2.4.4",
"org.apache.spark:spark-sql_2.12:2.4.4",
# For testing.
"org.apache.spark:spark-catalyst_2.12:jar:tests:2.4.4",
"org.apache.spark:spark-core_2.12:jar:tests:2.4.4",
"org.apache.spark:spark-sql_2.12:jar:tests:2.4.4",
],
repositories = [
"https://jcenter.bintray.com/",
"https://repo1.maven.org/maven2",
],
)
NOTE: maven_install で取得するartifactを固定する
rules_jvm_external の maven_install ルールは、推移的な依存関係を解決し、必要なJARファイルをダウンロードします。
例えば、 org.apache.spark:spark-sql_2.12:2.4.4 は org.apache.parquet:parquet-hadoop:1.10.1 に依存しています。 maven_install の artifacts に org.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。処理の高速化だけでなく、Yarnのyarn.lockやPipenvのPipfile.lockのように、推移的な依存関係を固定することで再現性を上げる効果もありそうです。
Bazelの query 機能を使うと、各ターゲットの依存関係などを調べることができます。Graphviz ( dot )がインストールされていれば、依存関係グラフを画像として出力できます。
例えば、 @maven//:org_apache_spark_spark_sql_2_12 の依存関係は、以下のコマンドでPNG画像に出力できます。(出力PNG画像: けっこう大きいので注意)。
$ bazel query --notool_deps --noimplicit_deps \
'deps(@maven//:org_apache_spark_spark_sql_2_12)' --output graph \
| dot -Tpng -o spark_sql_deps.png
パイプラインのコードを追加する
テキストファイル中の単語をカウントする単純なSparkパイプラインを
src/main/scala/jp/flywheel/WordCount.scala に作成します。
// src/main/scala/jp/flywheel/WordCount.scala
package jp.flywheel
import org.apache.spark.sql.{Dataset, SparkSession, Row}
import org.apache.spark.sql.functions.{col, count, explode, split}
object WordCount {
def main(args: Array[String]): Unit = {
val inputTextFilePattern = args(0)
val outputPath = args(1)
val spark = SparkSession.builder.appName("WordCount").getOrCreate
try {
val textDF = spark.read.text(inputTextFilePattern)
val wordCountDF = countWords(textDF)
wordCountDF
.repartition(1)
.sortWithinPartitions("word")
.write
.csv(outputPath)
} finally {
spark.close
}
}
def countWords(textDF: Dataset[Row]): Dataset[Row] =
textDF
.select(explode(split(col("value"), " ")).alias("word"))
.filter(col("word").isNotNull)
.filter(col("word").notEqual(""))
.groupBy(col("word"))
.agg(count(col("word")).alias("count"))
}
BUILD.bazel も追加します。
# src/main/scala/jp/flywheel/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_binary", "scala_library")
package(default_visibility = ["//src/test/scala/jp/flywheel:__subpackages__"])
scala_library(
name = "spark_deps",
exports = [
"@maven//:org_apache_spark_spark_catalyst_2_12",
"@maven//:org_apache_spark_spark_core_2_12",
"@maven//:org_apache_spark_spark_sql_2_12",
],
)
scala_library(
name = "word_count_lib",
srcs = ["WordCount.scala"],
deps = [
":spark_deps",
],
)
scala_binary(
name = "word_count_bin",
main_class = "jp.flywheel.WordCount",
runtime_deps = [":word_count_lib"],
)
java_binary ルールと同様に、 scala_binary ルールでもターゲット名に _deploy.jar を追加することで、uber JARを作成できます。
$ bazel build //src/main/scala/jp/flywheel:word_count_bin_deploy.jar
# 生成物は bazel-bin/src/main/scala/jp/flywheel/word_count_bin_deploy.jar
生成したuber JARはMavenやGradleで作成した場合と同様に、 spark-submit などで実行できます。
$ spark-submit \
--master 'local[*]' \
bazel-bin/src/main/scala/jp/flywheel/word_count_bin_deploy.jar \
INPUT_FILE_PATH \
OUTPUT_FILE_PATH
テストコードを追加する
WordCount.scala をテストするためのコードを、 src/test/scala/jp/flywheel/WordCountTest.scala に作成します。
// src/test/scala/jp/flywheel/WordCountTest.scala
package jp.flywheel
import org.apache.spark.sql.Row
import org.apache.spark.sql.test.SharedSparkSession
import org.apache.spark.sql.types.{DataTypes, StructField, StructType}
import org.scalatest.{FunSuite, Matchers}
class WordCountTest extends FunSuite with Matchers with SharedSparkSession {
test("countWords") {
val rows = Seq(
Row("word1 word2 word3"),
Row("word1 word2"),
Row("word2 word3"),
Row(" word3 word4 ")
)
val schema = StructType(Seq(StructField("value", DataTypes.StringType)))
val textDF =
spark.createDataFrame(spark.sparkContext.parallelize(rows), schema)
WordCount.countWords(textDF).collect should contain theSameElementsAs Seq(
Row("word1", 2L),
Row("word2", 3L),
Row("word3", 3L),
Row("word4", 1L)
)
}
}
SharedSparkSession を使いたいため、Sparkのテスト用のartifact ( @maven//:org_apache_spark_spark_sql_2_12_tests )を testonly 属性付きで追加しています。
# src/test/scala/jp/flywheel/BUILD.bazel
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library", "scala_test")
scala_library(
name = "spark_test_deps",
testonly = True,
exports = [
"//src/main/scala/jp/flywheel:spark_deps",
"@maven//:org_apache_spark_spark_catalyst_2_12_tests",
"@maven//:org_apache_spark_spark_core_2_12_tests",
"@maven//:org_apache_spark_spark_sql_2_12_tests",
],
)
scala_test(
name = "word_count_test",
srcs = [
"WordCountTest.scala",
],
deps = [
":spark_test_deps",
"//src/main/scala/jp/flywheel:word_count_lib",
],
)
$ bazel test //src/test/scala/jp/flywheel:word_count_test
//src/test/scala/jp/flywheel:word_count_test PASSED in 9.2s
以上で簡単なSparkパイプラインが出来上がりました。
参考サイトなど
公式サイト
Bazelのビジョンなどがまとまっています。
複数のバージョンについての情報が書かれているため、今見ているページがどのバージョンのことについて書かれているかをしっかり確認した方が良いと思います。サイト内検索を使用する際は、過去のバージョンのページに移動してしまう場合があるため特に注意。
リリースについてのblog記事(例えばBazel 1.1)のCommunityの項目には、Bazelのチュートリアル記事などのリンクがあります。
Awesome Bazel
Bazelの拡張ルールや、周辺ツール、ブログ記事などのリソースへのリンクがまとめられています。
Buildifier
BuildifierはBazel関連ファイル( WORKSPACE , BUILD , BUILD.bazel , *.bzl など)のフォーマッタです。Buildifierに –lint=warn フラグを追加することで、Bazel関連ファイルをチェックすることができます(Warning一覧)。Bazelの情報はサイトやソースコードが書かれた時期によりバラバラで、廃止予定(deprecated)の機能が書かれたサイトやソースコードがあります。現時点で何が廃止予定の機能なのかはBuildifierでチェックするのが確実な上、楽です。
bazelbuild内のソースコード
結局のところ、Bazel本体や拡張ルールのソースコードやissueを読むのが手っ取り早くて正確だと思います。
まとめ・感想
Bazelは使いやすいのですが、まとまっている情報が少ないため、本記事では調べた事や実際に使ってみた感想などをまとめました。
Bazelや周辺ツールの進化が速いため、本記事の有効期間は短いかもしれませんが、誰かのお役に立てば幸いです。
Notes
- Bazel FAQ: How do you pronounce “Bazel”?
カタカナ表記だと、ベイゼルでしょうか?
「Javaビルドツール入門 Maven/Gradle/SBT/Bazel対応」では、バゼルと表記されています。 ↩ - Starlarkは以前はSkylarkという名前であったため、今でもSkylarkという表現をときどき見かけます。
https://blog.bazel.build/2018/08/17/starlark.html ↩ - https://blog.bazel.build/2015/06/25/ErrorProne.html ↩
- https://github.com/bazelbuild/rules_jvm_external#pinning-artifacts-and-integration-with-bazels-downloader ↩