protobuf_parse/protoc/
command.rs
1#![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#[derive(Debug)]
48pub(crate) struct DescriptorSetOutArgs {
49 protoc: Protoc,
50 out: Option<PathBuf>,
52 includes: Vec<PathBuf>,
54 inputs: Vec<PathBuf>,
56 include_imports: bool,
58 extra_args: Vec<OsString>,
60 capture_stderr: bool,
62}
63
64impl DescriptorSetOutArgs {
65 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 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 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 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 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 pub fn include_imports(&mut self, include_imports: bool) -> &mut Self {
101 self.include_imports = include_imports;
102 self
103 }
104
105 pub fn extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
107 self.extra_args.push(arg.into());
108 self
109 }
110
111 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 pub(crate) fn capture_stderr(&mut self, capture_stderr: bool) -> &mut Self {
121 self.capture_stderr = capture_stderr;
122 self
123 }
124
125 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 let include_flags = self.includes.iter().map(|include| {
135 let mut flag = OsString::from("-I");
136 flag.push(include);
137 flag
138 });
139
140 let mut descriptor_set_out_flag = OsString::from("--descriptor_set_out=");
142 descriptor_set_out_flag.push(out);
143
144 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#[derive(Clone, Debug)]
162pub(crate) struct Protoc {
163 exec: OsString,
164}
165
166impl Protoc {
167 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 pub(crate) fn from_path(path: impl AsRef<OsStr>) -> Protoc {
195 Protoc {
196 exec: path.as_ref().to_owned(),
197 }
198 }
199
200 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 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 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 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
291pub(crate) struct Version {
293 version: String,
294}
295
296impl Version {
297 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}