Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache templates compiled by proc-macro #125

Merged
merged 2 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sailfish-compiler/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};

#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash)]
pub struct Config {
pub delimiter: char,
pub escape: bool,
Expand Down
72 changes: 64 additions & 8 deletions sailfish-compiler/src/procmacro.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, thread};
use syn::parse::{ParseStream, Parser, Result as ParseResult};
use syn::punctuated::Punctuated;
use syn::{Fields, Ident, ItemStruct, LitBool, LitChar, LitStr, Token};
Expand Down Expand Up @@ -100,7 +102,7 @@ fn resolve_template_file(path: &str, template_dirs: &[PathBuf]) -> Option<PathBu
None
}

fn filename_hash(path: &Path) -> String {
fn filename_hash(path: &Path, config: &Config) -> String {
use std::fmt::Write;

let mut path_with_hash = String::with_capacity(16);
Expand All @@ -114,8 +116,11 @@ fn filename_hash(path: &Path) -> String {
path_with_hash.push('-');
}

let input_bytes = std::fs::read(path).unwrap();

let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
input_bytes.hash(&mut hasher);
config.hash(&mut hasher);
let hash = hasher.finish();
let _ = write!(path_with_hash, "{:016x}", hash);

Expand Down Expand Up @@ -204,13 +209,64 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
)?
};

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.
let mut output_file = PathBuf::from(env!("OUT_DIR"));
output_file.push("templates");
output_file.push(filename_hash(&*input_file));
output_file.push(filename_hash(&*input_file, &config));

merge_config_options(&mut config, &all_options);
let report = compile(&*input_file, &*output_file, config)
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
std::fs::create_dir_all(&output_file.parent().unwrap()).unwrap();

const DEPS_END_MARKER: &str = "=--end-of-deps--=";
let dep_file = output_file.with_extension("deps");

// 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.
let dep_file_status = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&dep_file);

let deps = match dep_file_status {
Ok(mut file) => {
// Successfully created new .deps file. Now template needs to be compiled.
let report = compile(&*input_file, &*output_file, config)
.map_err(|e| syn::Error::new(Span::call_site(), e))?;

for dep in &report.deps {
writeln!(file, "{}", dep.to_str().unwrap()).unwrap();
}
writeln!(file, "{}", DEPS_END_MARKER).unwrap();

report.deps
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
// .deps file exists, template is already (currently being?) compiled.
let mut load_attempts = 0;
loop {
let dep_file_content = std::fs::read_to_string(&dep_file).unwrap();
let mut lines_reversed = dep_file_content.rsplit_terminator('\n');
if lines_reversed.next() == Some(DEPS_END_MARKER) {
// .deps file is complete, so we can continue.
break lines_reversed.map(PathBuf::from).collect();
}

// .deps file exists, but appears incomplete. Wait a bit and try again.
load_attempts += 1;
if load_attempts > 100 {
panic!("file {:?} is incomplete. Try deleting it.", dep_file);
}

thread::sleep(Duration::from_millis(10));
}
}
Err(e) => panic!("{:?}: {}. Maybe try `cargo clean`?", dep_file, e),
};

let input_file_string = input_file
.to_str()
Expand All @@ -220,7 +276,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
.unwrap_or_else(|| panic!("Non UTF-8 file name: {:?}", output_file));

let mut include_bytes_seq = quote! { include_bytes!(#input_file_string); };
for dep in report.deps {
for dep in deps {
if let Some(dep_string) = dep.to_str() {
include_bytes_seq.extend(quote! { include_bytes!(#dep_string); });
}
Expand Down