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

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

所要時間: 30 分

始める前に

Bazel をインストールする

始める前に、まだインストールしていない場合は、まず bazel をインストールします。

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

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

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

ただし、go getgo 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 つのサブディレクトリ(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 ターゲットがあります。

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 getgo 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.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++JavaAndroidiOS のチュートリアルをご覧ください。
  • 他の言語の推奨ルールのリストを確認することもできます。
  • Go の詳細については、rules_go モジュール、特に Core Go ルールのドキュメントをご覧ください。
  • プロジェクト外の Bazel モジュールの使用方法については、外部依存関係をご覧ください。特に、Bazel のモジュール システムを介して Go モジュールとツールチェーンに依存する方法については、bzlmod を使用した Go をご覧ください。