catgirl_engine_macros/
lib.rs

1//! Procedural Macros for the game engine
2#![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/// Embeds resources folder into the binary
19///
20/// # Panics
21///
22/// Panics if passed no tokens or tokens other than a literal or env!() macro
23#[proc_macro]
24pub fn generate_embedded_resources(tokens: TokenStream) -> TokenStream {
25    // Parse Resources Path
26    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/// Determines if a path should be embedded
44#[rustfmt::skip]
45fn should_embed(path: &Path) -> bool {
46    let resources_path: PathBuf = PathBuf::from("resources");
47
48    [
49        // Locales should always be embedded
50        resources_path.join("locales"),
51
52        // Only embed if the build target does not support filesystems
53        #[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
78/// Recursively retrieve within the specified directory
79fn 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                // println!("FP: {:?}; SFP: {:?}", &full_path, &path);
95                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
106/// Shorten the path to only the part after the tail end
107fn 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
131/// Parses resource macros
132fn parse_resource_macro(tokens: TokenStream) -> Result<String, TokenStream> {
133    // Parses tokens into a rust expression
134    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    // Match against the two types of tokens we care about and parse
140    let token_expr: Expr = token_expr_result.unwrap();
141    match token_expr {
142        // Parses for string literal ("/path/to/resources")
143        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
152/// Parses String Literals
153fn 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
159/// Parses Expression Macros
160fn 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
180/// Create's an error as a `TokenStream` to unwind to compiler
181fn 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}