このチュートリアルでは、Go(Golang)プロジェクトをビルドする方法を説明することで、Bazel の基本を紹介します。ワークスペースの設定、小さなプログラムのビルド、ライブラリのインポート、テストの実行の方法を学びます。この過程で、ターゲットや BUILD
ファイルなどの Bazel の重要なコンセプトを学びます。
推定所要時間: 30 分
始める前に
Bazel をインストールする
始める前に、まだインストールしていない場合は、まず bazel をインストールします。
Bazel がインストールされているかどうかを確認するには、任意のディレクトリで bazel version
を実行します。
Go をインストールする(省略可)
Bazel で Go プロジェクトをビルドするために Go をインストールする必要はありません。Bazel Go ルールセットは、マシンにインストールされているツールチェーンを使用する代わりに、Go ツールチェーンを自動的にダウンロードして使用します。これにより、プロジェクトのすべてのデベロッパーが同じバージョンの Go でビルドできるようになります。
ただし、go
get
や go mod tidy
などのコマンドを実行するために、Go ツールチェーンをインストールする必要がある場合があります。
Go がインストールされているかどうかを確認するには、任意のディレクトリで go version
を実行します。
サンプル プロジェクトを取得する
Bazel の例は Git リポジトリに保存されているため、まだ Git をインストールしていない場合は、インストールする必要があります。サンプル リポジトリをダウンロードするには、次のコマンドを実行します。
git clone https://github.com/bazelbuild/examples
このチュートリアルのサンプル プロジェクトは examples/go-tutorial
ディレクトリにあります。内容を確認します。
go-tutorial/
└── stage1
└── stage2
└── stage3
3 つのサブディレクトリ(stage1
、stage2
、stage3
)があり、それぞれこのチュートリアルの異なるセクションに対応しています。各ステージは前のステージを基盤としています。
Bazel を使用したビルド
stage1
ディレクトリから始めます。ここにプログラムがあります。bazel build
でビルドして実行できます。
$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
$ bazel-bin/hello_/hello
Hello, Bazel! 💚
また、1 つの bazel run
コマンドでプログラムをビルドして実行することもできます。
$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚
プロジェクト構造を理解する
作成したプロジェクトを見てみましょう。
hello.go
には、プログラムの Go ソースコードが含まれています。
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
には、ビルドする内容を Bazel に伝えるための指示が含まれています。通常、このようなファイルは各ディレクトリに書き込みます。このプロジェクトでは、hello.go
からプログラムをビルドする 1 つの go_binary
ターゲットがあります。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel
はプロジェクトの依存関係を追跡します。また、プロジェクトのルート ディレクトリもマークされるため、プロジェクトごとに 1 つの MODULE.bazel
ファイルのみが書き込まれます。これは Go の go.mod
ファイルと同様の役割を果たします。Bazel プロジェクトに go.mod
ファイルは必要ありませんが、依存関係の管理に go get
と go mod tidy
を引き続き使用できるように、ファイルを用意しておくと便利です。Bazel Go ルールセットは go.mod
から依存関係をインポートできますが、これについては別のチュートリアルで説明します。
この MODULE.bazel
ファイルには、Go ルールセットである rules_go への依存関係が 1 つ含まれています。Bazel には Go の組み込みサポートがないため、この依存関係が必要です。
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
最後に、MODULE.bazel.lock
は Bazel によって生成されるファイルで、依存関係に関するハッシュやその他のメタデータが含まれています。これには Bazel 自体が追加した暗黙的な依存関係が含まれているため、かなり長くなります。ここでは表示しません。go.sum
と同様に、プロジェクトのすべてのユーザーが各依存関係の同じバージョンを取得できるように、MODULE.bazel.lock
ファイルをソース管理にコミットする必要があります。MODULE.bazel.lock
を手動で編集する必要はありません。
BUILD ファイルを理解する
Bazel の操作のほとんどは BUILD
ファイル(または同等の BUILD.bazel
ファイル)を介して行われるため、これらのファイルが何を行うかを理解することが重要です。
BUILD
ファイルは、Python の制限付きサブセットである Starlark と呼ばれるスクリプト言語で記述されます。
BUILD
ファイルには、ターゲットのリストが含まれます。ターゲットは、バイナリ、ライブラリ、テストなど、Bazel がビルドできるものです。
ターゲットは、ビルドする内容を記述する 属性のリストを使用してルール関数を呼び出します。この例には 2 つの属性があります。name
はコマンドラインのターゲットを識別し、srcs
はソースファイルパスのリストです(スラッシュ区切り、BUILD
ファイルを含むディレクトリを基準とする相対パス)。
ルールは、ターゲットのビルド方法を Bazel に指示します。この例では、go_binary
ルールを使用しました。各ルールは、一連の出力ファイルを生成するアクション(コマンド)を定義します。たとえば、go_binary
は、実行可能出力ファイルを生成する Go コンパイルとリンクのアクションを定義します。
Bazel には、Java や C++ などのいくつかの言語用の組み込みルールがあります。これらのドキュメントは、Build Encyclopedia で確認できます。他の多くの言語やツールのルールセットは、Bazel Central Registry(BCR)で確認できます。
ライブラリを追加する
stage2
ディレクトリに移動します。ここに、運勢を表示する新しいプログラムを作成します。このプログラムは、事前定義されたメッセージのリストから運勢を選択するライブラリとして、別の Go パッケージを使用します。
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go
は、ライブラリのソースファイルです。fortune
ライブラリは別の Go パッケージであるため、そのソースファイルは別のディレクトリにあります。Bazel では Go パッケージを個別のディレクトリに保持する必要はありませんが、Go エコシステムでは一般的な慣例であり、これに従うことで他の Go ツールとの互換性を維持できます。
package fortune
import "math/rand"
var fortunes = []string{
"Your build will complete quickly.",
"Your dependencies will be free of bugs.",
"Your tests will pass.",
}
func Get() string {
return fortunes[rand.Intn(len(fortunes))]
}
fortune
ディレクトリには、このパッケージのビルド方法を Bazel に指示する独自の BUILD
ファイルがあります。ここでは go_binary
ではなく go_library
を使用します。
また、importpath
属性を、ライブラリを他の Go ソースファイルにインポートできる文字列に設定する必要があります。この名前は、リポジトリ内のディレクトリと連結されたリポジトリパス(またはモジュールパス)にする必要があります。
最後に、visibility
属性を ["//visibility:public"]
に設定する必要があります。visibility
は任意のターゲットに設定できます。これにより、どの Bazel パッケージがこのターゲットに依存できるかが決まります。このライブラリを任意のターゲットが依存できるようにするため、特別な値 //visibility:public
を使用します。
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
visibility = ["//visibility:public"],
)
このライブラリは、次のコマンドでビルドできます。
$ bazel build //fortune
次に、print_fortune.go
がこのパッケージをどのように使用するかを確認します。
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
は、fortune
ライブラリの importpath
属性で宣言されたものと同じ文字列を使用してパッケージをインポートします。
この依存関係を Bazel にも宣言する必要があります。stage2
ディレクトリの BUILD
ファイルは次のとおりです。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
これは、次のコマンドで実行できます。
bazel run //:print_fortune
print_fortune
ターゲットには、依存する他のターゲットのリストである deps
属性があります。これには、fortune
ディレクトリ内の fortune
という名前のターゲットを参照するラベル文字列 "//fortune"
が含まれています。
Bazel では、すべてのターゲットが deps
などの属性を使用して依存関係を明示的に宣言する必要があります。依存関係はソースファイルでも指定されているため、この方法は面倒に思えるかもしれませんが、Bazel の明示性にはメリットがあります。Bazel は、ソースファイルを読み取らずに、コマンドを実行する前に、すべてのコマンド、入力、出力を含むアクション グラフを構築します。これにより、Bazel は言語固有の組み込みロジックなしで、アクションの結果をキャッシュに保存したり、リモート実行用にアクションを送信したりできます。
ラベルについて
ラベルは、ターゲットまたはファイルを識別するために Bazel が使用する文字列です。ラベルは、コマンドライン引数と deps
などの BUILD
ファイル属性で使用されます。//fortune
、//:print-fortune
、@rules_go//go:def.bzl
など、すでにいくつか見てきました。
ラベルは、リポジトリ名、パッケージ名、ターゲット(またはファイル)名の 3 つの部分で構成されます。
リポジトリ名は @
と //
の間に記述され、別の Bazel モジュールからターゲットを参照するために使用されます(歴史的な理由から、モジュールとリポジトリが同義語として使用されることがあります)。ラベル @rules_go//go:def.bzl
のリポジトリ名は rules_go
です。同じリポジトリ内のターゲットを参照する場合は、リポジトリ名を省略できます。
パッケージ名は //
と :
の間に記述され、別の Bazel パッケージからターゲットを参照するために使用されます。ラベル @rules_go//go:def.bzl
では、パッケージ名は go
です。Bazel のパッケージは、最上位ディレクトリの BUILD
ファイルまたは BUILD.bazel
ファイルで定義されたファイルとターゲットのセットです。パッケージ名は、モジュールのルート ディレクトリ(MODULE.bazel
を含む)から BUILD
ファイルを含むディレクトリまでのスラッシュ区切りのパスです。パッケージにはサブディレクトリを含めることができますが、独自のパッケージを定義する BUILD
ファイルが含まれていない場合に限ります。
ほとんどの Go プロジェクトでは、ディレクトリごとに 1 つの BUILD
ファイルがあり、BUILD
ファイルごとに 1 つの Go パッケージがあります。同じディレクトリ内のターゲットを参照する場合は、ラベルのパッケージ名を省略できます。
ターゲット名は :
の後に記述され、パッケージ内のターゲットを参照します。ターゲット名がパッケージ名の最後のコンポーネントと同じ場合(//a/b/c:c
が //a/b/c
と同じ、//fortune:fortune
が //fortune
と同じ場合)、ターゲット名は省略できます。
コマンドラインでは、...
をワイルドカードとして使用して、パッケージ内のすべてのターゲットを参照できます。これは、リポジトリ内のすべてのターゲットをビルドまたはテストする場合に便利です。
# Build everything
$ bazel build //...
プロジェクトをテストする
次に、stage3
ディレクトリに移動して、テストを追加します。
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go
は新しいテストソースファイルです。
package fortune
import (
"slices"
"testing"
)
// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
msg := Get()
if i := slices.Index(fortunes, msg); i < 0 {
t.Errorf("Get returned %q, not one the expected messages", msg)
}
}
このファイルはエクスポートされていない fortunes
変数を使用するため、fortune.go
と同じ Go パッケージにコンパイルする必要があります。BUILD
ファイルで、その仕組みを確認します。
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
visibility = ["//visibility:public"],
)
go_test(
name = "fortune_test",
srcs = ["fortune_test.go"],
embed = [":fortune"],
)
go_test
ルールを使用してテスト実行可能ファイルをコンパイルしてリンクする新しい fortune_test
ターゲットがあります。go_test
は fortune.go
と fortune_test.go
を同じコマンドでコンパイルする必要があるため、ここでは embed
属性を使用して fortune
ターゲットの属性を fortune_test
に組み込みます。embed
は go_test
および go_binary
とともに使用されるのが一般的ですが、go_library
とともに使用することもできます。これは、生成されたコードで役立つことがあります。
embed
属性が、実行可能ファイルにコピーされたデータファイルへのアクセスに使用される Go の embed
パッケージに関連しているかどうか疑問に思われるかもしれません。これは、ルール名が重複したためです。rules_go の embed
属性は、Go の embed
パッケージより前に導入されました。代わりに、rules_go は embedsrcs
を使用して、embed
パッケージで読み込むことができるファイルを一覧表示します。
bazel test
を使用してテストを実行してみます。
$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test PASSED in 0.3s
Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
...
ワイルドカードを使用すると、すべてのテストを実行できます。Bazel はテスト以外のターゲットもビルドするため、テストのないパッケージでもコンパイル エラーを検出できます。
$ bazel test //...
まとめとその他の参考資料
このチュートリアルでは、Bazel を使用して小さな Go プロジェクトをビルドしてテストし、その過程で Bazel の基本的なコンセプトを学びました。
- Bazel を使用して他のアプリケーションのビルドを開始するには、C++、Java、Android、iOS のチュートリアルをご覧ください。
- 他の言語の推奨ルールのリストも確認できます。
- Go の詳細については、rules_go モジュール(特に Core Go rules のドキュメント)をご覧ください。
- プロジェクト外の Bazel モジュールの操作について詳しくは、外部依存関係をご覧ください。特に、Bazel のモジュール システムを介して Go モジュールとツールチェーンに依存する方法については、bzlmod を使用した Go をご覧ください。