@@ -606,42 +606,97 @@ impl Linker {
606606 return Ok ( result) ;
607607 }
608608
609- for entry in WalkDir :: new ( search_root)
610- . follow_links ( false )
611- . into_iter ( )
612- . filter_map ( |e| e. ok ( ) )
613- {
614- // Only process files
615- if !entry. file_type ( ) . is_file ( ) {
616- continue ;
617- }
609+ self . for_each_nested_glob_match (
610+ search_root,
611+ glob_pattern,
612+ excludes,
613+ options,
614+ |full_path, rel_path| {
615+ let dest_str = Self :: expand_destination_template ( dest_template, rel_path) ;
616+ if dest_str. is_empty ( ) {
617+ if options. verbose {
618+ println ! (
619+ " {} Destination template produced empty path for: {}" ,
620+ "!" . yellow( ) ,
621+ full_path. display( )
622+ ) ;
623+ }
624+ result. skipped += 1 ;
625+ return Ok ( ( ) ) ;
626+ }
627+
628+ let dest = self . project_root . join ( & dest_str) ;
629+
630+ let resolved = ResolvedSource {
631+ path : full_path. to_path_buf ( ) ,
632+ exists : true ,
633+ } ;
634+
635+ let item_result = self . create_symlink ( & resolved, & dest, options) ?;
636+ result. created += item_result. created ;
637+ result. updated += item_result. updated ;
638+ result. skipped += item_result. skipped ;
639+
640+ Ok ( ( ) )
641+ } ,
642+ ) ?;
643+
644+ Ok ( result)
645+ }
646+
647+ fn for_each_nested_glob_match < F > (
648+ & self ,
649+ search_root : & Path ,
650+ glob_pattern : & str ,
651+ excludes : & [ String ] ,
652+ options : & SyncOptions ,
653+ mut on_match : F ,
654+ ) -> Result < ( ) >
655+ where
656+ F : FnMut ( & Path , & Path ) -> Result < ( ) > ,
657+ {
658+ let mut it = WalkDir :: new ( search_root) . follow_links ( false ) . into_iter ( ) ;
659+
660+ while let Some ( entry) = it. next ( ) {
661+ let entry = match entry {
662+ Ok ( entry) => entry,
663+ Err ( err) => {
664+ if options. verbose {
665+ let path = err
666+ . path ( )
667+ . map ( |p| p. display ( ) . to_string ( ) )
668+ . unwrap_or_else ( || "<unknown path>" . to_string ( ) ) ;
669+ println ! (
670+ " {} WalkDir error while traversing {}: {}" ,
671+ "!" . yellow( ) ,
672+ path,
673+ err
674+ ) ;
675+ }
676+ tracing:: debug!( error = %err, path = ?err. path( ) , "WalkDir entry skipped during nested-glob traversal" ) ;
677+ continue ;
678+ }
679+ } ;
618680
619- // Compute path relative to search root
620681 let full_path = entry. path ( ) ;
621682 let rel_path = match full_path. strip_prefix ( search_root) {
622- Ok ( p ) => p ,
683+ Ok ( path ) => path ,
623684 Err ( _) => continue ,
624685 } ;
625686
626- // Convert to forward-slash string for pattern matching
627687 let rel_str = rel_path
628688 . components ( )
629- . map ( |c| c . as_os_str ( ) . to_string_lossy ( ) . into_owned ( ) )
689+ . map ( |component| component . as_os_str ( ) . to_string_lossy ( ) . into_owned ( ) )
630690 . collect :: < Vec < _ > > ( )
631691 . join ( "/" ) ;
632692
633- // Match against the glob pattern
634- if !matches_path_glob ( & rel_str, glob_pattern) {
693+ if rel_str. is_empty ( ) {
635694 continue ;
636695 }
637696
638- // Check exclusion patterns.
639- // Uses `find()` instead of `any()` to:
640- // 1. Skip iteration when excludes list is empty (short-circuit)
641- // 2. Determine which pattern matched (for verbose logging)
642697 if let Some ( matched_exclude) = excludes
643698 . iter ( )
644- . find ( |excl | matches_path_glob ( & rel_str, excl ) )
699+ . find ( |exclude | matches_path_glob ( & rel_str, exclude ) )
645700 {
646701 if options. verbose {
647702 println ! (
@@ -651,37 +706,25 @@ impl Linker {
651706 full_path. display( )
652707 ) ;
653708 }
654- continue ;
655- }
656709
657- // Expand the destination template
658- let dest_str = Self :: expand_destination_template ( dest_template, rel_path) ;
659- if dest_str. is_empty ( ) {
660- if options. verbose {
661- println ! (
662- " {} Destination template produced empty path for: {}" ,
663- "!" . yellow( ) ,
664- full_path. display( )
665- ) ;
710+ if entry. file_type ( ) . is_dir ( ) {
711+ it. skip_current_dir ( ) ;
666712 }
667- result. skipped += 1 ;
668713 continue ;
669714 }
670715
671- let dest = self . project_root . join ( & dest_str) ;
716+ if !entry. file_type ( ) . is_file ( ) {
717+ continue ;
718+ }
672719
673- let resolved = ResolvedSource {
674- path : full_path. to_path_buf ( ) ,
675- exists : true ,
676- } ;
720+ if !matches_path_glob ( & rel_str, glob_pattern) {
721+ continue ;
722+ }
677723
678- let item_result = self . create_symlink ( & resolved, & dest, options) ?;
679- result. created += item_result. created ;
680- result. updated += item_result. updated ;
681- result. skipped += item_result. skipped ;
724+ on_match ( full_path, rel_path) ?;
682725 }
683726
684- Ok ( result )
727+ Ok ( ( ) )
685728 }
686729
687730 /// Process a `module-map` target: iterate mappings and create a symlink
@@ -813,48 +856,36 @@ impl Linker {
813856 let dest_template = & target_config. destination ;
814857 let excludes = & target_config. exclude ;
815858
816- for entry in WalkDir :: new ( & search_root)
817- . follow_links ( false )
818- . into_iter ( )
819- . filter_map ( |e| e. ok ( ) )
820- {
821- if !entry. file_type ( ) . is_file ( ) {
822- continue ;
823- }
824- let rel_path = match entry. path ( ) . strip_prefix ( & search_root) {
825- Ok ( p) => p,
826- Err ( _) => continue ,
827- } ;
828- let rel_str = rel_path
829- . components ( )
830- . map ( |c| c. as_os_str ( ) . to_string_lossy ( ) . into_owned ( ) )
831- . collect :: < Vec < _ > > ( )
832- . join ( "/" ) ;
833- if !matches_path_glob ( & rel_str, glob_pattern) {
834- continue ;
835- }
836- if excludes
837- . iter ( )
838- . any ( |excl| matches_path_glob ( & rel_str, excl) )
839- {
840- continue ;
841- }
842- let dest_str =
843- Self :: expand_destination_template ( dest_template, rel_path) ;
844- if dest_str. is_empty ( ) {
845- continue ;
846- }
847- let dest = self . project_root . join ( & dest_str) ;
848- if dest. is_symlink ( ) {
849- if options. dry_run {
850- println ! ( " {} Would remove: {}" , "→" . cyan( ) , dest. display( ) ) ;
851- } else {
852- fs:: remove_file ( & dest) ?;
853- println ! ( " {} Removed: {}" , "✔" . green( ) , dest. display( ) ) ;
859+ self . for_each_nested_glob_match (
860+ & search_root,
861+ glob_pattern,
862+ excludes,
863+ options,
864+ |_, rel_path| {
865+ let dest_str =
866+ Self :: expand_destination_template ( dest_template, rel_path) ;
867+ if dest_str. is_empty ( ) {
868+ return Ok ( ( ) ) ;
854869 }
855- result. removed += 1 ;
856- }
857- }
870+
871+ let dest = self . project_root . join ( & dest_str) ;
872+ if dest. is_symlink ( ) {
873+ if options. dry_run {
874+ println ! (
875+ " {} Would remove: {}" ,
876+ "→" . cyan( ) ,
877+ dest. display( )
878+ ) ;
879+ } else {
880+ fs:: remove_file ( & dest) ?;
881+ println ! ( " {} Removed: {}" , "✔" . green( ) , dest. display( ) ) ;
882+ }
883+ result. removed += 1 ;
884+ }
885+
886+ Ok ( ( ) )
887+ } ,
888+ ) ?;
858889 }
859890 SyncType :: SymlinkContents => {
860891 let dest = self . project_root . join ( & target_config. destination ) ;
0 commit comments