Carpe Diem

備忘録

Bazelを使ってみる その2(protobufのビルド)

概要

前回に引き続き今回もBazelについて書きます。

christina04.hatenablog.com

今回はProtocol BuffersをビルドしてGoで利用できるコードを生成します。

環境

  • Bazel 4.2.2

設定

protoファイル

以下のような依存関係を持つprotoファイルを用意します。

proto/
├── address.proto
├── user.proto
└── zipcode.proto

zipcode.proto

syntax = "proto3";

option go_package = "github.com/jun06t/bazel-sample/protobuf/proto;user";

package user;

message ZipCode {
  string code = 1;
}

address.proto

syntax = "proto3";

option go_package = "github.com/jun06t/bazel-sample/protobuf/proto;user";

package user;

import "proto/zipcode.proto";

message Address {
  string  city     = 1;
  ZipCode zip_code = 2;
}

user.proto

syntax = "proto3";

option go_package = "github.com/jun06t/bazel-sample/protobuf/proto;user";

package user;

import "proto/address.proto";
import "google/protobuf/any.proto";

message User {
  string              id      = 1;
  string              name    = 2;
  Address             address = 3;
  google.protobuf.Any tags    = 4;
}

今回Well Known Typesであるgoogle/protobuf/any.protoにも依存しているとします。

Bazelの設定

WORKSPACEBUILD.bazelを用意します。

.
├── BUILD.bazel
├── Makefile
├── WORKSPACE
└── proto
    ├── address.proto
    ├── user.proto
    └── zipcode.proto

WORKSPACE

前回のWORKSPACEに下記を追加しただけのものを用意します。

http_archive(
    name = "com_google_protobuf",
    sha256 = "d0f5f605d0d656007ce6c8b5a82df3037e1d8fe8b121ed42e536f569dec16113",
    strip_prefix = "protobuf-3.14.0",
    urls = [
        "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz",
        "https://github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz",
    ],
)

load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()

ルートのBUILD.bazel

BUILD.bazelはprefix以外は変わりません。

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

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

この状態で一度

$ bazel run //:gazelle

を実行して、protoディレクトリにもBUILD.bazelが配置されるようにします。

すると以下のBUILD.bazelが生成されます。

load("@rules_proto//proto:defs.bzl", "proto_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 = "user_proto",
    srcs = [
        "address.proto",
        "user.proto",
        "zipcode.proto",
    ],
    visibility = ["//visibility:public"],
    deps = ["@com_google_protobuf//:any_proto"],
)

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/jun06t/bazel-sample/protobuf/proto",
    proto = ":user_proto",
    visibility = ["//visibility:public"],
)

go_library(
    name = "proto",
    embed = [":user_go_proto"],
    importpath = "github.com/jun06t/bazel-sample/protobuf/proto",
    visibility = ["//visibility:public"],
)

user.protoにあったgoogle/protobuf/any.protoへの依存については@com_google_protobuf//:any_protoで解決していることが分かります。

ビルド

ではprotobufのビルドをしてみます。

$ bazel build //proto

ローカルとの差分

ビルドした際のprotocやプラグインのバージョンの差異がありますが、コードの部分はほぼ同じであることが分かります。

$ diff bazel-bin/proto/user_go_proto_/github.com/jun06t/bazel-sample/protobuf/proto/user.pb.go dist/user.pb.go
3,4c3,4
< //    protoc-gen-go v1.27.1
< //    protoc        v3.14.0
---
> //    protoc-gen-go v1.25.0
> //    protoc        v3.17.3
9a10
>       proto "github.com/golang/protobuf/proto"
23a25,28
> // This is a compile-time assertion that a sufficiently up-to-date version
> // of the legacy proto package is being used.
> const _ = proto.ProtoPackageIsVersion4
>

GoGo Protobufを使う場合はどうするか

次にgogo protobufの場合どうするかです。
先程のuser.protoがgogo.protoに依存しているとします。

syntax = "proto3";

option go_package = "github.com/jun06t/bazel-sample/gogoproto/proto;user";

package user;

import "proto/address.proto";
import "google/protobuf/any.proto";
import "github.com/gogo/protobuf/gogoproto/gogo.proto";

message User {
  string              id      = 1 [(gogoproto.customname) = "ID"];
  string              name    = 2;
  Address             address = 3;
  google.protobuf.Any tags    = 4;
}

Bazelの設定

WORKSPACE

go_repositoryにgogoprotoを追記します。

go_repository(
    name = "com_github_gogo_protobuf",
    importpath = "github.com/gogo/protobuf",
    sum = "h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=",
    version = "v1.3.2",
    build_file_proto_mode = "disable",
)

