prost_build/lib.rs
1#![doc(html_root_url = "https://docs.rs/prost-build/0.14.4")]
2
3//! `prost-build` compiles `.proto` files into Rust.
4//!
5//! `prost-build` is designed to be used for build-time code generation as part of a Cargo
6//! build-script.
7//!
8//! ## Example
9//!
10//! Let's create a small library crate, `snazzy`, that defines a collection of
11//! snazzy new items in a protobuf file.
12//!
13//! ```bash
14//! $ cargo new --lib snazzy && cd snazzy
15//! ```
16//!
17//! First, add `prost-build` and `prost` as dependencies to `Cargo.toml`:
18//!
19//! ```bash
20//! $ cargo add --build prost-build
21//! $ cargo add prost
22//! ```
23//!
24//! Next, add `src/items.proto` to the project:
25//!
26//! ```proto
27//! syntax = "proto3";
28//!
29//! package snazzy.items;
30//!
31//! // A snazzy new shirt!
32//! message Shirt {
33//! // Label sizes
34//! enum Size {
35//! SMALL = 0;
36//! MEDIUM = 1;
37//! LARGE = 2;
38//! }
39//!
40//! // The base color
41//! string color = 1;
42//! // The size as stated on the label
43//! Size size = 2;
44//! }
45//! ```
46//!
47//! To generate Rust code from `items.proto`, we use `prost-build` in the crate's
48//! `build.rs` build-script:
49//!
50//! ```rust,no_run
51//! use std::io::Result;
52//! fn main() -> Result<()> {
53//! prost_build::compile_protos(&["src/items.proto"], &["src/"])?;
54//! Ok(())
55//! }
56//! ```
57//!
58//! And finally, in `lib.rs`, include the generated code:
59//!
60//! ```rust,ignore
61//! // Include the `items` module, which is generated from items.proto.
62//! // It is important to maintain the same structure as in the proto.
63//! pub mod snazzy {
64//! pub mod items {
65//! include!(concat!(env!("OUT_DIR"), "/snazzy.items.rs"));
66//! }
67//! }
68//!
69//! use snazzy::items;
70//!
71//! /// Returns a large shirt of the specified color
72//! pub fn create_large_shirt(color: String) -> items::Shirt {
73//! let mut shirt = items::Shirt::default();
74//! shirt.color = color;
75//! shirt.set_size(items::shirt::Size::Large);
76//! shirt
77//! }
78//! ```
79//!
80//! That's it! Run `cargo doc` to see documentation for the generated code. The full
81//! example project can be found on [GitHub](https://github.com/danburkert/snazzy).
82//!
83//! ## Feature Flags
84//! - `format`: Format the generated output. This feature is enabled by default.
85//! - `cleanup-markdown`: Clean up Markdown in protobuf docs. Enable this to clean up protobuf files from third parties.
86//!
87//! ### Cleaning up Markdown in code docs
88//!
89//! If you are using protobuf files from third parties, where the author of the protobuf
90//! is not treating comments as Markdown, or is, but has codeblocks in their docs,
91//! then you may need to clean up the documentation in order that `cargo test --doc`
92//! will not fail spuriously, and that `cargo doc` doesn't attempt to render the
93//! codeblocks as Rust code.
94//!
95//! To do this, in your `Cargo.toml`, add `features = ["cleanup-markdown"]` to the inclusion
96//! of the `prost-build` crate and when your code is generated, the code docs will automatically
97//! be cleaned up a bit.
98//!
99//! ## Sourcing `protoc`
100//!
101//! `prost-build` depends on the Protocol Buffers compiler, `protoc`, to parse `.proto` files into
102//! a representation that can be transformed into Rust.
103//!
104//! The easiest way for `prost-build` to find `protoc` is to install it in your `PATH`.
105//! This can be done by following the [`protoc` install instructions]. `prost-build` will search
106//! the current path for `protoc` or `protoc.exe`.
107//!
108//! When `protoc` is installed in a different location, set `PROTOC` to the path of the executable.
109//! If set, `prost-build` uses the `PROTOC`
110//! for locating `protoc`. For example, on a macOS system where Protobuf is installed
111//! with Homebrew, set the environment variables to:
112//!
113//! ```bash
114//! PROTOC=/usr/local/bin/protoc
115//! ```
116//!
117//! Alternatively, the path to `protoc` executable can be explicitly set
118//! via [`Config::protoc_executable()`].
119//!
120//! If `prost-build` can not find `protoc`
121//! via these methods the `compile_protos` method will fail.
122//!
123//! [`protoc` install instructions]: https://github.com/protocolbuffers/protobuf#protocol-compiler-installation
124//!
125//! ### Compiling `protoc` from source
126//!
127//! To compile `protoc` from source you can use the `protobuf-src` crate and
128//! set the path to `protoc`.
129//! ```no_run,ignore, rust
130//! let mut prost_build = prost_build::Config::new();
131//! prost_build.protoc_executable(protobuf_src::protoc());
132//!
133//! // Now compile your proto files with the configuration
134//! ```
135//!
136//! [`protobuf-src`]: https://docs.rs/protobuf-src
137
138use std::io::Result;
139use std::path::Path;
140
141use prost_types::FileDescriptorSet;
142
143mod ast;
144pub use crate::ast::{Comments, Method, Service};
145
146mod collections;
147pub(crate) use collections::{BytesType, MapType};
148
149mod code_generator;
150mod context;
151mod extern_paths;
152mod ident;
153mod message_graph;
154mod path;
155
156mod config;
157pub use config::{
158 error_message_protoc_not_found, protoc_from_env, protoc_include_from_env, Config,
159};
160
161mod module;
162pub use module::Module;
163
164/// A service generator takes a service descriptor and generates Rust code.
165///
166/// `ServiceGenerator` can be used to generate application-specific interfaces
167/// or implementations for Protobuf service definitions.
168///
169/// Service generators are registered with a code generator using the
170/// `Config::service_generator` method.
171///
172/// A viable scenario is that an RPC framework provides a service generator. It generates a trait
173/// describing methods of the service and some glue code to call the methods of the trait, defining
174/// details like how errors are handled or if it is asynchronous. Then the user provides an
175/// implementation of the generated trait in the application code and plugs it into the framework.
176///
177/// Such framework isn't part of Prost at present.
178pub trait ServiceGenerator {
179 /// Generates a Rust interface or implementation for a service, writing the
180 /// result to `buf`.
181 fn generate(&mut self, service: Service, buf: &mut String);
182
183 /// Finalizes the generation process.
184 ///
185 /// In case there's something that needs to be output at the end of the generation process, it
186 /// goes here. Similar to [`generate`](Self::generate), the output should be appended to
187 /// `buf`.
188 ///
189 /// An example can be a module or other thing that needs to appear just once, not for each
190 /// service generated.
191 ///
192 /// This still can be called multiple times in a lifetime of the service generator, because it
193 /// is called once per `.proto` file.
194 ///
195 /// The default implementation is empty and does nothing.
196 fn finalize(&mut self, _buf: &mut String) {}
197
198 /// Finalizes the generation process for an entire protobuf package.
199 ///
200 /// This differs from [`finalize`](Self::finalize) by where (and how often) it is called
201 /// during the service generator life cycle. This method is called once per protobuf package,
202 /// making it ideal for grouping services within a single package spread across multiple
203 /// `.proto` files.
204 ///
205 /// The default implementation is empty and does nothing.
206 fn finalize_package(&mut self, _package: &str, _buf: &mut String) {}
207}
208
209/// Compile `.proto` files into Rust files during a Cargo build.
210///
211/// The generated `.rs` files are written to the Cargo `OUT_DIR` directory, suitable for use with
212/// the [include!][1] macro. See the [Cargo `build.rs` code generation][2] example for more info.
213///
214/// This function should be called in a project's `build.rs`.
215///
216/// # Arguments
217///
218/// **`protos`** - Paths to `.proto` files to compile. Any transitively [imported][3] `.proto`
219/// files are automatically be included.
220///
221/// **`includes`** - Paths to directories in which to search for imports. Directories are searched
222/// in order. The `.proto` files passed in **`protos`** must be found in one of the provided
223/// include directories.
224///
225/// # Errors
226///
227/// This function can fail for a number of reasons:
228///
229/// - Failure to locate or download `protoc`.
230/// - Failure to parse the `.proto`s.
231/// - Failure to locate an imported `.proto`.
232/// - Failure to compile a `.proto` without a [package specifier][4].
233///
234/// It's expected that this function call be `unwrap`ed in a `build.rs`; there is typically no
235/// reason to gracefully recover from errors during a build.
236///
237/// # Example `build.rs`
238///
239/// ```rust,no_run
240/// # use std::io::Result;
241/// fn main() -> Result<()> {
242/// prost_build::compile_protos(&["src/frontend.proto", "src/backend.proto"], &["src"])?;
243/// Ok(())
244/// }
245/// ```
246///
247/// [1]: https://doc.rust-lang.org/std/macro.include.html
248/// [2]: https://doc.rust-lang.org/cargo/reference/build-script-examples.html
249/// [3]: https://protobuf.dev/programming-guides/proto3/#importing
250/// [4]: https://protobuf.dev/programming-guides/proto3/#packages
251pub fn compile_protos(protos: &[impl AsRef<Path>], includes: &[impl AsRef<Path>]) -> Result<()> {
252 Config::new().compile_protos(protos, includes)
253}
254
255/// Compile a [`FileDescriptorSet`] into Rust files during a Cargo build.
256///
257/// The generated `.rs` files are written to the Cargo `OUT_DIR` directory, suitable for use with
258/// the [include!][1] macro. See the [Cargo `build.rs` code generation][2] example for more info.
259///
260/// This function should be called in a project's `build.rs`.
261///
262/// This function can be combined with a crate like [`protox`] which outputs a
263/// [`FileDescriptorSet`] and is a pure Rust implementation of `protoc`.
264///
265/// # Example
266/// ```rust,no_run
267/// # use prost_types::FileDescriptorSet;
268/// # fn fds() -> FileDescriptorSet { todo!() }
269/// fn main() -> std::io::Result<()> {
270/// let file_descriptor_set = fds();
271///
272/// prost_build::compile_fds(file_descriptor_set)
273/// }
274/// ```
275///
276/// [`protox`]: https://github.com/andrewhickman/protox
277/// [1]: https://doc.rust-lang.org/std/macro.include.html
278/// [2]: https://doc.rust-lang.org/cargo/reference/build-script-examples.html
279pub fn compile_fds(fds: FileDescriptorSet) -> Result<()> {
280 Config::new().compile_fds(fds)
281}
282
283#[cfg(test)]
284mod tests {
285 use std::cell::RefCell;
286 use std::rc::Rc;
287
288 use super::*;
289
290 macro_rules! assert_eq_fixture_file {
291 ($expected_path:expr, $actual_path:expr) => {{
292 let actual = std::fs::read_to_string($actual_path).expect("Failed to read actual file");
293
294 // Normalizes windows and Linux-style EOL
295 let actual = actual.replace("\r\n", "\n");
296
297 assert_eq_fixture_contents!($expected_path, actual);
298 }};
299 }
300
301 macro_rules! assert_eq_fixture_contents {
302 ($expected_path:expr, $actual:expr) => {{
303 let expected =
304 std::fs::read_to_string($expected_path).expect("Failed to read expected file");
305
306 // Normalizes windows and Linux-style EOL
307 let expected = expected.replace("\r\n", "\n");
308
309 if expected != $actual {
310 std::fs::write($expected_path, &$actual).expect("Failed to write expected file");
311 }
312
313 assert_eq!(expected, $actual);
314 }};
315 }
316
317 /// An example service generator that generates a trait with methods corresponding to the
318 /// service methods.
319 struct ServiceTraitGenerator;
320
321 impl ServiceGenerator for ServiceTraitGenerator {
322 fn generate(&mut self, service: Service, buf: &mut String) {
323 // Generate a trait for the service.
324 service.comments.append_with_indent(0, buf);
325 buf.push_str(&format!("trait {} {{\n", &service.name));
326
327 // Generate the service methods.
328 for method in service.methods {
329 method.comments.append_with_indent(1, buf);
330 buf.push_str(&format!(
331 " fn {}(_: {}) -> {};\n",
332 method.name, method.input_type, method.output_type
333 ));
334 }
335
336 // Close out the trait.
337 buf.push_str("}\n");
338 }
339 fn finalize(&mut self, buf: &mut String) {
340 // Needs to be present only once, no matter how many services there are
341 buf.push_str("pub mod utils { }\n");
342 }
343 }
344
345 /// Implements `ServiceGenerator` and provides some state for assertions.
346 struct MockServiceGenerator {
347 state: Rc<RefCell<MockState>>,
348 }
349
350 /// Holds state for `MockServiceGenerator`
351 #[derive(Default)]
352 struct MockState {
353 service_names: Vec<String>,
354 package_names: Vec<String>,
355 finalized: u32,
356 }
357
358 impl MockServiceGenerator {
359 fn new(state: Rc<RefCell<MockState>>) -> Self {
360 Self { state }
361 }
362 }
363
364 impl ServiceGenerator for MockServiceGenerator {
365 fn generate(&mut self, service: Service, _buf: &mut String) {
366 let mut state = self.state.borrow_mut();
367 state.service_names.push(service.name);
368 }
369
370 fn finalize(&mut self, _buf: &mut String) {
371 let mut state = self.state.borrow_mut();
372 state.finalized += 1;
373 }
374
375 fn finalize_package(&mut self, package: &str, _buf: &mut String) {
376 let mut state = self.state.borrow_mut();
377 state.package_names.push(package.to_string());
378 }
379 }
380
381 #[test]
382 fn smoke_test() {
383 let _ = env_logger::try_init();
384 let tempdir = tempfile::tempdir().unwrap();
385
386 Config::new()
387 .service_generator(Box::new(ServiceTraitGenerator))
388 .out_dir(tempdir.path())
389 .compile_protos(&["src/fixtures/smoke_test/smoke_test.proto"], &["src"])
390 .unwrap();
391
392 // Check all generated files against fixture
393 for entry in std::fs::read_dir(tempdir.path()).unwrap() {
394 let file = entry.unwrap();
395 let file_name = file.file_name().into_string().unwrap();
396
397 assert_eq!(file_name, "smoke_test.rs");
398 assert_eq_fixture_file!(
399 if cfg!(feature = "format") {
400 "src/fixtures/smoke_test/_expected_smoke_test_formatted.rs"
401 } else {
402 "src/fixtures/smoke_test/_expected_smoke_test.rs"
403 },
404 file.path()
405 );
406 }
407 }
408
409 #[test]
410 fn finalize_package() {
411 let _ = env_logger::try_init();
412 let tempdir = tempfile::tempdir().unwrap();
413
414 let state = Rc::new(RefCell::new(MockState::default()));
415 let generator = MockServiceGenerator::new(Rc::clone(&state));
416
417 Config::new()
418 .service_generator(Box::new(generator))
419 .include_file("_protos.rs")
420 .out_dir(tempdir.path())
421 .compile_protos(
422 &[
423 "src/fixtures/helloworld/hello.proto",
424 "src/fixtures/helloworld/goodbye.proto",
425 ],
426 &["src/fixtures/helloworld"],
427 )
428 .unwrap();
429
430 let state = state.borrow();
431 assert_eq!(&state.service_names, &["Greeting", "Farewell"]);
432 assert_eq!(&state.package_names, &["helloworld"]);
433 assert_eq!(state.finalized, 3);
434 }
435
436 #[test]
437 fn test_generate_message_attributes() {
438 let _ = env_logger::try_init();
439 let tempdir = tempfile::tempdir().unwrap();
440
441 let mut config = Config::new();
442 config
443 .out_dir(tempdir.path())
444 // Add attributes to all messages and enums
445 .message_attribute(".", "#[derive(derive_builder::Builder)]")
446 .enum_attribute(".", "#[some_enum_attr(u8)]");
447
448 let fds = config
449 .load_fds(
450 &["src/fixtures/helloworld/hello.proto"],
451 &["src/fixtures/helloworld"],
452 )
453 .unwrap();
454
455 // Add custom attributes to messages that are service inputs or outputs.
456 for file in &fds.file {
457 for service in &file.service {
458 for method in &service.method {
459 if let Some(input) = &method.input_type {
460 config.message_attribute(input, "#[derive(custom_proto::Input)]");
461 }
462 if let Some(output) = &method.output_type {
463 config.message_attribute(output, "#[derive(custom_proto::Output)]");
464 }
465 }
466 }
467 }
468
469 config.compile_fds(fds).unwrap();
470
471 // Check all generated files against fixture
472 for entry in std::fs::read_dir(tempdir.path()).unwrap() {
473 let file = entry.unwrap();
474 let file_name = file.file_name().into_string().unwrap();
475
476 assert_eq_fixture_file!(
477 format!("src/fixtures/helloworld/_expected_{file_name}"),
478 file.path()
479 );
480 }
481 }
482
483 #[test]
484 fn test_generate_no_empty_outputs() {
485 let _ = env_logger::try_init();
486 let state = Rc::new(RefCell::new(MockState::default()));
487 let generator = MockServiceGenerator::new(Rc::clone(&state));
488 let include_file = "_include.rs";
489 let tempdir = tempfile::tempdir().unwrap();
490 let previously_empty_proto_path = tempdir.path().join(Path::new("google.protobuf.rs"));
491
492 Config::new()
493 .service_generator(Box::new(generator))
494 .include_file(include_file)
495 .out_dir(tempdir.path())
496 .compile_protos(
497 &["src/fixtures/imports_empty/imports_empty.proto"],
498 &["src/fixtures/imports_empty"],
499 )
500 .unwrap();
501
502 // Prior to PR introducing this test, the generated include file would have the file
503 // google.protobuf.rs which was an empty file. Now that file should only exist if it has content
504 assert!(!std::fs::exists(previously_empty_proto_path).unwrap());
505
506 // Check all generated files against fixture
507 for entry in std::fs::read_dir(tempdir.path()).unwrap() {
508 let file = entry.unwrap();
509 let file_name = file.file_name().into_string().unwrap();
510 if file_name == include_file {
511 // `google.protobuf.rs` wasn't generated so the result include file should not reference it
512 assert_eq_fixture_file!(
513 "src/fixtures/imports_empty/_expected_include.rs",
514 file.path()
515 );
516 } else if file_name == "com.prost_test.test.v1.rs" {
517 let content = std::fs::read_to_string(file.path()).unwrap();
518 assert!(content.contains("struct TestConfig"));
519 assert!(content.contains("struct GetTestResponse"));
520 } else {
521 panic!("Found unexpected file: {}", file_name);
522 }
523 }
524 }
525
526 #[test]
527 fn test_generate_field_attributes() {
528 let _ = env_logger::try_init();
529 let tempdir = tempfile::tempdir().unwrap();
530
531 Config::new()
532 .out_dir(tempdir.path())
533 .boxed("Container.data.foo")
534 .boxed("Bar.qux")
535 .compile_protos(
536 &["src/fixtures/field_attributes/field_attributes.proto"],
537 &["src/fixtures/field_attributes"],
538 )
539 .unwrap();
540
541 assert_eq_fixture_file!(
542 if cfg!(feature = "format") {
543 "src/fixtures/field_attributes/_expected_field_attributes_formatted.rs"
544 } else {
545 "src/fixtures/field_attributes/_expected_field_attributes.rs"
546 },
547 tempdir.path().join("field_attributes.rs")
548 );
549 }
550
551 #[test]
552 fn deterministic_include_file() {
553 let _ = env_logger::try_init();
554
555 for _ in 1..10 {
556 let state = Rc::new(RefCell::new(MockState::default()));
557 let generator = MockServiceGenerator::new(Rc::clone(&state));
558 let include_file = "_include.rs";
559 let tempdir = tempfile::tempdir().unwrap();
560
561 Config::new()
562 .service_generator(Box::new(generator))
563 .include_file(include_file)
564 .out_dir(tempdir.path())
565 .compile_protos(
566 &[
567 "src/fixtures/alphabet/a.proto",
568 "src/fixtures/alphabet/b.proto",
569 "src/fixtures/alphabet/c.proto",
570 "src/fixtures/alphabet/d.proto",
571 "src/fixtures/alphabet/e.proto",
572 "src/fixtures/alphabet/f.proto",
573 ],
574 &["src/fixtures/alphabet"],
575 )
576 .unwrap();
577
578 assert_eq_fixture_file!(
579 "src/fixtures/alphabet/_expected_include.rs",
580 tempdir.path().join(Path::new(include_file))
581 );
582 }
583 }
584
585 #[test]
586 fn write_includes() {
587 let modules = [
588 Module::from_protobuf_package_name("foo.bar.baz"),
589 Module::from_protobuf_package_name(""),
590 Module::from_protobuf_package_name("foo.bar"),
591 Module::from_protobuf_package_name("bar"),
592 Module::from_protobuf_package_name("foo"),
593 Module::from_protobuf_package_name("foo.bar.qux"),
594 Module::from_protobuf_package_name("foo.bar.a.b.c"),
595 ];
596
597 let file_names = modules
598 .iter()
599 .map(|m| (m.clone(), m.to_file_name_or("_.default")))
600 .collect();
601
602 let mut buf = Vec::new();
603 Config::new()
604 .default_package_filename("_.default")
605 .write_includes(modules.iter().collect(), &mut buf, None, &file_names)
606 .unwrap();
607 let actual = String::from_utf8(buf).unwrap();
608 assert_eq_fixture_contents!("src/fixtures/write_includes/_.includes.rs", actual);
609 }
610
611 #[test]
612 fn test_generate_deprecated() {
613 let _ = env_logger::try_init();
614 let tempdir = tempfile::tempdir().unwrap();
615
616 Config::new()
617 .out_dir(tempdir.path())
618 .compile_protos(
619 &["src/fixtures/deprecated/all_deprecated.proto"],
620 &["src/fixtures/deprecated"],
621 )
622 .unwrap();
623
624 assert_eq_fixture_file!(
625 "src/fixtures/deprecated/_all_deprecated.rs",
626 tempdir.path().join("all_deprecated.rs")
627 );
628 }
629}