tinc_build/
lib.rs

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