blob: 3b8f629c32a7098f9b34f208c34fafc81021f34a [file] [log] [blame] [edit]
use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use anyhow::Context;
use protobuf_parse::Parser;
use crate::customize::CustomizeCallback;
use crate::customize::CustomizeCallbackHolder;
use crate::gen_and_write::gen_and_write;
use crate::Customize;
#[derive(Debug)]
enum WhichParser {
Pure,
Protoc,
}
impl Default for WhichParser {
fn default() -> WhichParser {
WhichParser::Pure
}
}
#[derive(Debug, thiserror::Error)]
enum CodegenError {
#[error("out_dir is not specified")]
OutDirNotSpecified,
}
/// Entry point for `.proto` to `.rs` code generation.
///
/// This is similar to `protoc --rust_out...`.
#[derive(Debug, Default)]
pub struct Codegen {
/// What parser to use to parse `.proto` files.
which_parser: Option<WhichParser>,
/// Create out directory.
create_out_dir: bool,
/// --lang_out= param
out_dir: Option<PathBuf>,
/// -I args
includes: Vec<PathBuf>,
/// List of .proto files to compile
inputs: Vec<PathBuf>,
/// Customize code generation
customize: Customize,
/// Customize code generation
customize_callback: CustomizeCallbackHolder,
/// Protoc command path
protoc: Option<PathBuf>,
/// Extra `protoc` args
protoc_extra_args: Vec<OsString>,
/// Capture stderr when running `protoc`.
capture_stderr: bool,
}
impl Codegen {
/// Create new codegen object.
///
/// Uses `protoc` from `$PATH` by default.
///
/// Can be switched to pure rust parser using [`pure`](Self::pure) function.
pub fn new() -> Self {
Self::default()
}
/// Switch to pure Rust parser of `.proto` files.
pub fn pure(&mut self) -> &mut Self {
self.which_parser = Some(WhichParser::Pure);
self
}
/// Switch to `protoc` parser of `.proto` files.
pub fn protoc(&mut self) -> &mut Self {
self.which_parser = Some(WhichParser::Protoc);
self
}
/// Output directory for generated code.
///
/// When invoking from `build.rs`, consider using
/// [`cargo_out_dir`](Self::cargo_out_dir) instead.
pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
self.out_dir = Some(out_dir.as_ref().to_owned());
self
}
/// Set output directory relative to Cargo output dir.
///
/// With this option, output directory is erased and recreated during invocation.
pub fn cargo_out_dir(&mut self, rel: &str) -> &mut Self {
let rel = Path::new(rel);
let mut not_empty = false;
for comp in rel.components() {
match comp {
Component::ParentDir => {
panic!("parent path in components of rel path: `{}`", rel.display());
}
Component::CurDir => {
continue;
}
Component::Normal(..) => {}
Component::RootDir | Component::Prefix(..) => {
panic!("root dir in components of rel path: `{}`", rel.display());
}
}
not_empty = true;
}
if !not_empty {
panic!("empty rel path: `{}`", rel.display());
}
let cargo_out_dir = env::var("OUT_DIR").expect("OUT_DIR env var not set");
let mut path = PathBuf::from(cargo_out_dir);
path.push(rel);
self.create_out_dir = true;
self.out_dir(path)
}
/// Add an include directory.
pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
self.includes.push(include.as_ref().to_owned());
self
}
/// Add include directories.
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
}
/// Specify `protoc` command path to be used when invoking code generation.
///
/// # Examples
///
/// ```no_run
/// # mod protoc_bin_vendored {
/// # pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> {
/// # unimplemented!()
/// # }
/// # }
///
/// use protobuf_codegen::Codegen;
///
/// Codegen::new()
/// .protoc()
/// .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap())
/// // ...
/// .run()
/// .unwrap();
/// ```
///
/// This option is ignored when pure Rust parser is used.
pub fn protoc_path(&mut self, protoc: &Path) -> &mut Self {
self.protoc = Some(protoc.to_owned());
self
}
/// Capture stderr to error when running `protoc`.
pub fn capture_stderr(&mut self) -> &mut Self {
self.capture_stderr = true;
self
}
/// Extra command line flags for `protoc` invocation.
///
/// For example, `--experimental_allow_proto3_optional` option.
///
/// This option is ignored when pure Rust parser is used.
pub fn protoc_extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
self.protoc_extra_args.push(arg.into());
self
}
/// Set options to customize code generation
pub fn customize(&mut self, customize: Customize) -> &mut Self {
self.customize.update_with(&customize);
self
}
/// Callback for dynamic per-element customization.
pub fn customize_callback(&mut self, callback: impl CustomizeCallback) -> &mut Self {
self.customize_callback = CustomizeCallbackHolder::new(callback);
self
}
/// Invoke the code generation.
///
/// This is roughly equivalent to `protoc --rust_out=...` but
/// without requiring `protoc-gen-rust` command in `$PATH`.
///
/// This function uses pure Rust parser or `protoc` parser depending on
/// how this object was configured.
pub fn run(&self) -> anyhow::Result<()> {
let out_dir = match &self.out_dir {
Some(out_dir) => out_dir,
None => return Err(CodegenError::OutDirNotSpecified.into()),
};
if self.create_out_dir {
if out_dir.exists() {
fs::remove_dir_all(&out_dir)?;
}
fs::create_dir(&out_dir)?;
}
let mut parser = Parser::new();
parser.protoc();
if let Some(protoc) = &self.protoc {
parser.protoc_path(protoc);
}
match &self.which_parser {
Some(WhichParser::Protoc) => {
parser.protoc();
}
Some(WhichParser::Pure) => {
parser.pure();
}
None => {}
}
parser.inputs(&self.inputs);
parser.includes(&self.includes);
if self.capture_stderr {
parser.capture_stderr();
}
let parsed_and_typechecked = parser
.parse_and_typecheck()
.context("parse and typecheck")?;
gen_and_write(
&parsed_and_typechecked.file_descriptors,
&parsed_and_typechecked.parser,
&parsed_and_typechecked.relative_paths,
&out_dir,
&self.customize,
&*self.customize_callback,
)
}
/// Similar to `run`, but prints the message to stderr and exits the process on error.
pub fn run_from_script(&self) {
if let Err(e) = self.run() {
eprintln!("codegen failed: {:?}", e);
process::exit(1);
}
}
}