22
33import static java .util .Arrays .asList ;
44import static java .util .Collections .singletonList ;
5- import static java .util .stream .Collectors .groupingBy ;
65
76import java .io .ByteArrayInputStream ;
87import java .io .Closeable ;
2120import java .nio .file .StandardCopyOption ;
2221import java .nio .file .attribute .BasicFileAttributes ;
2322import java .nio .file .attribute .FileTime ;
23+ import java .util .ArrayDeque ;
2424import java .util .ArrayList ;
2525import java .util .Collection ;
2626import java .util .Collections ;
27+ import java .util .Deque ;
2728import java .util .HashMap ;
2829import java .util .HashSet ;
2930import java .util .Iterator ;
5253import java .util .stream .Stream ;
5354
5455import org .jboss .jandex .ClassInfo ;
56+ import org .jboss .jandex .DotName ;
5557import org .jboss .jandex .Index ;
5658import org .jboss .jandex .IndexView ;
5759import org .jboss .jandex .Indexer ;
@@ -104,6 +106,7 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
104106 private final TimestampSet main = new TimestampSet ();
105107 private final TimestampSet test = new TimestampSet ();
106108 final Map <Path , Long > sourceFileTimestamps = new ConcurrentHashMap <>();
109+ private Map <DotName , Set <DotName >> classToRecompilationTargets = new HashMap <>();
107110
108111 private final List <Runnable > preScanSteps = new CopyOnWriteArrayList <>();
109112 private final List <Runnable > postRestartSteps = new CopyOnWriteArrayList <>();
@@ -709,6 +712,24 @@ ClassScanResult checkForChangedTestClasses(boolean firstScan) {
709712 return ret ;
710713 }
711714
715+ private void collectRecompilationTargets (DotName changedDependency , Set <DotName > knownRecompilationTargets ) {
716+ Deque <DotName > toResolve = new ArrayDeque <>();
717+ toResolve .add (changedDependency );
718+ while (!toResolve .isEmpty ()) {
719+ DotName currentDependency = toResolve .poll ();
720+
721+ Set <DotName > recompilationTargets = classToRecompilationTargets .get (currentDependency );
722+
723+ if (recompilationTargets != null ) {
724+ for (DotName className : recompilationTargets ) {
725+ if (knownRecompilationTargets .add (className )) {
726+ toResolve .add (className );
727+ }
728+ }
729+ }
730+ }
731+ }
732+
712733 /**
713734 * A first scan is considered done when we have visited all modules at least once.
714735 * This is useful in two ways.
@@ -720,33 +741,81 @@ ClassScanResult checkForChangedClasses(QuarkusCompiler compiler,
720741 Function <DevModeContext .ModuleInfo , DevModeContext .CompilationUnit > cuf , boolean firstScan ,
721742 TimestampSet timestampSet , boolean compilingTests ) {
722743 ClassScanResult classScanResult = new ClassScanResult ();
723- boolean ignoreFirstScanChanges = firstScan ;
744+
745+ record RecompilableLocationsBySourcePath (Path sourcePath , Set <File > changedFiles , Set <File > changedDependencies ) {
746+ }
747+ record ChangeDetectionResult (DevModeContext .ModuleInfo moduleInfo ,
748+ List <RecompilableLocationsBySourcePath > changedLocations ) {
749+ }
750+ List <ChangeDetectionResult > changeDetectionResults = new ArrayList <>();
751+ Set <DotName > knownRecompilationTargets = new HashSet <>();
724752
725753 for (DevModeContext .ModuleInfo module : context .getAllModules ()) {
726- final List < Path > moduleChangedSourceFilePaths = new ArrayList <>();
754+ ChangeDetectionResult changeDetectionResult = new ChangeDetectionResult ( module , new ArrayList <>() );
727755
728756 for (Path sourcePath : cuf .apply (module ).getSourcePaths ()) {
729757 if (!Files .exists (sourcePath )) {
730758 continue ;
731759 }
760+
732761 final Set <File > changedSourceFiles ;
733762 try (final Stream <Path > sourcesStream = Files .walk (sourcePath )) {
734763 changedSourceFiles = sourcesStream
735764 .parallel ()
736765 .filter (p -> matchingHandledExtension (p ).isPresent ()
737- && sourceFileWasRecentModified (p , ignoreFirstScanChanges , firstScan ))
766+ && sourceFileWasRecentModified (p , firstScan , firstScan ))
738767 .map (Path ::toFile )
739768 //Needing a concurrent Set, not many standard options:
740769 .collect (Collectors .toCollection (ConcurrentSkipListSet ::new ));
741770 } catch (IOException e ) {
742771 throw new RuntimeException (e );
743772 }
773+
744774 if (!changedSourceFiles .isEmpty ()) {
775+ RecompilableLocationsBySourcePath recompilableLocationsBySourcePath = new RecompilableLocationsBySourcePath (
776+ sourcePath , changedSourceFiles , new HashSet <>());
777+ changeDetectionResult .changedLocations ().add (recompilableLocationsBySourcePath );
778+
779+ for (File changedSourceFile : changedSourceFiles ) {
780+ String changedDependency = convertFileToClassname (sourcePath , changedSourceFile );
781+
782+ collectRecompilationTargets (DotName .createSimple (changedDependency ), knownRecompilationTargets );
783+ }
784+ }
785+ }
786+ changeDetectionResults .add (changeDetectionResult );
787+ }
788+
789+ for (DotName recompilationTarget : knownRecompilationTargets ) {
790+ String partialRelativePath = recompilationTarget .toString ('/' );
791+
792+ OUT : for (ChangeDetectionResult changeDetectionResult : changeDetectionResults ) {
793+ for (RecompilableLocationsBySourcePath recompilableLocationsBySourcePath : changeDetectionResult
794+ .changedLocations ()) {
795+ for (String extension : compiler .allHandledExtensions ()) {
796+ Path resolved = recompilableLocationsBySourcePath .sourcePath ().resolve (partialRelativePath + extension );
797+
798+ if (Files .exists (resolved )) {
799+ recompilableLocationsBySourcePath .changedDependencies ().add (resolved .toFile ());
800+ break OUT ;
801+ }
802+ }
803+ }
804+ }
805+ }
806+
807+ for (ChangeDetectionResult changeDetectionResult : changeDetectionResults ) {
808+ final List <Path > moduleChangedSourceFilePaths = new ArrayList <>();
809+ for (RecompilableLocationsBySourcePath recompilableLocationsBySourcePath : changeDetectionResult
810+ .changedLocations ()) {
811+ Path sourcePath = recompilableLocationsBySourcePath .sourcePath ();
812+ Set <File > changedSourceFiles = recompilableLocationsBySourcePath .changedFiles ();
813+ if (!changedSourceFiles .isEmpty () || !recompilableLocationsBySourcePath .changedDependencies ().isEmpty ()) {
745814 classScanResult .compilationHappened = true ;
746815 //so this is pretty yuck, but on a lot of systems a write is actually a truncate + write
747816 //its possible we see the truncated file timestamp, then the write updates the timestamp
748817 //which will then re-trigger continuous testing/live reload
749- //the empty fine does not normally cause issues as by the time we actually compile it the write
818+ //the empty file does not normally cause issues as by the time we actually compile it the write
750819 //has completed (but the old timestamp is used)
751820 for (File i : changedSourceFiles ) {
752821 if (i .length () == 0 ) {
@@ -761,6 +830,7 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
761830 }
762831 }
763832 }
833+
764834 Map <File , Long > compileTimestamps = new HashMap <>();
765835
766836 //now we record the timestamps as they are before the compile phase
@@ -769,12 +839,18 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
769839 }
770840 for (;;) {
771841 try {
772- final Set <Path > changedPaths = changedSourceFiles .stream ()
773- .map (File ::toPath )
774- .collect (Collectors .toSet ());
842+ Map <String , Set <File >> changedFilesByExtension = new HashMap <>();
843+ Set <Path > changedPaths = new HashSet <>();
844+ Stream .concat (changedSourceFiles .stream (),
845+ recompilableLocationsBySourcePath .changedDependencies .stream ()).forEach (file -> {
846+ changedPaths .add (file .toPath ());
847+
848+ Set <File > files = changedFilesByExtension .computeIfAbsent (this .getFileExtension (file ),
849+ k -> new HashSet <>());
850+ files .add (file );
851+ });
775852 moduleChangedSourceFilePaths .addAll (changedPaths );
776- compiler .compile (sourcePath .toString (), changedSourceFiles .stream ()
777- .collect (groupingBy (this ::getFileExtension , Collectors .toSet ())));
853+ compiler .compile (sourcePath .toString (), changedFilesByExtension );
778854 compileProblem = null ;
779855 if (compilingTests ) {
780856 testCompileProblem = null ;
@@ -812,12 +888,10 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, firstScan))
812888 sourceFileTimestamps .put (entry .getKey ().toPath (), entry .getValue ());
813889 }
814890 }
815-
816891 }
817-
818- checkForClassFilesChangesInModule ( module , moduleChangedSourceFilePaths , ignoreFirstScanChanges , classScanResult ,
892+ checkForClassFilesChangesInModule ( changeDetectionResult . moduleInfo (), moduleChangedSourceFilePaths ,
893+ firstScan , classScanResult ,
819894 cuf , timestampSet );
820-
821895 }
822896
823897 return classScanResult ;
@@ -922,6 +996,19 @@ private String getFileExtension(File file) {
922996 return name .substring (lastIndexOf );
923997 }
924998
999+ // convert a filename to a class name with package
1000+ private String convertFileToClassname (Path sourcePath , File file ) {
1001+ String className = sourcePath .relativize (file .toPath ())
1002+ .toString ();
1003+ className = className .replace (File .separatorChar , '.' );
1004+
1005+ int lastIndexOf = className .lastIndexOf ('.' );
1006+ if (lastIndexOf > 0 ) {
1007+ className = className .substring (0 , lastIndexOf );
1008+ }
1009+ return className ;
1010+ }
1011+
9251012 Set <String > checkForFileChange () {
9261013 return checkForFileChange (DevModeContext .ModuleInfo ::getMain , main );
9271014 }
@@ -1155,6 +1242,11 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate(
11551242 return this ;
11561243 }
11571244
1245+ public RuntimeUpdatesProcessor setClassToRecompilationTargets (Map <DotName , Set <DotName >> classToRecompilationTargets ) {
1246+ this .classToRecompilationTargets = classToRecompilationTargets ;
1247+ return this ;
1248+ }
1249+
11581250 public RuntimeUpdatesProcessor setWatchedFilePaths (Map <String , Boolean > watchedFilePaths ,
11591251 List <Entry <Predicate <String >, Boolean >> watchedFilePredicates , boolean isTest ) {
11601252 if (isTest ) {
0 commit comments