1#![cfg_attr(feature = "docs", doc = "## Feature flags")]
3#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
4#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
26#![cfg_attr(docsrs, feature(doc_auto_cfg))]
27#![deny(missing_docs)]
28#![deny(unsafe_code)]
29#![deny(unreachable_pub)]
30#![cfg_attr(not(feature = "prost"), allow(unused_variables, dead_code))]
31
32use std::io::ErrorKind;
33use std::path::{Path, PathBuf};
34
35use anyhow::Context;
36use extern_paths::ExternPaths;
37
38use crate::path_set::PathSet;
39
40mod codegen;
41mod extern_paths;
42mod path_set;
43
44#[cfg(feature = "prost")]
45mod prost_explore;
46
47mod types;
48
49#[derive(Debug, Clone, Copy)]
51pub enum Mode {
52 #[cfg(feature = "prost")]
54 Prost,
55}
56
57impl quote::ToTokens for Mode {
58 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
59 match self {
60 #[cfg(feature = "prost")]
61 Mode::Prost => quote::quote!(prost).to_tokens(tokens),
62 #[cfg(not(feature = "prost"))]
63 _ => unreachable!(),
64 }
65 }
66}
67
68#[derive(Default, Debug)]
69struct PathConfigs {
70 btree_maps: Vec<String>,
71 bytes: Vec<String>,
72 boxed: Vec<String>,
73 floats_with_non_finite_vals: PathSet,
74}
75
76#[derive(Debug)]
78pub struct Config {
79 disable_tinc_include: bool,
80 root_module: bool,
81 mode: Mode,
82 paths: PathConfigs,
83 extern_paths: ExternPaths,
84 out_dir: PathBuf,
85}
86
87impl Config {
88 #[cfg(feature = "prost")]
90 pub fn prost() -> Self {
91 Self::new(Mode::Prost)
92 }
93
94 pub fn new(mode: Mode) -> Self {
96 Self::new_with_out_dir(mode, std::env::var_os("OUT_DIR").expect("OUT_DIR not set"))
97 }
98
99 pub fn new_with_out_dir(mode: Mode, out_dir: impl Into<PathBuf>) -> Self {
101 Self {
102 disable_tinc_include: false,
103 mode,
104 paths: PathConfigs::default(),
105 extern_paths: ExternPaths::new(mode),
106 root_module: true,
107 out_dir: out_dir.into(),
108 }
109 }
110
111 pub fn disable_tinc_include(&mut self) -> &mut Self {
114 self.disable_tinc_include = true;
115 self
116 }
117
118 pub fn disable_root_module(&mut self) -> &mut Self {
122 self.root_module = false;
123 self
124 }
125
126 pub fn btree_map(&mut self, path: impl std::fmt::Display) -> &mut Self {
128 self.paths.btree_maps.push(path.to_string());
129 self
130 }
131
132 pub fn bytes(&mut self, path: impl std::fmt::Display) -> &mut Self {
134 self.paths.bytes.push(path.to_string());
135 self
136 }
137
138 pub fn boxed(&mut self, path: impl std::fmt::Display) -> &mut Self {
140 self.paths.boxed.push(path.to_string());
141 self
142 }
143
144 pub fn float_with_non_finite_vals(&mut self, path: impl std::fmt::Display) -> &mut Self {
147 self.paths.floats_with_non_finite_vals.insert(path);
148 self
149 }
150
151 pub fn compile_protos(&mut self, protos: &[impl AsRef<Path>], includes: &[impl AsRef<Path>]) -> anyhow::Result<()> {
153 match self.mode {
154 #[cfg(feature = "prost")]
155 Mode::Prost => self.compile_protos_prost(protos, includes),
156 }
157 }
158
159 pub fn load_fds(&mut self, fds: impl bytes::Buf) -> anyhow::Result<()> {
161 match self.mode {
162 #[cfg(feature = "prost")]
163 Mode::Prost => self.load_fds_prost(fds),
164 }
165 }
166
167 #[cfg(feature = "prost")]
168 fn compile_protos_prost(&mut self, protos: &[impl AsRef<Path>], includes: &[impl AsRef<Path>]) -> anyhow::Result<()> {
169 let fd_path = self.out_dir.join("tinc.fd.bin");
170
171 let mut config = prost_build::Config::new();
172 config.file_descriptor_set_path(&fd_path);
173
174 let mut includes = includes.iter().map(|i| i.as_ref()).collect::<Vec<_>>();
175
176 {
177 let tinc_out = self.out_dir.join("tinc");
178 std::fs::create_dir_all(&tinc_out).context("failed to create tinc directory")?;
179 std::fs::write(tinc_out.join("annotations.proto"), tinc_pb_prost::TINC_ANNOTATIONS)
180 .context("failed to write tinc_annotations.rs")?;
181 includes.push(&self.out_dir);
182 }
183
184 config.load_fds(protos, &includes).context("failed to generate tonic fds")?;
185 let fds_bytes = std::fs::read(fd_path).context("failed to read tonic fds")?;
186 self.load_fds_prost(fds_bytes.as_slice())
187 }
188
189 #[cfg(feature = "prost")]
190 fn load_fds_prost(&mut self, fds: impl bytes::Buf) -> anyhow::Result<()> {
191 use std::collections::BTreeMap;
192
193 use codegen::prost_sanatize::to_snake;
194 use codegen::utils::get_common_import_path;
195 use proc_macro2::Span;
196 use prost::Message;
197 use prost_reflect::DescriptorPool;
198 use prost_types::FileDescriptorSet;
199 use quote::{ToTokens, quote};
200 use syn::parse_quote;
201 use types::{ProtoPath, ProtoTypeRegistry};
202
203 let pool = DescriptorPool::decode(fds).context("failed to add tonic fds")?;
204
205 let mut registry = ProtoTypeRegistry::new(
206 self.mode,
207 self.extern_paths.clone(),
208 self.paths.floats_with_non_finite_vals.clone(),
209 );
210
211 let mut config = prost_build::Config::new();
212
213 config.compile_well_known_types();
217
218 config.btree_map(self.paths.btree_maps.iter());
219 self.paths.boxed.iter().for_each(|path| {
220 config.boxed(path);
221 });
222 config.bytes(self.paths.bytes.iter());
223
224 for (proto, rust) in self.extern_paths.paths() {
225 let proto = if proto.starts_with('.') {
226 proto.to_string()
227 } else {
228 format!(".{proto}")
229 };
230 config.extern_path(proto, rust.to_token_stream().to_string());
231 }
232
233 prost_explore::Extensions::new(&pool)
234 .process(&mut registry)
235 .context("failed to process extensions")?;
236
237 let mut packages = codegen::generate_modules(®istry)?;
238
239 packages.iter_mut().for_each(|(path, package)| {
240 if self.extern_paths.contains(path) {
241 return;
242 }
243
244 package.enum_configs().for_each(|(path, enum_config)| {
245 if self.extern_paths.contains(path) {
246 return;
247 }
248
249 enum_config.attributes().for_each(|attribute| {
250 config.enum_attribute(path, attribute.to_token_stream().to_string());
251 });
252 enum_config.variants().for_each(|variant| {
253 let path = format!("{path}.{variant}");
254 enum_config.variant_attributes(variant).for_each(|attribute| {
255 config.field_attribute(&path, attribute.to_token_stream().to_string());
256 });
257 });
258 });
259
260 package.message_configs().for_each(|(path, message_config)| {
261 if self.extern_paths.contains(path) {
262 return;
263 }
264
265 message_config.attributes().for_each(|attribute| {
266 config.message_attribute(path, attribute.to_token_stream().to_string());
267 });
268 message_config.fields().for_each(|field| {
269 let path = format!("{path}.{field}");
270 message_config.field_attributes(field).for_each(|attribute| {
271 config.field_attribute(&path, attribute.to_token_stream().to_string());
272 });
273 });
274 message_config.oneof_configs().for_each(|(field, oneof_config)| {
275 let path = format!("{path}.{field}");
276 oneof_config.attributes().for_each(|attribute| {
277 config.enum_attribute(&path, attribute.to_token_stream().to_string());
279 });
280 oneof_config.fields().for_each(|field| {
281 let path = format!("{path}.{field}");
282 oneof_config.field_attributes(field).for_each(|attribute| {
283 config.field_attribute(&path, attribute.to_token_stream().to_string());
284 });
285 });
286 });
287 });
288
289 package.extra_items.extend(package.services.iter().flat_map(|service| {
290 let mut builder = tonic_build::CodeGenBuilder::new();
291
292 builder.emit_package(true).build_transport(true);
293
294 let make_service = |is_client: bool| {
295 let mut builder = tonic_build::manual::Service::builder()
296 .name(service.name())
297 .package(&service.package);
298
299 if !service.comments.is_empty() {
300 builder = builder.comment(service.comments.to_string());
301 }
302
303 service
304 .methods
305 .iter()
306 .fold(builder, |service_builder, (name, method)| {
307 let codec_path =
308 if let Some(Some(codec_path)) = (!is_client).then_some(method.codec_path.as_ref()) {
309 let path = get_common_import_path(&service.full_name, codec_path);
310 quote!(#path::<::tinc::reexports::tonic_prost::ProstCodec<_, _>>)
311 } else {
312 quote!(::tinc::reexports::tonic_prost::ProstCodec)
313 };
314
315 let mut builder = tonic_build::manual::Method::builder()
316 .input_type(
317 registry
318 .resolve_rust_path(&service.full_name, method.input.value_type().proto_path())
319 .unwrap()
320 .to_token_stream()
321 .to_string(),
322 )
323 .output_type(
324 registry
325 .resolve_rust_path(&service.full_name, method.output.value_type().proto_path())
326 .unwrap()
327 .to_token_stream()
328 .to_string(),
329 )
330 .codec_path(codec_path.to_string())
331 .name(to_snake(name))
332 .route_name(name);
333
334 if method.input.is_stream() {
335 builder = builder.client_streaming()
336 }
337
338 if method.output.is_stream() {
339 builder = builder.server_streaming();
340 }
341
342 if !method.comments.is_empty() {
343 builder = builder.comment(method.comments.to_string());
344 }
345
346 service_builder.method(builder.build())
347 })
348 .build()
349 };
350
351 let mut client: syn::ItemMod = syn::parse2(builder.generate_client(&make_service(true), "")).unwrap();
352 client.content.as_mut().unwrap().1.insert(
353 0,
354 parse_quote!(
355 use ::tinc::reexports::tonic;
356 ),
357 );
358
359 let mut server: syn::ItemMod = syn::parse2(builder.generate_server(&make_service(false), "")).unwrap();
360 server.content.as_mut().unwrap().1.insert(
361 0,
362 parse_quote!(
363 use ::tinc::reexports::tonic;
364 ),
365 );
366
367 [client.into(), server.into()]
368 }));
369 });
370
371 for package in packages.keys() {
372 match std::fs::remove_file(self.out_dir.join(format!("{package}.rs"))) {
373 Err(err) if err.kind() != ErrorKind::NotFound => return Err(anyhow::anyhow!(err).context("remove")),
374 _ => {}
375 }
376 }
377
378 let fds = FileDescriptorSet {
379 file: pool.file_descriptor_protos().cloned().collect(),
380 };
381
382 let fd_path = self.out_dir.join("tinc.fd.bin");
383 std::fs::write(fd_path, fds.encode_to_vec()).context("write fds")?;
384
385 config.compile_fds(fds).context("prost compile")?;
386
387 for (package, module) in &mut packages {
388 if self.extern_paths.contains(package) {
389 continue;
390 };
391
392 let path = self.out_dir.join(format!("{package}.rs"));
393 write_module(&path, std::mem::take(&mut module.extra_items)).with_context(|| package.to_owned())?;
394 }
395
396 #[derive(Default)]
397 struct Module<'a> {
398 proto_path: Option<&'a ProtoPath>,
399 children: BTreeMap<&'a str, Module<'a>>,
400 }
401
402 impl ToTokens for Module<'_> {
403 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
404 let include = self
405 .proto_path
406 .map(|p| p.as_ref())
407 .map(|path| quote!(include!(concat!(#path, ".rs"));));
408 let children = self.children.iter().map(|(part, child)| {
409 let ident = syn::Ident::new(&to_snake(part), Span::call_site());
410 quote! {
411 pub mod #ident {
412 #child
413 }
414 }
415 });
416 quote! {
417 #include
418 #(#children)*
419 }
420 .to_tokens(tokens);
421 }
422 }
423
424 if self.root_module {
425 let mut module = Module::default();
426 for package in packages.keys() {
427 let mut module = &mut module;
428 for part in package.split('.') {
429 module = module.children.entry(part).or_default();
430 }
431 module.proto_path = Some(package);
432 }
433
434 let file: syn::File = parse_quote!(#module);
435 std::fs::write(self.out_dir.join("___root_module.rs"), prettyplease::unparse(&file))
436 .context("write root module")?;
437 }
438
439 Ok(())
440 }
441}
442
443fn write_module(path: &std::path::Path, module: Vec<syn::Item>) -> anyhow::Result<()> {
444 let mut file = match std::fs::read_to_string(path) {
445 Ok(content) if !content.is_empty() => syn::parse_file(&content).context("parse")?,
446 Err(err) if err.kind() != ErrorKind::NotFound => return Err(anyhow::anyhow!(err).context("read")),
447 _ => syn::File {
448 attrs: Vec::new(),
449 items: Vec::new(),
450 shebang: None,
451 },
452 };
453
454 file.items.extend(module);
455 std::fs::write(path, prettyplease::unparse(&file)).context("write")?;
456
457 Ok(())
458}