2020-06-04 16:39:33 -04:00
|
|
|
use proc_macro2::{Span, TokenStream};
|
2020-12-16 05:55:28 -05:00
|
|
|
use quote::quote;
|
2020-12-16 04:44:24 -05:00
|
|
|
use std::collections::hash_map::DefaultHasher;
|
|
|
|
use std::hash::{Hash, Hasher};
|
2023-07-28 10:28:02 -04:00
|
|
|
use std::io::Write;
|
2023-10-02 13:31:14 -04:00
|
|
|
use std::iter;
|
2020-06-04 16:39:33 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
2023-07-28 10:28:02 -04:00
|
|
|
use std::time::Duration;
|
|
|
|
use std::{env, thread};
|
2020-12-16 05:55:28 -05:00
|
|
|
use syn::parse::{ParseStream, Parser, Result as ParseResult};
|
2020-06-04 16:39:33 -04:00
|
|
|
use syn::punctuated::Punctuated;
|
2020-06-07 10:23:45 -04:00
|
|
|
use syn::{Fields, Ident, ItemStruct, LitBool, LitChar, LitStr, Token};
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2023-10-02 13:31:14 -04:00
|
|
|
use crate::compiler::Compiler;
|
2020-06-07 04:58:52 -04:00
|
|
|
use crate::config::Config;
|
2020-06-04 16:39:33 -04:00
|
|
|
use crate::error::*;
|
2023-10-02 13:31:14 -04:00
|
|
|
use crate::util::filetime;
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-06-13 09:58:57 -04:00
|
|
|
// options for `template` attributes
|
2020-06-04 16:39:33 -04:00
|
|
|
#[derive(Default)]
|
|
|
|
struct DeriveTemplateOptions {
|
2020-12-16 05:55:28 -05:00
|
|
|
found_keys: Vec<Ident>,
|
2020-06-04 16:39:33 -04:00
|
|
|
path: Option<LitStr>,
|
|
|
|
delimiter: Option<LitChar>,
|
|
|
|
escape: Option<LitBool>,
|
2020-06-06 18:01:15 -04:00
|
|
|
rm_whitespace: Option<LitBool>,
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
|
2020-12-16 05:55:28 -05:00
|
|
|
impl DeriveTemplateOptions {
|
|
|
|
fn parser<'s>(&'s mut self) -> impl Parser + 's {
|
2023-04-12 10:02:05 -04:00
|
|
|
move |s: ParseStream| -> ParseResult<()> {
|
2020-12-16 05:55:28 -05:00
|
|
|
while !s.is_empty() {
|
|
|
|
let key = s.parse::<Ident>()?;
|
|
|
|
s.parse::<Token![=]>()?;
|
|
|
|
|
|
|
|
// check if argument is repeated
|
|
|
|
if self.found_keys.iter().any(|e| *e == key) {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
key.span(),
|
|
|
|
format!("Argument `{}` was repeated.", key),
|
|
|
|
));
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-12-16 05:55:28 -05:00
|
|
|
if key == "path" {
|
|
|
|
self.path = Some(s.parse::<LitStr>()?);
|
|
|
|
} else if key == "delimiter" {
|
|
|
|
self.delimiter = Some(s.parse::<LitChar>()?);
|
|
|
|
} else if key == "escape" {
|
|
|
|
self.escape = Some(s.parse::<LitBool>()?);
|
|
|
|
} else if key == "rm_whitespace" {
|
|
|
|
self.rm_whitespace = Some(s.parse::<LitBool>()?);
|
|
|
|
} else {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
key.span(),
|
|
|
|
format!("Unknown option: `{}`", key),
|
|
|
|
));
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-12-16 05:55:28 -05:00
|
|
|
self.found_keys.push(key);
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-12-16 05:55:28 -05:00
|
|
|
// consume comma token
|
|
|
|
if s.is_empty() {
|
|
|
|
break;
|
2020-06-04 16:39:33 -04:00
|
|
|
} else {
|
2020-12-16 05:55:28 -05:00
|
|
|
s.parse::<Token![,]>()?;
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-16 05:55:28 -05:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-14 16:36:39 -04:00
|
|
|
fn merge_config_options(config: &mut Config, options: &DeriveTemplateOptions) {
|
2020-06-04 16:39:33 -04:00
|
|
|
if let Some(ref delimiter) = options.delimiter {
|
2020-06-05 22:58:14 -04:00
|
|
|
config.delimiter = delimiter.value();
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
if let Some(ref escape) = options.escape {
|
2020-06-05 22:58:14 -04:00
|
|
|
config.escape = escape.value;
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
2020-06-06 18:01:15 -04:00
|
|
|
if let Some(ref rm_whitespace) = options.rm_whitespace {
|
|
|
|
config.rm_whitespace = rm_whitespace.value;
|
|
|
|
}
|
2020-06-14 16:36:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn resolve_template_file(path: &str, template_dirs: &[PathBuf]) -> Option<PathBuf> {
|
|
|
|
for template_dir in template_dirs.iter().rev() {
|
|
|
|
let p = template_dir.join(path);
|
|
|
|
if p.is_file() {
|
|
|
|
return Some(p);
|
|
|
|
}
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-06-14 16:36:39 -04:00
|
|
|
let mut fallback = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
|
|
|
|
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
|
|
|
|
));
|
|
|
|
fallback.push("templates");
|
|
|
|
fallback.push(path);
|
|
|
|
|
|
|
|
if fallback.is_file() {
|
|
|
|
return Some(fallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2023-07-28 10:28:02 -04:00
|
|
|
fn filename_hash(path: &Path, config: &Config) -> String {
|
2020-12-16 04:44:24 -05:00
|
|
|
let mut hasher = DefaultHasher::new();
|
2023-07-28 10:28:02 -04:00
|
|
|
config.hash(&mut hasher);
|
2023-10-21 17:09:00 -04:00
|
|
|
let config_hash = hasher.finish();
|
|
|
|
|
|
|
|
path.hash(&mut hasher);
|
|
|
|
let path_hash = hasher.finish();
|
2020-06-14 16:36:39 -04:00
|
|
|
|
2023-10-21 17:09:00 -04:00
|
|
|
format!("{:016x}-{:016x}", config_hash, path_hash)
|
2020-06-14 16:36:39 -04:00
|
|
|
}
|
|
|
|
|
2023-10-02 13:31:14 -04:00
|
|
|
fn with_compiler<T, F: FnOnce(Compiler) -> Result<T, Error>>(
|
2020-06-14 16:36:39 -04:00
|
|
|
config: Config,
|
2023-10-02 13:31:14 -04:00
|
|
|
apply: F,
|
|
|
|
) -> Result<T, Error> {
|
2021-02-03 01:52:14 -05:00
|
|
|
struct FallbackScope {}
|
|
|
|
|
|
|
|
impl FallbackScope {
|
|
|
|
fn new() -> Self {
|
|
|
|
// SAFETY:
|
|
|
|
// Any token or span constructed after `proc_macro2::fallback::force()` must
|
|
|
|
// not outlive after `unforce()` because it causes span mismatch error. In
|
|
|
|
// this case, we must ensure that `compile_file` does not return any token or
|
|
|
|
// span.
|
|
|
|
proc_macro2::fallback::force();
|
|
|
|
FallbackScope {}
|
|
|
|
}
|
|
|
|
}
|
2021-02-02 12:56:15 -05:00
|
|
|
|
2021-02-03 01:52:14 -05:00
|
|
|
impl Drop for FallbackScope {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
proc_macro2::fallback::unforce();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let compiler = Compiler::with_config(config);
|
2021-02-02 12:56:15 -05:00
|
|
|
|
2021-02-03 01:52:14 -05:00
|
|
|
let _scope = FallbackScope::new();
|
2023-10-02 13:31:14 -04:00
|
|
|
apply(compiler)
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
|
|
|
|
let strct = syn::parse2::<ItemStruct>(tokens)?;
|
|
|
|
|
|
|
|
let mut all_options = DeriveTemplateOptions::default();
|
|
|
|
for attr in strct.attrs {
|
2023-04-12 10:02:05 -04:00
|
|
|
if attr.path().is_ident("template") {
|
|
|
|
attr.parse_args_with(all_options.parser())?;
|
2020-06-06 14:44:30 -04:00
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
|
2020-06-14 17:44:49 -04:00
|
|
|
#[cfg(feature = "config")]
|
2020-07-11 04:02:31 -04:00
|
|
|
let mut config = {
|
|
|
|
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
|
|
|
|
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
|
|
|
|
));
|
|
|
|
|
|
|
|
Config::search_file_and_read(&*manifest_dir)
|
|
|
|
.map_err(|e| syn::Error::new(Span::call_site(), e))?
|
|
|
|
};
|
2020-06-14 17:44:49 -04:00
|
|
|
|
|
|
|
#[cfg(not(feature = "config"))]
|
|
|
|
let mut config = Config::default();
|
2020-06-06 09:49:01 -04:00
|
|
|
|
2020-06-11 02:31:55 -04:00
|
|
|
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
|
2020-06-14 16:36:39 -04:00
|
|
|
let template_dir = env::current_dir()
|
|
|
|
.unwrap()
|
2020-06-11 02:31:55 -04:00
|
|
|
.ancestors()
|
|
|
|
.find(|p| p.join("LICENSE").exists())
|
|
|
|
.unwrap()
|
2020-07-14 09:46:58 -04:00
|
|
|
.join("sailfish-tests")
|
2020-06-11 02:31:55 -04:00
|
|
|
.join("integration-tests")
|
|
|
|
.join("tests")
|
|
|
|
.join("fails")
|
|
|
|
.join("templates");
|
2020-06-14 16:36:39 -04:00
|
|
|
config.template_dirs.push(template_dir);
|
2020-06-11 02:31:55 -04:00
|
|
|
}
|
|
|
|
|
2020-06-14 16:36:39 -04:00
|
|
|
let input_file = {
|
|
|
|
let path = all_options.path.as_ref().ok_or_else(|| {
|
|
|
|
syn::Error::new(Span::call_site(), "`path` option must be specified.")
|
|
|
|
})?;
|
2023-10-02 13:31:14 -04:00
|
|
|
resolve_template_file(&*path.value(), &*config.template_dirs)
|
|
|
|
.and_then(|path| path.canonicalize().ok())
|
|
|
|
.ok_or_else(|| {
|
2020-06-14 16:36:39 -04:00
|
|
|
syn::Error::new(
|
|
|
|
path.span(),
|
|
|
|
format!("Template file {:?} not found", path.value()),
|
|
|
|
)
|
2023-10-02 13:31:14 -04:00
|
|
|
})?
|
2020-06-04 16:39:33 -04:00
|
|
|
};
|
|
|
|
|
2023-07-28 10:28:02 -04:00
|
|
|
merge_config_options(&mut config, &all_options);
|
|
|
|
|
|
|
|
// Template compilation through this proc-macro uses a caching mechanism. Output file
|
|
|
|
// names include a hash calculated from input file contents and compiler
|
|
|
|
// configuration. This way, existing files never need updating and can simply be
|
|
|
|
// re-used if they exist.
|
2020-12-28 15:19:12 -05:00
|
|
|
let mut output_file = PathBuf::from(env!("OUT_DIR"));
|
2020-06-04 16:39:33 -04:00
|
|
|
output_file.push("templates");
|
2023-07-28 10:28:02 -04:00
|
|
|
output_file.push(filename_hash(&*input_file, &config));
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2023-07-28 10:28:02 -04:00
|
|
|
std::fs::create_dir_all(&output_file.parent().unwrap()).unwrap();
|
|
|
|
|
|
|
|
// This makes sure max 1 process creates a new file, "create_new" check+create is an
|
|
|
|
// atomic operation. Cargo sometimes runs multiple macro invocations for the same
|
|
|
|
// file in parallel, so that's important to prevent a race condition.
|
2023-10-02 13:31:14 -04:00
|
|
|
struct Lock<'path> {
|
|
|
|
path: &'path Path,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'path> Lock<'path> {
|
|
|
|
fn new(path: &'path Path) -> std::io::Result<Self> {
|
|
|
|
std::fs::OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create_new(true)
|
|
|
|
.open(path)
|
|
|
|
.map(|_| Lock { path })
|
|
|
|
}
|
|
|
|
}
|
2023-07-28 10:28:02 -04:00
|
|
|
|
2023-10-02 13:31:14 -04:00
|
|
|
impl<'path> Drop for Lock<'path> {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
std::fs::remove_file(self.path)
|
|
|
|
.expect("Failed to clean up lock file {}. Delete it manually, or run `cargo clean`.");
|
2023-07-28 10:28:02 -04:00
|
|
|
}
|
2023-10-02 13:31:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
let deps = with_compiler(config, |compiler| {
|
|
|
|
let dep_path = output_file.with_extension("deps");
|
|
|
|
let lock_path = output_file.with_extension("lock");
|
|
|
|
let lock = Lock::new(&lock_path);
|
|
|
|
match lock {
|
|
|
|
Ok(lock) => {
|
|
|
|
let (tsource, report) = compiler.resolve_file(&input_file)?;
|
|
|
|
|
|
|
|
let output_filetime = filetime(&output_file);
|
|
|
|
let input_filetime = iter::once(&input_file)
|
|
|
|
.chain(&report.deps)
|
|
|
|
.map(|path| filetime(path))
|
|
|
|
.max()
|
|
|
|
.expect("Iterator contains at least `input_file`");
|
|
|
|
|
|
|
|
// Recompile template if any included templates were changed
|
|
|
|
// since the last time we compiled.
|
|
|
|
if input_filetime > output_filetime {
|
|
|
|
compiler.compile_file(&input_file, tsource, &output_file)?;
|
|
|
|
|
|
|
|
// Write access to `dep_path` is serialized by `lock`.
|
|
|
|
let mut dep_file = std::fs::OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(true)
|
|
|
|
.truncate(true)
|
|
|
|
.open(&dep_path)
|
|
|
|
.unwrap_or_else(|e| {
|
|
|
|
panic!("Failed to open {:?}: {}", dep_path, e)
|
|
|
|
});
|
|
|
|
|
|
|
|
// Write out dependencies for concurrent processes to reuse.
|
|
|
|
for dep in &report.deps {
|
|
|
|
writeln!(&mut dep_file, "{}", dep.to_str().unwrap()).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prevent output file from being tracked by Cargo. Without this hack,
|
|
|
|
// every change to a template causes two recompilations:
|
|
|
|
//
|
|
|
|
// 1. Change a template at timestamp t.
|
|
|
|
// 2. Cargo detects template change due to `include_bytes!` macro below.
|
|
|
|
// 3. Sailfish compiler generates an output file with a later timestamp t'.
|
|
|
|
// 4. Build finishes with timestamp t.
|
|
|
|
// 5. Next cargo build detects output file with timestamp t' > t and rebuilds.
|
|
|
|
// 6. Sailfish compiler does not regenerate output due to timestamp logic above.
|
|
|
|
// 7. Build finishes with timestamp t'.
|
|
|
|
let _ = filetime::set_file_times(
|
|
|
|
&output_file,
|
|
|
|
input_filetime,
|
|
|
|
input_filetime,
|
|
|
|
);
|
2023-07-28 10:28:02 -04:00
|
|
|
}
|
|
|
|
|
2023-10-02 13:31:14 -04:00
|
|
|
drop(lock);
|
|
|
|
Ok(report.deps)
|
|
|
|
}
|
|
|
|
// Lock file exists, template is already (currently being?) compiled.
|
|
|
|
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
|
|
|
let mut load_attempts = 0;
|
|
|
|
while lock_path.exists() {
|
|
|
|
load_attempts += 1;
|
|
|
|
if load_attempts > 100 {
|
|
|
|
panic!("Lock file {:?} is stuck. Try deleting it.", lock_path);
|
|
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(10));
|
2023-07-28 10:28:02 -04:00
|
|
|
}
|
|
|
|
|
2023-10-02 13:31:14 -04:00
|
|
|
Ok(std::fs::read_to_string(&dep_path)
|
|
|
|
.unwrap()
|
|
|
|
.trim()
|
|
|
|
.lines()
|
|
|
|
.map(PathBuf::from)
|
|
|
|
.collect())
|
2023-07-28 10:28:02 -04:00
|
|
|
}
|
2023-10-02 13:31:14 -04:00
|
|
|
Err(e) => panic!("{:?}: {}. Maybe try `cargo clean`?", lock_path, e),
|
2023-07-28 10:28:02 -04:00
|
|
|
}
|
2023-10-02 13:31:14 -04:00
|
|
|
})
|
|
|
|
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-12-16 05:13:37 -05:00
|
|
|
let input_file_string = input_file
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_else(|| panic!("Non UTF-8 file name: {:?}", input_file));
|
|
|
|
let output_file_string = output_file
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_else(|| panic!("Non UTF-8 file name: {:?}", output_file));
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-06-13 09:58:57 -04:00
|
|
|
let mut include_bytes_seq = quote! { include_bytes!(#input_file_string); };
|
2023-07-28 10:28:02 -04:00
|
|
|
for dep in deps {
|
2020-12-16 05:13:37 -05:00
|
|
|
if let Some(dep_string) = dep.to_str() {
|
|
|
|
include_bytes_seq.extend(quote! { include_bytes!(#dep_string); });
|
|
|
|
}
|
2020-06-13 09:58:57 -04:00
|
|
|
}
|
|
|
|
|
2020-06-04 16:39:33 -04:00
|
|
|
// Generate tokens
|
|
|
|
|
|
|
|
let name = strct.ident;
|
|
|
|
|
|
|
|
let field_names: Punctuated<Ident, Token![,]> = match strct.fields {
|
|
|
|
Fields::Named(fields) => fields
|
|
|
|
.named
|
|
|
|
.into_iter()
|
|
|
|
.map(|f| {
|
|
|
|
f.ident.expect(
|
|
|
|
"Internal error: Failed to get field name (error code: 73621)",
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
Fields::Unit => Punctuated::new(),
|
|
|
|
_ => {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
Span::call_site(),
|
2024-03-11 17:33:11 -04:00
|
|
|
"You cannot derive `Render` or `RenderOnce` for tuple struct",
|
2020-06-04 16:39:33 -04:00
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let (impl_generics, ty_generics, where_clause) = strct.generics.split_for_impl();
|
|
|
|
|
2024-03-11 17:33:11 -04:00
|
|
|
let inline = if cfg!(feature = "perf-inline") {
|
|
|
|
Some(quote!(#[inline]))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2020-06-18 05:35:39 -04:00
|
|
|
|
2024-03-11 17:33:11 -04:00
|
|
|
let tokens = quote! {
|
|
|
|
impl #impl_generics sailfish::RenderOnce for #name #ty_generics #where_clause {
|
|
|
|
#inline
|
|
|
|
fn render_once(self, __sf_buf: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> {
|
2023-03-05 04:08:56 -05:00
|
|
|
// This line is required for cargo to track child templates
|
2020-12-16 04:34:26 -05:00
|
|
|
#include_bytes_seq;
|
|
|
|
use sailfish::runtime as __sf_rt;
|
2020-06-04 16:39:33 -04:00
|
|
|
let #name { #field_names } = self;
|
|
|
|
include!(#output_file_string);
|
|
|
|
|
2020-06-18 05:35:39 -04:00
|
|
|
Ok(())
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(tokens)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn derive_template(tokens: TokenStream) -> TokenStream {
|
|
|
|
derive_template_impl(tokens).unwrap_or_else(|e| e.to_compile_error())
|
|
|
|
}
|