Skip to content

Commit 222a265

Browse files
authored
Merge pull request #135 from nwtnni/main
Use timestamps for change detection
2 parents a93f66f + 01ba067 commit 222a265

3 files changed

Lines changed: 127 additions & 78 deletions

File tree

sailfish-compiler/src/compiler.rs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::optimizer::Optimizer;
1212
use crate::parser::Parser;
1313
use crate::resolver::Resolver;
1414
use crate::translator::{TranslatedSource, Translator};
15-
use crate::util::{copy_filetimes, read_to_string, rustfmt_block};
15+
use crate::util::{read_to_string, rustfmt_block};
1616

1717
#[derive(Default)]
1818
pub struct Compiler {
@@ -42,34 +42,35 @@ impl Compiler {
4242
translator.translate(stream)
4343
}
4444

45-
pub fn compile_file(
45+
pub fn resolve_file(
4646
&self,
4747
input: &Path,
48-
output: &Path,
49-
) -> Result<CompilationReport, Error> {
50-
// TODO: introduce cache system
51-
52-
let input = input
53-
.canonicalize()
54-
.map_err(|_| format!("Template file not found: {:?}", input))?;
55-
48+
) -> Result<(TranslatedSource, CompilationReport), Error> {
5649
let include_handler = Arc::new(|child_file: &Path| -> Result<_, Error> {
5750
Ok(self.translate_file_contents(&*child_file)?.ast)
5851
});
5952

6053
let resolver = Resolver::new().include_handler(include_handler);
54+
let mut tsource = self.translate_file_contents(input)?;
55+
let mut report = CompilationReport { deps: Vec::new() };
56+
57+
let r = resolver.resolve(input, &mut tsource.ast)?;
58+
report.deps = r.deps;
59+
Ok((tsource, report))
60+
}
61+
62+
pub fn compile_file(
63+
&self,
64+
input: &Path,
65+
tsource: TranslatedSource,
66+
output: &Path,
67+
) -> Result<(), Error> {
6168
let analyzer = Analyzer::new();
6269
let optimizer = Optimizer::new().rm_whitespace(self.config.rm_whitespace);
6370

64-
let compile_file = |input: &Path,
71+
let compile_file = |mut tsource: TranslatedSource,
6572
output: &Path|
66-
-> Result<CompilationReport, Error> {
67-
let mut tsource = self.translate_file_contents(input)?;
68-
let mut report = CompilationReport { deps: Vec::new() };
69-
70-
let r = resolver.resolve(&*input, &mut tsource.ast)?;
71-
report.deps = r.deps;
72-
73+
-> Result<(), Error> {
7374
analyzer.analyze(&mut tsource.ast)?;
7475
optimizer.optimize(&mut tsource.ast);
7576

@@ -86,14 +87,10 @@ impl Compiler {
8687
.chain_err(|| format!("Failed to write artifact into {:?}", output))?;
8788
drop(f);
8889

89-
// FIXME: This is a silly hack to prevent output file from being tracking by
90-
// cargo. Another better solution should be considered.
91-
let _ = copy_filetimes(input, output);
92-
93-
Ok(report)
90+
Ok(())
9491
};
9592

96-
compile_file(&*input, &*output)
93+
compile_file(tsource, output)
9794
.chain_err(|| "Failed to compile template.")
9895
.map_err(|mut e| {
9996
e.source = fs::read_to_string(&*input).ok();

sailfish-compiler/src/procmacro.rs

Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ use quote::quote;
33
use std::collections::hash_map::DefaultHasher;
44
use std::hash::{Hash, Hasher};
55
use std::io::Write;
6+
use std::iter;
67
use std::path::{Path, PathBuf};
78
use std::time::Duration;
89
use std::{env, thread};
910
use syn::parse::{ParseStream, Parser, Result as ParseResult};
1011
use syn::punctuated::Punctuated;
1112
use syn::{Fields, Ident, ItemStruct, LitBool, LitChar, LitStr, Token};
1213

13-
use crate::compiler::{CompilationReport, Compiler};
14+
use crate::compiler::Compiler;
1415
use crate::config::Config;
1516
use crate::error::*;
17+
use crate::util::filetime;
1618

1719
// options for `template` attributes
1820
#[derive(Default)]
@@ -116,22 +118,18 @@ fn filename_hash(path: &Path, config: &Config) -> String {
116118
path_with_hash.push('-');
117119
}
118120

119-
let input_bytes = std::fs::read(path).unwrap();
120-
121121
let mut hasher = DefaultHasher::new();
122-
input_bytes.hash(&mut hasher);
123122
config.hash(&mut hasher);
124123
let hash = hasher.finish();
125124
let _ = write!(path_with_hash, "{:016x}", hash);
126125

127126
path_with_hash
128127
}
129128

130-
fn compile(
131-
input_file: &Path,
132-
output_file: &Path,
129+
fn with_compiler<T, F: FnOnce(Compiler) -> Result<T, Error>>(
133130
config: Config,
134-
) -> Result<CompilationReport, Error> {
131+
apply: F,
132+
) -> Result<T, Error> {
135133
struct FallbackScope {}
136134

137135
impl FallbackScope {
@@ -155,7 +153,7 @@ fn compile(
155153
let compiler = Compiler::with_config(config);
156154

157155
let _scope = FallbackScope::new();
158-
compiler.compile_file(input_file, &*output_file)
156+
apply(compiler)
159157
}
160158

161159
fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
@@ -199,14 +197,14 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
199197
let path = all_options.path.as_ref().ok_or_else(|| {
200198
syn::Error::new(Span::call_site(), "`path` option must be specified.")
201199
})?;
202-
resolve_template_file(&*path.value(), &*config.template_dirs).ok_or_else(
203-
|| {
200+
resolve_template_file(&*path.value(), &*config.template_dirs)
201+
.and_then(|path| path.canonicalize().ok())
202+
.ok_or_else(|| {
204203
syn::Error::new(
205204
path.span(),
206205
format!("Template file {:?} not found", path.value()),
207206
)
208-
},
209-
)?
207+
})?
210208
};
211209

212210
merge_config_options(&mut config, &all_options);
@@ -221,52 +219,107 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
221219

222220
std::fs::create_dir_all(&output_file.parent().unwrap()).unwrap();
223221

224-
const DEPS_END_MARKER: &str = "=--end-of-deps--=";
225-
let dep_file = output_file.with_extension("deps");
226-
227222
// This makes sure max 1 process creates a new file, "create_new" check+create is an
228223
// atomic operation. Cargo sometimes runs multiple macro invocations for the same
229224
// file in parallel, so that's important to prevent a race condition.
230-
let dep_file_status = std::fs::OpenOptions::new()
231-
.write(true)
232-
.create_new(true)
233-
.open(&dep_file);
234-
235-
let deps = match dep_file_status {
236-
Ok(mut file) => {
237-
// Successfully created new .deps file. Now template needs to be compiled.
238-
let report = compile(&*input_file, &*output_file, config)
239-
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
240-
241-
for dep in &report.deps {
242-
writeln!(file, "{}", dep.to_str().unwrap()).unwrap();
243-
}
244-
writeln!(file, "{}", DEPS_END_MARKER).unwrap();
225+
struct Lock<'path> {
226+
path: &'path Path,
227+
}
228+
229+
impl<'path> Lock<'path> {
230+
fn new(path: &'path Path) -> std::io::Result<Self> {
231+
std::fs::OpenOptions::new()
232+
.write(true)
233+
.create_new(true)
234+
.open(path)
235+
.map(|_| Lock { path })
236+
}
237+
}
245238

246-
report.deps
239+
impl<'path> Drop for Lock<'path> {
240+
fn drop(&mut self) {
241+
std::fs::remove_file(self.path)
242+
.expect("Failed to clean up lock file {}. Delete it manually, or run `cargo clean`.");
247243
}
248-
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
249-
// .deps file exists, template is already (currently being?) compiled.
250-
let mut load_attempts = 0;
251-
loop {
252-
let dep_file_content = std::fs::read_to_string(&dep_file).unwrap();
253-
let mut lines_reversed = dep_file_content.rsplit_terminator('\n');
254-
if lines_reversed.next() == Some(DEPS_END_MARKER) {
255-
// .deps file is complete, so we can continue.
256-
break lines_reversed.map(PathBuf::from).collect();
244+
}
245+
246+
let deps = with_compiler(config, |compiler| {
247+
let dep_path = output_file.with_extension("deps");
248+
let lock_path = output_file.with_extension("lock");
249+
let lock = Lock::new(&lock_path);
250+
match lock {
251+
Ok(lock) => {
252+
let (tsource, report) = compiler.resolve_file(&input_file)?;
253+
254+
let output_filetime = filetime(&output_file);
255+
let input_filetime = iter::once(&input_file)
256+
.chain(&report.deps)
257+
.map(|path| filetime(path))
258+
.max()
259+
.expect("Iterator contains at least `input_file`");
260+
261+
// Recompile template if any included templates were changed
262+
// since the last time we compiled.
263+
if input_filetime > output_filetime {
264+
compiler.compile_file(&input_file, tsource, &output_file)?;
265+
266+
// Write access to `dep_path` is serialized by `lock`.
267+
let mut dep_file = std::fs::OpenOptions::new()
268+
.write(true)
269+
.create(true)
270+
.truncate(true)
271+
.open(&dep_path)
272+
.unwrap_or_else(|e| {
273+
panic!("Failed to open {:?}: {}", dep_path, e)
274+
});
275+
276+
// Write out dependencies for concurrent processes to reuse.
277+
for dep in &report.deps {
278+
writeln!(&mut dep_file, "{}", dep.to_str().unwrap()).unwrap();
279+
}
280+
281+
// Prevent output file from being tracked by Cargo. Without this hack,
282+
// every change to a template causes two recompilations:
283+
//
284+
// 1. Change a template at timestamp t.
285+
// 2. Cargo detects template change due to `include_bytes!` macro below.
286+
// 3. Sailfish compiler generates an output file with a later timestamp t'.
287+
// 4. Build finishes with timestamp t.
288+
// 5. Next cargo build detects output file with timestamp t' > t and rebuilds.
289+
// 6. Sailfish compiler does not regenerate output due to timestamp logic above.
290+
// 7. Build finishes with timestamp t'.
291+
let _ = filetime::set_file_times(
292+
&output_file,
293+
input_filetime,
294+
input_filetime,
295+
);
257296
}
258297

259-
// .deps file exists, but appears incomplete. Wait a bit and try again.
260-
load_attempts += 1;
261-
if load_attempts > 100 {
262-
panic!("file {:?} is incomplete. Try deleting it.", dep_file);
298+
drop(lock);
299+
Ok(report.deps)
300+
}
301+
// Lock file exists, template is already (currently being?) compiled.
302+
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
303+
let mut load_attempts = 0;
304+
while lock_path.exists() {
305+
load_attempts += 1;
306+
if load_attempts > 100 {
307+
panic!("Lock file {:?} is stuck. Try deleting it.", lock_path);
308+
}
309+
thread::sleep(Duration::from_millis(10));
263310
}
264311

265-
thread::sleep(Duration::from_millis(10));
312+
Ok(std::fs::read_to_string(&dep_path)
313+
.unwrap()
314+
.trim()
315+
.lines()
316+
.map(PathBuf::from)
317+
.collect())
266318
}
319+
Err(e) => panic!("{:?}: {}. Maybe try `cargo clean`?", lock_path, e),
267320
}
268-
Err(e) => panic!("{:?}: {}. Maybe try `cargo clean`?", dep_file, e),
269-
};
321+
})
322+
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
270323

271324
let input_file_string = input_file
272325
.to_str()

sailfish-compiler/src/util.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use filetime::FileTime;
21
use std::fs;
32
use std::io::{self, Write};
43
use std::path::{Path, PathBuf};
@@ -78,10 +77,10 @@ pub fn rustfmt_block(source: &str) -> io::Result<String> {
7877
}
7978
}
8079

81-
pub fn copy_filetimes(input: &Path, output: &Path) -> io::Result<()> {
82-
let mtime = fs::metadata(input)
80+
#[cfg(feature = "procmacro")]
81+
pub fn filetime(input: &Path) -> filetime::FileTime {
82+
use filetime::FileTime;
83+
fs::metadata(input)
8384
.and_then(|metadata| metadata.modified())
84-
.map_or(FileTime::zero(), |time| FileTime::from_system_time(time));
85-
86-
filetime::set_file_times(output, mtime, mtime)
85+
.map_or(FileTime::zero(), |time| FileTime::from_system_time(time))
8786
}

0 commit comments

Comments
 (0)