Bazelは再現性に優れた、強力なビルドツールとして利用することができます。実際、会社ではビルド環境としてBazelが使われています。弊社では使用されている実績は以下の記事が詳しいです。
私もプロジェクト内でシェルスクリプトであったり、MakefileにやらせていたことをBazelのルールに一気に置き換えたりしてBazelに慣れてきました。
これを読んだみなさんが、Bazel ルールを書きたいけど、どこから手を付ければいいのか?となった時に参考になれば幸いです。
Bazelで出来ること
ソフトウェアのビルドに使われることの多いBazelですが、以下のことがまず基本的にできます。
- ファイル/アーカイブのダウンロード
- ファイルの実行
- シェルスクリプトの実行
- expand_template を使って、アクションが実行された際にマクロに沿ってシェルスクリプトを実行することが多いです。
これが出来ることに伴って、これらを組み合わせてビルドルールを記述することで何かを生成してそれを含めるようなことをBazelにやらせることができます。
BazelルールのQ&A
作ったルールが合っているか分からない、どうすれば?
Bazelルールはテストを書くことができます。書き方は公式ドキュメントがとても分かりやすい内容で取り上げているのでそちらを参考にしてみてください。
出力を元にしたテストを書くのは難しいので、最初はルールが自分の予期したものであるか(作成されたシェルスクリプトが合っているかどうか)を確認するところからはじめて良いと思います。
ルールのドキュメントを書きたい、どうすれば?
bazelbuild/stardocを使いましょう。Starlarkで記述したルールのコメントや、attrs
のdoc
などで明示したドキュメントを元にドキュメントを生成することができます。
ルールを作るために見た方が良い場所は?
自分の知っているプロジェクトでBazelが使われていたらそれを参考にしましょう。
もしない場合は、bazelbuild/examplesに様々なBazel ルールが存在します。主にビルドツールとして使う際のルールが多いです。これらのルールにあるように、きちんと出力を定義することで、Bazelの特徴であるキャッシュなどの恩恵を十分に受けることができます。
実際に私が書いたルールはMakefileの代替として使うぐらいの気持ちで作成したもの(例: atpons/rules_helm3)が多いです。
ワークスペース内に書き込みしたいんだけど?
Bazelの良いところは再現性ですが、ビルドツールとして出力を使い回すのではなく、リポジトリにその出力を含めたい場合などにはどうすれば良いでしょうか?
bazel run
で実行されるexecutableなoutputは、BazelによってBUILD_WORKSPACE_DIRECTORY
という環境変数がつきます。これを使ってサンドボックス内からワークスペースに入ることができます。
そこで何かを生成し、さらにそれをまたBazelで使用する場合はBUILDファイルにも手を加えてBazelに認識させる必要があります。(手で書いてもいいですし、それ用のツールを書くのもありだと思います)
Bazelの情報を集めるには?
私がBazelを主にGoで使っているので、GoでBazelを使っている大規模OSSを読んでいます。
Bazel自体の最新情報はGitHubのbazelbuildを見ています。Slackコミュニティもあるのでそれを参考にするのも良いと思います。
awesome-bazel
Bazelを利用しているプロジェクトや、ビルドルールを眺めることができます。
kubernetes/kubernetes
KubernetesはBazelを使ってビルドされています。hack/
ディレクトリにBazelルール用のシェルスクリプトが入っています。
Go Modulesのvendorや、外部バイナリの外部からの注入などが参考になりました。Buildozerの使い方なども参考になります。
atlassian/bazel-tools
Atlassianが使用しているルールやツールのリポジトリです。主にGolangで使用されているようです。
Bazelの不便なところとして、bazel run
のターゲットを並列に実行する機能がまだありません。(issue)
Bazelを依存が少ないMakefileの代替としてゆるく使っている時には不便です。
それを解決するmultirunというルールを作ってくれています。私はrules_dockerと組み合わせて、複数のコンテナレジストリにプッシュするターゲットを作った時に、同時に実行もできるようにしておきたいときに使いました。
container_push(
name = "push-region-1",
format = "Docker",
image = ":image",
registry = "asia.gcr.io",
repository = "example/example",
stamp = True,
tag = "{BUILD_TIMESTAMP}",
)
container_push(
name = "push-region-2",
format = "Docker",
image = ":image",
registry = "asia.gcr.io",
repository = "example/example",
stamp = True,
tag = "{BUILD_TIMESTAMP}",
)
multirun(
name = "push",
commands = [
"//tool:push-region-1",
"//tool:push-region-2",
],
parallel = True,
)
最後に
Bazelを全体に導入するのはなかなか難しいので、最初は特定の部分で使用していって、そのうちCIをBazelだけで回せるようにしていくやり方が一番導入しやすいと思います。
その上で、今までMakefileなどで行っていたワークフローをBazelに移行していくことでビルドの再現性や、外部への依存を少しでもわかりやすくしていくことが良いと思います。
そのために"ゆるい"Bazelルールを書いてBazelに慣れていくことをおすすめします。この記事がその参考になれば幸いです。