最近、メインの仕事ではGoで書かれたサーバをいじっていて、ビルドするのにBazelを使っている。使っているとBazelなかなかいいやつだな、と好感を持つのだけど、でもGoの場合、標準ツールとの相性というのがなあ、という微妙さもある。
その辺の個人的な雑感です。
Bazelとは
Googleが公開しているオープンソースのビルドシステム。もともと社内に存在していたビルドシステムのオープンソース版という位置づけ。社内版と何がどう違うのかはよく知らない。BUILDという名前のファイルにビルドレシピを書く。構文はそこそこ簡潔で、わりと宣言的に書ける。ちなみに構文はPythonのサブセットなのだけど、Bazel自体はJavaで書かれている。なぜ……いやまあそれはべつにいい。
社内版ビルドシステムと構文が同じなので、社内版プロダクトがわりとそのまま(もしくは構文解析後のノードの簡単な書き換えによって)オープンソース化できる、とか、両者の行き来が楽になってうれしい、といったあたりがオープンソース化した存在意義かと思われる。
Bazelはサーバプロセスとコマンドラインツールに分かれていて、サーバプロセスは適宜自動的に起動され、依存グラフの情報などを保存してくれる構成になっている。サーバからはイベントデータが取ってこれて、これをフックして各種のバックエンド拡張を行えているんじゃないかなとおもう。Bazelそれ自体には、グーグルの巨大な社内レポジトリを扱うほどの機能はビルトインされていないと思うけれど、そうやって社内依存度の高い部分は切り離す設計になっているとおもわれる。
GoでBazelを使う意味
ところで、Goは標準のツールチェインがよくできていて、ビルドとかの面倒を見てくれる。go getとかgo buildとかすればいいわけで、なぜBazelを使うのか?というのはあまり自明ではない。実際、チームのみんなもBazelが好きだから使いたいというのでもなく、実際には必要だから使っている、といったところではないかと思う。
WORKSPACEと依存関係の固定
Bazelにはワークスペースという概念があって、このワークスペースに依存する外部ライブラリのレポジトリを指定できる。Bazelはビルドレシピを解析して、依存する先が外部ライブラリになっていると、当該のgitレポジトリを自分でpullしてリビジョンを維持してくれる。リビジョンを指定しておけばそのリビジョンに固定される。
ビルドシステムが外部の依存ライブラリのリビジョンも管理している、というのは一見するとちょっとキモい構造なのだけど、いい面もある。というのは、新しいリビジョンへの同期などがビルドの作業の途中の、依存性として表現されるからだ。ようは、誰かがワークスペースを編集したり、あるいは自分のローカルな作業ブランチの一部が古いワークスペースになっていても、開発者はあまり気にせず、bazel buildするだけで勝手に現在の環境に更新しつつビルドしてくれて、問題が顕在化しない。
実際、repoやgclientを使っていると、git bisectがそこそこ手間なのだけど、ビルドプロセスと外部依存ライブラリの管理が統合されていると、そこの手間が全然ない。
Goの場合、いまはvendorディレクトリもあるし、godepみたいなツールもある。でもまあ、なんだかんだでビルドプロセスと一緒になっているのは便利だなと思う。
ちなみにbazelの場合は外部ライブラリにもBUILDファイルが必要になるので、下手をするとぜんぶ手書きする必要があるのだが、bazel用にgazelleというツールがあって、Goプログラムを解析してBUILDファイルを自動生成してくれるので、ふつうはそういう手間は存在しない。
コード生成
Bazelは一般的なビルドツールなので、Go以外のコンパイルもできるし、コード生成もできる。仕事のプロダクトではProtocol Buffers / gRPCを多用しているので、そういうコード生成が大量発生しているし、自前のコード生成ツールも使ったりしている。
Goでコード生成というとgo generateコマンドがあるのだけど、このコマンドはさすがにちょっとシンプルすぎると思う。というか、用途と想定されるワークフローが違うということだろうか。
go generateは自動的には動作しない。レポジトリには生成されたGoのファイルをチェックインすることが前提になっていて、開発者は必要に応じて手動でgo generateを走らせて再生成してチェックインしてね、という仕組みだ。なのでgo getなどは.goファイルだけを相手にすればいいし、コードジェネレータに対する依存関係とか、ややこしい問題が発生しない。
しかし正直に言って、ジェネレータが生成したコードとかチェックインしたくない。生成物に依存する入力ファイルや生成ツールの依存関係もわかっていることなので、どういうときにどのファイルを生成するか、というのはビルドプロセスの一部であってほしい。
Bazelはごくふつうの汎用ビルドツールなので、この辺は(ルールをちゃんと書けば)きちんとやってくれる。当たり前のことですがね……。とくにprotobufまわりのサポートは充実しているので、まあ、楽。
GoでBazelを使うと困るところ
そういうわけでなかなかいいのだけれど、困っている面もあって、具体的にいうと、ふつうのgoコマンドはまともに動かない。
わたしがかかわっているプロダクトは、最終的にバイナリをつくってdockerイメージとかを作るので、まあいいのかもしれない。でもこれがライブラリとか外部ツールだった場合、ふつうみんなはgo getしたら使える、というのを想定するだろう。go getだけじゃ動かない、なぜならコード生成が……とか言われたら、常識的にいって何らかの罵声が飛んでくることであろう。
それだけじゃない。いろんな外部ツールが、goコマンドの動作やその想定するgopath環境というものを前提につくられている。ようするにリントチェッカやコード書き換えツール(gofmt、gosimpleなど)が動かない。テスト、raceチェッカ、ベンチマーク、などは動くが、なぜかBazelのテストはコードカバレッジの情報を取得できないというバグがあるので、現状ではカバレッジを取りたかったらgo testする必要がある。
自分たちのプロジェクトでは、Bazelがとってきた依存ライブラリへのシンボリックリンクをvendorの下に展開するスクリプトがあるけれど、まあこんなのはハックであって、何もしなくてもうまいこと動いてくれたらいいのにな、とは思う。
まとめ
あくまでも雑感なので、とくに有意義な結論はないのだが、BazelとGoがうまくまとまるような、いい方法はないものかな、と思っている。
ひとつにはBazelがワークスペース(~/.cache下の領域)にGOPATHと同じような環境を構築できたら良いのかもしれない。そうすればもっと簡単にリントツールとか外部ツールを動かしたりできる。
ライブラリなどの用途を考えた場合とコード生成の成果物はチェックインしたくない、というのを両立させるには、コード生成等の処理を済ませたあとのソースコードを配布する場所がやっぱりあるとうれしい。CPANとかgemsとかnpmとか……いや、それならgopkg.inでいいのかも。ビルドツール側がコード生成やvendorディレクトリの作成、さらにはパッケージ参照なども書き換えた上で別ブランチにマージでき、そのブランチをgopkg.inか、あるいは別のURLから参照できるようにすればいいのかなと思う。kubernetesはそういうことをやってそうな気配がある。この辺はCI/CDまわりで頑張るべきか。
Goのツールチェインから歩み寄る方向性はあるだろうか? go buildやgo testにフックがあるといろいろ捗るだろうな、とは思う。ただ、どういうフックがどう設定できたらいいのか?go getとかでどういうフックがどう動くべきで、レポジトリにどう設定できるのか?とか、考え出すときりがない。
ローカルなワークスペースについては、vendorディレクトリが解決してくれた感はあるが、vendorディレクトリ以下の管理はそんなに自明ではない。godepが標準ツールと統合されたりするとうれしいだろうか?
なんだかんだ書いたが、BazelとGoはちゃんと動くような整備がかなり整っている方だと思う。cgoがあってもコンパイルできるし、go_proto_libraryとかは全然心配しなくていいし、カバレッジがとれないのは困るけどrace testingが動くのはうれしい。総じてなかなかよくできている。このへん見てるとyuguiさんが尽力した後が垣間見えて、お世話になってるなあと思ったりしている。
From Java’s perspective, it’s conventional for build tools to manage dependencies. See maven, gradle or sbt.
いいねいいね
そういやその辺はそうですね
いいねいいね