protobuf_parse/protoc/
command.rs

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-rs` present in `$PATH`.
7
8#![deny(missing_docs)]
9#![deny(rustdoc::broken_intra_doc_links)]
10
11use std::ffi::OsStr;
12use std::ffi::OsString;
13use std::fmt;
14use std::io;
15use std::path::Path;
16use std::path::PathBuf;
17use std::process;
18use std::process::Stdio;
19
20use log::info;
21
22#[derive(Debug, thiserror::Error)]
23enum 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)]
48pub(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
64impl DescriptorSetOutArgs {
65    /// Set `--file_descriptor_out=...` param
66    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
72    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
78    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
86    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
92    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`
100    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`.
106    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`.
112    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.
120    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=`
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)]
162pub(crate) struct Protoc {
163    exec: OsString,
164}
165
166impl Protoc {
167    /// New `protoc` command from `$PATH`
168    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    /// ```
194    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
201    pub(crate) fn _check(&self) -> anyhow::Result<()> {
202        self.version()?;
203        Ok(())
204    }
205
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
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.
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.
278    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.
292pub(crate) struct Version {
293    version: String,
294}
295
296impl Version {
297    /// `true` if the protoc major version is 3.
298    pub fn _is_3(&self) -> bool {
299        self.version.starts_with("3")
300    }
301}
302
303impl fmt::Display for Version {
304    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
305        fmt::Display::fmt(&self.version, f)
306    }
307}
308
309#[cfg(test)]
310mod test {
311    use super::*;
312
313    #[test]
314    fn version() {
315        Protoc::from_env_path().version().expect("version");
316    }
317}