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 anyhow::Context;
33use extern_paths::ExternPaths;
34mod codegen;
35mod extern_paths;
36
37#[cfg(feature = "prost")]
38mod prost_explore;
39
40mod types;
41
42#[derive(Debug, Clone, Copy)]
44pub enum Mode {
45 #[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#[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 #[cfg(feature = "prost")]
80 pub fn prost() -> Self {
81 Self::new(Mode::Prost)
82 }
83
84 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 pub fn disable_tinc_include(&mut self) -> &mut Self {
97 self.disable_tinc_include = true;
98 self
99 }
100
101 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 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 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 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(®istry)?;
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 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}