Skip to main content

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}