ディレクトリ配下の全てのptoroファイルからgRPC/protobufのコードを自動生成するバッチ(Windows)


このTIPSでは、ディレクトリ配下の全てのprotoファイルから、gRPC/protobufのコードを自動生成するバッチファイルを作成します。gRPC/protobufがサポートしている言語は全て出力しています。しかし、今のところ僕が実際に生成コードを使用しているのはC++のみで、その他の言語では使っていません。もしかすると生成方法に誤りがあるかもしれませんので、問題がある場合はコメントをお願いします。

前置き

gRPCやprotobufのクイックスタートでは、1つのptoroファイルをコンパイル(protoファイルからコードを自動生成する)方法が記載されています。とりあえず試すぶんにはクイックスタートの通りで全然困りません。
実際に開発でgRPCやprotobufを使用するとなると、沢山のprotoファイルを作成することになります。必要に応じてパッケージも分けるでしょうし、途中でprotoファイルが足りないことが分かって、追加することもあるでしょう。また、同じメッセージを複数のprotoファイルで使いまわすため、import機能を使うことになると思います。protoファイル同士がお互いに影響するようになるため、protoファイルを修正したら、全てのprotoファイルが正しくコンパイルできることを確認する必要があります。コンパイルは開発を通して何十回も必要になります。
ptorocコマンドは、ディレクトリ配下のprotoファイルを再帰的に処理する機能を持っていません。とはいえ、protoファイルごとにコマンドを手打ちするなんて勿論無理です。なので、コマンドをバッチ化しておく必要があります。何も考えずにバッチ化すると、protocコマンドがずらっと並ぶバッチが出来上がります。これでは、protoファイルを追加・削除した際に、バッチを毎回手直ししなければなりません。めんどくさいですね。手直しの要らないバッチを作ろう、というのがこのTIPSの目的です。

ディレクトリ構成

ディレクトリ構成
root----------------------------------(1)
│  build.cmd -------------------------(2)
│  grpc_cpp_plugin.exe----------------(3)
│  grpc_csharp_plugin.exe
│  grpc_node_plugin.exe
│  grpc_objective_c_plugin.exe
│  grpc_php_plugin.exe
│  grpc_python_plugin.exe
│  grpc_ruby_plugin.exe
│  protoc-gen-go.exe
│  protoc-gen-grpc-gateway.exe
│  protoc-gen-grpc-java.exe
│  protoc-gen-swagger.exe
│  protoc.exe
│
├─google------------------------------(4)
│  └─api
│          annotations.proto
│          http.proto
│
└─test--------------------------------(5)
    ├─entity
    │      item.proto
    │      user.proto
    │
    └─service
            item_manager.proto
            user_manager.proto

解説

(1) root
protoファイル格納ディレクトリのルートです。名前は何でも構いません。
ただし、バッチ側で考慮していないため、絶対パス内にスペースを含まないようにしてください。

(2) build.cmd
バッチです。内容は後述します。

(3) protocコマンドとプラグイン類
protocコマンドは、指定するプラグインをPATHから探してくれません。環境依存はなるべく減らしたいので、割り切って同一フォルダに実行ファイルを全部置いてしまっています。

go関連のコマンドは、以下のgo getコマンドで、%GOPATH%\binに配置されます。

go get -u github.com/golang/protobuf/protoc-gen-go
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

go関連以外のコマンドのビルド方法は、以前の投稿を参照してください。言語によっては、パッケージマネージャでgrpcをインストールすると持ってこれるかもしれません。あんまり調べてません。だってC++使うにはどうせビルドしなきゃいけないので。

(4) google apiディレクトリ
grpc-gateway生成用のアノテーション定義が格納されたディレクトリです。
アノテーションを使用すると、関連する生成コードも必要となるため、一式rootディレクトリ配下に置いています。
grpc-gatewayを使用しないなら必要ありません。

上記の内容は、以下のgo getコマンドで、%GOPATH%\src\github.com\grpc-ecosystem\grpc-gateway\third_party\googleapis\googleに配置されます。※前述のコマンドの1つと同じものです。

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

(5) 自分で作成したprotoファイル
自分で作成したprotoファイルです。上記の例では、test/entityにメッセージを、test/serviceにはgRPC用のサービス定義を置いています。item_manager.protoがitem.protoをImportし、user_manager.protoからはuser.ptoroをImportする関係です。正直ここらのパッケージ分け方は、ベストプラクティスが何なのかよくわかっていません。

ここで一点注意です。protoファイルを配置するディレクトリ構成は、パッケージ階層と正確に一致させる必要があります。具体的には、それぞれのprotoファイルのパッケージは以下のように定義します。

  • ****\root\test\entity\item.proto
