gnodet commented on code in PR #1157:
URL: 
https://github.com/apache/maven-compiler-plugin/pull/1157#discussion_r4192588460


##########
src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java:
##########
@@ -1398,7 +1423,21 @@ public Options parseParameters(final OptionChecker 
compiler) {
      */
     @SuppressWarnings("UseSpecificCatch")
     private void compile(final JavaCompiler compiler, final Options 
configuration) throws IOException {
-        final ToolExecutor executor = createExecutor(null);
+        var executor = createExecutor(null);
+        if (!"timestamp".equalsIgnoreCase(incrementalStrategy) && 
!"graph".equalsIgnoreCase(incrementalStrategy)) {
+            throw new MojoException(
+                    "Unknown incrementalStrategy: '" + incrementalStrategy + 
"'. Valid values are: timestamp, graph");
+        }
+        if ("graph".equalsIgnoreCase(incrementalStrategy) && 
!Boolean.FALSE.equals(useIncrementalCompilation)) {
+            if (Runtime.version().feature() < 24) {
+                logger.warn("Graph incremental strategy requires JDK 24 or 
later "
+                        + "(running JDK " + Runtime.version().feature() + "). "
+                        + "Falling back to timestamp strategy.");
+            } else {
+                executor.compileWithGraphIncremental(compiler, configuration, 
this);

Review Comment:
   _Claude Code on behalf of Guillaume Nodet_
   
   Fixed: updated incrementalStrategy Javadoc to mention JDK 24+ runtime 
requirement and the automatic fallback to timestamp strategy on older JDKs.



##########
src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java:
##########
@@ -915,6 +919,241 @@ private static boolean removeFirsts(Deque<Path> paths, 
Integer count) {
         }
     }
 
+    /**
+     * Compiles using the dependency-graph incremental strategy. This method 
handles the full
+     * lifecycle: determining what to compile, running javac, cascading on 
changed classes,
+     * and persisting state.
+     *
+     * @param compiler the compiler
+     * @param configuration the options to give to the Java compiler
+     * @param mojo the MOJO for configuration access
+     * @throws IOException if an error occurred while reading or writing a file
+     * @throws MojoException if the compilation failed
+     */
+    void compileWithGraphIncremental(JavaCompiler compiler, Options 
configuration, AbstractCompilerMojo mojo)
+            throws IOException {
+        var graphBuild = new GraphIncrementalBuild(outputDirectory);
+
+        // Collect annotation processor path for processor classification
+        var processorPaths = new ArrayList<Path>();
+        for (var entry : dependencies.entrySet()) {
+            if (entry.getKey() instanceof JavaPathType type) {
+                var location = type.location();
+                if (location.isPresent()
+                        && (location.get() == 
StandardLocation.ANNOTATION_PROCESSOR_PATH
+                                || location.get() == 
StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH)) {
+                    processorPaths.addAll(entry.getValue());
+                }
+            }
+        }
+        if (!processorPaths.isEmpty()) {
+            graphBuild.setProcessorPath(processorPaths);
+        }
+
+        // Collect classpath entries for external dependency tracking
+        var classpathEntries = new ArrayList<Path>();
+        var reactorModulePaths = new LinkedHashSet<Path>();
+        for (var entry : dependencies.entrySet()) {
+            if (entry.getKey() instanceof JavaPathType type) {
+                var location = type.location();
+                if (location.isPresent() && location.get() == 
StandardLocation.CLASS_PATH) {
+                    for (Path p : entry.getValue()) {
+                        classpathEntries.add(p);
+                        if (Files.isDirectory(p)) {
+                            reactorModulePaths.add(p);
+                        }
+                    }
+                }
+            }
+        }
+        if (!classpathEntries.isEmpty()) {
+            graphBuild.setClasspathEntries(classpathEntries);
+        }
+        if (!reactorModulePaths.isEmpty()) {
+            graphBuild.setReactorModulePaths(reactorModulePaths);
+        }
+
+        // Hash module-info-patch.maven files for config change detection
+        graphBuild.setConfigHash(computeConfigHash(configuration));
+
+        // Collect all source file paths
+        var allSourcePaths = new ArrayList<Path>();
+        for (SourceFile sf : sourceFiles) {
+            allSourcePaths.add(sf.file);
+        }
+
+        Set<Path> toCompile = graphBuild.initialize(allSourcePaths);
+        if (toCompile.isEmpty()) {
+            logger.info("Nothing to compile - all classes are up to date 
(graph strategy).");
+            graphBuild.finish();
+            return;
+        }
+
+        logger.info(
+                graphBuild.isFullBuild()
+                        ? "Compiling " + toCompile.size() + " source file(s) 
(graph: full build)."
+                        : "Compiling " + toCompile.size() + " source file(s) 
(graph: incremental).");
+        if (mojo.showCompilationChanges && graphBuild.getRebuildCause() != 
null) {
+            logger.info("Rebuild cause: " + graphBuild.getRebuildCause());
+            for (Path f : toCompile) {
+                logger.info("  " + f);
+            }
+        }
+
+        var originalSourceFiles = new ArrayList<>(sourceFiles);
+        boolean success = true;
+        // Safety bound: the compile set is monotonically growing (bounded by 
total source count).
+        // If a bug causes processCompiledClasses to return files already 
compiled, this prevents
+        // an infinite loop. In practice this limit should never be reached.
+        int maxRounds = originalSourceFiles.size() + 1;
+        int rounds = 0;
+
+        try {
+            while (!toCompile.isEmpty()) {
+                if (++rounds > maxRounds) {
+                    throw new IllegalStateException("graph cascade loop did 
not converge after " + maxRounds
+                            + " rounds — " + "possible dependency cycle or bug 
in processCompiledClasses()");
+                }
+                Set<Path> compileSet = toCompile;
+                sourceFiles = originalSourceFiles.stream()
+                        .filter(sf -> compileSet.contains(sf.file))
+                        .collect(Collectors.toList());
+
+                if (sourceFiles.isEmpty()) {
+                    break;
+                }
+
+                var compilerOutput = new StringWriter();
+                success = compileIncrementalRound(compiler, configuration, 
compilerOutput, graphBuild);
+                String output = compilerOutput.toString();
+                if (!output.isBlank()) {
+                    logger.warn(output);
+                }
+                if (!success) {
+                    break;
+                }
+
+                toCompile = graphBuild.processCompiledClasses(compileSet);
+                if (!toCompile.isEmpty()) {
+                    logger.info("graph cascade: recompiling " + 
toCompile.size() + " additional file(s).");
+                    if (mojo.showCompilationChanges) {
+                        for (Path f : toCompile) {
+                            logger.info("  " + f);
+                        }
+                    }
+                }
+            }
+        } finally {
+            sourceFiles = originalSourceFiles;
+        }
+
+        if (success) {
+            graphBuild.finish();
+            logger.info("Compiled " + graphBuild.compiledCount() + " file(s), 
" + graphBuild.unchangedCount()
+                    + " unchanged (graph strategy).");
+        } else {
+            graphBuild.invalidate();
+            throw new CompilationFailureException("Compilation failed (graph 
incremental strategy).");
+        }
+    }
+
+    private String computeConfigHash(Options configuration) {
+        var digest = new StringBuilder();
+
+        // Include compiler options in the config hash so changes to 
-source/-target/-release
+        // etc. trigger a full rebuild under the graph strategy.
+        String optionsRepr = String.join("|", configuration.options);
+        digest.append("opts:").append(optionsRepr).append(';');
+
+        for (SourceDirectory source : sourceDirectories) {
+            Path patchFile = source.root.resolve(ModuleInfoPatch.FILENAME);
+            if (Files.isRegularFile(patchFile)) {
+                try {
+                    byte[] content = Files.readAllBytes(patchFile);
+                    digest.append(patchFile)

Review Comment:
   _Claude Code on behalf of Guillaume Nodet_
   
   The empty SOURCE_PATH is intentional to prevent javac from auto-discovering 
sources outside the current compile set. For modular projects, the compiled 
module-info.class in the output directory (which is on the classpath) provides 
the module descriptor. The moduleInfoChangeIsDetectedAsIncremental test 
verifies this path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to