catgirl_engine_macros/
lib.rs1#![warn(missing_docs)]
3#![doc(
4 html_favicon_url = "https://engine.catgirl.land/resources/assets/vanilla/texture/logo/logo.svg",
5 html_logo_url = "https://engine.catgirl.land/resources/assets/vanilla/texture/logo/logo.svg",
6 html_playground_url = "https://play.rust-lang.org"
7)]
8
9use common::resources::{EmbeddedFile, EmbeddedFiles};
10use std::{
11 collections::VecDeque,
12 path::{Path, PathBuf},
13};
14
15use proc_macro::TokenStream;
16use syn::{Expr, LitStr};
17
18#[proc_macro]
24pub fn generate_embedded_resources(tokens: TokenStream) -> TokenStream {
25 let resources_path_result: Result<String, TokenStream> = parse_resource_macro(tokens);
27 if let Err(error_message) = resources_path_result {
28 return error_message;
29 }
30
31 let files: EmbeddedFiles = get_files(&PathBuf::from(&resources_path_result.unwrap()));
32 let files_json: String = serde_json::to_string(&files).unwrap();
33
34 quote::quote! {
35 pub(super) fn get_embedded_resources() -> common::resources::EmbeddedFiles {
36 let files_json: String = #files_json.to_string();
37 serde_json::from_str::<common::resources::EmbeddedFiles>(&files_json).unwrap()
38 }
39 }
40 .into()
41}
42
43#[rustfmt::skip]
45fn should_embed(path: &Path) -> bool {
46 let resources_path: PathBuf = PathBuf::from("resources");
47
48 [
49 resources_path.join("locales"),
51
52 #[cfg(feature = "embed-assets")]
54 resources_path.join("assets"),
55
56 #[cfg(all(feature = "embed-assets", target_os = "linux"))]
57 resources_path.join("linux"),
58
59 #[cfg(all(feature = "embed-assets", target_os = "windows"))]
60 resources_path.join("windows"),
61
62 #[cfg(all(feature = "embed-assets", target_os = "macos"))]
63 resources_path.join("osx"),
64
65 #[cfg(all(feature = "embed-assets", target_os = "android"))]
66 resources_path.join("android"),
67
68 #[cfg(all(feature = "embed-assets", target_os = "ios"))]
69 resources_path.join("ios"),
70
71 #[cfg(all(feature = "embed-assets", target_family = "wasm"))]
72 resources_path.join("wasm"),
73 ]
74 .into_iter()
75 .any(|embed_path| path.starts_with(embed_path))
76}
77
78fn get_files(resources_path: &Path) -> EmbeddedFiles {
80 let mut dirs: VecDeque<std::fs::ReadDir> =
81 VecDeque::from([std::fs::read_dir(resources_path).unwrap()]);
82 let mut files: EmbeddedFiles = EmbeddedFiles { inner: Vec::new() };
83
84 while !dirs.is_empty() {
85 let dir: std::fs::ReadDir = VecDeque::pop_front(&mut dirs).unwrap();
86
87 for dir_entry in dir {
88 let full_path: PathBuf = dir_entry.as_ref().unwrap().path();
89 let path: PathBuf = shorten_file_paths(resources_path, &full_path);
90
91 if full_path.is_dir() {
92 dirs.push_back(std::fs::read_dir(&full_path).unwrap());
93 } else if should_embed(&path) {
94 files.inner.push(EmbeddedFile {
96 path: path.to_str().unwrap().to_string(),
97 contents: std::fs::read(path).unwrap(),
98 });
99 }
100 }
101 }
102
103 files
104}
105
106fn shorten_file_paths(resources_path: &Path, file_path: &Path) -> PathBuf {
108 let path_components: std::path::Components<'_> = file_path.components();
109
110 let mut shortened_file_path: PathBuf = PathBuf::new();
111 let mut temp_base_path: PathBuf = PathBuf::new();
112 let mut found_root_path: bool = false;
113 for component in path_components {
114 temp_base_path.push(component);
115
116 if !found_root_path && temp_base_path.eq(resources_path) {
117 temp_base_path.clear();
118 temp_base_path.push(PathBuf::from(resources_path.file_name().unwrap()));
119
120 found_root_path = true;
121 }
122
123 if found_root_path {
124 shortened_file_path.clone_from(&temp_base_path);
125 }
126 }
127
128 shortened_file_path
129}
130
131fn parse_resource_macro(tokens: TokenStream) -> Result<String, TokenStream> {
133 let token_expr_result: Result<Expr, syn::Error> = syn::parse::<Expr>(tokens);
135 if let Err(error) = token_expr_result {
136 return Err(error.to_compile_error().into());
137 }
138
139 let token_expr: Expr = token_expr_result.unwrap();
141 match token_expr {
142 syn::Expr::Lit(path_lit) => Ok(parse_string_literal(path_lit)),
144 syn::Expr::Macro(path_macro) => parse_expr_macro(path_macro),
145 _ => Err(create_error(
146 token_expr,
147 "Cannot parse embedded resource expression...",
148 )),
149 }
150}
151
152fn parse_string_literal(string_literal: syn::ExprLit) -> String {
154 syn::parse2::<syn::LitStr>(quote::ToTokens::to_token_stream(&string_literal))
155 .unwrap()
156 .value()
157}
158
159fn parse_expr_macro(macro_token: syn::ExprMacro) -> Result<String, TokenStream> {
161 let macro_segments: &syn::PathSegment = macro_token.mac.path.segments.first().unwrap();
162 let macro_identifier: String = macro_segments.ident.to_string();
163
164 if macro_identifier.eq("env") {
165 let macro_tokens = macro_token.mac.tokens.clone();
166 let macro_string: String = syn::parse2::<LitStr>(macro_tokens).unwrap().value();
167 let env_var: Result<String, std::env::VarError> = std::env::var(macro_string);
168
169 if let Ok(environment_variable) = env_var {
170 return Ok(environment_variable);
171 }
172 }
173
174 Err(create_error(
175 macro_token.into(),
176 "Could not parse expression macro...",
177 ))
178}
179
180fn create_error(token_expr: Expr, message: &str) -> TokenStream {
182 use syn::spanned::Spanned;
183
184 syn::Error::new(token_expr.span(), message)
185 .to_compile_error()
186 .into()
187}