はじめに
gRPCプロジェクトで mvn compile を実行したとき、こんなエラーが出たことはないでしょうか。
[ERROR] Failed to execute goal org.xolstice.maven.plugins:protobuf-maven-plugin:0.6.1:compile
(default) on project my-grpc-core: protoc did not exit cleanly.
Review output for more information. -> [Help 1]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
「Review output for more information」と言われても、そのoutputがない。
今回遭遇したのはまさにこの状況でした。
原因はproto3の地味な制約違反——1つの .proto ファイルに package 宣言が2回書かれていた——というシンプルなものでしたが、エラーログでは何も教えてくれません。
LLM(GitHub Copilot)を使ってどのように原因を特定したか、という過程も含めて紹介します。
先にまとめ(忙しい人向け)
- 原因:1つの
.protoファイルにpackage宣言が2つ書かれていた(proto3違反) - 直し方:余分な
package宣言以降の行を削除するだけ - 発見方法:GitHub Copilotに全protoファイルを構文チェックさせた
- 再発防止:
buf lintをCIに組み込む
状況:新規処理追加後、gRPCビルドが失敗
Mavenで管理しているgRPCコアプロジェクト(protobuf-maven-plugin使用)で、新しい .proto ファイルをいくつか追加した直後からビルドが通らなくなりました。
プロジェクト構成はこんな感じです。
src/main/proto/
├── product/
│ └── v1/
│ ├── model/
│ │ ├── sample.proto
│ │ └── ...(モデル定義ファイル群)
│ └── service/
│ ├── sample_service.proto ← 犯人
│ └── ...(サービス定義ファイル群)
└── notification/
└── v1/
└── ...(他ドメイン)
ファイル数は全体で数十本。
どれが壊れているか、エラーログには一切ヒントがありませんでした。
調査はLLMに全任
「大量のファイルを1本ずつ目視確認する」のは時間がかかります。
エラーメッセージには「-e と -X を使え」と書いてありますが、実際に試しても protoc 自体の出力が空のため有効な情報は得られませんでした。
こういう「構造的に全ファイルを舐めていく」作業はLLMが得意なので、GitHub Copilotに助けを求めました。
投げたプロンプトはこんな内容です。
プロンプト(意訳):
Mavenのprotobuf-maven-pluginがprotoc did not exit cleanlyで失敗しています。エラーログにはどのファイルが原因か出ていません。すべての.protoファイルを確認して、以下の観点でおかしなファイルを探してください。
syntax,package,optionの宣言が重複・不正なものはないかimportしているファイルが存在しないものはないかmessage/service名の重複はないか
LLMはインポート解決の正当性確認、ディレクトリ構造との照合、各ファイルの先頭宣言チェックを順番に進めていき、最終的にこう返してきました。
LLMの返答(要約):
sample_service.protoの末尾に 2つ目のpackage宣言が混入しています。これが protoc のエラーの原因です。
原因:ファイル末尾に混入した2つ目のpackage宣言
指摘された sample_service.proto を確認すると、こうなっていました。
問題のファイル(簡略化):
syntax = "proto3";
package com.example.proto.product.v1.service; // ← 1つ目のpackage宣言(正しい)
option java_multiple_files = true;
option java_package = "com.example.proto.product.v1.service";
option java_outer_classname = "SampleServiceProto";
import "product/v1/model/sample.proto";
service SampleService {
rpc GetSample (GetSampleRequest) returns (SampleResponse);
// ...その他のRPC定義...
}
message GetSampleRequest {
string sample_param = 1;
}
// sample.proto ← ここから下が混入していた
package com.example.proto.product.v1.model; // ← 2つ目のpackage宣言(問題)
option java_package = "com.example.proto.product.v1.model";
option java_outer_classname = "SampleProto";
ファイルの末尾4行が完全に余計なものでした。
proto3の制約:1ファイルに package は1つだけ
proto3の仕様では、1つの .proto ファイルに package 宣言は1回しか書けません。これはほとんどのプログラミング言語のモジュール/パッケージ概念と同じですが、proto3はこの違反を標準エラーで親切に教えてくれません。
protoc コマンドは異常終了コードを返し、Maven plugin は「exit cleanly しなかった」とだけ報告する——それだけです。
なぜ混入したか:LLMの生成コードに含まれていた別ファイルの断片
新しいサービスファイルを作成する際、既存ファイルをもとに生成するようLLMへ指示しました。
LLMは同タイミングで新規作成が必要な別ファイルの package 宣言と option 文も
出力しており、それがサービスファイルの末尾に混入した形でした。
生成内容をビルドで検証したところ今回のエラーが発生しました。
package の後に続くのがコード生成設定(option 文)だけなので、
エディタ上では一見「何かのコメントブロック」のように見え、気づきにくい内容でした。
IDEのprotoプラグインが警告を出してくれない環境だと、目視でのレビューでも見落としやすい点です。
修正:該当行を削除するだけ
修正は単純です。混入していた4行を削除します。
修正後(正しい状態):
syntax = "proto3";
package com.example.proto.product.v1.service;
option java_multiple_files = true;
option java_package = "com.example.proto.product.v1.service";
option java_outer_classname = "SampleServiceProto";
import "product/v1/model/sample.proto";
service SampleService {
rpc GetSample (GetSampleRequest) returns (SampleResponse);
// ...
}
message GetSampleRequest {
string sample_param = 1;
}
// ← ここで終わり。余計なpackage宣言なし
これで mvn compile が通りました。
再発防止:CIでproto lintを入れる
今回は運よくLLMが発見してくれましたが、根本的な対策はCIでのlintです。
buf というツールを使うと、proto3ファイルの静的解析をCIに組み込めます。
# buf.yaml(プロジェクトルートに配置)
version: v2
lint:
use:
- DEFAULT
buf lint
buf lint は重複宣言・存在しないimport・命名規約違反などを検出してくれます。protoc did not exit cleanly よりはるかに親切なメッセージが出ます。
Mavenプロジェクトであれば、exec-maven-plugin で buf lint をvalidateフェーズに組み込む方法もあります。
<!-- pom.xml -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>buf-lint</id>
<phase>validate</phase>
<goals><goal>exec</goal></goals>
<configuration>
<executable>buf</executable>
<arguments><argument>lint</argument></arguments>
</configuration>
</execution>
</executions>
</plugin>
詳細な設定オプションは buf 公式ドキュメント を参照してください。
まとめ
- Mavenの
-e/-Xフラグは最初の手がかりになるが、proto構文エラーには効かないことがある - 「どのファイルが壊れているか分からない」状況こそLLMの出番。全ファイルを特定の観点で調べさせると速い
- proto3は1ファイルに
packageは1つ。原点に返り重複していないか確認を