Carpe Diem

備忘録

Bazelを使ってみる その1(Goのビルド)

背景

Bazelは優れたビルドツールである一方で、導入したチームには1人はBazel職人が必要と言われるほどキャッチアップコストが高くハマったときに開発が止まると言われます。
そのためKubernetesからも削除されるほどです。

しかしながら導入の善し悪しを判断する上で最低限の知識は必要なので、一通りのことはできるようにとキャッチアップしてみます。

ちなみに発音は公式サイトでは「ベイゼル」とのこと。

How do you pronounce “Bazel”?

The same way as “basil” (the herb) in US English: “BAY-zel”. It rhymes with “hazel”. IPA: /ˈbeɪzˌəl/

ref: FAQ  |  Bazel

Bazelの特徴

Bazelの特徴として以下があります。

  • ビルドの再現性が保たれる
  • サンドボックス環境で実行される(Bazel1つだけあればビルドできる)
  • キャッシュの活用(ローカル・リモート)によるビルド時間の高速化

特に1つ目の特徴である「inputが同じであればoutputも同じ」という再現性のため、Bazelはinputの明示を重視します

通常inputというとソースコードが浮かびますが、それ以外にも

といった様々な要素があります。

大抵Bazelのビルドでコケるのはこのinput(特に依存ライブラリの関係)が不足しているケースが多いです。

環境

  • Bazel v4.2.2

Hands on

それではBazel でGoのビルドをしてみましょう。

事前環境

あらかじめ以下のGoの環境を用意しておきます。

.
├── cmd
│   └── main.go
├── go.mod
├── go.sum
└── uuid
    └── uuid.go

cmd/main.go

依存ライブラリ(uuid)から生成されたIDを表示するだけのプログラムです。

package main

import (
    "log"

    "github.com/jun06t/bazel-sample/basic/uuid"
)

func main() {
    id, err := uuid.Generate()
    if err != nil {
        log.Fatal(err)
    }
    log.Println(id)
}

uuid/uuid.go

このライブラリ自身は google/uuid に依存しています。

package uuid

import (
    "github.com/google/uuid"
)

func Generate() (string, error) {
    u, err := uuid.NewUUID()
    if err != nil {
        return "", err
    }
    return u.String(), nil
}

Bazel環境の準備

次にBazelの環境を整えます。

GoでBazelを使う際はgazelleという非常に便利なコードジェネレーターが用意されています。gazelle自体もBazel内で取得して実行します。

まずはWORKSPACEBUILD.bazelという2つのファイルを用意します。

.
├── BUILD.bazel  <-
├── WORKSPACE  <-
├── cmd
│   └── main.go
├── go.mod
├── go.sum
└── uuid
    └── uuid.go

WORKSPACEの設定

以下の内容のWORKSPACEファイルを置きます。中身はgazelleのsetupの通りです。

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "io_bazel_rules_go",
    sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip",
        "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip",
    ],
)

http_archive(
    name = "bazel_gazelle",
    sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
    ],
)

load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")

############################################################
# Define your own dependencies here using go_repository.
# Else, dependencies declared by rules_go/gazelle will be used.
# The first declaration of an external repository "wins".
############################################################

go_rules_dependencies()

go_register_toolchains(version = "1.17.2")

gazelle_dependencies()

バージョンなどは定期的に更新されるでしょうが、基本的な流れとしては

load("<リポジトリ><パッケージ>:<ターゲット>", "importしたい関数(ルール)")

のフォーマットで使用したい関数(ルール)をimportします。

Bazelではいくつか標準関数が提供されており、その中でも特にhttp_archiveを使うことが多いでしょう。

WORKSPACEではgoやgazelleのリポジトリから新たな関数をimportし、それを呼び出しています。

ルートのBUILD.bazelの設定

以下の内容のBUILD.bazelを置きます。

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/jun06t/bazel-sample/basic
gazelle(name = "gazelle")

# gazelle:prefixのところはディレクティブで、コメントでなく意味を持ちます。

ここではリポジトリディレクトリルートを指定するようにしてください。そうすることで依存関係のpathが適切に記述されるようになります。

gazelleでビルドファイルの生成

2つのファイルが用意できたら以下のコマンドを実行します。

$ bazel run //:gazelle

すると各ディレクトリにBUILD.bazelが生成されます。

.
├── BUILD.bazel
├── WORKSPACE
├── cmd
│   ├── BUILD.bazel
│   └── main.go
├── go.mod
├── go.sum
└── uuid
    ├── BUILD.bazel
    └── uuid.go

cmd/BUILD.bazel

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

go_library(
    name = "cmd_lib",
    srcs = ["main.go"],
    importpath = "github.com/jun06t/bazel-sample/basic/cmd",
    visibility = ["//visibility:private"],
    deps = ["//uuid"],
)

go_binary(
    name = "cmd",
    embed = [":cmd_lib"],
    visibility = ["//visibility:public"],
    pure = "on",
)

go_library

go_libraryは1つのパッケージをビルドする関数です。
depsには依存するライブラリを指定します。

ライブラリのpathはgithub.com/jun06t/bazel-sample/basic/uuidですが、先程の# gazelle:prefixによりprefixが解決されて//uuidとだけで済みます。

namecmd_libとして定義しており、このcmdパッケージライブラリを使いたい場合はこれを指定します。

go_binary

go_binaryは実行ファイルを生成する関数です。

embedに先程のcmd_libを指定しています。
コロンが付いている理由はBazelのラベルの概念で後述します。

pure = "on"はcgoを無効にする設定です(CGO_ENABLED=0)。

cgoを使わないGoのクロスコンパイル時に -installsuffix cgo が不要になってた - Carpe Diem

で書いたように意図せずCGO_ENABLED=1になることもあるので、cgoを使わないのであれば基本付けるようにするのが良いでしょう。

