Context
buffa-build already parses .proto files into a FileDescriptorSet internally — either via protoc (default), buf (Config::use_buf()), or a pre-built file (Config::descriptor_set(path)). The bytes then flow into buffa-codegen for Rust generation.
Those descriptor bytes aren't exposed to the build-time consumer. There's only an input descriptor_set(path) method.
Use case
Implementing gRPC Server Reflection v1 (grpc.reflection.v1.ServerReflection) requires the server to serve raw FileDescriptorProto bytes back to clients like grpcurl. The natural place to obtain those is from the same codegen pipeline that emits the message types — a single descriptor set covers the whole service surface.
This came up while writing a generic ServerReflection handler over connectrpc. The straightforward expectation was something like:
// build.rs
buffa_build::Config::new()
.files(&["proto/myservice.proto"])
.includes(&["proto/"])
.emit_descriptor_set("myservice_descriptor.bin") // <-- desired
.compile()?;
// src/lib.rs
pub const FILE_DESCRIPTOR_SET: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/myservice_descriptor.bin"));
Workaround today
Invoke protoc a second time directly from build.rs, separate from buffa-build:
let _ = Command::new("protoc")
.arg(format!("--descriptor_set_out={}", out.join("descriptor.bin").display()))
.arg("--include_imports")
.arg(format!("-I{}", proto_dir.display()))
.arg(&proto_file)
.status()?;
This works but:
- Runs
protoc twice over the same files
- Diverges from
Config::use_buf() (the workaround hard-codes protoc)
- Requires every consumer to re-derive the include paths
Proposal
Add Config::emit_descriptor_set(name: impl Into<String>) -> Self that writes the internally-parsed FileDescriptorSet bytes to <out_dir>/<name>. Internally a one-liner since the parsed set is already on the stack — at minimum:
let bytes = parsed_descriptor_set.encode_to_vec();
std::fs::write(out_dir.join(&name), bytes)?;
Flag with --include_imports semantics (transitive closure) by default, matching what reflection clients need; an opt-out toggle could be added if needed.
The same opportunity applies symmetrically to connectrpc-build, which wraps buffa-build and would naturally surface the option as Config::emit_descriptor_set(...) too.
Why this matters
Without runtime FileDescriptorSet access, the grpcreflect story for the buffa + connectrpc stack requires a parallel protoc invocation in every consumer's build.rs. Closing the gap inside buffa-build makes server reflection a 1-line opt-in instead of a per-project recipe.
Context
buffa-buildalready parses.protofiles into aFileDescriptorSetinternally — either viaprotoc(default),buf(Config::use_buf()), or a pre-built file (Config::descriptor_set(path)). The bytes then flow intobuffa-codegenfor Rust generation.Those descriptor bytes aren't exposed to the build-time consumer. There's only an input
descriptor_set(path)method.Use case
Implementing gRPC Server Reflection v1 (
grpc.reflection.v1.ServerReflection) requires the server to serve rawFileDescriptorProtobytes back to clients likegrpcurl. The natural place to obtain those is from the same codegen pipeline that emits the message types — a single descriptor set covers the whole service surface.This came up while writing a generic
ServerReflectionhandler overconnectrpc. The straightforward expectation was something like:Workaround today
Invoke
protoca second time directly frombuild.rs, separate frombuffa-build:This works but:
protoctwice over the same filesConfig::use_buf()(the workaround hard-codesprotoc)Proposal
Add
Config::emit_descriptor_set(name: impl Into<String>) -> Selfthat writes the internally-parsedFileDescriptorSetbytes to<out_dir>/<name>. Internally a one-liner since the parsed set is already on the stack — at minimum:Flag with
--include_importssemantics (transitive closure) by default, matching what reflection clients need; an opt-out toggle could be added if needed.The same opportunity applies symmetrically to
connectrpc-build, which wrapsbuffa-buildand would naturally surface the option asConfig::emit_descriptor_set(...)too.Why this matters
Without runtime FileDescriptorSet access, the
grpcreflectstory for thebuffa+connectrpcstack requires a parallelprotocinvocation in every consumer'sbuild.rs. Closing the gap insidebuffa-buildmakes server reflection a 1-line opt-in instead of a per-project recipe.