1 //! API to invoke `protoc` command.
2 //!
3 //! `protoc` command must be in `$PATH`, along with `protoc-gen-LANG` command.
4 //!
5 //! Note that to generate `rust` code from `.proto` files, `protoc-rust` crate
6 //! can be used, which does not require `protoc-gen-rust` present in `$PATH`.
7 
8 #![deny(missing_docs)]
9 #![deny(rustdoc::broken_intra_doc_links)]
10 
11 use std::ffi::OsStr;
12 use std::ffi::OsString;
13 use std::fmt;
14 use std::io;
15 use std::path::Path;
16 use std::path::PathBuf;
17 use std::process;
18 use std::process::Stdio;
19 
20 use log::info;
21 
22 #[derive(Debug, thiserror::Error)]
23 enum Error {
24     #[error("protoc command exited with non-zero code")]
25     ProtocNonZero,
26     #[error("protoc command {0} exited with non-zero code")]
27     ProtocNamedNonZero(String),
28     #[error("protoc command {0} exited with non-zero code; stderr: {1:?}")]
29     ProtocNamedNonZeroStderr(String, String),
30     #[error("input is empty")]
31     InputIsEmpty,
32     #[error("output is empty")]
33     OutputIsEmpty,
34     #[error("output does not start with prefix")]
35     OutputDoesNotStartWithPrefix,
36     #[error("version is empty")]
37     VersionIsEmpty,
38     #[error("version does not start with digit")]
39     VersionDoesNotStartWithDigit,
40     #[error("failed to spawn command `{0}`")]
41     FailedToSpawnCommand(String, #[source] io::Error),
42     #[error("protoc output is not UTF-8")]
43     ProtocOutputIsNotUtf8,
44 }
45 
46 /// `Protoc --descriptor_set_out...` args
47 #[derive(Debug)]
48 pub(crate) struct DescriptorSetOutArgs {
49     protoc: Protoc,
50     /// `--file_descriptor_out=...` param
51     out: Option<PathBuf>,
52     /// `-I` args
53     includes: Vec<PathBuf>,
54     /// List of `.proto` files to compile
55     inputs: Vec<PathBuf>,
56     /// `--include_imports`
57     include_imports: bool,
58     /// Extra command line flags (like `--experimental_allow_proto3_optional`)
59     extra_args: Vec<OsString>,
60     /// Capture stderr instead of inheriting it.
61     capture_stderr: bool,
62 }
63 
64 impl DescriptorSetOutArgs {
65     /// Set `--file_descriptor_out=...` param
out(&mut self, out: impl AsRef<Path>) -> &mut Self66     pub fn out(&mut self, out: impl AsRef<Path>) -> &mut Self {
67         self.out = Some(out.as_ref().to_owned());
68         self
69     }
70 
71     /// Append a path to `-I` args
include(&mut self, include: impl AsRef<Path>) -> &mut Self72     pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
73         self.includes.push(include.as_ref().to_owned());
74         self
75     }
76 
77     /// Append multiple paths to `-I` args
includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self78     pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
79         for include in includes {
80             self.include(include);
81         }
82         self
83     }
84 
85     /// Append a `.proto` file path to compile
input(&mut self, input: impl AsRef<Path>) -> &mut Self86     pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
87         self.inputs.push(input.as_ref().to_owned());
88         self
89     }
90 
91     /// Append multiple `.proto` file paths to compile
inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self92     pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
93         for input in inputs {
94             self.input(input);
95         }
96         self
97     }
98 
99     /// Set `--include_imports`
include_imports(&mut self, include_imports: bool) -> &mut Self100     pub fn include_imports(&mut self, include_imports: bool) -> &mut Self {
101         self.include_imports = include_imports;
102         self
103     }
104 
105     /// Add command line flags like `--experimental_allow_proto3_optional`.
extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self106     pub fn extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
107         self.extra_args.push(arg.into());
108         self
109     }
110 
111     /// Add command line flags like `--experimental_allow_proto3_optional`.
extra_args(&mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> &mut Self112     pub fn extra_args(&mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> &mut Self {
113         for arg in args {
114             self.extra_arg(arg);
115         }
116         self
117     }
118 
119     /// Capture stderr instead of inheriting it.
capture_stderr(&mut self, capture_stderr: bool) -> &mut Self120     pub(crate) fn capture_stderr(&mut self, capture_stderr: bool) -> &mut Self {
121         self.capture_stderr = capture_stderr;
122         self
123     }
124 
125     /// Execute `protoc --descriptor_set_out=`
write_descriptor_set(&self) -> anyhow::Result<()>126     pub fn write_descriptor_set(&self) -> anyhow::Result<()> {
127         if self.inputs.is_empty() {
128             return Err(Error::InputIsEmpty.into());
129         }
130 
131         let out = self.out.as_ref().ok_or_else(|| Error::OutputIsEmpty)?;
132 
133         // -I{include}
134         let include_flags = self.includes.iter().map(|include| {
135             let mut flag = OsString::from("-I");
136             flag.push(include);
137             flag
138         });
139 
140         // --descriptor_set_out={out}
141         let mut descriptor_set_out_flag = OsString::from("--descriptor_set_out=");
142         descriptor_set_out_flag.push(out);
143 
144         // --include_imports
145         let include_imports_flag = match self.include_imports {
146             false => None,
147             true => Some("--include_imports".into()),
148         };
149 
150         let mut cmd_args = Vec::new();
151         cmd_args.extend(include_flags);
152         cmd_args.push(descriptor_set_out_flag);
153         cmd_args.extend(include_imports_flag);
154         cmd_args.extend(self.inputs.iter().map(|path| path.as_os_str().to_owned()));
155         cmd_args.extend(self.extra_args.iter().cloned());
156         self.protoc.run_with_args(cmd_args, self.capture_stderr)
157     }
158 }
159 
160 /// Protoc command.
161 #[derive(Clone, Debug)]
162 pub(crate) struct Protoc {
163     exec: OsString,
164 }
165 
166 impl Protoc {
167     /// New `protoc` command from `$PATH`
from_env_path() -> Protoc168     pub(crate) fn from_env_path() -> Protoc {
169         match which::which("protoc") {
170             Ok(path) => Protoc {
171                 exec: path.into_os_string(),
172             },
173             Err(e) => {
174                 panic!("protoc binary not found: {}", e);
175             }
176         }
177     }
178 
179     /// New `protoc` command from specified path
180     ///
181     /// # Examples
182     ///
183     /// ```no_run
184     /// # mod protoc_bin_vendored {
185     /// #   pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> {
186     /// #       unimplemented!()
187     /// #   }
188     /// # }
189     ///
190     /// // Use a binary from `protoc-bin-vendored` crate
191     /// let protoc = protoc::Protoc::from_path(
192     ///     protoc_bin_vendored::protoc_bin_path().unwrap());
193     /// ```
from_path(path: impl AsRef<OsStr>) -> Protoc194     pub(crate) fn from_path(path: impl AsRef<OsStr>) -> Protoc {
195         Protoc {
196             exec: path.as_ref().to_owned(),
197         }
198     }
199 
200     /// Check `protoc` command found and valid
_check(&self) -> anyhow::Result<()>201     pub(crate) fn _check(&self) -> anyhow::Result<()> {
202         self.version()?;
203         Ok(())
204     }
205 
spawn(&self, cmd: &mut process::Command) -> anyhow::Result<process::Child>206     fn spawn(&self, cmd: &mut process::Command) -> anyhow::Result<process::Child> {
207         info!("spawning command {:?}", cmd);
208 
209         cmd.spawn()
210             .map_err(|e| Error::FailedToSpawnCommand(format!("{:?}", cmd), e).into())
211     }
212 
213     /// Obtain `protoc` version
version(&self) -> anyhow::Result<Version>214     pub(crate) fn version(&self) -> anyhow::Result<Version> {
215         let child = self.spawn(
216             process::Command::new(&self.exec)
217                 .stdin(process::Stdio::null())
218                 .stdout(process::Stdio::piped())
219                 .stderr(process::Stdio::piped())
220                 .args(&["--version"]),
221         )?;
222 
223         let output = child.wait_with_output()?;
224         if !output.status.success() {
225             return Err(Error::ProtocNonZero.into());
226         }
227         let output = String::from_utf8(output.stdout).map_err(|_| Error::ProtocOutputIsNotUtf8)?;
228         let output = match output.lines().next() {
229             None => return Err(Error::OutputIsEmpty.into()),
230             Some(line) => line,
231         };
232         let prefix = "libprotoc ";
233         if !output.starts_with(prefix) {
234             return Err(Error::OutputDoesNotStartWithPrefix.into());
235         }
236         let output = &output[prefix.len()..];
237         if output.is_empty() {
238             return Err(Error::VersionIsEmpty.into());
239         }
240         let first = output.chars().next().unwrap();
241         if !first.is_digit(10) {
242             return Err(Error::VersionDoesNotStartWithDigit.into());
243         }
244         Ok(Version {
245             version: output.to_owned(),
246         })
247     }
248 
249     /// Execute `protoc` command with given args, check it completed correctly.
run_with_args(&self, args: Vec<OsString>, capture_stderr: bool) -> anyhow::Result<()>250     fn run_with_args(&self, args: Vec<OsString>, capture_stderr: bool) -> anyhow::Result<()> {
251         let mut cmd = process::Command::new(&self.exec);
252         cmd.stdin(process::Stdio::null());
253         cmd.args(args);
254 
255         if capture_stderr {
256             cmd.stderr(Stdio::piped());
257         }
258 
259         let mut child = self.spawn(&mut cmd)?;
260 
261         if capture_stderr {
262             let output = child.wait_with_output()?;
263             if !output.status.success() {
264                 let stderr = String::from_utf8_lossy(&output.stderr);
265                 let stderr = stderr.trim_end().to_owned();
266                 return Err(Error::ProtocNamedNonZeroStderr(format!("{:?}", cmd), stderr).into());
267             }
268         } else {
269             if !child.wait()?.success() {
270                 return Err(Error::ProtocNamedNonZero(format!("{:?}", cmd)).into());
271             }
272         }
273 
274         Ok(())
275     }
276 
277     /// Get default DescriptorSetOutArgs for this command.
descriptor_set_out_args(&self) -> DescriptorSetOutArgs278     pub(crate) fn descriptor_set_out_args(&self) -> DescriptorSetOutArgs {
279         DescriptorSetOutArgs {
280             protoc: self.clone(),
281             out: None,
282             includes: Vec::new(),
283             inputs: Vec::new(),
284             include_imports: false,
285             extra_args: Vec::new(),
286             capture_stderr: false,
287         }
288     }
289 }
290 
291 /// Protobuf (protoc) version.
292 pub(crate) struct Version {
293     version: String,
294 }
295 
296 impl Version {
297     /// `true` if the protoc major version is 3.
_is_3(&self) -> bool298     pub fn _is_3(&self) -> bool {
299         self.version.starts_with("3")
300     }
301 }
302 
303 impl fmt::Display for Version {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result304     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
305         fmt::Display::fmt(&self.version, f)
306     }
307 }
308 
309 #[cfg(test)]
310 mod test {
311     use super::*;
312 
313     #[test]
version()314     fn version() {
315         Protoc::from_env_path().version().expect("version");
316     }
317 }
318