LoginSignup
9

More than 3 years have passed since last update.

posted at

updated at

ProtocolBuffersを使っているGoのプロジェクトをBazelでビルドする

はじめに

この記事では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というファイルを作り,以下のように記述する.

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ファイルを以下のように作成する.

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を用いたターゲット指定されていることが分かる.

helloworld/BUILD.bazel
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_librarygo_binaryが作成される.

greeter_server/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_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が作成される.

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も同様に作成される.

mock_helloworld/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_libraryimportpathのprefixが決まります.

helloworld/BUILD.bazel
...
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によって,プロジェクトに含まれるパッケージに対する依存関係は内部参照となります.

greeter_server/BUILD.bazel
...
    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に結果をアップロード

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9