rust_silos_macros/
lib.rs

1//! Proc-macro for rust-silos: generates a PHF map of static str to EmbedEntry.
2
3extern crate proc_macro;
4use proc_macro::TokenStream;
5use quote::{quote, quote_spanned};
6use std::fs;
7use std::path::Path;
8use syn::{
9    parse::{Parse, ParseStream},
10    parse_macro_input, LitStr, Token,
11};
12use walkdir::WalkDir;
13
14/// Internal: Macro input parser for `silo!` macro. Accepts a path and optional force argument.
15/// Path must be a string literal. Force is a bool literal.
16struct SiloMacroInput {
17    path: LitStr,
18    force: Option<(syn::Ident, syn::LitBool)>,
19    crate_path: Option<syn::Path>,
20}
21
22/// Parse implementation for macro input. Handles path and optional force argument.
23impl Parse for SiloMacroInput {
24    fn parse(input: ParseStream) -> syn::Result<Self> {
25        let path: LitStr = input.parse()?;
26        let mut force = None;
27        let mut crate_path = None;
28        while input.peek(Token![,]) {
29            input.parse::<Token![,]>()?;
30            let ident: syn::Ident = input.parse()?;
31            input.parse::<Token![=]>()?;
32            if ident == "force" {
33                let value: syn::LitBool = input.parse()?;
34                force = Some((ident, value));
35            } else if ident == "crate" {
36                let path: syn::Path = input.parse()?;
37                crate_path = Some(path);
38            } else {
39                return Err(syn::Error::new(ident.span(), "Unknown argument to embed_silo!"));
40            }
41        }
42        Ok(SiloMacroInput { path, force, crate_path })
43    }
44}
45
46/// Macro to embed all files in a directory as a PHF map for fast, allocation-free access.
47///
48/// Usage: `let silo = embed_silo!("assets");` or `let silo = embed_silo!("assets", force = true);`
49/// In debug mode, uses dynamic loading unless `force = true`.
50/// Directory path must exist at build time for embedding.
51#[proc_macro]
52pub fn embed_silo(input: TokenStream) -> TokenStream {
53    let SiloMacroInput { path, force, crate_path } = parse_macro_input!(input as SiloMacroInput);
54    let dir_path = path.value();
55    let call_span = path.span();
56    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| String::new());
57    if manifest_dir.is_empty() {
58        return compile_error("embed_silo!: CARGO_MANIFEST_DIR not set", call_span);
59    }
60    let abs_path = Path::new(&manifest_dir).join(&dir_path);
61    let abs_path = match abs_path.canonicalize() {
62        Ok(p) => p,
63        Err(_) => {
64            return compile_error(
65                format!("embed_silo!: failed to resolve path: {}", dir_path),
66                call_span,
67            )
68        }
69    };
70    let abs_path_str = match abs_path.to_str() {
71        Some(p) => p,
72        None => return compile_error("embed_silo!: path must be valid UTF-8", call_span),
73    };
74    if !abs_path_str.starts_with(&manifest_dir) {
75        let msg = format!(
76            "embed_silo!: directory not found:\n  {}\n  expected to be inside crate root:\n  {}\n  relative path: {}",
77            abs_path_str, manifest_dir, dir_path
78        );
79        return compile_error(&msg, call_span);
80    }
81    let force_embed = force.as_ref().map_or(false, |(_, v)| v.value());
82    let debug = cfg!(debug_assertions);
83    let use_embed = force_embed || !debug;
84    let crate_root = crate_path
85        .map(|p| quote! { #p })
86        .unwrap_or_else(|| quote! { ::rust_silos });
87    if use_embed {
88        // Generate PHF map at compile time
89        let (entries, errors) = collect_embed_entries(abs_path_str, call_span);
90        if !errors.is_empty() {
91            return quote! { #(#errors)* }.into();
92        }
93        let phf_pairs = generate_phf_map(&entries, &crate_root);
94        let root = dir_path.clone();
95        // Use a hash of the absolute path for uniqueness
96        let mut hasher = std::collections::hash_map::DefaultHasher::new();
97        use std::hash::{Hash, Hasher};
98        abs_path_str.hash(&mut hasher);
99        let hash = hasher.finish();
100        let map_ident = quote::format_ident!("__EMBED_MAP_{:x}", hash);
101        let expanded = quote! {
102            {
103                static #map_ident: #crate_root::phf::Map<&'static str, #crate_root::EmbedEntry> = #crate_root::phf::phf_map! {
104                    #phf_pairs
105                };
106                #crate_root::Silo::from_embedded(&#map_ident, #root)
107            }
108        };
109        expanded.into()
110    } else {
111        let expanded = quote! {
112            #crate_root::Silo::from_path(#dir_path)
113        };
114        expanded.into()
115    }
116}
117
118/// Recursively collects all files in the given directory for embedding.
119/// Returns (entries, errors):
120///   - entries: Vec<(relative_path, abs_path, size, modified)>
121///   - errors: Vec<TokenStream> for compile_error!s
122fn collect_embed_entries(dir: &str, span: proc_macro2::Span) -> (Vec<(String, String, usize, u64)>, Vec<proc_macro2::TokenStream>) {
123    let mut entries = Vec::new();
124    let mut errors = Vec::new();
125    let root = Path::new(dir);
126    for entry in WalkDir::new(root).into_iter() {
127        let entry = match entry {
128            Ok(e) => e,
129            Err(e) => {
130                let msg = format!("embed_silo!: failed to read entry: {}", e);
131                errors.push(quote_spanned! {span=> compile_error!(#msg); });
132                continue;
133            }
134        };
135        if entry.file_type().is_file() {
136            let path = entry.path();
137            let rel_path = match path.strip_prefix(root) {
138                Ok(r) => r.to_string_lossy().replace('\\', "/"),
139                Err(_) => {
140                    let msg = "embed_silo!: failed to get relative path";
141                    errors.push(quote_spanned! {span=> compile_error!(#msg); });
142                    continue;
143                }
144            };
145            let abs_path = match path.canonicalize() {
146                Ok(p) => p.to_string_lossy().to_string(),
147                Err(_) => {
148                    let msg = format!("embed_silo!: failed to canonicalize file: {}", path.display());
149                    errors.push(quote_spanned! {span=> compile_error!(#msg); });
150                    continue;
151                }
152            };
153            let size = match fs::metadata(path) {
154                Ok(meta) => meta.len() as usize,
155                Err(_) => 0,
156            };
157            let modified = match fs::metadata(path)
158                .and_then(|m| m.modified())
159                .ok()
160                .and_then(|mtime| mtime.duration_since(std::time::UNIX_EPOCH).ok())
161            {
162                Some(d) => d.as_secs(),
163                None => 0,
164            };
165            entries.push((rel_path, abs_path, size, modified));
166        }
167    }
168    (entries, errors)
169}
170
171// emit_compile_error removed; use quote_spanned! inline instead
172
173/// Emit compile_error! and return from macro expansion.
174fn compile_error<S: AsRef<str>>(msg: S, span: proc_macro2::Span) -> proc_macro::TokenStream {
175    let lit = syn::LitStr::new(msg.as_ref(), span);
176    let tokens = quote!(compile_error!(#lit));
177    tokens.into()
178}
179
180/// Generates a PHF map token stream from the collected entries.
181/// Used internally by the macro. Expects (rel_path, abs_path, size, modified) tuples.
182fn generate_phf_map(entries: &[(String, String, usize, u64)], crate_root: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
183    let pairs = entries.iter().map(|(rel_path, abs_path, size, modified)| {
184        let rel_path_lit = syn::LitStr::new(rel_path, proc_macro2::Span::call_site());
185        let abs_path_lit = syn::LitStr::new(abs_path, proc_macro2::Span::call_site());
186        let size_lit = syn::LitInt::new(&size.to_string(), proc_macro2::Span::call_site());
187        let mod_lit = syn::LitInt::new(&modified.to_string(), proc_macro2::Span::call_site());
188        quote! {
189            #rel_path_lit => #crate_root::EmbedEntry {
190                path: #rel_path_lit,
191                contents: include_bytes!(#abs_path_lit),
192                size: #size_lit,
193                modified: #mod_lit,
194            },
195        }
196    });
197    quote! {
198        #(#pairs)*
199    }
200}