はじめに
この記事ではProtocol Buffersを使ったGoのプロジェクトをBazelでビルドする方法をハンズオン形式で紹介する.
書かれていること:
基本的にbazelbuild/bazel-gazelleに書かれていること以上の情報はないが,ステップ・バイ・ステップに進めていけるよう解説する.
書かれていないこと:
Protocol Buffers, Go, Bazelって何?
それぞれの解説は他のドキュメントに譲ります.
目標
gRPCのGo Quick Startで紹介されているexampleをBazelでbuildできるようにする.
前提
Go 1.6以上がインストールされていることをまず確認する.
$ go version
go version go1.11 darwin/amd64
Bazelを使うことで,依存ライブラリはもちろん,protocのようなツールまでも,設定ファイルで管理することができる.gRPCのインストールも,protocのインストールも不要.
また,ソースコードを $GOPATH/src
の下に配置する必要もない.そこで,適当なディレクトリーにgRPC Goのコードをcloneする.
$ git clone https://github.com/grpc/grpc-go.git
今回,Bazel化をしていく題材にするのは examples/helloworld
以下のファイル.その他のファイルは不要です.
$ cd grpc-go/examples/helloworld
このプロジェクトでは,すでに,helloworld.protoファイルから生成されたhelloworld.pb.goファイルが含まれているが,これもbazelでbuildできることを確認するため削除する.
$ rm helloworld/helloworld.pb.go
Bazel対応
WORKSPACE
プロジェクトのトップディレクトリーにWORKSPACEというファイルを作り,以下のように記述する.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# download go bazel tools
http_archive(
name = "io_bazel_rules_go",
urls = ["https://github.com/bazelbuild/rules_go/releases/download/0.15.1/rules_go-0.15.1.tar.gz"],
sha256 = "5f3b0304cdf0c505ec9e5b3c4fc4a87b5ca21b13d8ecc780c97df3d1809b9ce6",
)
# download the gazelle tool
http_archive(
name = "bazel_gazelle",
urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.14.0/bazel-gazelle-0.14.0.tar.gz"],
sha256 = "c0a5739d12c6d05b6c1ad56f2200cb0b57c5a70e03ebd2f7b87ce88cabf09c7b",
)
# load go rules
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
go_rules_dependencies()
go_register_toolchains()
# load gazelle
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
gazelle_dependencies()
# external dependencies
load("@bazel_gazelle//:deps.bzl", "go_repository")
go_repository(
name = "com_github_golang_protobuf",
importpath = "github.com/golang/protobuf",
tag = "v1.14.0",
)
この記事の時点のバージョンが指定されているので,BazelのGenerating build filesを参考に最新のバージョンを指定するとよい.
go_repository
によって依存ライブラリを記述する.godep
におけるvendor
の管理にあたる.ここではprotobuf
を使うことを記述している.go_repository
の詳しい解説はこちらに.
Gazelle実行
WORKSPACE
と同じくプロジェクトのトップディレクトリーにBUILD.bazel
ファイルを以下のように作成する.
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:prefix google.golang.org/grpc/examples/helloworld
gazelle(name = "gazelle")
そして,gazelle
を実行.
$ bazel run //:gazelle
必要なファイルがダウンロードされ,各パッケージディレクトリーにBUILD.bazelが作成される.はじめての実行時は依存ライブラリのダウンロードとビルドが実行されるため時間がかかります.
実行が終わると,protobufを定義しているhelloworld
パッケージのBUILD.bazel
が作成され,proto_library
, go_proto_library
, go_library
を用いたターゲット指定されていることが分かる.
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
proto_library(
name = "helloworld_proto",
srcs = ["helloworld.proto"],
visibility = ["//visibility:public"],
)
go_proto_library(
name = "helloworld_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
importpath = "google.golang.org/grpc/examples/helloworld/helloworld",
proto = ":helloworld_proto",
visibility = ["//visibility:public"],
)
go_library(
name = "go_default_library",
embed = [":helloworld_go_proto"],
importpath = "google.golang.org/grpc/examples/helloworld/helloworld",
visibility = ["//visibility:public"],
)
サーバーのパッケージであるgreeter_server
中のBUILD.bazel
には,go_library
とgo_binary
が作成される.
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "google.golang.org/grpc/examples/helloworld/greeter_server",
visibility = ["//visibility:private"],
deps = [
"//helloworld:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//reflection:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
go_binary(
name = "greeter_server",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
greeter_client
にも同様にBUILD.bazel
が作成される.
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "google.golang.org/grpc/examples/helloworld/greeter_client",
visibility = ["//visibility:private"],
deps = [
"//helloworld:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
go_binary(
name = "greeter_client",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
Build
buildの実行はターゲットを指定するだけ.
$ bazel build //greeter_server
$ bazel build //greeter_client
実行
bazel-bin以下に実行ファイルができている.
serverの起動
$ bazel run //greeter_server
clientの実行
$ bazel run //greeter_client
2018/09/03 17:10:21 Greeting: Hello world
Test
test mockに対するBUILD.bazel
も同様に作成される.
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["hw_mock.go"],
importpath = "google.golang.org/grpc/examples/helloworld/mock_helloworld",
visibility = ["//visibility:public"],
deps = [
"//helloworld:go_default_library",
"@com_github_golang_mock//gomock:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["hw_mock_test.go"],
embed = [":go_default_library"],
deps = [
"//helloworld:go_default_library",
"@com_github_golang_mock//gomock:go_default_library",
"@com_github_golang_protobuf//proto:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
testの実行は
$ bazel test
考察
あまりにマジカルですが,Goのビルドそのものがgo build
で実行できることを思うとむしろ手間がかかっているともいえます.メリットとしては$GOPATH
に全く依存しないため,環境依存でビルドに失敗するというトラブルを避けることができます.godepでも依存ライブラリを記述できますが,BazelではGoのバージョンですら指定することができます.
もちろん,Dockerの中でbuildすれば依存ライブラリやツールチェインのバージョン問題もないのですが,開発中の頻繁にビルドを繰り返す際にそれは効率が悪いでしょう.
さて,実際のところ注意すべき点はどこなのかを振り返ってみます.
BUILD.bazel
でのgazelle:prefix
プロジェクトトップのBUILD.bazel
で
# gazelle:prefix google.golang.org/grpc/examples/helloworld
という指定がありました.コメントの形式ですが,これはGazelleに対する有効なdirectiveであり,このプロジェクト自身のパスを指定します.これによってGazelleが生成するBUILD.bazel
中でのgo_library
, go_proto_library
のimportpath
のprefixが決まります.
...
go_proto_library(
name = "helloworld_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
importpath = "google.golang.org/grpc/examples/helloworld/helloworld",
proto = ":helloworld_proto",
visibility = ["//visibility:public"],
)
...
また,このprefixによって,プロジェクトに含まれるパッケージに対する依存関係は内部参照となります.
...
deps = [
"//helloworld:go_default_library",
...
このようにgreeter_{server,client}
がローカルのhelloworldパッケージをリンクするというBUILD.bazel
が生成されます.もしこれが @org...
のような外部参照になっていた場合は正しくprefixが設定されていないということです.
*.goでのimportの指定
上記のようにBUILD.bazel
に依存関係が設定されたら,実際のコードからhelloworld.proto
を利用できます.
greeter_{server,client}/main.go
で
pb "google.golang.org/grpc/examples/helloworld/helloworld"
とimportしています.
google.golang.org/grpc/examples/helloworld
というprefixを自分のプロジェクトパスに変更してみるとより理解が深まるかもしれません.
まとめ
Bazelを用いて,Go + Protocol Buffersを利用して作られたgRPCのサーバーおよびクライアントをビルドし実行できました.
上記の手順の結果をGithubにあげておきます。
stn/grpc-helloworld-bazel
開発者の環境に依存しないビルド環境を提供するBazelは「指定された手順通りやったけどビルドできない」「何もしていないのにいつの間にか壊れた」といったトラブル(例えばこんなissues)を避けることができます.
編集履歴
2018/09/05 実行は bazel run
でよかった.
2018/10/13 Githubに結果をアップロード