Skip to content

Commit 6d4e92a

Browse files
authored
Merge branch 'quarkusio:main' into issue-51290-csrf-cors-docs
2 parents 2969bd4 + e5e03eb commit 6d4e92a

File tree

27 files changed

+956
-23
lines changed

27 files changed

+956
-23
lines changed

bom/application/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
<!-- GraalVM sdk 23.1.2 has a minimum JDK requirement of 17+ at runtime -->
9090
<graal-sdk.version>23.1.2</graal-sdk.version>
9191
<gizmo.version>1.9.0</gizmo.version>
92-
<gizmo2.version>2.0.0.Beta10</gizmo2.version>
92+
<gizmo2.version>2.0.0.CR1</gizmo2.version>
9393
<jackson-bom.version>2.20.1</jackson-bom.version>
9494
<commons-logging-jboss-logging.version>1.0.0.Final</commons-logging-jboss-logging.version>
9595
<commons-lang3.version>3.20.0</commons-lang3.version>
@@ -109,7 +109,7 @@
109109
<slf4j-jboss-logmanager.version>2.0.2.Final</slf4j-jboss-logmanager.version>
110110
<wildfly-common.version>2.0.1</wildfly-common.version>
111111
<wildfly-client-config.version>1.0.1.Final</wildfly-client-config.version>
112-
<wildfly-elytron.version>2.7.0.Final</wildfly-elytron.version>
112+
<wildfly-elytron.version>2.7.1.Final</wildfly-elytron.version>
113113
<jboss-marshalling.version>2.2.3.Final</jboss-marshalling.version>
114114
<jboss-threads.version>3.9.2</jboss-threads.version>
115115
<vertx.version>4.5.22</vertx.version>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.quarkus.deployment.dev;
2+
3+
import java.util.Map;
4+
import java.util.Set;
5+
6+
import org.jboss.jandex.DotName;
7+
8+
import io.quarkus.builder.item.MultiBuildItem;
9+
10+
/**
11+
* Defines the relationship between classes for the purposes of recompilation.
12+
* <p/>
13+
* Idea is that multiple classes can be dependent on one other class. For this BuildItem a class can be an outermost class, a
14+
* nested class, an annotation, an interface, an enum, or basically anything that has an FQN.
15+
* A consolidation step in {@link RecompilationDependenciesProcessor} cleans up the collected recompilation dependencies and
16+
* removes duplicates.
17+
* <p/>
18+
* The collected recompilation dependencies are used to build a relationship tree between the classes in the
19+
* {@link RuntimeUpdatesProcessor}, which is build and traversed from the root up and outwards. Class a -> Set of Class -> where
20+
* each element class can resolve to another set of classes.
21+
* <p/>
22+
* The collected recompilation dependencies are used to determine the set of classes which need to additionally be recompiled
23+
* when one other class changes.
24+
* <p/>
25+
* For performance reasons, the scope of the collected recompilation dependencies should be quite narrow. For an
26+
* example, the quarkus-mapstruct extension uses this BuildItem to recompile the dependent Mapper classes, once any of the
27+
* referenced model classes
28+
* changes.
29+
*/
30+
public final class RecompilationDependenciesBuildItem extends MultiBuildItem {
31+
32+
private final Map<DotName, Set<DotName>> classToRecompilationTargets;
33+
34+
public RecompilationDependenciesBuildItem(Map<DotName, Set<DotName>> classToRecompilationTargets) {
35+
this.classToRecompilationTargets = classToRecompilationTargets;
36+
}
37+
38+
/**
39+
* Returns the map of recompilation dependencies.
40+
* <p>
41+
* The key is a dependency class name, while the value is a set of dependent class names that need to be recompiled as well
42+
* when the
43+
* dependency class changes.
44+
*
45+
* @return a map where each key is a dependency class name and each value is a set of class names that depend on it
46+
*/
47+
public Map<DotName, Set<DotName>> getClassToRecompilationTargets() {
48+
return classToRecompilationTargets;
49+
}
50+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.quarkus.deployment.dev;
2+
3+
import java.util.HashMap;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Set;
8+
9+
import org.jboss.jandex.ClassInfo;
10+
import org.jboss.jandex.DotName;
11+
import org.jboss.jandex.IndexView;
12+
13+
import io.quarkus.deployment.annotations.BuildStep;
14+
import io.quarkus.deployment.annotations.Produce;
15+
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
16+
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
17+
18+
public class RecompilationDependenciesProcessor {
19+
20+
/**
21+
* Consolidation step, which prepares recompilation of dependent classes.
22+
* <p/>
23+
* Resolves inner classes to their outer classes. Inner classes and outer classes share
24+
* source file (at least in java). For our goal of recompilation we therefore only need to outer class.
25+
* <p/>
26+
* After resolving outer classes, all the mappings classToRecompilationTargets are combined into one mapping,
27+
* deduplicated by class name, and
28+
* then given to the {@link RuntimeUpdatesProcessor#setClassToRecompilationTargets(Map)}.
29+
*
30+
* @param combinedIndexBuildItem Index, only the precomputed index is used, since for the purpose of (re)compilation the
31+
* class has to be in an application source path - which are always indexed.
32+
* @param recompilationDependenciesBuildItems Provides the dependencies between classes. Currently, produced only by
33+
* extensions.
34+
*/
35+
@BuildStep
36+
@Produce(ServiceStartBuildItem.class)
37+
public void consolidateRecompilationDependencies(CombinedIndexBuildItem combinedIndexBuildItem,
38+
List<RecompilationDependenciesBuildItem> recompilationDependenciesBuildItems) {
39+
40+
// Cleanup and combine all the classToRecompilationTargets maps:
41+
// - Resolve inner classes to their top-level class names
42+
// - Remove entries where the class is not in the index
43+
// - combine all classToRecompilationTargets values based on their keys
44+
45+
Map<DotName, Set<DotName>> classToRecompilationTargets = new HashMap<>();
46+
for (RecompilationDependenciesBuildItem buildItem : recompilationDependenciesBuildItems) {
47+
buildItem.getClassToRecompilationTargets().forEach((dependency, recompilationTargets) -> {
48+
dependency = resolveOutermostClassName(dependency, combinedIndexBuildItem.getIndex());
49+
if (dependency == null) {
50+
return;
51+
}
52+
53+
for (DotName recompilationTarget : recompilationTargets) {
54+
recompilationTarget = resolveOutermostClassName(recompilationTarget, combinedIndexBuildItem.getIndex());
55+
if (recompilationTarget == null) {
56+
continue;
57+
}
58+
59+
classToRecompilationTargets.computeIfAbsent(dependency, k -> new HashSet<>()).add(recompilationTarget);
60+
}
61+
});
62+
}
63+
64+
if (RuntimeUpdatesProcessor.INSTANCE != null) {
65+
RuntimeUpdatesProcessor.INSTANCE.setClassToRecompilationTargets(classToRecompilationTargets);
66+
}
67+
}
68+
69+
private DotName resolveOutermostClassName(DotName name, IndexView index) {
70+
71+
ClassInfo classInfo = index.getClassByName(name);
72+
if (classInfo == null) {
73+
return null;
74+
}
75+
76+
if (classInfo.nestingType() != ClassInfo.NestingType.TOP_LEVEL) {
77+
return resolveOutermostClassName(classInfo.enclosingClassAlways(), index);
78+
}
79+
80+
return name;
81+
}
82+
}

core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static java.util.Arrays.asList;
44
import static java.util.Collections.singletonList;
5-
import static java.util.stream.Collectors.groupingBy;
65

76
import java.io.ByteArrayInputStream;
87
import java.io.Closeable;
@@ -21,9 +20,11 @@
2120
import java.nio.file.StandardCopyOption;
2221
import java.nio.file.attribute.BasicFileAttributes;
2322
import java.nio.file.attribute.FileTime;
23+
import java.util.ArrayDeque;
2424
import java.util.ArrayList;
2525
import java.util.Collection;
2626
import java.util.Collections;
27+
import java.util.Deque;
2728
import java.util.HashMap;
2829
import java.util.HashSet;
2930
import java.util.Iterator;
@@ -52,6 +53,7 @@
5253
import java.util.stream.Stream;
5354

5455
import org.jboss.jandex.ClassInfo;
56+
import org.jboss.jandex.DotName;
5557
import org.jboss.jandex.Index;
5658
import org.jboss.jandex.IndexView;
5759
import 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

Comments
 (0)