Skip to content

connectrpc-build: add Config::emit_descriptor_set for gRPC server reflection#141

Open
christopherwxyz wants to merge 2 commits into
anthropics:mainfrom
officialunofficial:feat/emit-descriptor-set
Open

connectrpc-build: add Config::emit_descriptor_set for gRPC server reflection#141
christopherwxyz wants to merge 2 commits into
anthropics:mainfrom
officialunofficial:feat/emit-descriptor-set

Conversation

@christopherwxyz
Copy link
Copy Markdown

connectrpc-build: add Config::emit_descriptor_set for gRPC Server Reflection

Motivation

Config::compile() already parses the .proto inputs into a full FileDescriptorSetrun_protoc is invoked with --include_imports, run_buf with --as-file-descriptor-set, and the precompiled path is read as-is, so the descriptor_bytes it decodes for codegen always carry the complete transitive import closure.

Today that set can be read in (Config::descriptor_set(path)) but never written out. A build script that needs those bytes — to serve grpc.reflection.v1.ServerReflection to clients like grpcurl via include_bytes!(concat!(env!("OUT_DIR"), "…")) — has to invoke protoc a second time with --descriptor_set_out --include_imports, duplicating work connectrpc-build already did and re-requiring a protoc install even when generation used buf or a precompiled set.

This adds the symmetric Config::emit_descriptor_set(name): write the descriptor set connectrpc-build already computed to <out_dir>/<name>.

This is the connectrpc-build counterpart of the buffa#125 follow-up the maintainers invited — buffa-build went with the embedded buffa-reflect pool for in-process reflection, explicitly leaving open the "standalone .bin for tooling outside the Rust build" case. Emitting at the connectrpc-build layer is the natural home for that: the bytes are already in hand, with --include_imports guaranteed, for every descriptor source.

API

/// Also write the parsed `FileDescriptorSet` (the descriptors used for
/// codegen) to `<out_dir>/<name>` as wire-format bytes.
///
/// The set carries the full transitive import closure for every descriptor
/// source (`protoc --include_imports`, `buf --as-file-descriptor-set`, or a
/// precompiled set), so it is ready to back `grpc.reflection.v1.ServerReflection`.
/// The inverse of [`Config::descriptor_set`], which *reads* a precompiled set.
#[must_use]
pub fn emit_descriptor_set(mut self, name: impl Into<String>) -> Self
// build.rs
connectrpc_build::Config::new()
    .files(&["proto/svc.proto"])
    .includes(&["proto/"])
    .emit_descriptor_set("svc_descriptor.bin")
    .compile()?;

// src/lib.rs
pub const FILE_DESCRIPTOR_SET: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/svc_descriptor.bin"));

Implementation (connectrpc-build/src/lib.rs)

Purely additive — no change to existing behavior; the field defaults to None.

  1. Config struct — new field:
     include_file: Option<String>,
+    emit_descriptor_set: Option<String>,
     emit_rerun_directives: bool,
  1. Config::new() — default:
             include_file: None,
+            emit_descriptor_set: None,
             emit_rerun_directives: true,
  1. Builder method (next to descriptor_set):
+    #[must_use]
+    pub fn emit_descriptor_set(mut self, name: impl Into<String>) -> Self {
+        self.emit_descriptor_set = Some(name.into());
+        self
+    }
  1. In compile(), right after create_dir_all(&out_dir) (with descriptor_bytes already in scope from the descriptor-source match):
+        // Emit the parsed descriptor set for reflection, if requested.
+        if let Some(name) = &self.emit_descriptor_set {
+            write_if_changed(&out_dir.join(name), &descriptor_bytes)?;
+        }

It reuses the existing write_if_changed (so it won't churn mtimes / trigger needless rebuilds), and works uniformly for the Protoc, Buf, and Precompiled sources because each already produces an import-complete descriptor_bytes.

Tests (included)

Two tests added to the existing mod tests, mirroring compile_precompiled_descriptor_set:

  • config_emit_descriptor_set_toggle — builder-state check.
  • emit_descriptor_set_writes_reflection_bin — end-to-end against the echo.fds.bin fixture (hermetic, Precompiled source, no protoc): asserts the file is written, FileDescriptorSet::decode_from_slice succeeds with a non-empty file list, and the bytes round-trip the source set exactly.

Verified locally: cargo build/clippy -p connectrpc-build clean; cargo test -p connectrpc-build20 passed, 0 failed.

Notes

  • Additive and backward-compatible; no existing API changes.
  • Complements buffa-reflect's in-process pool — this is the wire-bytes path for serving reflection v1 to external clients.

Follow-up to anthropics/buffa#125.

…er reflection

compile() already parses a full FileDescriptorSet (protoc --include_imports,
buf --as-file-descriptor-set, or a precompiled set), but it can only be read in
via descriptor_set() — never written out. Build scripts that need those bytes to
serve grpc.reflection.v1.ServerReflection (e.g. for grpcurl) must run protoc a
second time with --descriptor_set_out --include_imports.

Add the symmetric Config::emit_descriptor_set(name): write the descriptor set
already computed during compile() to <out_dir>/<name> as wire-format bytes, ready
to back server reflection. Works uniformly across all three descriptor sources
(import closure already guaranteed); reuses write_if_changed; purely additive.

Follow-up to anthropics/buffa#125 (buffa-build chose the embedded buffa-reflect
pool and left the standalone-.bin case open).

Tests: builder-state toggle + end-to-end emit/decode/round-trip against the
echo.fds.bin fixture. cargo test -p connectrpc-build: 20 passed.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@christopherwxyz
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request May 27, 2026
@christopherwxyz
Copy link
Copy Markdown
Author

recheck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant