End-to-end demonstration that the buffa and connectrpc protoc plugins
work cleanly under Bazel. A bazel test //... invocation:
- Runs
protocwith all three plugins (protoc-gen-buffa,protoc-gen-buffa-packaging,protoc-gen-connect-rust) via agenrule, producing four.rsfiles. A secondgenruleruns the same plugins throughbuf generate(driven by therules_buftoolchain) and ash_testproves the two output trees are byte-identical. - Pulls
buffa,connectrpc, and their transitive deps from crates.io viarules_rust+crates_universe. - Compiles a
rust_librarywhose srcs includesrc/lib.rsplus the four generated files. - Runs three
rust_tests that exercise the generated message types (construction, encode/decode round-trip viabuffa::Message) and the connectrpc service-stub constants. - Runs
buf_lint_test(fromrules_buf) over the proto sources, enforcing the buf STANDARD lint ruleset on every CI run.
No env!-baked paths anywhere in the generated code, no checked-in
generated .rs files, no build.rs.
| File | Purpose |
|---|---|
MODULE.bazel |
bzlmod deps: protobuf, rules_proto, rules_buf, rules_rust, crates_universe. |
BUILD.bazel |
The two codegen genrules (gen_code, gen_code_via_buf), the gen_outputs_match_test diff check, the greet_proto proto_library, greet_proto_lint, the greet_lib library, and greet_lib_test. |
.bazelrc |
Enables --experimental_proto_descriptor_sets_include_source_info so buf_lint_test can read source positions from descriptor sets. |
buf.yaml |
The buf module + lint/breaking config, shared with the local buf CLI. |
Cargo.toml / Cargo.lock |
Dependency manifest fed to crates_universe.from_cargo. |
stub.rs |
Empty crate body — cargo metadata needs a target so crates_universe can resolve the graph. |
proto/anthropic/connectrpc/examples/greet/v1/greet.proto |
Minimal GreetService definition. |
tools/BUILD.bazel |
Exposes the plugin binaries as Bazel labels. |
setup.sh |
Builds the plugins via cargo and symlinks them under tools/. |
tools/diff_outputs.sh |
Pairwise byte-diff invoked by gen_outputs_match_test. |
src/lib.rs |
Mounts both generated trees via #[path = "..."] and contains the tests. |
Build the plugin binaries (one-time, repeat if you change the codegen crates):
./setup.shThis builds protoc-gen-buffa and protoc-gen-buffa-packaging from the
sibling buffa checkout, builds protoc-gen-connect-rust from this
repo, and symlinks all three into tools/. The symlinks are gitignored.
bazel test //...On the first invocation Bazel will build protoc from source (~30s),
fetch the buf toolchain, fetch + compile ~70 crates from crates.io,
and produce the generated sources. Subsequent runs are cached. Expected
output:
//:gen_outputs_match_test PASSED
//:greet_lib_test PASSED
//:greet_proto_lint PASSED
The greet_lib_test log shows three Rust unit tests passing:
test tests::message_types_are_constructible ... ok
test tests::message_types_round_trip_through_buffa ... ok
test tests::service_name_constant_is_correct ... ok
There are two parallel codegen genrules. Both drive the same three
plugins and emit byte-identical output (verified by
gen_outputs_match_test):
| Rule | Tool | Plugin config |
|---|---|---|
gen_code |
protoc directly |
inline in the genrule's cmd |
gen_code_via_buf |
buf generate (from the rules_buf toolchain) |
a buf.gen.yaml generated at build time, with plugin paths resolved via $(location ...) |
Output naming is deterministic from the proto file path: a file at
proto/anthropic/connectrpc/examples/greet/v1/greet.proto becomes
anthropic.connectrpc.examples.greet.v1.greet.rs. The packaging plugin
emits a mod.rs that nests pub mod blocks matching the proto's
package declaration and include!s the per-file output as a sibling.
Two output trees per genrule — one with buffa message types, one with
connectrpc service stubs — because the plugins emit colliding filenames
(both mod.rs and <package>.rs). The packaging plugin runs twice,
once over each tree (the second invocation passes filter=services so
files without services are skipped from the connect output).
The generated mod.rs files use sibling-relative include!("foo.rs"),
so consuming the output requires no env!("OUT_DIR") indirection.
src/lib.rs mounts each tree with a single #[path = "..."] attribute.
For this single-proto example the two patterns are equivalent. In a real codebase:
protocdirectly is cleanest when plugin invocations live only in Bazel — fewer files, every flag visible inBUILD.bazel, no dependence on the buf toolchain.buf generatewins when you want the same plugin config to drive bothbazel buildand the localbuf generateworkflow. Plugin options live in one sharedbuf.gen.yamlinstead of being duplicated between Bazel and developer scripts.
crates_universe.from_cargo reads a Cargo manifest to discover the
dependency graph, so the example needs one. It is a stub crate (the
[lib] points at an empty stub.rs) — Bazel does the actual building.
The packaging plugin emits the mod.rs module-tree file. It runs once
for each output tree we want a mod.rs for: once over the buffa output
(default behavior, includes every file) and once over the connect output
with filter=services (skip files that contain no services).
rules_buf provides buf_dependencies, buf_lint_test,
buf_breaking_test, and buf_format — proto source management and
quality checks that work alongside rules_proto's proto_library. It
does not provide a buf_generate rule, so when you want
buf-driven codegen as a Bazel build action you wrap the buf binary
(supplied by the rules_buf_toolchains target) in a genrule
yourself, as gen_code_via_buf does.
Here we wire in buf_lint_test against the greet_proto
proto_library, sharing config (buf.yaml) with the local buf CLI.
buf_breaking_test against a snapshot or git ref would be the natural
next addition. If the proto imported from BSR (e.g.
buf.build/protocolbuffers/wellknowntypes), buf_dependencies would
fetch and expose those modules — this example has no BSR imports.