use std::{env, fs, path::PathBuf};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{LitStr, parse_macro_input};
#[proc_macro]
pub fn include_packed(input: TokenStream) -> TokenStream {
let lit_str = parse_macro_input!(input as LitStr);
env::var("INCLUDE_PACKED_TARGET_ARCH").map_or_else(|_| syn::Error::new(
lit_str.span(),
"include_packed: build script has not run. This is expected during analysis (e.g., by rust-analyzer).",
)
.to_compile_error()
.into(), |target_arch| if target_arch == "wasm32" {
get_tokens_wasm(&lit_str).into()
} else {
get_tokens_native(&lit_str).into()
})
}
fn get_tokens_wasm(lit_str: &LitStr) -> TokenStream2 {
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::Span;
use syn::Ident;
let path_str = lit_str.value();
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR is not set; this macro must be run by Cargo.");
let path = PathBuf::from(manifest_dir).join(&path_str);
let content = match fs::read(&path) {
Ok(bytes) => bytes,
Err(err) => {
let msg = format!(
"include_packed: could not read file '{}' for wasm target: {err}",
path.display()
);
return syn::Error::new(lit_str.span(), msg).to_compile_error();
}
};
let compressed_content = zstd::encode_all(&*content, zstd::DEFAULT_COMPRESSION_LEVEL)
.expect("zstd compression failed in proc-macro");
let compressed_len = compressed_content.len();
let crate_name = match crate_name("include_packed") {
Ok(FoundCrate::Name(name)) => Ident::new(&name, Span::call_site()),
Ok(FoundCrate::Itself) => Ident::new("crate", Span::call_site()),
Err(_) => Ident::new("include_packed", Span::call_site()), };
quote! {
{
const COMPRESSED_DATA: [u8; #compressed_len] = [#(#compressed_content),*];
#crate_name::decompress(&COMPRESSED_DATA)
}
}
}
fn get_tokens_native(lit_str: &LitStr) -> TokenStream2 {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::Span;
use syn::Ident;
let out_dir =
env::var("OUT_DIR").expect("OUT_DIR is not set; this macro must be run by Cargo.");
let path_str = lit_str.value();
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR is not set; this macro must be run by Cargo.");
let mut path = PathBuf::from(&manifest_dir);
path.push(&path_str);
let canonical_path = path
.canonicalize()
.unwrap_or_else(|e| panic!("Could not find file '{}': {e}", path.display()));
let path_for_hashing = canonical_path
.strip_prefix(&manifest_dir)
.unwrap_or(&canonical_path)
.to_path_buf();
let metadata = fs::metadata(&canonical_path).unwrap_or_else(|e| {
panic!(
"Could not read metadata for '{}': {e}",
canonical_path.display()
)
});
let modified_time = metadata.modified().unwrap_or_else(|e| {
panic!(
"Could not read modification time for '{}': {e}",
canonical_path.display()
)
});
let mut hasher = DefaultHasher::new();
path_for_hashing.hash(&mut hasher);
modified_time.hash(&mut hasher);
let unique_name = format!("include_packed_{:016x}", hasher.finish());
let len_path = PathBuf::from(&out_dir).join(format!("{unique_name}.len"));
let Ok(len_str) = fs::read_to_string(&len_path) else {
let msg = format!(
"include_packed: failed to read .len file for asset at '{path_str}'\nexpected at: {}",
len_path.display()
);
return syn::Error::new(lit_str.span(), msg).to_compile_error();
};
let compressed_len: usize = len_str.parse().unwrap_or_else(|_| {
panic!(
"include_packed: corrupt .len file at '{}'",
len_path.display()
)
});
let crate_name = match crate_name("include_packed") {
Ok(FoundCrate::Name(name)) => Ident::new(&name, Span::call_site()),
Ok(FoundCrate::Itself) => Ident::new("crate", Span::call_site()),
Err(_) => Ident::new("include_packed", Span::call_site()), };
quote! {
{
unsafe extern "C" {
#[link_name = #unique_name]
static STATIC: [u8; #compressed_len];
}
#crate_name::decompress(unsafe { &STATIC })
}
}
}