1extern 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
14struct SiloMacroInput {
17 path: LitStr,
18 force: Option<(syn::Ident, syn::LitBool)>,
19 crate_path: Option<syn::Path>,
20}
21
22impl 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#[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 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 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
118fn 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
171fn 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
180fn 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}