Bazel チュートリアル: Go プロジェクトのビルド

このチュートリアルでは、Go(Golang)プロジェクトをビルドする方法を説明しながら、Bazel の基本を紹介します。ワークスペースの設定、小さなプログラムのビルド、ライブラリのインポート、テストの実行方法について学習します。また、ターゲットや BUILD ファイルなど、Bazel の重要なコンセプトについても説明します。

推定完了時間: 30 分

始める前に

Bazel をインストールする

始める前に、Bazel をインストールします(まだインストールしていない場合)。

Bazel がインストールされているかどうかは、任意のディレクトリで bazel version を実行して確認できます。

Go をインストールする(省略可)

Bazel で Go プロジェクトをビルドするために Go をインストールする必要はありません 。Bazel Go ルールセットは、マシンにインストールされているツールチェーンを使用する代わりに、Go ツールチェーンを自動的にダウンロードして使用します。これにより、プロジェクトのすべてのデベロッパーが同じバージョンの Go でビルドできます。

ただし、go getgo 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 つのサブディレクトリ(stage1stage2stage3)があり、それぞれこのチュートリアルの異なるセクションに対応しています。各ステージは前のステージを基に構築されています。

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 ターゲットが 1 つあります。

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "hello",
    srcs = ["hello.go"],
)

MODULE.bazel は、プロジェクトの依存関係を追跡します。また、プロジェクトのルート ディレクトリをマークするため、プロジェクトごとに MODULE.bazel ファイルを 1 つだけ作成します。Go の go.mod ファイルと同様の役割を果たします。Bazel プロジェクトでは go.mod ファイルは必要ありませんが、依存関係の管理に go getgo 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 ファイルをソース管理に commit して、プロジェクトのすべてのユーザーが各依存関係の同じバージョンを取得できるようにする必要があります。MODULE.bazel.lock を手動で編集する必要はありません。

BUILD ファイルについて

Bazel の操作のほとんどは BUILD ファイル(または BUILD.bazel ファイル)を介して行われるため、これらのファイルの役割を理解することが重要です。

BUILD ファイルは、Starlark というスクリプト言語で記述されます。これは Python の限定的なサブセットです。

A 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 ディレクトリには独自の BUILD ファイルがあり、このパッケージをビルドする方法を Bazel に伝えます。ここでは 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"],
)

新しい fortune_test ターゲットがあります。これは go_test ルールを使用してテスト実行可能ファイルをコンパイルしてリンクします。go_testfortune.gofortune_test.go を同じコマンドでコンパイルする必要があるため、ここでは embed 属性を使用して fortune ターゲットの属性を fortune_test に組み込みます。embedgo_testgo_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++JavaAndroid、および iOS のチュートリアルをご覧ください。
  • 他の言語の推奨ルールのリストも確認できます。
  • Go の詳細については、 rules_go モジュール(特に Core Go rules のドキュメント)をご覧ください。
  • プロジェクト外の Bazel モジュールの操作の詳細については、 外部依存関係をご覧ください。特に、Bazel のモジュール システムを介して Go モジュールとツールチェーンに依存する方法については、bzlmod を使用した Goをご覧ください。