blob: f29d7e3779ed14cc84d979b7668ca867e3a5e27a [file] [log] [blame] [edit]
//! 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<PathBuf>,
/// `-I` args
includes: Vec<PathBuf>,
/// List of `.proto` files to compile
inputs: Vec<PathBuf>,
/// `--include_imports`
include_imports: bool,
/// Extra command line flags (like `--experimental_allow_proto3_optional`)
extra_args: Vec<OsString>,
/// Capture stderr instead of inheriting it.
capture_stderr: bool,
}
impl DescriptorSetOutArgs {
/// Set `--file_descriptor_out=...` param
pub fn out(&mut self, out: impl AsRef<Path>) -> &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<Path>) -> &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<Item = impl AsRef<Path>>) -> &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<Path>) -> &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<Item = impl AsRef<Path>>) -> &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<OsString>) -> &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<Item = impl Into<OsString>>) -> &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<std::path::PathBuf, std::io::Error> {
/// # 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<OsStr>) -> 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<process::Child> {
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<Version> {
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<OsString>, 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");
}
}