背景
Bazelは優れたビルドツールである一方で、導入したチームには1人はBazel職人が必要と言われるほどキャッチアップコストが高くハマったときに開発が止まると言われます。
そのためKubernetesからも削除されるほどです。
しかしながら導入の善し悪しを判断する上で最低限の知識は必要なので、一通りのことはできるようにとキャッチアップしてみます。
ちなみに発音は公式サイトでは「ベイゼル」とのこと。
How do you pronounce “Bazel”?
The same way as “basil” (the herb) in US English: “BAY-zel”. It rhymes with “hazel”. IPA: /ˈbeɪzˌəl/
ref: FAQ | Bazel
Bazelの特徴
Bazelの特徴として以下があります。
- ビルドの再現性が保たれる
- サンドボックス環境で実行される(Bazel1つだけあればビルドできる)
- キャッシュの活用(ローカル・リモート)によるビルド時間の高速化
特に1つ目の特徴である「inputが同じであればoutputも同じ」という再現性のため、Bazelはinputの明示を重視します。
通常inputというとソースコードが浮かびますが、それ以外にも
- 依存ライブラリ(内部・外部)
- 環境変数
- コンパイル環境(言語、ツール)
- 実行ホストの環境
といった様々な要素があります。
大抵Bazelのビルドでコケるのはこのinput(特に依存ライブラリの関係)が不足しているケースが多いです。
環境
- Bazel v4.2.2
Hands on
それではBazel でGoのビルドをしてみましょう。
事前環境
あらかじめ以下のGoの環境を用意しておきます。
. ├── cmd │ └── main.go ├── go.mod ├── go.sum └── uuid └── uuid.go
cmd/main.go
依存ライブラリ(uuid)から生成されたIDを表示するだけのプログラムです。
package main import ( "log" "github.com/jun06t/bazel-sample/basic/uuid" ) func main() { id, err := uuid.Generate() if err != nil { log.Fatal(err) } log.Println(id) }
uuid/uuid.go
このライブラリ自身は google/uuid に依存しています。
package uuid import ( "github.com/google/uuid" ) func Generate() (string, error) { u, err := uuid.NewUUID() if err != nil { return "", err } return u.String(), nil }
Bazel環境の準備
次にBazelの環境を整えます。
GoでBazelを使う際はgazelleという非常に便利なコードジェネレーターが用意されています。gazelle自体もBazel内で取得して実行します。
まずはWORKSPACE
とBUILD.bazel
という2つのファイルを用意します。
. ├── BUILD.bazel <- ├── WORKSPACE <- ├── cmd │ └── main.go ├── go.mod ├── go.sum └── uuid └── uuid.go
WORKSPACEの設定
以下の内容のWORKSPACE
ファイルを置きます。中身はgazelleのsetupの通りです。
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "io_bazel_rules_go", sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", ], ) http_archive( name = "bazel_gazelle", sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", ], ) load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") ############################################################ # Define your own dependencies here using go_repository. # Else, dependencies declared by rules_go/gazelle will be used. # The first declaration of an external repository "wins". ############################################################ go_rules_dependencies() go_register_toolchains(version = "1.17.2") gazelle_dependencies()
バージョンなどは定期的に更新されるでしょうが、基本的な流れとしては
load("<リポジトリ><パッケージ>:<ターゲット>", "importしたい関数(ルール)")
のフォーマットで使用したい関数(ルール)をimportします。
Bazelではいくつか標準関数が提供されており、その中でも特にhttp_archiveを使うことが多いでしょう。
WORKSPACE
ではgoやgazelleのリポジトリから新たな関数をimportし、それを呼び出しています。
ルートのBUILD.bazelの設定
以下の内容のBUILD.bazel
を置きます。
load("@bazel_gazelle//:def.bzl", "gazelle") # gazelle:prefix github.com/jun06t/bazel-sample/basic gazelle(name = "gazelle")
# gazelle:prefix
のところはディレクティブで、コメントでなく意味を持ちます。
ここではリポジトリのディレクトリルートを指定するようにしてください。そうすることで依存関係のpathが適切に記述されるようになります。
gazelleでビルドファイルの生成
2つのファイルが用意できたら以下のコマンドを実行します。
$ bazel run //:gazelle
すると各ディレクトリにBUILD.bazel
が生成されます。
. ├── BUILD.bazel ├── WORKSPACE ├── cmd │ ├── BUILD.bazel │ └── main.go ├── go.mod ├── go.sum └── uuid ├── BUILD.bazel └── uuid.go
cmd/BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") go_library( name = "cmd_lib", srcs = ["main.go"], importpath = "github.com/jun06t/bazel-sample/basic/cmd", visibility = ["//visibility:private"], deps = ["//uuid"], ) go_binary( name = "cmd", embed = [":cmd_lib"], visibility = ["//visibility:public"], pure = "on", )
go_library
go_libraryは1つのパッケージをビルドする関数です。
deps
には依存するライブラリを指定します。
ライブラリのpathはgithub.com/jun06t/bazel-sample/basic/uuid
ですが、先程の# gazelle:prefix
によりprefixが解決されて//uuid
とだけで済みます。
name
にcmd_lib
として定義しており、このcmdパッケージライブラリを使いたい場合はこれを指定します。
go_binary
go_binaryは実行ファイルを生成する関数です。
embed
に先程のcmd_lib
を指定しています。
コロンが付いている理由はBazelのラベルの概念で後述します。
pure = "on"
はcgoを無効にする設定です(CGO_ENABLED=0
)。
cgoを使わないGoのクロスコンパイル時に -installsuffix cgo が不要になってた - Carpe Diem
で書いたように意図せずCGO_ENABLED=1
になることもあるので、cgoを使わないのであれば基本付けるようにするのが良いでしょう。
uuid/BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "uuid", srcs = ["uuid.go"], importpath = "github.com/jun06t/bazel-sample/basic/uuid", visibility = ["//visibility:public"], deps = ["@com_github_google_uuid//:go_default_library"], )
今度はdeps
に外部ライブラリ github.com/google/uuid
が書かれています。
最初に述べたようにBazelは依存関係を明示的に書く必要がありますが、これまで用意されたファイルには書かれていません。
なので実行ファイルを作ろうとするとエラーになります。
$ bazel build //cmd ERROR: /Users/jun06t/.go/src/github.com/jun06t/bazel-sample/basic/uuid/BUILD.bazel:3:11: no such package '@com_github_google_uuid//': The repository '@com_github_google_uuid' could not be resolved and referenced by '//uuid:uuid' ERROR: Analysis of target '//cmd:cmd' failed; build aborted: no such package '@com_github_google_uuid//': The repository '@com_github_google_uuid' could not be resolved INFO: Elapsed time: 0.303s INFO: 0 processes. FAILED: Build did NOT complete successfully (2 packages loaded, 63 targets configured)
そこでWORKSPACE
の方でgo_repository()
という関数で依存ライブラリのことを書く必要があるのですが、それもgazelleが自動生成してくれます。
以下のコマンドを実行します。
$ bazel run //:gazelle -- update-repos -from_file=go.mod
するとWORKSPACE
に以下が追記されます。
go_repository( name = "com_github_google_uuid", importpath = "github.com/google/uuid", sum = "h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=", version = "v1.3.0", )
これでuuid
も依存ファイルを知ることができました。
再度ビルドしてみます。
$ bazel build //cmd INFO: Analyzed target //cmd:cmd (4 packages loaded, 173 targets configured). INFO: Found 1 target... Target //cmd:cmd up-to-date: bazel-bin/cmd/cmd_/cmd INFO: Elapsed time: 7.375s, Critical Path: 1.07s INFO: 7 processes: 3 internal, 4 darwin-sandbox. INFO: Build completed successfully, 7 total actions
今度はビルドできました。
実行してみます。
$ bazel run //cmd INFO: Analyzed target //cmd:cmd (0 packages loaded, 0 targets configured). INFO: Found 1 target... Target //cmd:cmd up-to-date: bazel-bin/cmd/cmd_/cmd INFO: Elapsed time: 0.224s, Critical Path: 0.02s INFO: 1 process: 1 internal. INFO: Build completed successfully, 1 total action INFO: Build completed successfully, 1 total action 2021/12/05 20:53:40 a8006314-542f-11ec-86da-acde48001122
ちゃんとUUIDが表示されています。
以上でBazelによる基本的なGoのビルドができました。
その他
サンプルコード
今回のサンプルコードはこちら
Q&A
気になったことをいくつか。
依存ライブラリを別ファイルで管理したい
通常だとWORKSPACE
に依存ライブラリが記述されますが、依存ライブラリだけ切り出して管理した場合は
$ bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=deps.bzl%go_dependencies
と実行するとdeps.bzl
ファイルを作ってくれます。WORKSPACE
にもそれを参照するような記述がされるので、
load("//:deps.bzl", "go_dependencies") # gazelle:repository_macro deps.bzl%go_dependencies go_dependencies()
以降はdeps.bzl
で管理されます。
ビルドするとsymlinkが生成されてる
bazel build //cmd
を実行すると以下のように多数のsymlinkが生成されていることに気づきます。
. ├── BUILD.bazel ├── WORKSPACE ├── bazel-basic -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__ ├── bazel-bin -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out/darwin-fastbuild/bin ├── bazel-out -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out ├── bazel-testlogs -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out/darwin-fastbuild/testlogs ├── cmd │ ├── BUILD.bazel │ └── main.go ├── go.mod ├── go.sum └── uuid ├── BUILD.bazel └── uuid.go
これらはsandbox環境であり、以降ローカルのキャッシュとして使われます。
Bazelによるビルドの成果物はこのsandboxに置かれるため、GoのバイナリやProtobufの.pb.go
コードはリポジトリルートには生成されません。
ワークスペース、リポジトリ、パッケージ、ターゲット
Bazelにはソースツリーの概念としてワークスペース、リポジトリ、パッケージ、ターゲットというものがあります。
これらの概念を元に使いたい関数(ルール)の指定方法(=ラベル)が決まります。
用語 | 役割 | ラベル例 |
---|---|---|
ワークスペース | ビルド全体を管理するディレクトリ。WORKSPACE ファイルが置かれているディレクトリをルートとする。サブディレクトリに別の WORKSPACE ファイルが存在すると、別のワークスペースを形成するため無視される。 |
-- |
リポジトリ | コードを管理する場所。WORKSPACE ファイルを含むディレクトリは@ とも呼ばれるメイン・リポジトリのルートとなる。外部リポジトリは、ワークスペースルールを使用して WORKSPACE ファイルで定義する。 |
@myrepo// |
パッケージ | 関連するファイルの集合体であり、ファイル間の依存関係を示すもの。BUILD またはBUILD.bazel という名前のファイルを含むディレクトリとして定義される。 |
@myrepo//my/app/cmd |
ターゲット | パッケージ内に用意されているファイルやルール(関数)。BUILD ファイル内のルールの宣言の name 属性の値。パッケージの後にコロンを付けてターゲットを記述。 |
@myrepo//my/app/cmd:app_binary |
ラベルの省略形
リポジトリに関して、ラベルが同じリポジトリを参照するケースでは、リポジトリの識別子は//
と略されます。
cmd/BUILD.bazel
でdeps = ["//uuid"]
となっていたのもそういった理由からです。
またパッケージパスの最後の要素とターゲット名が同じ場合は省略することができます。
なので今回の
$ bazel build //cmd
は、正確に書くと
$ bazel build @//cmd:cmd
となります。
どこでもビルドできる
Bazelはこのようにソースツリーの概念がはっきり決まっているので、どのディレクトリからでも上記のラベルを用いればビルドできます。
今回だとcmd
、uuid
のディレクトリ内でも
$ bazel build //cmd
とすればビルド&実行できます。
まとめ
Bazelの基本的な使い方を学ぶため、Goのビルドをベースにハンズオン形式で確認してみました。