//! API to invoke `protoc` command. //! //! `protoc` command must be in `$PATH`, along with `protoc-gen-LANG` command. //! //! Note that to generate `rust` code from `.proto` files, `protoc-rust` crate //! can be used, which does not require `protoc-gen-rust` present in `$PATH`. #![deny(missing_docs)] #![deny(rustdoc::broken_intra_doc_links)] use std::ffi::OsStr; use std::ffi::OsString; use std::fmt; use std::io; use std::path::Path; use std::path::PathBuf; use std::process; use std::process::Stdio; use log::info; #[derive(Debug, thiserror::Error)] enum Error { #[error("protoc command exited with non-zero code")] ProtocNonZero, #[error("protoc command {0} exited with non-zero code")] ProtocNamedNonZero(String), #[error("protoc command {0} exited with non-zero code; stderr: {1:?}")] ProtocNamedNonZeroStderr(String, String), #[error("input is empty")] InputIsEmpty, #[error("output is empty")] OutputIsEmpty, #[error("output does not start with prefix")] OutputDoesNotStartWithPrefix, #[error("version is empty")] VersionIsEmpty, #[error("version does not start with digit")] VersionDoesNotStartWithDigit, #[error("failed to spawn command `{0}`")] FailedToSpawnCommand(String, #[source] io::Error), #[error("protoc output is not UTF-8")] ProtocOutputIsNotUtf8, } /// `Protoc --descriptor_set_out...` args #[derive(Debug)] pub(crate) struct DescriptorSetOutArgs { protoc: Protoc, /// `--file_descriptor_out=...` param out: Option, /// `-I` args includes: Vec, /// List of `.proto` files to compile inputs: Vec, /// `--include_imports` include_imports: bool, /// Extra command line flags (like `--experimental_allow_proto3_optional`) extra_args: Vec, /// Capture stderr instead of inheriting it. capture_stderr: bool, } impl DescriptorSetOutArgs { /// Set `--file_descriptor_out=...` param pub fn out(&mut self, out: impl AsRef) -> &mut Self { self.out = Some(out.as_ref().to_owned()); self } /// Append a path to `-I` args pub fn include(&mut self, include: impl AsRef) -> &mut Self { self.includes.push(include.as_ref().to_owned()); self } /// Append multiple paths to `-I` args pub fn includes(&mut self, includes: impl IntoIterator>) -> &mut Self { for include in includes { self.include(include); } self } /// Append a `.proto` file path to compile pub fn input(&mut self, input: impl AsRef) -> &mut Self { self.inputs.push(input.as_ref().to_owned()); self } /// Append multiple `.proto` file paths to compile pub fn inputs(&mut self, inputs: impl IntoIterator>) -> &mut Self { for input in inputs { self.input(input); } self } /// Set `--include_imports` pub fn include_imports(&mut self, include_imports: bool) -> &mut Self { self.include_imports = include_imports; self } /// Add command line flags like `--experimental_allow_proto3_optional`. pub fn extra_arg(&mut self, arg: impl Into) -> &mut Self { self.extra_args.push(arg.into()); self } /// Add command line flags like `--experimental_allow_proto3_optional`. pub fn extra_args(&mut self, args: impl IntoIterator>) -> &mut Self { for arg in args { self.extra_arg(arg); } self } /// Capture stderr instead of inheriting it. pub(crate) fn capture_stderr(&mut self, capture_stderr: bool) -> &mut Self { self.capture_stderr = capture_stderr; self } /// Execute `protoc --descriptor_set_out=` pub fn write_descriptor_set(&self) -> anyhow::Result<()> { if self.inputs.is_empty() { return Err(Error::InputIsEmpty.into()); } let out = self.out.as_ref().ok_or_else(|| Error::OutputIsEmpty)?; // -I{include} let include_flags = self.includes.iter().map(|include| { let mut flag = OsString::from("-I"); flag.push(include); flag }); // --descriptor_set_out={out} let mut descriptor_set_out_flag = OsString::from("--descriptor_set_out="); descriptor_set_out_flag.push(out); // --include_imports let include_imports_flag = match self.include_imports { false => None, true => Some("--include_imports".into()), }; let mut cmd_args = Vec::new(); cmd_args.extend(include_flags); cmd_args.push(descriptor_set_out_flag); cmd_args.extend(include_imports_flag); cmd_args.extend(self.inputs.iter().map(|path| path.as_os_str().to_owned())); cmd_args.extend(self.extra_args.iter().cloned()); self.protoc.run_with_args(cmd_args, self.capture_stderr) } } /// Protoc command. #[derive(Clone, Debug)] pub(crate) struct Protoc { exec: OsString, } impl Protoc { /// New `protoc` command from `$PATH` pub(crate) fn from_env_path() -> Protoc { match which::which("protoc") { Ok(path) => Protoc { exec: path.into_os_string(), }, Err(e) => { panic!("protoc binary not found: {}", e); } } } /// New `protoc` command from specified path /// /// # Examples /// /// ```no_run /// # mod protoc_bin_vendored { /// # pub fn protoc_bin_path() -> Result { /// # unimplemented!() /// # } /// # } /// /// // Use a binary from `protoc-bin-vendored` crate /// let protoc = protoc::Protoc::from_path( /// protoc_bin_vendored::protoc_bin_path().unwrap()); /// ``` pub(crate) fn from_path(path: impl AsRef) -> Protoc { Protoc { exec: path.as_ref().to_owned(), } } /// Check `protoc` command found and valid pub(crate) fn _check(&self) -> anyhow::Result<()> { self.version()?; Ok(()) } fn spawn(&self, cmd: &mut process::Command) -> anyhow::Result { info!("spawning command {:?}", cmd); cmd.spawn() .map_err(|e| Error::FailedToSpawnCommand(format!("{:?}", cmd), e).into()) } /// Obtain `protoc` version pub(crate) fn version(&self) -> anyhow::Result { let child = self.spawn( process::Command::new(&self.exec) .stdin(process::Stdio::null()) .stdout(process::Stdio::piped()) .stderr(process::Stdio::piped()) .args(&["--version"]), )?; let output = child.wait_with_output()?; if !output.status.success() { return Err(Error::ProtocNonZero.into()); } let output = String::from_utf8(output.stdout).map_err(|_| Error::ProtocOutputIsNotUtf8)?; let output = match output.lines().next() { None => return Err(Error::OutputIsEmpty.into()), Some(line) => line, }; let prefix = "libprotoc "; if !output.starts_with(prefix) { return Err(Error::OutputDoesNotStartWithPrefix.into()); } let output = &output[prefix.len()..]; if output.is_empty() { return Err(Error::VersionIsEmpty.into()); } let first = output.chars().next().unwrap(); if !first.is_digit(10) { return Err(Error::VersionDoesNotStartWithDigit.into()); } Ok(Version { version: output.to_owned(), }) } /// Execute `protoc` command with given args, check it completed correctly. fn run_with_args(&self, args: Vec, capture_stderr: bool) -> anyhow::Result<()> { let mut cmd = process::Command::new(&self.exec); cmd.stdin(process::Stdio::null()); cmd.args(args); if capture_stderr { cmd.stderr(Stdio::piped()); } let mut child = self.spawn(&mut cmd)?; if capture_stderr { let output = child.wait_with_output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stderr = stderr.trim_end().to_owned(); return Err(Error::ProtocNamedNonZeroStderr(format!("{:?}", cmd), stderr).into()); } } else { if !child.wait()?.success() { return Err(Error::ProtocNamedNonZero(format!("{:?}", cmd)).into()); } } Ok(()) } /// Get default DescriptorSetOutArgs for this command. pub(crate) fn descriptor_set_out_args(&self) -> DescriptorSetOutArgs { DescriptorSetOutArgs { protoc: self.clone(), out: None, includes: Vec::new(), inputs: Vec::new(), include_imports: false, extra_args: Vec::new(), capture_stderr: false, } } } /// Protobuf (protoc) version. pub(crate) struct Version { version: String, } impl Version { /// `true` if the protoc major version is 3. pub fn _is_3(&self) -> bool { self.version.starts_with("3") } } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.version, f) } } #[cfg(test)] mod test { use super::*; #[test] fn version() { Protoc::from_env_path().version().expect("version"); } }