golang + bazel + protobuf で構成するプロジェクト作成の備忘録

はじめに

Go 言語、Bazel および、Protocol Buffers を組み合わせたプロジェクト作成でハマった内容を記載する。
この記事を書いたのは、protobuf 定義で、外部定義(Well-Known Types)をインポートして使用する場合に、依存関係の解決ができず、悶絶したためとなる。
作成するプロジェクトでは、gRPC で protobuf を利用する設定を行う。
この手順が正解か不明だが、参考になれば幸いである。

環境

bazel を Windows 上で利用する場合、rule によっては、shell が必須となる。(bazel は、徐々に shell に依存しない方向に向かっている模様)
通常は、MSYS2 を利用することで実行できるが、rule によっては、対応していない場合もあるようだ。
当方の Windows 環境では、gazelle 関連の rule が動作しないため(原因は未調査だが、Windows は未サポートなのかも)、bat ファイルで実行する。
shell が必須となることも想定して、WSL 上でも動作確認はしておく。
プロジェクト自体は、Windows 側のディレクトリに作成する。
WSL 上の開発環境は、この記事を参考に構築する。
ここでは、Go 言語をインストールした開発環境を前提とする。
WSL 上の開発環境を、以降「開発環境」と表記する。
  • Windows 10
  • WSL2(Ubuntu-20.04)

利用ツール

Bazelisk は、bazel のラッパーコマンドであり、プロジェクト毎に設定したバージョンの bazel を自動的にダウンロードして実行してくれる優れもの。
bazel のみインストールすれば、Go をインストールせずとも、以下のようなコマンドで、コマンドが実行可能だが、Bazelisk を簡単にインストールするために、Go をインストールしている。
bazelisk run @go_sdk//:bin/go -- version
IntelliJ は、必須ではない。

環境構築

Windows および、開発環境の構築を行う。
Windows には、Go および、IntellilJ がインストール済みとする。
インストール方法は、割愛する。当方では、パッケージ管理ツールの scoop を利用している。
Bazelisk は、Windows および WSL 環境に、以下のコマンドでインストールする。
go install github.com/bazelbuild/bazelisk@latest

bazel は、WSL と Windows 上の両方で動作させてみたが、今のところ動作しているように見える。
ただし、bazel のビルド作業用ディレクトリは、Windows と WSL では、別の場所にシンボリックリンクが設定されるため、bazel で実行する時に、場合によってはシンボリックリンクが、別の環境用のままとなり、エラーとなる。その場合は、bazelisk build //server などのような別のコマンドを実行して、シンボリックリンクが再作成されるようにするとよい。

プロジェクトの作成

以降の説明は、サンプルプロジェクトを確認しながら読み進めるとよい。
任意の方法で、プロジェクトを作成する。ここでは、IntelliJ で Go プロジェクトを作成した。
IntelliJ では、go.mod ファイルを自動で作成してくれる。無い場合は、手動で作成する。

protobuf の定義ファイルを作成

proto ディレクトリに、profobuf の定義ファイルを格納する。
user.proto ファイルでは、外部定義(google/protobuf/any.proto)の参照を行っている。

bazel の設定ファイルを作成

bazel の設定ファイルは、プロジェクトルートディレクトリに、WORKSPACE.bazel および、BUILD.bazel ファイルを作成する。
bazel の設定ファイルの拡張子は、無しでも認識されるが、WORKSPACE.bazel の設定を若干変更する必要があるので注意。

go の依存パッケージを追加

gRPC を go で利用するために、パッケージを手動で追加しておく。
これにより、protobuf の定義でインポートしている外部定義も解決できる。
依存関係をすべて手作業で bazel 定義ファイルに追加する方法もあるが、やりたくない。
プロジェクトルートで以下のコマンドを実行すると、go.mod ファイルが更新される。
go get -u google.golang.org/grpc

依存パッケージを事前に追加しない場合、以下のようなエラーで protobuf の定義がビルドできない。
compilepkg: missing strict dependencies:
        /home/ubuntu/.cache/bazel/_bazel_ubuntu/60c27803c2e555e5df9e00daff60b40e/sandbox/linux-sandbox/103/execroot/__main__/external/org_golang_google_grpc/internal/syscall/syscall_linux.go: import of "golang.org/x/sys/unix"
