1 //! `tonic-build` compiles `proto` files via `prost` and generates service stubs 2 //! and proto definitiones for use with `tonic`. 3 //! 4 //! # Examples 5 //! Simple 6 //! 7 //! ```rust,no_run 8 //! fn main() -> Result<(), Box<dyn std::error::Error>> { 9 //! tonic_build::compile_protos("proto/service.proto")?; 10 //! Ok(()) 11 //! } 12 //! ``` 13 //! 14 //! Configuration 15 //! 16 //! ```rust,no_run 17 //! fn main() -> Result<(), Box<dyn std::error::Error>> { 18 //! tonic_build::configure() 19 //! .build_server(false) 20 //! .compile( 21 //! &["proto/helloworld/helloworld.proto"], 22 //! &["proto/helloworld"], 23 //! )?; 24 //! Ok(()) 25 //! } 26 //! ``` 27 28 use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; 29 use prost_build::Config; 30 use quote::TokenStreamExt; 31 32 #[cfg(feature = "rustfmt")] 33 use std::process::Command; 34 use std::{ 35 io, 36 path::{Path, PathBuf}, 37 }; 38 39 mod client; 40 mod service; 41 42 #[derive(Clone)] 43 pub struct Builder { 44 build_client: bool, 45 build_server: bool, 46 out_dir: Option<PathBuf>, 47 } 48 49 impl Builder { 50 /// Enable or disable gRPC client code generation. 51 pub fn build_client(mut self, enable: bool) -> Self { 52 self.build_client = enable; 53 self 54 } 55 56 /// Enable or disable gRPC server code generation. 57 pub fn build_server(mut self, enable: bool) -> Self { 58 self.build_server = enable; 59 self 60 } 61 62 /// Set the output directory to generate code to. 63 /// 64 /// Defaults to the `OUT_DIR` environment variable. 65 pub fn out_dir(mut self, out_dir: impl AsRef<Path>) -> Self { 66 self.out_dir = Some(out_dir.as_ref().to_path_buf()); 67 self 68 } 69 70 /// Compile the .proto files and execute code generation. 71 pub fn compile<P: AsRef<Path>>(self, protos: &[P], includes: &[P]) -> io::Result<()> { 72 let mut config = Config::new(); 73 74 let out_dir = self 75 .out_dir 76 .clone() 77 .unwrap_or_else(|| PathBuf::from(std::env::var("OUT_DIR").unwrap())); 78 79 config.out_dir(out_dir.clone()); 80 config.service_generator(Box::new(ServiceGenerator::new(self))); 81 config.compile_protos(protos, includes)?; 82 83 #[cfg(feature = "rustfmt")] 84 fmt(out_dir.to_str().expect("Expected utf8 out_dir")); 85 86 Ok(()) 87 } 88 } 89 90 /// Configure tonic-build code generation. 91 /// 92 /// Use [`compile_protos`] instead if you don't need to tweak anything. 93 pub fn configure() -> Builder { 94 Builder { 95 build_client: true, 96 build_server: true, 97 out_dir: None, 98 } 99 } 100 101 /// Simple `.proto` compiling. Use [`configure`] instead if you need more options. 102 /// 103 /// The include directory will be the parent folder of the specified path. 104 /// The package name will be the filename without the extension. 105 pub fn compile_protos(proto_path: impl AsRef<Path>) -> io::Result<()> { 106 let proto_path: &Path = proto_path.as_ref(); 107 108 // directory the main .proto file resides in 109 let proto_dir = proto_path 110 .parent() 111 .expect("proto file should reside in a directory"); 112 113 self::configure().compile(&[proto_path], &[proto_dir])?; 114 115 Ok(()) 116 } 117 118 #[cfg(feature = "rustfmt")] 119 fn fmt(out_dir: &str) { 120 let dir = std::fs::read_dir(out_dir).unwrap(); 121 122 for entry in dir { 123 let file = entry.unwrap().file_name().into_string().unwrap(); 124 let out = Command::new("rustfmt") 125 .arg("--emit") 126 .arg("files") 127 .arg("--edition") 128 .arg("2018") 129 .arg(format!("{}/{}", out_dir, file)) 130 .output() 131 .unwrap(); 132 133 println!("out: {:?}", out); 134 assert!(out.status.success()); 135 } 136 } 137 138 pub struct ServiceGenerator { 139 builder: Builder, 140 clients: TokenStream, 141 servers: TokenStream, 142 } 143 144 impl ServiceGenerator { 145 fn new(builder: Builder) -> Self { 146 ServiceGenerator { 147 builder, 148 clients: TokenStream::default(), 149 servers: TokenStream::default(), 150 } 151 } 152 } 153 154 impl prost_build::ServiceGenerator for ServiceGenerator { 155 fn generate(&mut self, service: prost_build::Service, _buf: &mut String) { 156 let path = "super"; 157 158 if self.builder.build_server { 159 let server = service::generate(&service, path); 160 self.servers.extend(server); 161 } 162 163 if self.builder.build_client { 164 let client = client::generate(&service, path); 165 self.clients.extend(client); 166 } 167 } 168 169 fn finalize(&mut self, buf: &mut String) { 170 if self.builder.build_client && !self.clients.is_empty() { 171 let clients = &self.clients; 172 173 let client_service = quote::quote! { 174 /// Generated client implementations. 175 pub mod client { 176 #![allow(unused_variables, dead_code, missing_docs)] 177 use tonic::codegen::*; 178 179 #clients 180 } 181 }; 182 183 let code = format!("{}", client_service); 184 buf.push_str(&code); 185 } 186 187 if self.builder.build_server && !self.servers.is_empty() { 188 let servers = &self.servers; 189 190 let server_service = quote::quote! { 191 /// Generated server implementations. 192 pub mod server { 193 #![allow(unused_variables, dead_code, missing_docs)] 194 use tonic::codegen::*; 195 196 #servers 197 } 198 }; 199 200 let code = format!("{}", server_service); 201 buf.push_str(&code); 202 } 203 } 204 } 205 206 // Generate a singular line of a doc comment 207 fn generate_doc_comment(comment: &str) -> TokenStream { 208 let mut doc_stream = TokenStream::new(); 209 210 doc_stream.append(Ident::new("doc", Span::call_site())); 211 doc_stream.append(Punct::new('=', Spacing::Alone)); 212 doc_stream.append(Literal::string(&comment)); 213 214 let group = Group::new(Delimiter::Bracket, doc_stream); 215 216 let mut stream = TokenStream::new(); 217 stream.append(Punct::new('#', Spacing::Alone)); 218 stream.append(group); 219 stream 220 } 221 222 // Generate a larger doc comment composed of many lines of doc comments 223 fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream { 224 let mut stream = TokenStream::new(); 225 226 for comment in comments { 227 stream.extend(generate_doc_comment(comment.as_ref())); 228 } 229 230 stream 231 } 232