詳細は後述しますがbuild_file_proto_mode = "disable",を付けているのがポイントです。

ルートのBUILD.bazel

3つほどDirectivesを追加します。

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

# gazelle:prefix github.com/jun06t/bazel-sample/gogoproto
# gazelle:resolve proto github.com/gogo/protobuf/gogoproto/gogo.proto @gogo_special_proto//github.com/gogo/protobuf/gogoproto
# gazelle:resolve proto go github.com/gogo/protobuf/gogoproto/gogo.proto @com_github_gogo_protobuf//gogoproto:go_default_library
# gazelle:go_proto_compilers @io_bazel_rules_go//proto:gogo_proto
gazelle(name = "gazelle")

解説

1. gazelle:resolve ... @gogo_special_proto//github.com/gogo/protobuf/gogoproto

ない場合

proto_libraryの依存解決がうまく行かない下記のようなBUILD.bazelになります。

proto_library(
    name = "user_proto",
    srcs = [
        "address.proto",
        "user.proto",
        "zipcode.proto",
    ],
    visibility = ["//visibility:public"],
    deps = [
        "//github.com/gogo/protobuf/gogoproto:gogoproto_proto",  // ココ
        "@com_google_protobuf//:any_proto",
    ],
)

しかし当然ローカルに該当のパッケージはないので依存が解決できません。

ある場合

proto_library(
    name = "user_proto",
    srcs = [
        "address.proto",
        "user.proto",
        "zipcode.proto",
    ],
    visibility = ["//visibility:public"],
    deps = [
        "@com_google_protobuf//:any_proto",
        "@gogo_special_proto//github.com/gogo/protobuf/gogoproto",
    ],
)

というBUILD.bazelになり、依存が解決できます。

gogo_special_protoはrules_goで定義されてるのでWORKSPACEに追記する必要はありません。

2. gazelle:resolve ... @com_github_gogo_protobuf//gogoproto:go_default_library

ない場合

以下のようになります。

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/jun06t/bazel-sample/gogoproto/proto",
    proto = ":user_proto",
    visibility = ["//visibility:public"],
    deps = [
        "@gogo_special_proto//github.com/gogo/protobuf/gogoproto",
    ],
)

ビルドしようとすると

in deps attribute of go_proto_library rule //proto:user_go_proto: '@gogo_special_proto//github.com/gogo/protobuf/gogoproto:gogoproto' does not have mandatory providers: 'GoLibrary'

というエラーが出ます。

ある場合

以下のようになります。

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/jun06t/bazel-sample/gogoproto/proto",
    proto = ":user_proto",
    visibility = ["//visibility:public"],
    deps = [
        "@com_github_gogo_protobuf//gogoproto:go_default_library",  // ココ
    ],
)

これは別途必要なので前述したようにWORKSPACEのgo_repositoryに追記が必要です。

3. gazelle:go_proto_compilers @io_bazel_rules_go//proto:gogo_proto

ない場合

go_proto_libraryのコンパイラがgogoprotoのものにならず、ビルド自体は成功するものの生成されたコードはgogoproto.customnameなどが反映されてない通常のprotobufのコードになってしまいます。

go_proto_library(
    name = "user_go_proto",
    importpath = "github.com/jun06t/bazel-sample/gogoproto/proto",
    proto = ":user_proto",
    visibility = ["//visibility:public"],
    deps = [
        "@com_github_gogo_protobuf//gogoproto:go_default_library",
    ],
)

ある場合

以下のようにコンパイラが指定されます。

go_proto_library(
    name = "user_go_proto",
    compilers = ["@io_bazel_rules_go//proto:gogo_proto"],  // ココ
    importpath = "github.com/jun06t/bazel-sample/gogoproto/proto",
    proto = ":user_proto",
    visibility = ["//visibility:public"],
    deps = [
        "@com_github_gogo_protobuf//gogoproto:go_default_library",
    ],
)

ビルドすると

Well Known Typesの部分がgogoprotoのものにreplaceされている点以外はローカルのビルドと一致しました。

