@@ -3,16 +3,18 @@ use quote::quote;
33use std:: collections:: hash_map:: DefaultHasher ;
44use std:: hash:: { Hash , Hasher } ;
55use std:: io:: Write ;
6+ use std:: iter;
67use std:: path:: { Path , PathBuf } ;
78use std:: time:: Duration ;
89use std:: { env, thread} ;
910use syn:: parse:: { ParseStream , Parser , Result as ParseResult } ;
1011use syn:: punctuated:: Punctuated ;
1112use syn:: { Fields , Ident , ItemStruct , LitBool , LitChar , LitStr , Token } ;
1213
13- use crate :: compiler:: { CompilationReport , Compiler } ;
14+ use crate :: compiler:: Compiler ;
1415use crate :: config:: Config ;
1516use 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
161159fn 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 ( )
0 commit comments