1 use std::env;
2 use std::ffi::OsString;
3 use std::fs;
4 use std::path::Component;
5 use std::path::Path;
6 use std::path::PathBuf;
7 use std::process;
8 
9 use anyhow::Context;
10 use protobuf_parse::Parser;
11 
12 use crate::customize::CustomizeCallback;
13 use crate::customize::CustomizeCallbackHolder;
14 use crate::gen_and_write::gen_and_write;
15 use crate::Customize;
16 
17 #[derive(Debug)]
18 enum WhichParser {
19     Pure,
20     Protoc,
21 }
22 
23 impl Default for WhichParser {
default() -> WhichParser24     fn default() -> WhichParser {
25         WhichParser::Pure
26     }
27 }
28 
29 #[derive(Debug, thiserror::Error)]
30 enum CodegenError {
31     #[error("out_dir is not specified")]
32     OutDirNotSpecified,
33 }
34 
35 /// Entry point for `.proto` to `.rs` code generation.
36 ///
37 /// This is similar to `protoc --rust_out...`.
38 #[derive(Debug, Default)]
39 pub struct Codegen {
40     /// What parser to use to parse `.proto` files.
41     which_parser: Option<WhichParser>,
42     /// Create out directory.
43     create_out_dir: bool,
44     /// --lang_out= param
45     out_dir: Option<PathBuf>,
46     /// -I args
47     includes: Vec<PathBuf>,
48     /// List of .proto files to compile
49     inputs: Vec<PathBuf>,
50     /// Customize code generation
51     customize: Customize,
52     /// Customize code generation
53     customize_callback: CustomizeCallbackHolder,
54     /// Protoc command path
55     protoc: Option<PathBuf>,
56     /// Extra `protoc` args
57     protoc_extra_args: Vec<OsString>,
58     /// Capture stderr when running `protoc`.
59     capture_stderr: bool,
60 }
61 
62 impl Codegen {
63     /// Create new codegen object.
64     ///
65     /// Uses `protoc` from `$PATH` by default.
66     ///
67     /// Can be switched to pure rust parser using [`pure`](Self::pure) function.
new() -> Self68     pub fn new() -> Self {
69         Self::default()
70     }
71 
72     /// Switch to pure Rust parser of `.proto` files.
pure(&mut self) -> &mut Self73     pub fn pure(&mut self) -> &mut Self {
74         self.which_parser = Some(WhichParser::Pure);
75         self
76     }
77 
78     /// Switch to `protoc` parser of `.proto` files.
protoc(&mut self) -> &mut Self79     pub fn protoc(&mut self) -> &mut Self {
80         self.which_parser = Some(WhichParser::Protoc);
81         self
82     }
83 
84     /// Output directory for generated code.
85     ///
86     /// When invoking from `build.rs`, consider using
87     /// [`cargo_out_dir`](Self::cargo_out_dir) instead.
out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self88     pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
89         self.out_dir = Some(out_dir.as_ref().to_owned());
90         self
91     }
92 
93     /// Set output directory relative to Cargo output dir.
94     ///
95     /// With this option, output directory is erased and recreated during invocation.
cargo_out_dir(&mut self, rel: &str) -> &mut Self96     pub fn cargo_out_dir(&mut self, rel: &str) -> &mut Self {
97         let rel = Path::new(rel);
98         let mut not_empty = false;
99         for comp in rel.components() {
100             match comp {
101                 Component::ParentDir => {
102                     panic!("parent path in components of rel path: `{}`", rel.display());
103                 }
104                 Component::CurDir => {
105                     continue;
106                 }
107                 Component::Normal(..) => {}
108                 Component::RootDir | Component::Prefix(..) => {
109                     panic!("root dir in components of rel path: `{}`", rel.display());
110                 }
111             }
112             not_empty = true;
113         }
114 
115         if !not_empty {
116             panic!("empty rel path: `{}`", rel.display());
117         }
118 
119         let cargo_out_dir = env::var("OUT_DIR").expect("OUT_DIR env var not set");
120         let mut path = PathBuf::from(cargo_out_dir);
121         path.push(rel);
122         self.create_out_dir = true;
123         self.out_dir(path)
124     }
125 
126     /// Add an include directory.
include(&mut self, include: impl AsRef<Path>) -> &mut Self127     pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
128         self.includes.push(include.as_ref().to_owned());
129         self
130     }
131 
132     /// Add include directories.
includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self133     pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
134         for include in includes {
135             self.include(include);
136         }
137         self
138     }
139 
140     /// Append a `.proto` file path to compile
input(&mut self, input: impl AsRef<Path>) -> &mut Self141     pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
142         self.inputs.push(input.as_ref().to_owned());
143         self
144     }
145 
146     /// Append multiple `.proto` file paths to compile
inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self147     pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
148         for input in inputs {
149             self.input(input);
150         }
151         self
152     }
153 
154     /// Specify `protoc` command path to be used when invoking code generation.
155     ///
156     /// # Examples
157     ///
158     /// ```no_run
159     /// # mod protoc_bin_vendored {
160     /// #   pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> {
161     /// #       unimplemented!()
162     /// #   }
163     /// # }
164     ///
165     /// use protobuf_codegen::Codegen;
166     ///
167     /// Codegen::new()
168     ///     .protoc()
169     ///     .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap())
170     ///     // ...
171     ///     .run()
172     ///     .unwrap();
173     /// ```
174     ///
175     /// This option is ignored when pure Rust parser is used.
protoc_path(&mut self, protoc: &Path) -> &mut Self176     pub fn protoc_path(&mut self, protoc: &Path) -> &mut Self {
177         self.protoc = Some(protoc.to_owned());
178         self
179     }
180 
181     /// Capture stderr to error when running `protoc`.
capture_stderr(&mut self) -> &mut Self182     pub fn capture_stderr(&mut self) -> &mut Self {
183         self.capture_stderr = true;
184         self
185     }
186 
187     /// Extra command line flags for `protoc` invocation.
188     ///
189     /// For example, `--experimental_allow_proto3_optional` option.
190     ///
191     /// This option is ignored when pure Rust parser is used.
protoc_extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self192     pub fn protoc_extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
193         self.protoc_extra_args.push(arg.into());
194         self
195     }
196 
197     /// Set options to customize code generation
customize(&mut self, customize: Customize) -> &mut Self198     pub fn customize(&mut self, customize: Customize) -> &mut Self {
199         self.customize.update_with(&customize);
200         self
201     }
202 
203     /// Callback for dynamic per-element customization.
customize_callback(&mut self, callback: impl CustomizeCallback) -> &mut Self204     pub fn customize_callback(&mut self, callback: impl CustomizeCallback) -> &mut Self {
205         self.customize_callback = CustomizeCallbackHolder::new(callback);
206         self
207     }
208 
209     /// Invoke the code generation.
210     ///
211     /// This is roughly equivalent to `protoc --rust_out=...` but
212     /// without requiring `protoc-gen-rust` command in `$PATH`.
213     ///
214     /// This function uses pure Rust parser or `protoc` parser depending on
215     /// how this object was configured.
run(&self) -> anyhow::Result<()>216     pub fn run(&self) -> anyhow::Result<()> {
217         let out_dir = match &self.out_dir {
218             Some(out_dir) => out_dir,
219             None => return Err(CodegenError::OutDirNotSpecified.into()),
220         };
221 
222         if self.create_out_dir {
223             if out_dir.exists() {
224                 fs::remove_dir_all(&out_dir)?;
225             }
226             fs::create_dir(&out_dir)?;
227         }
228 
229         let mut parser = Parser::new();
230         parser.protoc();
231         if let Some(protoc) = &self.protoc {
232             parser.protoc_path(protoc);
233         }
234         match &self.which_parser {
235             Some(WhichParser::Protoc) => {
236                 parser.protoc();
237             }
238             Some(WhichParser::Pure) => {
239                 parser.pure();
240             }
241             None => {}
242         }
243 
244         parser.inputs(&self.inputs);
245         parser.includes(&self.includes);
246 
247         if self.capture_stderr {
248             parser.capture_stderr();
249         }
250 
251         let parsed_and_typechecked = parser
252             .parse_and_typecheck()
253             .context("parse and typecheck")?;
254 
255         gen_and_write(
256             &parsed_and_typechecked.file_descriptors,
257             &parsed_and_typechecked.parser,
258             &parsed_and_typechecked.relative_paths,
259             &out_dir,
260             &self.customize,
261             &*self.customize_callback,
262         )
263     }
264 
265     /// Similar to `run`, but prints the message to stderr and exits the process on error.
run_from_script(&self)266     pub fn run_from_script(&self) {
267         if let Err(e) = self.run() {
268             eprintln!("codegen failed: {:?}", e);
269             process::exit(1);
270         }
271     }
272 }
273