From 21c0ac98373386674a9b2e4eb5a2148306824e81 Mon Sep 17 00:00:00 2001 From: d3rpp Date: Tue, 5 Nov 2024 15:04:42 +1300 Subject: [PATCH] added codegen --- Cargo.lock | 4 + Cargo.toml | 2 +- crates/chur-build/Cargo.toml | 14 +++ crates/chur-build/src/cfg.rs | 3 + crates/chur-build/src/cfg/builder.rs | 24 +++++ crates/chur-build/src/execute.rs | 16 +++ crates/chur-build/src/include_tree.rs | 142 ++++++++++++++++++++++++++ crates/chur-build/src/lib.rs | 3 + example/build.rs | 1 + example/src/pb.rs | 20 +++- 10 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 crates/chur-build/src/include_tree.rs diff --git a/Cargo.lock b/Cargo.lock index b32f8db..bd7c9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,9 +241,13 @@ version = "0.3.0" dependencies = [ "archiver-rs", "lazy_static", + "prettyplease", + "proc-macro2", + "quote", "ron", "serde", "sha", + "syn", "thiserror", "tonic-build", "ureq", diff --git a/Cargo.toml b/Cargo.toml index 83b0a2b..f8ff079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT" readme = "./README.md" repository = "https://github.com/d3rpp/chur" - version = "0.3.0" + version = "0.3.1-dev.1" publish = true [workspace.dependencies] diff --git a/crates/chur-build/Cargo.toml b/crates/chur-build/Cargo.toml index 339cee2..82e33d4 100644 --- a/crates/chur-build/Cargo.toml +++ b/crates/chur-build/Cargo.toml @@ -9,6 +9,15 @@ repository.workspace = true version.workspace = true +[features] + default = ["codegen"] + codegen = [ + "dep:proc-macro2", + "dep:syn", + "dep:quote", + "dep:prettyplease" + ] + [dependencies] lazy_static.workspace = true thiserror.workspace = true @@ -23,3 +32,8 @@ sha.workspace = true archiver-rs.workspace = true + + proc-macro2 = { version = "^1.0", optional = true } + syn = { version = "^2.0", optional = true } + quote = { version = "^1.0", optional = true } + prettyplease = { version = "^0.2", optional = true } \ No newline at end of file diff --git a/crates/chur-build/src/cfg.rs b/crates/chur-build/src/cfg.rs index 23cd4d2..bb3b618 100644 --- a/crates/chur-build/src/cfg.rs +++ b/crates/chur-build/src/cfg.rs @@ -12,6 +12,9 @@ pub struct Config { pub(crate) dependencies: Vec, pub(crate) file_descriptors: bool, + + #[cfg(feature = "codegen")] + pub(crate) codegen: Option, } impl Config { diff --git a/crates/chur-build/src/cfg/builder.rs b/crates/chur-build/src/cfg/builder.rs index 26ae6c9..49a08ff 100644 --- a/crates/chur-build/src/cfg/builder.rs +++ b/crates/chur-build/src/cfg/builder.rs @@ -1,5 +1,8 @@ use std::fmt::Display; +#[cfg(feature = "codegen")] +use std::path::PathBuf; + use crate::{defined_constants::ROOT_MANIFEST_DIR, dependency::Dependency}; use super::Config; @@ -13,6 +16,9 @@ pub struct ConfigBuilder { protos: Vec, file_descriptors: bool, + + #[cfg(feature = "codegen")] + codegen: Option, } impl ConfigBuilder { @@ -51,11 +57,26 @@ impl ConfigBuilder { self } + /// Generate file descriptors pub fn file_descriptors(mut self, file_descriptors: bool) -> Self { self.file_descriptors = file_descriptors; self } + #[cfg(feature = "codegen")] + /// Instead of just making the manifest, forcing to use [`chur::include_tree`][include_tree] + /// just dump the code into a file at the provided path. + /// + /// This works better with rust-analyzer. + /// + /// Provided path should be relative to the workspace `Cargo.toml`. + /// + /// [include_tree]: https://docs.rs/chur/latest/chur/macro.include_tree.html + pub fn codegen(mut self, codegen_path: impl ToString) -> Self { + self.codegen = Some(codegen_path.to_string()); + self + } + /// Build the [ConfigBuilder] into a [Config] /// /// This changes the directories used to be absolute. @@ -73,6 +94,9 @@ impl ConfigBuilder { protos, dependencies: self.dependencies, file_descriptors: self.file_descriptors, + + #[cfg(feature = "codegen")] + codegen: self.codegen.map(PathBuf::from), }) } } diff --git a/crates/chur-build/src/execute.rs b/crates/chur-build/src/execute.rs index 41191ec..acd1ecc 100644 --- a/crates/chur-build/src/execute.rs +++ b/crates/chur-build/src/execute.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "codegen")] +use std::io::Write; + use crate::{ defined_constants::{ DEPENDENCY_INCLUDE_DIR, GENERATED_DESCRIPTORS_FILE, GENERATED_SOURCES_DIR, @@ -39,5 +42,18 @@ pub fn execute(cfg: Config) -> ChurResult<()> { builder.compile(&cfg.protos, &include_dirs)?; + + #[cfg(feature = "codegen")] + if let Some(codegen_output) = cfg.codegen { + let absolute_output = crate::defined_constants::ROOT_MANIFEST_DIR.join(codegen_output); + let mut output_file = std::fs::OpenOptions::new().truncate(true).write(true).open(absolute_output)?; + + let parsed = syn::parse2(crate::include_tree::include_tree()).unwrap(); + + let output_string = prettyplease::unparse(&parsed); + + output_file.write_all(output_string.as_bytes())?; + } + Ok(()) } diff --git a/crates/chur-build/src/include_tree.rs b/crates/chur-build/src/include_tree.rs new file mode 100644 index 0000000..dda31d0 --- /dev/null +++ b/crates/chur-build/src/include_tree.rs @@ -0,0 +1,142 @@ +use std::{ + env, path::{Path, PathBuf} +}; + +use proc_macro2::{Span, TokenStream}; +use syn::{Ident, LitStr}; + +use quote::{quote, quote_spanned}; + +#[derive(Debug, Default)] +struct Mod(String, Vec); + +#[derive(Debug)] +struct File(String); + +#[derive(Debug)] +enum TreeItem { + Mod(Mod), + File(File), +} + +fn mod_to_token_stream(mod_item: Mod, root_dir: &Path) -> TokenStream { + let mod_children = mod_item.1.into_iter().map(|item| match item { + TreeItem::Mod(mod_item) => mod_to_token_stream(mod_item, root_dir), + TreeItem::File(file_item) => file_to_token_stream(file_item, root_dir), + }); + + let mod_name = syn::parse_str::(mod_item.0.as_str()).expect("invalid mod ident"); + + quote! { + pub mod #mod_name { + #(#mod_children)* + } + } +} + +fn file_to_token_stream(file_item: File, root_dir: &Path) -> TokenStream { + let path = root_dir + .join(format!("{}.rs", file_item.0)) + .display() + .to_string(); + let path_tok = syn::Lit::Str(LitStr::new(&path, Span::call_site())); + + quote!(include!(#path_tok);) +} + +fn insert_into_mod<'a>( + mod_item: &mut Mod, + mut path_chunk_iter: impl Iterator, + original_path_name: String, +) { + if let Some(mod_name) = path_chunk_iter.next() { + let child_mod = mod_item.1.iter_mut().find_map(|m| { + if let TreeItem::Mod(existant_mod) = m { + if existant_mod.0 == mod_name { + Some(existant_mod) + } else { + None + } + } else { + None + } + }); + + if let Some(child) = child_mod { + insert_into_mod(child, path_chunk_iter, original_path_name); + } else { + let mut new_mod = Mod(mod_name.to_string(), vec![]); + + insert_into_mod(&mut new_mod, path_chunk_iter, original_path_name); + mod_item.1.push(TreeItem::Mod(new_mod)) + } + } else { + mod_item.1.push(TreeItem::File(File(original_path_name))) + } +} + +pub(super) fn include_tree() -> TokenStream { + let out_dir = env::var("OUT_DIR").unwrap(); + + // path to file descriptor if present + let mut fd: Option = None; + + let out_dir_path = Path::new(out_dir.as_str()); + let dir_contents = out_dir_path.read_dir().unwrap().filter_map(|entry| { + if let Ok(entry) = entry { + if entry.path().is_file() { + if let Some(file_name) = entry.path().file_name() { + let fn_string = file_name.to_string_lossy(); + if fn_string.starts_with("__fd") { + fd = Some(entry.path()) + } else if let Some(ext) = entry.path().extension() { + if ext == "rs" { + return Some( + entry + .file_name() + .to_string_lossy() + .to_string() + .trim_end_matches(".rs") + .to_string(), + ); + } + } else { + return None; + } + } else { + return None; + } + } + } + + None + }); + + let mut root_tree = Mod::default(); + + dir_contents.for_each(|item| { + insert_into_mod(&mut root_tree, item.split('.'), item.clone()); + }); + + let tree_items = root_tree.1.into_iter().map(|item| match item { + TreeItem::Mod(mod_item) => mod_to_token_stream(mod_item, out_dir_path), + TreeItem::File(file_item) => file_to_token_stream(file_item, out_dir_path), + }); + + let call_site = Span::call_site(); + + let fd_token_stream = if let Some(fd_path) = fd { + let file_path = fd_path.to_str().unwrap(); + Some( + quote_spanned!(call_site=> pub const FILE_DESCRIPTOR_BYTES: &'static [u8] = include_bytes!(#file_path);), + ) + } else { + None + }; + + quote! { + #(#tree_items)* + + #fd_token_stream + } +} diff --git a/crates/chur-build/src/lib.rs b/crates/chur-build/src/lib.rs index 9b43e18..6302314 100644 --- a/crates/chur-build/src/lib.rs +++ b/crates/chur-build/src/lib.rs @@ -3,6 +3,9 @@ mod defined_constants; mod execute; mod manifest; +#[cfg(feature = "codegen")] +mod include_tree; + pub mod dependency; pub mod error; diff --git a/example/build.rs b/example/build.rs index f9e491e..773fe18 100644 --- a/example/build.rs +++ b/example/build.rs @@ -14,6 +14,7 @@ fn main() -> Result<(), ChurError> { ]) .dependency(Dependency::github("googleapis/api-common-protos", None)) .file_descriptors(true) + .codegen("example/src/pb.rs") .build() .unwrap(); diff --git a/example/src/pb.rs b/example/src/pb.rs index 03dfbf3..5ccca25 100644 --- a/example/src/pb.rs +++ b/example/src/pb.rs @@ -1 +1,19 @@ -chur::include_tree!(); +pub mod google { + pub mod r#type { + include!( + "/home/d3rpp/code/chur/target/debug/build/chur-example-a6531aff33ada8de/out/google.r#type.rs" + ); + } +} +pub mod example { + pub mod hello_world { + pub mod v1 { + include!( + "/home/d3rpp/code/chur/target/debug/build/chur-example-a6531aff33ada8de/out/example.hello_world.v1.rs" + ); + } + } +} +pub const FILE_DESCRIPTOR_BYTES: &'static [u8] = include_bytes!( + "/home/d3rpp/code/chur/target/debug/build/chur-example-a6531aff33ada8de/out/__fd.bin" +);