proto_build_kit/compile.rs
1// SPDX-License-Identifier: MIT
2//! Compile `.proto` files via `protox` (pure Rust, no `protoc` subprocess).
3
4use std::path::Path;
5
6use prost::Message as _;
7
8use crate::Error;
9
10/// Result of [`compile_protos`].
11pub struct CompileOutput {
12 /// The in-memory descriptor pool. **Preserves custom-option VALUES**
13 /// on `MethodOptions`, which the FDS-encode path drops. Use this
14 /// for [`crate::extract_method_string_extension`] and any other
15 /// annotation-driven downstream work.
16 pub pool: prost_reflect::DescriptorPool,
17 /// Encoded `FileDescriptorSet` bytes — suitable for passing to
18 /// `tonic_prost_build::Builder::compile_fds(...)` and similar
19 /// codegen drivers.
20 pub fds_bytes: Vec<u8>,
21}
22
23/// Compile a set of `.proto` files (and all their transitive imports)
24/// via `protox`.
25///
26/// `includes` is the protoc-style include path. Pass the path returned
27/// by [`crate::Stager::stage()`] plus any caller-provided directories.
28/// Well-known types (`google/protobuf/*.proto`) are bundled in `protox`
29/// and resolve automatically.
30///
31/// # Errors
32///
33/// Returns [`Error::Protox`] if `protox` cannot resolve or parse the
34/// inputs.
35///
36/// # Examples
37///
38/// ```no_run
39/// use proto_build_kit::{compile_protos, Stager};
40///
41/// fn build() -> Result<(), Box<dyn std::error::Error>> {
42/// let staged = Stager::new()
43/// .add("my/v1/svc.proto", b"syntax = \"proto3\"; package my.v1;")
44/// .stage()?;
45/// let out = compile_protos(
46/// &["my/v1/svc.proto"],
47/// &[staged.path()],
48/// )?;
49/// assert!(!out.fds_bytes.is_empty());
50/// Ok(())
51/// }
52/// ```
53pub fn compile_protos<P: AsRef<Path>, Q: AsRef<Path>>(
54 protos: &[P],
55 includes: &[Q],
56) -> Result<CompileOutput, Error> {
57 let mut compiler = protox::Compiler::new(includes.iter().map(AsRef::as_ref))?;
58 compiler.include_imports(true);
59 compiler.include_source_info(false);
60 for p in protos {
61 compiler.open_file(p.as_ref())?;
62 }
63
64 let pool = compiler.descriptor_pool();
65 let fds_bytes = compiler.file_descriptor_set().encode_to_vec();
66
67 Ok(CompileOutput { pool, fds_bytes })
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::Stager;
74
75 #[test]
76 fn compiles_trivial_proto() {
77 let staged = Stager::new()
78 .add(
79 "fixture/v1/x.proto",
80 b"syntax = \"proto3\"; package fixture.v1; message Foo { string id = 1; }",
81 )
82 .stage()
83 .unwrap();
84
85 let out = compile_protos(&["fixture/v1/x.proto"], &[staged.path()]).expect("compile");
86
87 assert!(!out.fds_bytes.is_empty());
88 let foo = out
89 .pool
90 .get_message_by_name("fixture.v1.Foo")
91 .expect("Foo in pool");
92 assert_eq!(foo.fields().count(), 1);
93 }
94}