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

問題を報告する ソースを表示 Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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