fs_embed_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{Lit, LitStr, parse::Parse, parse_macro_input};
5
6
7
8/// Embed a directory at compile time, returning a `Dir` enum. The path should be a literal string
9/// and strictly relative to the crate root.
10/// fs_embed!("dir")                 → Dir::from_embedded
11#[proc_macro]
12pub fn fs_embed(input: TokenStream) -> TokenStream {
13    let args = parse_macro_input!(input as EmbedArgs);
14
15    let rel_lit: LitStr = match args.path {
16        Lit::Str(s) => s,
17        other => return compile_error("first argument must be a string literal", other.span()),
18    };
19
20    let rel_path = rel_lit.value();
21    let call_span = rel_lit.span(); // proc_macro2::Span
22
23    // ── validate directory exists inside crate root ────────────────────────
24    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
25        Ok(dir) => dir,
26        Err(_) => return compile_error("fs_embed!: CARGO_MANIFEST_DIR not set", call_span),
27    };
28
29    let full_path = match std::path::Path::new(&manifest_dir)
30        .join(&rel_path)
31        .canonicalize()
32        .map_err(|_| {
33            syn::Error::new(
34                call_span,
35                format!("fs_embed!: failed to resolve path: {}", rel_path),
36            )
37        }) {
38        Ok(p) => p,
39        Err(msg) => return compile_error(msg.to_string(), call_span),
40    };
41
42    let full_path = match full_path.to_str() {
43        Some(p) => p,
44        None => return compile_error("fs_embed!: path must be valid UTF-8", call_span),
45    };
46
47    if !full_path.starts_with(&manifest_dir) {
48        let msg = format!(
49            "fs_embed!: directory not found:\n  {full_path}\n  expected to be inside crate root:\n  {manifest_dir}\n  relative path: {rel_path}",
50        );
51        return compile_error(&msg, call_span);
52    };
53
54    let full_literal: LitStr = LitStr::new(full_path, call_span);
55
56    let embed_code = quote! {
57        ::fs_embed::Dir::from_embedded(include_dir::include_dir!(#full_literal), #full_literal)
58    };
59
60    quote! { #embed_code }.into()
61}
62
63
64
65/// Emit `compile_error!($msg)` at the given span.
66#[doc(hidden)]
67fn compile_error<S: AsRef<str>>(msg: S, span: Span) -> TokenStream {
68    let lit = LitStr::new(msg.as_ref(), span);
69    quote!(compile_error!(#lit)).into()
70}
71
72struct EmbedArgs {
73    path: Lit,
74}
75
76impl Parse for EmbedArgs {
77    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
78        let path: Lit = input.parse()?;
79        Ok(EmbedArgs { path })
80    }
81}