Carpe Diem

備忘録

Bazel を使う上での Tips

概要

Bazelを導入する過程で学んだTipsをまとめます。

環境

  • Bazel v4.2.2

Tips

bazeliskを使う

Bazelのバージョンを環境別に管理したい場合、bazelbuild/bazeliskを使うのが良いです。

.bazelversionに以下のようなバージョンを書いておけばそのバージョンでビルドしてくれます。

4.2.2

インストールはhomebrewでできます。

$ brew install bazelisk

macOSであればbazelエイリアスが勝手につくので、bazelコマンドで中身はbazeliskが実行されます。

bash completion

1点課題なのがbash completion(Tab補完)の設定が大変な点です。
通常のBazelであれば

Command-line completion - Bazel main

の通りにすればいいですが、bazeliskはbazel自体をビルドしてbash completeファイルを生成する必要があります。

$ git clone https://github.com/bazelbuild/bazel
$ bazel build //scripts:bazel-complete.bash

このビルドがめちゃくちゃ長いです。

参考

.bazelrcの活用

bazelで毎回多くのオプションを指定しなくて済むよう、常にセットしたいオプションは.bazelrcに書くようにします。

過去の解説でもちょこちょこ使っていますね。

継承という概念

Bazelを使ってみる その5(リモートキャッシュ) - Carpe Diem

でも軽く説明しましたが、以下のコマンドはすべてbuildを継承しているので、別で追記する必要はありません。

  • test
  • run
  • clean
  • mobile-install
  • info
  • print_action
  • config
  • cquery
  • aquery

なので.bazelrc

build --show_timestamps

と書けば、

bazel buildだけでなくbazel runbazel testの際もtimestampが表示されるようになります。

coverageについてはtestを継承しています。

CIのときだけ実行したい

例えば

  • CIの時だけremote cacheを使いたい
  • CIはLinuxなのでplatformパラメータを指定したい

といったケースがあると思います。

その場合は

# for Local
build --show_timestamps --verbose_failures
test --test_output=errors --test_verbose_timeout_warnings

# for CI
build:ci --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64
build:ci --remote_cache=https://storage.googleapis.com/xxx --google_credentials=xxx.json

のように:の後に任意の文字列を付けておきます。今回だと:ciですね。

そして実行時にciの文字列をconfigフラグで指定すると、:ciが付いたオプションのみ適用してくれます。

$ bazel build --config=ci //...

参考

Stamping(ビルド成果物への埋め込み)

過去の記事でも書いたことがありますが、Goはldflagsを使うことでビルド時に変数に値を埋め込む事が可能です。

christina04.hatenablog.com

一般にこれをStampingと言い、Bazelでもその仕組みが用意されています。

例えば

.
├── BUILD.bazel
├── Makefile
├── WORKSPACE
├── alive
│   ├── BUILD.bazel
│   └── alive.go
├── cmd
│   ├── BUILD.bazel
│   └── main.go
└── go.mod

という構成でalive packageに以下の変数を用意していた場合、

package alive

var (
    BuildVersion = "none"
    Timestamp    = "none"
)

BUILD.bazelの設定

そのalive packageのBUILD.bazelx_defsを設定するか、

go_library(
    name = "alive",
    srcs = ["alive.go"],
    importpath = "github.com/jun06t/bazel-sample/ldflags/alive",
    visibility = ["//visibility:public"],
    x_defs = {
        "BuildVersion": "{GIT_COMMIT}",
        "Timestamp": "{DATE}",
    },
)

もしくはcmd/BUILD.bazelの方でpathをきちんと付けてx_defsを設定します。

go_binary(
    name = "cmd",
    embed = [":cmd_lib"],
    pure = "on",
    visibility = ["//visibility:public"],
    x_defs = {
        "github.com/jun06t/bazel-sample/ldflags/alive.BuildVersion": "{GIT_COMMIT}",
        "github.com/jun06t/bazel-sample/ldflags/alive.Timestamp": "{DATE}",
    },
)

どちらか片方でOKです。

もしかしたらcmd/main.goだけいじってalive/BUILD.bazelの方でx_defsを指定すると、timestamp変わらない?と思いましたがちゃんと新しい値で埋め込み直してくれました。

スクリプトの用意

次にスクリプトを用意します。

#!/bin/bash

echo GIT_COMMIT $(git rev-parse --verify HEAD)
echo DATE $(date '+%Y/%m/%d %H:%M:%S %Z')

実行して動作確認しておきます。