package test.entity;
  • ****\root\test\service\item_manager.proto
package test.service;

ディレクトリ構成名と一致しているのが確認してもらえると思います。
逆に、パッケージを定義していないprotoファイルはrootディレクトリ直下に置きます。

バッチ

build.cmd
@echo off
rem --- path definition ---
set PROTOBUF_SRC=grpc-1.2.3\third_party\protobuf\src

set CMD_DIR=%~dp0
set INCLUDE=-I%CMD_DIR% -I%PROTOBUF_SRC%

cd /d %CMD_DIR%
for /r %%i in (*.proto) do (
  rem --- cpp ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe --grpc_out=. --cpp_out=. %%i

  rem --- java ---
  rem (To generate Java interfaces with protobuf lite: --grpc-java_out=lite:. nano: --grpc-java_out=nano:.)
  protoc %INCLUDE% --plugin=protoc-gen-grpc-java=protoc-gen-grpc-java.exe --grpc-java_out=. --java_out=. %%i

  rem --- python ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc_python=grpc_python_plugin.exe --python_out=. --grpc_python_out=. %%i

  rem --- go ---
  protoc %INCLUDE% --go_out=plugins=grpc:. %%i

  rem --- ruby ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_ruby_plugin.exe --ruby_out=. --grpc_out=. %%i

  rem --- csharp ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe --grpc_out=%%~dpi --csharp_out=%%~dpi %%i

  rem --- node(static_codegen) ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_node_plugin.exe --js_out=%%~dpi --grpc_out=. %%i

  rem --- objective-c ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_objective_c_plugin.exe --objc_out=. --grpc_out=. %%i

  rem --- php(static_codegen) ---
  protoc %INCLUDE% --plugin=protoc-gen-grpc=grpc_php_plugin.exe --php_out=. --grpc_out=. %%i

  rem --- grpc-gateway (with swagger) ---
  protoc %INCLUDE% --grpc-gateway_out=logtostderr=true:. --swagger_out=logtostderr=true:. %%i
)

おわかりでしょうか、コマンド名の統一されてそうで実は統一されてない感じ。

解説

--- path definition ---
google/protobuf/empty.protoなどのWell-Known Typesを使用する場合は、protobuf/src配下をインクルードに指定する必要があります。ここでまた1点注意で、パスは、絶対パスで指定する必要があります。protocコマンドは、パスが絶対パスなのか相対パスなのか見分けてくれません。ここが一番気に入らないところで、絶対パスをバッチ中に書くと、環境に依存してしまいます。かといって、protobuf/src配下をrootディレクトリ配下に置くのはオススメしません。protobuf/srcにはproto2のprotoファイルが多く含まれます。多くの言語はproto3からサポートされているため、root配下にsrc/googleを置くと、おびただしい量のワーニングが出ます。Well-Known Types関係のprotoファイルだけ置けば解決するかもしれません。いい方法が確認できたら更新することにします。

--- java ---
javaのソースにはnano版、lite版があるそうで、コマンドを切り替えることで生成可能です。コメントのとおりです。
何が違うのかよくわかってません。

--- csharp ---
C#のソースは、--csharp_out=.とすると、protoファイルと同じディレクトリに生成されません。C#の名前空間の命名がパスカルケースだからでしょうか。。このバッチでは、無理やり同じディレクトリに生成されるようパスを指定しています。

--- node(static_codegen) ---
nodeのソースもC#と同様、--js_out=.とすると、protoファイルと同じディレクトリに生成されません。node触ったことないので、ソースをどう置くべきか自体わかってないです。変なことしてたら教えて下さいね。。ちなみにnodeでは、今回のように事前にコード生成する方法(static_codegen)と、実行時に勝手にコード生成してくれる方法(dynamic_codegen)があります。dynamic_codegenはphpでも使えるそうです。使い分けはよくわからんです。

--- grpc-gateway (with swagger) ---
protoc-gen-grpc-gateway.exeおよびprotoc-gen-swagger.exeは、以下のgo getコマンドで、%GOPATH%\binに配置されます。

出力結果

build.cmdを実行すると、以下のようにファイルが生成されます。
基本的にはprotoファイルと同一階層にソースが生成されますが、protoファイルに言語毎のパッケージオプションが定義されている場合は、パッケージに対応するディレクトリが生成されます。また、phpではGPBMetadataというメタデータが生成されるようです(よくわからん)