No dependencies were provided.
Check that imports in Go sources match importpath attributes in deps.

以下のコマンドで、go.mod の依存関係を WORKSPACE.bazel ファイルに反映する。gazelle-update-repos コマンドは、プロジェクトルートの BUILD.bazel ファイルに定義している。
Windows の場合
build-tools\run_gazelle_update_repos.bat
WSL の場合
bazelisk run //:gazelle-update-repos
コマンドを実行すると、deps.bzl ファイルが新規作成され、WORKSPACE.bazel ファイルに以下の行が追加される。
load("//:deps.bzl", "go_dependencies")

# gazelle:repository_macro deps.bzl%go_dependencies
go_dependencies()
次に、プロジェクトルート以外の BUILD.bazel を自動生成する。
プロジェクトルートで以下のコマンドを実行する。
Windows の場合
build-tools\run_gazelle.bat
WSL の場合
bazelisk run //:gazelle
proto/BUILD.bazel ファイルが作成される。
次に、protobuf の定義をビルドする。
bazelisk build //proto
生成されたソースは、bazel のサンドボックス内(bazel-out/k8-fastbuild/bin/proto/proto_go_proto_/github.com/i-chi-li/example-go-bazel-protobuf/proto/address.pb.go など)に格納されている。
bazel のデフォルト動作では、ホームディレクトリにビルドしたファイルなどが格納される。
これらのファイルは、Windows 側から直接参照できないため、IntelliJ などから、protobuf で生成したソースをコード補完などで利用できない。
bazel のビルドファイルの出力先は、オプション(--output_base 、--output_user_root)で変更できるが、現時点ではエラー(external/go_sdk/pkg/tool/linux_amd64/link: mapping output file failed: input/output error)となる。(原因は未調査)
そのため、生成したソースをサンドボックスからプロジェクト側にコピーする。
手動で行っても良いが、以下を参考に bazel でコマンド化することにする。
コマンドの実装は、build-tools/copy_proto.bzl に記載してあり、プロジェクトルートの BUILD.bazel ファイル内から呼び出している(copy_proto)。
以下のコマンドを実行すると、protobuf で生成したファイルを generated/proto ディレクトリにコピーされる。
bazelisk run //:copy-proto

サンプルの実行

サンプルサーバとクライアントは、以下のように実行可能。
Windows 側で実行することもできる。
bazel で実行する場合。
bazelisk run //server
bazelisk run //client
go コマンド(環境にインストールした go )で実行する場合。
go run server/main.go
go run client/main.go
Windows 上で go コマンド(bazel で定義した go)で実行する場合。(.exe 拡張子をつけないと動作しなかった)
bazelisk run @go_sdk//:bin/go.exe -- run server/main.go
bazelisk run @go_sdk//:bin/go.exe -- run client/main.go
WSL 上で go コマンド(bazel で定義した go)で実行する場合。
bazelisk run @go_sdk//:bin/go -- run server/main.go
bazelisk run @go_sdk//:bin/go -- run client/main.go

最後に

見よう見真似でやってみたが、bazel の入り口にも到達できていない気がする。
特に Windows 上では、一筋縄ではいかない。
自動生成したファイルを、ワークスペースにコピーするような機能は、bazel のコミュニティでも話題になっているようで、なにかしらの機能として取り込まれつつあるようだが、詳細はわからなかった。
今回作成した、protobuf で生成したファイルをコピーするルールは、bazel の理念からは逸脱しているような気がする・・・
bazel の言語を含むツール類を、ビルド時にダウンロードして、環境の違いによって、問題が発生し難くする思想は、たいへん惹きつけられるのだが、如何せん学習コストが高い気がする。導入部分をやってみて、どうにもすっきりしなかった。まだまだ自己研鑽が必要だと思った。
bazel 自体も、Windows 上で遜色なく動作するように、shell に依存しない機能が追加されていくようなので、今後に期待したい。

参考