$ ./tag.sh
GIT_COMMIT 53be3f01ff58e5ed1c973f107f96020f1069ae13
DATE 2022/01/18 14:54:17 JST

ビルド方法

最後にビルド時に--stampフラグを付けます。

$ bazel build --stamp --workspace_status_command=$(pwd)/tag.sh

これで埋め込むことができました。

動作確認

$ bazel run --stamp --workspace_status_command=$(pwd)/tag.sh //cmd

APIを叩いてみます。

$ curl localhost:8080/alive
{"version":"53be3f01ff58e5ed1c973f107f96020f1069ae13","timestamp":"2022/01/18 14:55:02 JST"}

期待する値が埋め込まれていることが確認できました。

ちなみにtag.shの方を修正しても肝心のビルド対象が変更されていないので、キャッシュを使ってしまい期待する挙動を確認できないため都度bazel cleanした方がよいです。

サンプルコード

ldflagsのサンプルコードはこちら↓

github.com

ビルド対象をフィルタする

Bazelはキャッシュを活用して差分ビルドする特徴がありますが、例えば「goバイナリはビルドしたいけど、protobufは生成したくない」というように単純にビルド対象を絞りたいケースがあると思います。

ディレクトリ分けで頑張る方法もありますが、Bazelはbazel queryというコマンドが便利です。

例えばgo_binaryだけ抽出したい場合は以下のように書きます。

$ bazel query 'kind("go_binary", //...)'
//server:server
//client:client

queryで使える関数はkind, attr, labelsなど多数ありますが、詳細は以下を確認してください。

Query language - Bazel main

コミット差分のみ対象に

さらにコミットした差分から絞りたい場合はBazel公式のスクリプトを参考に以下のようなスクリプトを用意すると良いでしょう。

#!/usr/bin/env bash
set -u

COMMIT_RANGE=${COMMIT_RANGE:-$(git merge-base origin/main HEAD)".."}

files=()
for file in $(git diff --name-only ${COMMIT_RANGE} ); do
  files+=($(bazelisk query --keep_going --noshow_progress $file))
done

buildables=$(bazelisk query \
    --keep_going \
    "kind(${TARGET:-go_binary}, rdeps(//..., set(${files[*]})))")

echo ${buildables}

Makefileを以下のようにし

build:
    bazel build -- ${BUILDABLE}

実行時に指定します。

$ target=`TARGET=go_binary ./scripts/buildable.sh`
$ make build BUILDABLE="${target}"

参考

goarchなどを設定するとgo_binaryで出てこない?

go_binaryでクロスコンパイルのためのプロパティであるgoarchgoospureなどを入れているとbazel query 'kind(go_binary, //...)'では出てこなくなりました。

この場合は

$ bazel query 'attr(goarch, "amd64", //...)'

など別の関数で一覧化することができます。

依存解決できない時はvendorディレクトリも含めてしまう

gazelleはgo.modがあるライブラリについては適切に依存関係を解決してくれますが、

  • 古くてgo.modに対応していない
  • go.modのバージョン規定を満たしていない

といったライブラリについては依存関係がおかしくなるケースがあります。

その場合手動で1つ1つの依存関係を解決する必要があるのですが、うまく行かない時は中々抜け出せない沼にハマってしまいます。

そうなった場合は一度

$ go mod vendor

でvendorディレクトリを入れて、vendorディレクトリをrulesの依存関係に組み込んでしまうと良いです。

最終手段のようなものではありますが、こうすることでずっと解決できない依存関係と戦うよりは前に進めることができます。

go_repositoryのsumとかはどうやって出すの?

go_repositoryで手動で特定のバージョンを指定したい場合は

$ go mod download -json <module>@<version>

とするとsumなどが返ってきます。

$ go mod download -json go.uber.org/zap@v1.20.0
{
        "Path": "go.uber.org/zap",
        "Version": "v1.20.0",
        "Info": "/Users/jun06t/.go/pkg/mod/cache/download/go.uber.org/zap/@v/v1.20.0.info",
        "GoMod": "/Users/jun06t/.go/pkg/mod/cache/download/go.uber.org/zap/@v/v1.20.0.mod",
        "Zip": "/Users/jun06t/.go/pkg/mod/cache/download/go.uber.org/zap/@v/v1.20.0.zip",
        "Dir": "/Users/jun06t/.go/pkg/mod/go.uber.org/zap@v1.20.0",
        "Sum": "h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc=",
        "GoModSum": "h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw="
}

参考

まとめ

BazelのTipsをまとめてみました。
運用を続けてまた新たに見つけたら追記していこうと思います。