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); } } }