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 anyhow::Context;
33use extern_paths::ExternPaths;
34mod codegen;
35mod extern_paths;
36
37#[cfg(feature = "prost")]
38mod prost_explore;
39
40mod types;
41
42/// The mode to use for the generator, currently we only support `prost` codegen.
43#[derive(Debug, Clone, Copy)]
44pub enum Mode {
45    /// Use `prost` to generate the protobuf structures
46    #[cfg(feature = "prost")]
47    Prost,
48}
49
50impl quote::ToTokens for Mode {
51    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
52        match self {
53            #[cfg(feature = "prost")]
54            Mode::Prost => quote::quote!(prost).to_tokens(tokens),
55            #[cfg(not(feature = "prost"))]
56            _ => unreachable!(),
57        }
58    }
59}
60
61#[derive(Default, Debug)]
62struct PathConfigs {
63    btree_maps: Vec<String>,
64    bytes: Vec<String>,
65    boxed: Vec<String>,
66}
67
68/// A config for configuring how tinc builds / generates code.
69#[derive(Debug)]
70pub struct Config {
71    disable_tinc_include: bool,
72    mode: Mode,
73    paths: PathConfigs,
74    extern_paths: ExternPaths,
75}
76
77impl Config {
78    /// New config with prost mode.
79    #[cfg(feature = "prost")]
80    pub fn prost() -> Self {
81        Self::new(Mode::Prost)
82    }
83
84    /// Make a new config with a given mode.
85    pub fn new(mode: Mode) -> Self {
86        Self {
87            disable_tinc_include: false,
88            mode,
89            paths: PathConfigs::default(),
90            extern_paths: ExternPaths::new(mode),
91        }
92    }
93
94    /// Disable tinc auto-include. By default tinc will add its own
95    /// annotations into the include path of protoc.
96    pub fn disable_tinc_include(&mut self) -> &mut Self {
97        self.disable_tinc_include = true;
98        self
99    }
100
101    /// Specify a path to generate a `BTreeMap` instead of a `HashMap` for proto map.
102    pub fn btree_map(&mut self, path: impl std::fmt::Display) -> &mut Self {
103        self.paths.btree_maps.push(path.to_string());
104        self
105    }
106
107    /// Specify a path to generate `bytes::Bytes` instead of `Vec<u8>` for proto bytes.
108    pub fn bytes(&mut self, path: impl std::fmt::Display) -> &mut Self {
109        self.paths.bytes.push(path.to_string());
110        self
111    }
112
113    /// Specify a path to wrap around a `Box` instead of including it directly into the struct.
114    pub fn boxed(&mut self, path: impl std::fmt::Display) -> &mut Self {
115        self.paths.boxed.push(path.to_string());
116        self
117    }
118
119    /// Compile and generate all the protos with the includes.
120    pub fn compile_protos(&mut self, protos: &[&str], includes: &[&str]) -> anyhow::Result<()> {
121        match self.mode {
122            #[cfg(feature = "prost")]
123            Mode::Prost => self.compile_protos_prost(protos, includes),
124        }
125    }
126
127    #[cfg(feature = "prost")]
128    fn compile_protos_prost(&mut self, protos: &[&str], includes: &[&str]) -> anyhow::Result<()> {
129        use codegen::prost_sanatize::to_snake;
130        use codegen::utils::get_common_import_path;
131        use prost_reflect::DescriptorPool;
132        use quote::{ToTokens, quote};
133        use syn::parse_quote;
134        use types::ProtoTypeRegistry;
135
136        let out_dir_str = std::env::var("OUT_DIR").context("OUT_DIR must be set, typically set by a cargo build script")?;
137        let out_dir = std::path::PathBuf::from(&out_dir_str);
138        let ft_path = out_dir.join("tinc.fd.bin");
139
140        let mut config = prost_build::Config::new();
141        config.file_descriptor_set_path(&ft_path);
142
143        config.btree_map(self.paths.btree_maps.iter());
144        self.paths.boxed.iter().for_each(|path| {
145            config.boxed(path);
146        });
147        config.bytes(self.paths.bytes.iter());
148
149        let mut includes = includes.to_vec();
150
151        {
152            let tinc_out = out_dir.join("tinc");
153            std::fs::create_dir_all(&tinc_out).context("failed to create tinc directory")?;
154            std::fs::write(tinc_out.join("annotations.proto"), tinc_pb_prost::TINC_ANNOTATIONS)
155                .context("failed to write tinc_annotations.rs")?;
156            includes.push(&out_dir_str);
157            config.protoc_arg(format!("--descriptor_set_in={}", tinc_pb_prost::TINC_ANNOTATIONS_PB_PATH));
158        }
159
160        let fds = config.load_fds(protos, &includes).context("failed to generate tonic fds")?;
161
162        let fds_bytes = std::fs::read(ft_path).context("failed to read tonic fds")?;
163
164        let pool = DescriptorPool::decode(&mut fds_bytes.as_slice()).context("failed to decode tonic fds")?;
165
166        let mut registry = ProtoTypeRegistry::new(self.mode, self.extern_paths.clone());
167
168        config.compile_well_known_types();
169        for (proto, rust) in self.extern_paths.paths() {
170            let proto = if proto.starts_with('.') {
171                proto.to_string()
172            } else {
173                format!(".{proto}")
174            };
175            config.extern_path(proto, rust.to_token_stream().to_string());
176        }
177
178        prost_explore::Extensions::new(&pool)
179            .process(&mut registry)
180            .context("failed to process extensions")?;
181
182        let mut packages = codegen::generate_modules(&registry)?;
183
184        packages.iter_mut().for_each(|(path, package)| {
185            if self.extern_paths.contains(path) {
186                return;
187            }
188
189            package.enum_configs().for_each(|(path, enum_config)| {
190                if self.extern_paths.contains(path) {
191                    return;
192                }
193
194                enum_config.attributes().for_each(|attribute| {
195                    config.enum_attribute(path, attribute.to_token_stream().to_string());
196                });
197                enum_config.variants().for_each(|variant| {
198                    let path = format!("{path}.{variant}");
199                    enum_config.variant_attributes(variant).for_each(|attribute| {
200                        config.field_attribute(&path, attribute.to_token_stream().to_string());
201                    });
202                });
203            });
204
205            package.message_configs().for_each(|(path, message_config)| {
206                if self.extern_paths.contains(path) {
207                    return;
208                }
209
210                message_config.attributes().for_each(|attribute| {
211                    config.message_attribute(path, attribute.to_token_stream().to_string());
212                });
213                message_config.fields().for_each(|field| {
214                    let path = format!("{path}.{field}");
215                    message_config.field_attributes(field).for_each(|attribute| {
216                        config.field_attribute(&path, attribute.to_token_stream().to_string());
217                    });
218                });
219                message_config.oneof_configs().for_each(|(field, oneof_config)| {
220                    let path = format!("{path}.{field}");
221                    oneof_config.attributes().for_each(|attribute| {
222                        // In prost oneofs (container) are treated as enums
223                        config.enum_attribute(&path, attribute.to_token_stream().to_string());
224                    });
225                    oneof_config.fields().for_each(|field| {
226                        let path = format!("{path}.{field}");
227                        oneof_config.field_attributes(field).for_each(|attribute| {
228                            config.field_attribute(&path, attribute.to_token_stream().to_string());
229                        });
230                    });
231                });
232            });
233
234            package.extra_items.extend(package.services.iter().flat_map(|service| {
235                let mut builder = tonic_build::CodeGenBuilder::new();
236
237                builder.emit_package(true).build_transport(true);
238
239                let make_service = |is_client: bool| {
240                    let mut builder = tonic_build::manual::Service::builder()
241                        .name(service.name())
242                        .package(&service.package);
243
244                    if !service.comments.is_empty() {
245                        builder = builder.comment(service.comments.to_string());
246                    }
247
248                    service
249                        .methods
250                        .iter()
251                        .fold(builder, |service_builder, (name, method)| {
252                            let codec_path = if is_client {
253                                quote!(::tinc::reexports::tonic::codec::ProstCodec)
254                            } else {
255                                let path = get_common_import_path(&service.full_name, &method.codec_path);
256                                quote!(#path::<::tinc::reexports::tonic::codec::ProstCodec<_, _>>)
257                            };
258
259                            let mut builder = tonic_build::manual::Method::builder()
260                                .input_type(
261                                    registry
262                                        .resolve_rust_path(&service.full_name, method.input.value_type().proto_path())
263                                        .unwrap()
264                                        .to_token_stream()
265                                        .to_string(),
266                                )
267                                .output_type(
268                                    registry
269                                        .resolve_rust_path(&service.full_name, method.output.value_type().proto_path())
270                                        .unwrap()
271                                        .to_token_stream()
272                                        .to_string(),
273                                )
274                                .codec_path(codec_path.to_string())
275                                .name(to_snake(name))
276                                .route_name(name);
277
278                            if method.input.is_stream() {
279                                builder = builder.client_streaming()
280                            }
281
282                            if method.output.is_stream() {
283                                builder = builder.server_streaming();
284                            }
285
286                            if !method.comments.is_empty() {
287                                builder = builder.comment(method.comments.to_string());
288                            }
289
290                            service_builder.method(builder.build())
291                        })
292                        .build()
293                };
294
295                let mut client: syn::ItemMod = syn::parse2(builder.generate_client(&make_service(true), "")).unwrap();
296                client.content.as_mut().unwrap().1.insert(
297                    0,
298                    parse_quote!(
299                        use ::tinc::reexports::tonic;
300                    ),
301                );
302
303                let mut server: syn::ItemMod = syn::parse2(builder.generate_server(&make_service(false), "")).unwrap();
304                server.content.as_mut().unwrap().1.insert(
305                    0,
306                    parse_quote!(
307                        use ::tinc::reexports::tonic;
308                    ),
309                );
310
311                [client.into(), server.into()]
312            }));
313        });
314
315        config.compile_fds(fds).context("prost compile")?;
316
317        for (package, module) in packages {
318            if self.extern_paths.contains(&package) {
319                continue;
320            };
321
322            let path = out_dir.join(format!("{package}.rs"));
323            write_module(&path, module.extra_items).with_context(|| package.to_owned())?;
324        }
325
326        Ok(())
327    }
328}
329
330fn write_module(path: &std::path::Path, module: Vec<syn::Item>) -> anyhow::Result<()> {
331    let file = std::fs::read_to_string(path).context("read")?;
332    let mut file = syn::parse_file(&file).context("parse")?;
333
334    file.items.extend(module);
335    std::fs::write(path, prettyplease::unparse(&file)).context("write")?;
336
337    Ok(())
338}