@@ -236,6 +236,14 @@ impl fmt::Display for IncrementalBuildError {
236236 }
237237}
238238
239+ fn with_build_lock < T > ( path : & Path , f : impl FnOnce ( ) -> T ) -> T {
240+ let build_folder = path. to_string_lossy ( ) . to_string ( ) ;
241+ let _lock = get_lock_or_exit ( LockKind :: Build , & build_folder) ;
242+ let result = f ( ) ;
243+ let _lock = drop_lock ( LockKind :: Build , & build_folder) ;
244+ result
245+ }
246+
239247pub fn incremental_build (
240248 build_state : & mut BuildCommandState ,
241249 default_timing : Option < Duration > ,
@@ -245,10 +253,31 @@ pub fn incremental_build(
245253 create_sourcedirs : bool ,
246254 plain_output : bool ,
247255) -> Result < ( ) , IncrementalBuildError > {
248- let build_folder = build_state. root_folder . to_string_lossy ( ) . to_string ( ) ;
249-
250- let _lock = get_lock_or_exit ( LockKind :: Build , & build_folder) ;
256+ let build_folder = build_state. root_folder . clone ( ) ;
257+ with_build_lock ( & build_folder, || {
258+ incremental_build_without_lock (
259+ build_state,
260+ default_timing,
261+ initial_build,
262+ show_progress,
263+ only_incremental,
264+ create_sourcedirs,
265+ plain_output,
266+ )
267+ } )
268+ }
251269
270+ // `build` needs to lock before initialization because initialization mutates previous
271+ // build artifacts. Keep the compile body separate so it can run under that wider lock.
272+ fn incremental_build_without_lock (
273+ build_state : & mut BuildCommandState ,
274+ default_timing : Option < Duration > ,
275+ initial_build : bool ,
276+ show_progress : bool ,
277+ only_incremental : bool ,
278+ create_sourcedirs : bool ,
279+ plain_output : bool ,
280+ ) -> Result < ( ) , IncrementalBuildError > {
252281 logs:: initialize ( & build_state. packages ) ;
253282 let num_dirty_modules = build_state. modules . values ( ) . filter ( |m| is_dirty ( m) ) . count ( ) as u64 ;
254283 let pb = if !plain_output && show_progress {
@@ -292,7 +321,6 @@ pub fn incremental_build(
292321 }
293322
294323 eprintln ! ( "{}" , & err) ;
295- let _lock = drop_lock ( LockKind :: Build , & build_folder) ;
296324
297325 return Err ( IncrementalBuildError {
298326 kind : IncrementalBuildErrorKind :: SourceFileParseError ,
@@ -356,13 +384,9 @@ pub fn incremental_build(
356384 || pb. inc ( 1 ) ,
357385 |size| pb. set_length ( size) ,
358386 )
359- . map_err ( |e| {
360- let _lock = drop_lock ( LockKind :: Build , & build_folder) ;
361-
362- IncrementalBuildError {
363- kind : IncrementalBuildErrorKind :: CompileError ( Some ( e. to_string ( ) ) ) ,
364- plain_output,
365- }
387+ . map_err ( |e| IncrementalBuildError {
388+ kind : IncrementalBuildErrorKind :: CompileError ( Some ( e. to_string ( ) ) ) ,
389+ plain_output,
366390 } ) ?;
367391
368392 let compile_duration = start_compiling. elapsed ( ) ;
@@ -397,8 +421,6 @@ pub fn incremental_build(
397421 eprintln ! ( "{}" , & compile_errors) ;
398422 }
399423
400- let _lock = drop_lock ( LockKind :: Build , & build_folder) ;
401-
402424 Err ( IncrementalBuildError {
403425 kind : IncrementalBuildErrorKind :: CompileError ( None ) ,
404426 plain_output,
@@ -429,7 +451,6 @@ pub fn incremental_build(
429451 // Write per-package compiler metadata to `lib/bs/compiler-info.json` (idempotent)
430452 write_compiler_info ( build_state) ;
431453
432- let _lock = drop_lock ( LockKind :: Build , & build_folder) ;
433454 Ok ( ( ) )
434455 }
435456}
@@ -529,43 +550,122 @@ pub fn build(
529550 None
530551 } ;
531552 let timing_total = Instant :: now ( ) ;
532- let mut build_state = initialize_build (
533- default_timing,
534- filter,
535- show_progress,
536- path,
537- plain_output,
538- warn_error,
539- )
540- . with_context ( || "Could not initialize build" ) ?;
541-
542- match incremental_build (
543- & mut build_state,
544- default_timing,
545- true ,
546- show_progress,
547- false ,
548- create_sourcedirs,
549- plain_output,
550- ) {
551- Ok ( _) => {
552- if !plain_output && show_progress {
553- let timing_total_elapsed = timing_total. elapsed ( ) ;
554- println ! (
555- "\n {}{}Finished Compilation in {:.2}s" ,
556- LINE_CLEAR ,
557- SPARKLES ,
558- default_timing. unwrap_or( timing_total_elapsed) . as_secs_f64( )
559- ) ;
553+ with_build_lock ( path, || {
554+ let mut build_state = initialize_build (
555+ default_timing,
556+ filter,
557+ show_progress,
558+ path,
559+ plain_output,
560+ warn_error,
561+ )
562+ . with_context ( || "Could not initialize build" ) ?;
563+
564+ match incremental_build_without_lock (
565+ & mut build_state,
566+ default_timing,
567+ true ,
568+ show_progress,
569+ false ,
570+ create_sourcedirs,
571+ plain_output,
572+ ) {
573+ Ok ( _) => {
574+ if !plain_output && show_progress {
575+ let timing_total_elapsed = timing_total. elapsed ( ) ;
576+ println ! (
577+ "\n {}{}Finished Compilation in {:.2}s" ,
578+ LINE_CLEAR ,
579+ SPARKLES ,
580+ default_timing. unwrap_or( timing_total_elapsed) . as_secs_f64( )
581+ ) ;
582+ }
583+ clean:: cleanup_after_build ( & build_state) ;
584+ write_build_ninja ( & build_state) ;
585+ Ok ( build_state)
586+ }
587+ Err ( e) => {
588+ clean:: cleanup_after_build ( & build_state) ;
589+ write_build_ninja ( & build_state) ;
590+ Err ( anyhow ! ( "Incremental build failed. Error: {e}" ) )
560591 }
561- clean:: cleanup_after_build ( & build_state) ;
562- write_build_ninja ( & build_state) ;
563- Ok ( build_state)
564- }
565- Err ( e) => {
566- clean:: cleanup_after_build ( & build_state) ;
567- write_build_ninja ( & build_state) ;
568- Err ( anyhow ! ( "Incremental build failed. Error: {e}" ) )
569592 }
593+ } )
594+ }
595+ #[ cfg( test) ]
596+ mod tests {
597+ use super :: * ;
598+ use std:: fs;
599+ use std:: process;
600+ use std:: sync:: mpsc;
601+ use std:: thread;
602+ use tempfile:: TempDir ;
603+
604+ #[ test]
605+ fn with_build_lock_holds_lock_while_running_work ( ) {
606+ let temp_dir = TempDir :: new ( ) . expect ( "temp dir should be created" ) ;
607+ let project_folder = temp_dir. path ( ) ;
608+ let build_lock_path = project_folder. join ( "lib" ) . join ( LockKind :: Build . file_name ( ) ) ;
609+
610+ let result = with_build_lock ( project_folder, || {
611+ assert ! (
612+ build_lock_path. exists( ) ,
613+ "build lock should be held while guarded work runs"
614+ ) ;
615+ 42
616+ } ) ;
617+
618+ assert_eq ! ( result, 42 ) ;
619+ assert ! (
620+ !build_lock_path. exists( ) ,
621+ "build lock should be removed after guarded work finishes"
622+ ) ;
623+ }
624+
625+ #[ test]
626+ fn with_build_lock_drops_lock_after_error_result ( ) {
627+ let temp_dir = TempDir :: new ( ) . expect ( "temp dir should be created" ) ;
628+ let project_folder = temp_dir. path ( ) ;
629+ let build_lock_path = project_folder. join ( "lib" ) . join ( LockKind :: Build . file_name ( ) ) ;
630+
631+ let result: Result < ( ) , & str > = with_build_lock ( project_folder, || Err ( "failed" ) ) ;
632+
633+ assert_eq ! ( result, Err ( "failed" ) ) ;
634+ assert ! (
635+ !build_lock_path. exists( ) ,
636+ "build lock should be removed after guarded work returns an error"
637+ ) ;
638+ }
639+
640+ #[ test]
641+ fn build_waits_for_lock_before_initializing ( ) {
642+ let temp_dir = TempDir :: new ( ) . expect ( "temp dir should be created" ) ;
643+ let project_folder = temp_dir. path ( ) . to_path_buf ( ) ;
644+ let lib_dir = project_folder. join ( "lib" ) ;
645+ let build_lock_path = lib_dir. join ( LockKind :: Build . file_name ( ) ) ;
646+ fs:: create_dir_all ( & lib_dir) . expect ( "lib directory should be created" ) ;
647+ fs:: write ( & build_lock_path, process:: id ( ) . to_string ( ) ) . expect ( "lockfile should be written" ) ;
648+
649+ let ( sender, receiver) = mpsc:: channel ( ) ;
650+ let build_project_folder = project_folder. clone ( ) ;
651+ let build_thread = thread:: spawn ( move || {
652+ // This temp project has no config, so initialization would fail immediately
653+ // if `build` did not wait for the lock first.
654+ let result = build ( & None , & build_project_folder, false , true , false , true , None ) ;
655+ sender. send ( result. is_err ( ) ) . expect ( "result should be sent" ) ;
656+ } ) ;
657+
658+ assert ! (
659+ receiver. recv_timeout( Duration :: from_millis( 250 ) ) . is_err( ) ,
660+ "build should wait for build.lock before running initialization"
661+ ) ;
662+
663+ fs:: remove_file ( & build_lock_path) . expect ( "lockfile should be removed" ) ;
664+ assert ! (
665+ receiver
666+ . recv_timeout( Duration :: from_secs( 5 ) )
667+ . expect( "build should finish after lock is removed" )
668+ ) ;
669+ build_thread. join ( ) . expect ( "build thread should complete" ) ;
570670 }
571671}
0 commit comments