tinc_build/
lib.rs

1//! The code generator for [`tinc`](https://crates.io/crates/tinc).
2#![cfg_attr(feature = "docs", doc = "## Feature flags")]
3#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
4//! ## Usage
5//!
6//! In your `build.rs`:
7//!
8//! ```rust,no_run
9//! # #[allow(clippy::needless_doctest_main)]
10//! fn main() {
11//!     tinc_build::Config::prost()
12//!         .compile_protos(&["proto/test.proto"], &["proto"])
13//!         .unwrap();
14//! }
15//! ```
16//!
17//! Look at [`Config`] to see different options to configure the generator.
18//!
19//! ## License
20//!
21//! This project is licensed under the MIT or Apache-2.0 license.
22//! You can choose between one of them if you use this work.
23//!
24//! `SPDX-License-Identifier: MIT OR Apache-2.0`
25#![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/// The mode to use for the generator, currently we only support `prost` codegen.
50#[derive(Debug, Clone, Copy)]
51pub enum Mode {
52    /// Use `prost` to generate the protobuf structures
53    #[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/// A config for configuring how tinc builds / generates code.
77#[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    /// New config with prost mode.
89    #[cfg(feature = "prost")]
90    pub fn prost() -> Self {
91        Self::new(Mode::Prost)
92    }
93
94    /// Make a new config with a given mode.
95    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    /// Make a new config with a given mode.
100    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    /// Disable tinc auto-include. By default tinc will add its own
112    /// annotations into the include path of protoc.
113    pub fn disable_tinc_include(&mut self) -> &mut Self {
114        self.disable_tinc_include = true;
115        self
116    }
117
118    /// Disable the root module generation
119    /// which allows for `tinc::include_protos!()` without
120    /// providing a package.
121    pub fn disable_root_module(&mut self) -> &mut Self {
122        self.root_module = false;
123        self
124    }
125
126    /// Specify a path to generate a `BTreeMap` instead of a `HashMap` for proto map.
127    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    /// Specify a path to generate `bytes::Bytes` instead of `Vec<u8>` for proto bytes.
133    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    /// Specify a path to wrap around a `Box` instead of including it directly into the struct.
139    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    /// Specify a path to float/double field (or derivative, like repeated float/double)
145    /// that must use serializer/deserializer with non-finite values support (NaN/Infinity).
146    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    /// Compile and generate all the protos with the includes.
152    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    /// Generate tinc code based on a precompiled FileDescriptorSet.
160    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        // This option is provided to make sure prost_build does not internally
214        // set extern_paths. We manage that via a re-export of prost_types in the
215        // tinc crate.
216        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(&registry)?;
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                        // In prost oneofs (container) are treated as enums
278                        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}