uuid/BUILD.bazel

load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
    name = "uuid",
    srcs = ["uuid.go"],
    importpath = "github.com/jun06t/bazel-sample/basic/uuid",
    visibility = ["//visibility:public"],
    deps = ["@com_github_google_uuid//:go_default_library"],
)

今度はdepsに外部ライブラリ github.com/google/uuid が書かれています。

最初に述べたようにBazelは依存関係を明示的に書く必要がありますが、これまで用意されたファイルには書かれていません。
なので実行ファイルを作ろうとするとエラーになります。

$ bazel build //cmd
ERROR: /Users/jun06t/.go/src/github.com/jun06t/bazel-sample/basic/uuid/BUILD.bazel:3:11: no such package '@com_github_google_uuid//': The repository '@com_github_google_uuid' could not be resolved and referenced by '//uuid:uuid'
ERROR: Analysis of target '//cmd:cmd' failed; build aborted: no such package '@com_github_google_uuid//': The repository '@com_github_google_uuid' could not be resolved
INFO: Elapsed time: 0.303s
INFO: 0 processes.
FAILED: Build did NOT complete successfully (2 packages loaded, 63 targets configured)

そこでWORKSPACEの方でgo_repository()という関数で依存ライブラリのことを書く必要があるのですが、それもgazelleが自動生成してくれます

以下のコマンドを実行します。

$ bazel run //:gazelle -- update-repos -from_file=go.mod

するとWORKSPACEに以下が追記されます。

go_repository(
    name = "com_github_google_uuid",
    importpath = "github.com/google/uuid",
    sum = "h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=",
    version = "v1.3.0",
)

これでuuidも依存ファイルを知ることができました。

再度ビルドしてみます。

$ bazel build //cmd
INFO: Analyzed target //cmd:cmd (4 packages loaded, 173 targets configured).
INFO: Found 1 target...
Target //cmd:cmd up-to-date:
  bazel-bin/cmd/cmd_/cmd
INFO: Elapsed time: 7.375s, Critical Path: 1.07s
INFO: 7 processes: 3 internal, 4 darwin-sandbox.
INFO: Build completed successfully, 7 total actions

今度はビルドできました。

実行してみます。

$ bazel run //cmd
INFO: Analyzed target //cmd:cmd (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd:cmd up-to-date:
  bazel-bin/cmd/cmd_/cmd
INFO: Elapsed time: 0.224s, Critical Path: 0.02s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
2021/12/05 20:53:40 a8006314-542f-11ec-86da-acde48001122

ちゃんとUUIDが表示されています。

以上でBazelによる基本的なGoのビルドができました。

サンプルコード

今回のサンプルコードはこちら

github.com

その他

気になったことをいくつか。

ビルドするとsymlinkが生成されてる

bazel build //cmdを実行すると以下のように多数のsymlinkが生成されていることに気づきます。

.
├── BUILD.bazel
├── WORKSPACE
├── bazel-basic -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__
├── bazel-bin -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out/darwin-fastbuild/bin
├── bazel-out -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out
├── bazel-testlogs -> /private/var/tmp/_bazel_jun06t/f2a2837a436f484c86936a6d6346778c/execroot/__main__/bazel-out/darwin-fastbuild/testlogs
├── cmd
│   ├── BUILD.bazel
│   └── main.go
├── go.mod
├── go.sum
└── uuid
    ├── BUILD.bazel
    └── uuid.go

これらはsandbox環境であり、以降ローカルのキャッシュとして使われます。
Bazelによるビルドの成果物はこのsandboxに置かれるため、GoのバイナリやProtobufの.pb.goコードはリポジトリルートには生成されません

ワークスペースリポジトリ、パッケージ、ターゲット

Bazelにはソースツリーの概念としてワークスペース、リポジトリ、パッケージ、ターゲットというものがあります。
これらの概念を元に使いたい関数(ルール)の指定方法(=ラベル)が決まります。

用語 役割 ラベル例
ワークスペース ビルド全体を管理するディレクトリ。
WORKSPACEファイルが置かれているディレクトリをルートとする。
サブディレクトリに別のWORKSPACEファイルが存在すると、別のワークスペースを形成するため無視される。
--
リポジトリ コードを管理する場所。
WORKSPACEファイルを含むディレクトリは@とも呼ばれるメイン・リポジトリのルートとなる。
外部リポジトリは、ワークスペースルールを使用してWORKSPACEファイルで定義する。
@myrepo//
パッケージ 関連するファイルの集合体であり、ファイル間の依存関係を示すもの。
BUILDまたはBUILD.bazelという名前のファイルを含むディレクトリとして定義される。
@myrepo//my/app/cmd
ターゲット パッケージ内に用意されているファイルやルール(関数)。
BUILDファイル内のルールの宣言の name 属性の値。
パッケージの後にコロンを付けてターゲットを記述。
@myrepo//my/app/cmd:app_binary

ラベルの省略形

リポジトリに関して、ラベルが同じリポジトリを参照するケースでは、リポジトリの識別子は//と略されます。
cmd/BUILD.bazeldeps = ["//uuid"]となっていたのもそういった理由からです。

またパッケージパスの最後の要素とターゲット名が同じ場合は省略することができます。

なので今回の

$ bazel build //cmd

は、正確に書くと

$ bazel build @//cmd:cmd

となります。

どこでもビルドできる

Bazelはこのようにソースツリーの概念がはっきり決まっているので、どのディレクトリからでも上記のラベルを用いればビルドできます
今回だとcmduuidディレクトリ内でも

$ bazel build //cmd

とすればビルド&実行できます。

まとめ

Bazelの基本的な使い方を学ぶため、Goのビルドをベースにハンズオン形式で確認してみました。

参考