1#![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#![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#[derive(Debug, Clone, Copy)]
45pub enum Mode {
46 #[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#[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 #[cfg(feature = "prost")]
81 pub fn prost() -> Self {
82 Self::new(Mode::Prost)
83 }
84
85 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 pub fn disable_tinc_include(&mut self) -> &mut Self {
98 self.disable_tinc_include = true;
99 self
100 }
101
102 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 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 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 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(®istry)?;
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 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#[cfg(feature = "docs")]
343#[scuffle_changelog::changelog]
344pub mod changelog {}