このチュートリアルでは、Go(Golang)プロジェクトをビルドする方法を示して、Bazel の基本について説明します。ワークスペースの設定、小さなプログラムの作成、ライブラリのインポート、テストの実行方法を学びます。この過程で、ターゲットや BUILD
ファイルなどの Bazel の重要なコンセプトを学びます。
所要時間: 30 分
始める前に
Bazel をインストールする
始める前に、まだインストールしていない場合は、まず bazel をインストールします。
Bazel がインストールされているかどうかを確認するには、任意のディレクトリで bazel version
を実行します。
Go をインストールする(省略可)
Bazel で Go プロジェクトをビルドするために Go をインストールする必要はありません。Bazel Go ルールセットは、マシンにインストールされている toolchain を使用する代わりに、Go toolchain を自動的にダウンロードして使用します。これにより、プロジェクトのすべてのデベロッパーが同じバージョンの Go でビルドできます。
ただし、go
get
や go mod tidy
などのコマンドを実行するために Go ツールチェーンをインストールしたい場合もあります。
任意のディレクトリで go version
を実行すると、Go がインストールされているかどうかを確認できます。
サンプル プロジェクトを取得する
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
からプログラムをビルドする単一の 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 に対する単一の依存関係が含まれています。Bazel には Go の組み込みサポートがないため、この依存関係が必要です。
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
最後に、MODULE.bazel.lock
は Bazel によって生成されたファイルで、依存関係に関するハッシュとその他のメタデータが含まれています。Bazel 自体によって追加される暗黙的な依存関係が含まれているため、かなり長くなるため、ここでは説明しません。go.sum
と同様に、プロジェクトの全員が各依存関係の同じバージョンを取得できるように、MODULE.bazel.lock
ファイルをソース管理に commit する必要があります。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 がターゲットまたはファイルを識別するために使用する文字列です。ラベルは、コマンドライン引数と BUILD
ファイル属性(deps
など)で使用されます。//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 ルールのドキュメントをご覧ください。
- プロジェクト外の Bazel モジュールの使用方法については、外部依存関係をご覧ください。特に、Bazel のモジュール システムを介して Go モジュールとツールチェーンに依存する方法については、bzlmod を使用した Go をご覧ください。