ディレクトリ構成
root
│  build.cmd
│  grpc_cpp_plugin.exe
│  grpc_csharp_plugin.exe
│  grpc_node_plugin.exe
│  grpc_objective_c_plugin.exe
│  grpc_php_plugin.exe
│  grpc_python_plugin.exe
│  grpc_ruby_plugin.exe
│  protoc-gen-go.exe
│  protoc-gen-grpc-gateway.exe
│  protoc-gen-grpc-java.exe
│  protoc-gen-swagger.exe
│  protoc.exe
│  UnittestEmpty.java
│
├─com
│  └─google
│      └─api
│              AnnotationsProto.java
│              CustomHttpPattern.java
│              CustomHttpPatternOrBuilder.java
│              Http.java
│              HttpOrBuilder.java
│              HttpProto.java
│              HttpRule.java
│              HttpRuleOrBuilder.java
│
├─google
│  └─api
│          Annotations.cs
│          annotations.grpc.pb.cc
│          annotations.grpc.pb.h
│          annotations.pb.cc
│          annotations.pb.h
│          Annotations.pbobjc.h
│          Annotations.pbobjc.m
│          annotations.proto
│          annotations.swagger.json
│          annotations_pb.rb
│          annotations_pb2.py
│          annotations_pb2_grpc.py
│          customhttppattern.js
│          CustomHttpPattern.php
│          Http.cs
│          http.grpc.pb.cc
│          http.grpc.pb.h
│          http.js
│          http.pb.cc
│          http.pb.h
│          HTTP.pbobjc.h
│          HTTP.pbobjc.m
│          Http.php
│          http.proto
│          http.swagger.json
│          httprule.js
│          HttpRule.php
│          http_pb.rb
│          http_pb2.py
│          http_pb2_grpc.py
│
├─google.golang.org
│  └─genproto
│      └─googleapis
│          └─api
│              └─annotations
│                      annotations.pb.go
│                      http.pb.go
│
├─GPBMetadata
│  ├─Google
│  │  └─Api
│  │          Annotations.php
│  │          Http.php
│  │
│  └─Test
│      ├─Entity
│      │      Item.php
│      │      User.php
│      │
│      └─Service
│              ItemManager.php
│              UserManager.php
│
└─test
    ├─entity
    │      Item.cs
    │      item.grpc.pb.cc
    │      item.grpc.pb.h
    │      item.js
    │      item.pb.cc
    │      item.pb.go
    │      item.pb.h
    │      Item.pbobjc.h
    │      Item.pbobjc.m
    │      Item.php
    │      item.proto
    │      item.swagger.json
    │      ItemOuterClass.java
    │      items.js
    │      Items.php
    │      item_pb.rb
    │      item_pb2.py
    │      item_pb2_grpc.py
    │      User.cs
    │      user.grpc.pb.cc
    │      user.grpc.pb.h
    │      user.js
    │      user.pb.cc
    │      user.pb.go
    │      user.pb.h
    │      User.pbobjc.h
    │      User.pbobjc.m
    │      User.php
    │      user.proto
    │      user.swagger.json
    │      UserOuterClass.java
    │      users.js
    │      Users.php
    │      user_pb.rb
    │      user_pb2.py
    │      user_pb2_grpc.py
    │
    └─service
            ItemManager.cs
            ItemManager.pbobjc.h
            ItemManager.pbobjc.m
            ItemManager.pbrpc.h
            ItemManager.pbrpc.m
            ItemManagerClient.php
            ItemManagerGrpc.cs
            ItemManagerGrpc.java
            ItemManagerOuterClass.java
            item_manager.grpc.pb.cc
            item_manager.grpc.pb.h
            item_manager.pb.cc
            item_manager.pb.go
            item_manager.pb.h
            item_manager.proto
            item_manager.swagger.json
            item_manager_grpc_pb.js
            item_manager_pb.rb
            item_manager_pb2.py
            item_manager_pb2_grpc.py
            item_manager_services_pb.rb
            UserManager.cs
            UserManager.pbobjc.h
            UserManager.pbobjc.m
            UserManager.pbrpc.h
            UserManager.pbrpc.m
            UserManagerClient.php
            UserManagerGrpc.cs
            UserManagerGrpc.java
            UserManagerOuterClass.java
            user_manager.grpc.pb.cc
            user_manager.grpc.pb.h
            user_manager.pb.cc
            user_manager.pb.go
            user_manager.pb.gw.go
            user_manager.pb.h
            user_manager.proto
            user_manager.swagger.json
            user_manager_grpc_pb.js
            user_manager_pb.rb
            user_manager_pb2.py
            user_manager_pb2_grpc.py
            user_manager_services_pb.rb

今回は以上です。お疲れ様でした。