#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
use std::ffi::OsStr;
use std::ffi::OsString;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process;
#[macro_use]
extern crate log;
pub type Error = io::Error;
pub type Result<T> = io::Result<T>;
fn err_other(s: impl AsRef<str>) -> Error {
Error::new(io::ErrorKind::Other, s.as_ref().to_owned())
}
#[derive(Default)]
pub struct ProtocLangOut {
protoc: Option<Protoc>,
lang: Option<String>,
out_dir: Option<PathBuf>,
plugin: Option<OsString>,
includes: Vec<PathBuf>,
inputs: Vec<PathBuf>,
}
impl ProtocLangOut {
pub fn new() -> Self {
Self::default()
}
pub fn lang(&mut self, lang: &str) -> &mut Self {
self.lang = Some(lang.to_owned());
self
}
pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
self.out_dir = Some(out_dir.as_ref().to_owned());
self
}
pub fn plugin(&mut self, plugin: impl AsRef<OsStr>) -> &mut Self {
self.plugin = Some(plugin.as_ref().to_owned());
self
}
pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
self.includes.push(include.as_ref().to_owned());
self
}
pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
for include in includes {
self.include(include);
}
self
}
pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
self.inputs.push(input.as_ref().to_owned());
self
}
pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
for input in inputs {
self.input(input);
}
self
}
pub fn run(&self) -> Result<()> {
let protoc = match &self.protoc {
Some(protoc) => protoc.clone(),
None => {
let protoc = Protoc::from_env_path();
protoc.check()?;
protoc
}
};
if self.inputs.is_empty() {
return Err(err_other("input is empty"));
}
let out_dir = self
.out_dir
.as_ref()
.ok_or_else(|| err_other("out_dir is empty"))?;
let lang = self
.lang
.as_ref()
.ok_or_else(|| err_other("lang is empty"))?;
let mut lang_out_flag = OsString::from("--");
lang_out_flag.push(lang);
lang_out_flag.push("_out=");
lang_out_flag.push(out_dir);
let plugin_flag = self.plugin.as_ref().map(|plugin| {
let mut flag = OsString::from("--plugin=");
flag.push(plugin);
flag
});
let include_flags = self.includes.iter().map(|include| {
let mut flag = OsString::from("-I");
flag.push(include);
flag
});
let mut cmd_args = Vec::new();
cmd_args.push(lang_out_flag);
cmd_args.extend(self.inputs.iter().map(|path| path.as_os_str().to_owned()));
cmd_args.extend(plugin_flag);
cmd_args.extend(include_flags);
protoc.run_with_args(cmd_args)
}
}
#[derive(Default)]
#[deprecated(since = "2.13", note = "Use ProtocLangOut instead")]
pub struct Args<'a> {
pub lang: &'a str,
pub out_dir: &'a str,
pub plugin: Option<&'a str>,
pub includes: &'a [&'a str],
pub input: &'a [&'a str],
}
#[derive(Debug)]
pub struct DescriptorSetOutArgs<'a> {
pub out: &'a str,
pub includes: &'a [&'a str],
pub input: &'a [&'a str],
pub include_imports: bool,
}
#[derive(Clone, Debug)]
pub struct Protoc {
exec: OsString,
}
impl Protoc {
pub 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);
}
}
}
pub fn from_path(path: impl AsRef<OsStr>) -> Protoc {
Protoc {
exec: path.as_ref().to_owned(),
}
}
pub fn check(&self) -> Result<()> {
self.version().map(|_| ())
}
fn spawn(&self, cmd: &mut process::Command) -> io::Result<process::Child> {
info!("spawning command {:?}", cmd);
cmd.spawn()
.map_err(|e| Error::new(e.kind(), format!("failed to spawn `{:?}`: {}", cmd, e)))
}
pub fn version(&self) -> 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(err_other("protoc failed with error"));
}
let output =
String::from_utf8(output.stdout).map_err(|e| Error::new(io::ErrorKind::Other, e))?;
let output = match output.lines().next() {
None => return Err(err_other("output is empty")),
Some(line) => line,
};
let prefix = "libprotoc ";
if !output.starts_with(prefix) {
return Err(err_other("output does not start with prefix"));
}
let output = &output[prefix.len()..];
if output.is_empty() {
return Err(err_other("version is empty"));
}
let first = output.chars().next().unwrap();
if !first.is_digit(10) {
return Err(err_other("version does not start with digit"));
}
Ok(Version {
version: output.to_owned(),
})
}
fn run_with_args(&self, args: Vec<OsString>) -> Result<()> {
let mut cmd = process::Command::new(&self.exec);
cmd.stdin(process::Stdio::null());
cmd.args(args);
let mut child = self.spawn(&mut cmd)?;
if !child.wait()?.success() {
return Err(err_other(&format!(
"protoc ({:?}) exited with non-zero exit code",
cmd
)));
}
Ok(())
}
#[deprecated(since = "2.13", note = "Use ProtocLangOut instead")]
#[allow(deprecated)]
pub fn run(&self, args: Args) -> Result<()> {
let mut cmd_args: Vec<OsString> = Vec::new();
if args.out_dir.is_empty() {
return Err(err_other("out_dir is empty"));
}
if args.lang.is_empty() {
return Err(err_other("lang is empty"));
}
cmd_args.push(format!("--{}_out={}", args.lang, args.out_dir).into());
if args.input.is_empty() {
return Err(err_other("input is empty"));
}
cmd_args.extend(args.input.into_iter().map(|a| OsString::from(*a)));
if let Some(plugin) = args.plugin {
cmd_args.push(format!("--plugin={}", plugin).into());
}
for include in args.includes {
cmd_args.push(format!("-I{}", include).into());
}
self.run_with_args(cmd_args)
}
pub fn write_descriptor_set(&self, args: DescriptorSetOutArgs) -> Result<()> {
let mut cmd_args: Vec<OsString> = Vec::new();
for include in args.includes {
cmd_args.push(format!("-I{}", include).into());
}
if args.out.is_empty() {
return Err(err_other("out is empty"));
}
cmd_args.push(format!("--descriptor_set_out={}", args.out).into());
if args.include_imports {
cmd_args.push("--include_imports".into());
}
if args.input.is_empty() {
return Err(err_other("input is empty"));
}
cmd_args.extend(args.input.into_iter().map(|a| OsString::from(*a)));
self.run_with_args(cmd_args)
}
}
#[deprecated(since = "2.13", note = "Use ProtocLangOut instead")]
#[allow(deprecated)]
pub fn run(args: Args) -> Result<()> {
let mut protoc_lang_out = ProtocLangOut::new();
if !args.lang.is_empty() {
protoc_lang_out.lang(args.lang);
}
if !args.out_dir.is_empty() {
protoc_lang_out.out_dir(args.out_dir);
}
if let Some(plugin) = args.plugin {
protoc_lang_out.plugin(plugin);
}
if !args.includes.is_empty() {
protoc_lang_out.includes(args.includes);
}
if !args.input.is_empty() {
protoc_lang_out.inputs(args.input);
}
protoc_lang_out.run()
}
pub struct Version {
version: String,
}
impl Version {
pub fn is_3(&self) -> bool {
self.version.starts_with("3")
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn version() {
Protoc::from_env_path().version().expect("version");
}
}