$ diff bazel-bin/proto/user_go_proto_/github.com/jun06t/bazel-sample/gogoproto/proto/user.pb.go proto/user.pb.go
10c10
<       types "github.com/gogo/protobuf/types"
---
>       anypb "google.golang.org/protobuf/types/known/anypb"
29c29
<       Tags                 *types.Any `protobuf:"bytes,4,opt,name=tags,proto3" json:"tags,omitempty"`
---
>       Tags                 *anypb.Any `protobuf:"bytes,4,opt,name=tags,proto3" json:"tags,omitempty"`
80c80
< func (m *User) GetTags() *types.Any {
---
> func (m *User) GetTags() *anypb.Any {

その他

試行錯誤していた頃に遭遇したエラーと対処法です。

no such package '@org_golang_google_protobuf//types/known/fieldmaskpb'

ERROR: ... no such package '@org_golang_google_protobuf//types/known/fieldmaskpb': BUILD file not found in directory 'types/known/fieldmaskpb' of external repository @org_golang_google_protobuf. Add a BUILD file to a directory to mark it as a package. and referenced by '@io_bazel_rules_go//proto:go_proto'

ERROR: ... no such package '@org_golang_google_protobuf//types/known/sourcecontextpb': BUILD file not found in directory 'types/known/sourcecontextpb' of external repository @org_golang_google_protobuf. Add a BUILD file to a directory to mark it as a package. and referenced by '@io_bazel_rules_go//proto:go_proto'

といったエラーです。

go_repositoryにorg_golang_google_protobufがあり、かつバージョンが古いと起きます。上記ディレクトリは新しいバージョンにしか存在しないためです。

f:id:quoll00:20211212154456p:plain

対応

update-reposでバージョンを上げればOKです。

$ bazel run //:gazelle -- update-repos google.golang.org/protobuf

cannot use E_Embed (type *impl.ExtensionInfo)...

ERROR: /private/var/tmp/_bazel_a13156/4844ac1f9ad7427341a4ebab3cf95b0f/external/com_github_gogo_protobuf/gogoproto/BUILD.bazel:19:11: GoCompilePkg external/com_github_gogo_protobuf/gogoproto/gogoproto.a failed: (Exit 1): builder failed: error executing command bazel-out/host/bin/external/go_sdk/builder compilepkg -sdk external/go_sdk -installsuffix darwin_amd64 -src ... (remaining 57 argument(s) skipped)

Use --sandbox_debug to see verbose messages from the sandbox
external/com_github_gogo_protobuf/gogoproto/helper.go:35:37: cannot use E_Embed (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:39:37: cannot use E_Nullable (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:43:37: cannot use E_Stdtime (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:47:37: cannot use E_Stdduration (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:51:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:55:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:59:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:63:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:67:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:71:37: cannot use E_Wktpointer (type *impl.ExtensionInfo) as type *"github.com/gogo/protobuf/proto".ExtensionDesc in argument to "github.com/gogo/protobuf/proto".GetBoolExtension
external/com_github_gogo_protobuf/gogoproto/helper.go:71:37: too many errors
compilepkg: error running subcommand external/go_sdk/pkg/tool/darwin_amd64/compile: exit status 2

対応

build_file_proto_mode = "disable",をWORKSPACEのgo_repositoryに追記します。

go_repository(
    name="com_github_gogo_protobuf",
    commit="1adfc126b41513cc696b209667c8656ea7aac67c",
    importpath="github.com/gogo/protobuf",
    build_file_proto_mode = "disable",  // ココ
)

ref: overwritting gogo/protobuf dependency makes protobuf compilation fail · Issue #1405 · bazelbuild/rules_go · GitHub

外部ライブラリが.proto.pb.goを持つ場合、デフォルトだとGazelleはそれらもビルド対象に含めてしまうためそれを避けるために設定するオプションです。

OpenCensusエラー

ERROR: .../external/com_github_census_instrumentation_opencensus_proto/gen-go/metrics/v1/BUILD.bazel:3:11: GoCompilePkg external/com_github_census_instrumentation_opencensus_proto/gen-go/metrics/v1/metrics.a failed: (Exit 1): builder failed: error executing command bazel-out/host/bin/external/go_sdk/builder compilepkg -sdk external/go_sdk -installsuffix darwin_amd64 -src external/com_github_census_instrumentation_opencensus_proto/gen-go/metrics/v1/metrics.pb.go ... (remaining 24 argument(s) skipped)

対応

build_extra_args = ["-exclude=src"]をWORKSPACEのgo_repositoryに追記します。

    go_repository(
        name = "com_github_census_instrumentation_opencensus_proto",
        importpath = "github.com/census-instrumentation/opencensus-proto",
        sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=",
        version = "v0.2.1",
        build_extra_args = ["-exclude=src"],  # keep        
    )

ref: Bazel problem with opencensus-proto as transitive dependency · Issue #200 · census-instrumentation/opencensus-proto · GitHub

まとめ

proto周りはとにかく色んなエラーに遭遇し、都度ググって出てきたIssueを元に対応していました。
dockerで環境を固定すればprotoの生成物の再現性は保てるので、無理にBazelでやり込む必要はないかもと感じるほどに大変でした。

サンプルコード

今回は通常のprotoとgogo protoの2種類用意しています。

通常

github.com

gogo proto

github.com

参考