From 0c8da4f4dfb6129c3b516bebaf8bc288fd1ffb2b Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 29 Jul 2026 16:17:37 +0200 Subject: [PATCH 1/2] experiment: leverage BuildContext API for incremental JAR creation Integrate the Maven 4 BuildContext API to skip JAR creation when no input files have changed since the last build. When forceCreation is false and the JAR file already exists, the plugin now registers and scans the classes directory via BuildContext.registerAndProcessInputs() and skips re-packaging if all inputs are UNMODIFIED. Also signals markSkipExecution() at all skip points (skipIfEmpty, unchanged inputs) so downstream mojos can react accordingly. Co-Authored-By: Claude Opus 4.6 --- pom.xml | 2 +- .../maven/plugins/jar/AbstractJarMojo.java | 108 ++++++++++++++---- .../apache/maven/plugins/jar/TestJarMojo.java | 1 + .../apache/maven/plugins/jar/JarMojoTest.java | 30 ++++- 4 files changed, 114 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 1515933..f80e6c2 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 17 - 4.0.0-rc-4 + 4.1.0-SNAPSHOT 5.1.0 5.14.4 diff --git a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java index 10e00af..b935fea 100644 --- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java +++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java @@ -23,6 +23,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.jar.Attributes; @@ -31,6 +33,9 @@ import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; import org.apache.maven.api.Session; +import org.apache.maven.api.build.context.BuildContext; +import org.apache.maven.api.build.context.Input; +import org.apache.maven.api.build.context.Status; import org.apache.maven.api.di.Inject; import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; @@ -111,6 +116,14 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj @Inject protected ProjectManager projectManager; + /** + * The build context for incremental build support. + * Used to detect whether input files have changed since the last build, + * allowing the plugin to skip JAR creation when nothing has changed. + */ + @Inject + protected BuildContext buildContext; + /** * Require the jar plugin to build a new JAR even if none of the contents appear to have changed. * By default, this plugin looks to see if the output JAR exists and inputs have not changed. @@ -303,32 +316,81 @@ public Path createArchive() throws MojoException { public void execute() throws MojoException { if (skipIfEmpty && isEmpty(getClassesDirectory())) { getLog().info(String.format("Skipping packaging of the %s.", getType())); - } else { - Path jarFile = createArchive(); - - if (attach) { - ProducedArtifact artifact; - String classifier = getClassifier(); - if (hasClassifier(classifier)) { - artifact = session.createProducedArtifact( - project.getGroupId(), - project.getArtifactId(), - project.getVersion(), - classifier, - null, - getType()); - } else { - if (projectHasAlreadySetAnArtifact()) { - throw new MojoException("You have to use a classifier " - + "to attach supplemental artifacts to the project instead of replacing them."); - } - artifact = project.getMainArtifact().get(); + buildContext.markSkipExecution(); + return; + } + + // Check if any input files have changed since the last build. + // When forceCreation is false and the JAR already exists, skip creation + // if no inputs have been added, modified, or removed. + if (!forceCreation) { + Path jarFile = getJarFile( + outputDirectory != null + ? outputDirectory + : Path.of(project.getBuild().getDirectory()), + finalName != null ? finalName : project.getBuild().getFinalName(), + getClassifier()); + if (Files.isRegularFile(jarFile) && !hasChangedInputs()) { + getLog().info("Nothing to package - all classes are up to date."); + buildContext.markSkipExecution(); + if (attach) { + attachArtifact(jarFile); } - projectManager.attachArtifact(project, artifact, jarFile); - } else { - getLog().debug("Skipping attachment of the " + getType() + " artifact to the project."); + return; + } + } + + Path jarFile = createArchive(); + + if (attach) { + attachArtifact(jarFile); + } else { + getLog().debug("Skipping attachment of the " + getType() + " artifact to the project."); + } + } + + /** + * Checks whether any input files in the classes directory have changed since the last build. + * Uses the BuildContext to register and scan the classes directory, returning {@code true} + * if at least one file has been added, modified, or removed. + * + * @return {@code true} if any input files have changed, {@code false} if all are up to date + */ + private boolean hasChangedInputs() { + Path classesDir = getClassesDirectory(); + if (!Files.isDirectory(classesDir)) { + return false; + } + Collection inputs = + buildContext.registerAndProcessInputs(classesDir, List.of("**/**"), List.of()); + for (Input input : inputs) { + if (input.getStatus() != Status.UNMODIFIED) { + return true; + } + } + return false; + } + + /** + * Attaches the given JAR file as a project artifact. + * + * @param jarFile the JAR file to attach + * @throws MojoException if the artifact cannot be attached + */ + private void attachArtifact(Path jarFile) { + ProducedArtifact artifact; + String classifier = getClassifier(); + if (hasClassifier(classifier)) { + artifact = session.createProducedArtifact( + project.getGroupId(), project.getArtifactId(), project.getVersion(), classifier, null, getType()); + } else { + if (projectHasAlreadySetAnArtifact()) { + throw new MojoException("You have to use a classifier " + + "to attach supplemental artifacts to the project instead of replacing them."); } + artifact = project.getMainArtifact().get(); } + projectManager.attachArtifact(project, artifact, jarFile); } private static boolean isEmpty(Path directory) { diff --git a/src/main/java/org/apache/maven/plugins/jar/TestJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/TestJarMojo.java index 84de519..da8dda6 100644 --- a/src/main/java/org/apache/maven/plugins/jar/TestJarMojo.java +++ b/src/main/java/org/apache/maven/plugins/jar/TestJarMojo.java @@ -82,6 +82,7 @@ protected Path getClassesDirectory() { public void execute() throws MojoException { if (skip) { getLog().info("Skipping packaging of the test-jar."); + buildContext.markSkipExecution(); } else { super.execute(); } diff --git a/src/test/java/org/apache/maven/plugins/jar/JarMojoTest.java b/src/test/java/org/apache/maven/plugins/jar/JarMojoTest.java index 145eadc..c36e51d 100644 --- a/src/test/java/org/apache/maven/plugins/jar/JarMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/jar/JarMojoTest.java @@ -18,9 +18,18 @@ */ package org.apache.maven.plugins.jar; -import org.apache.maven.api.plugin.testing.Basedir; -import org.apache.maven.api.plugin.testing.InjectMojo; -import org.apache.maven.api.plugin.testing.MojoTest; +import java.util.HashMap; + +import org.apache.maven.api.build.context.BuildContext; +import org.apache.maven.api.di.Priority; +import org.apache.maven.api.di.Provides; +import org.apache.maven.api.services.PathMatcherFactory; +import org.apache.maven.impl.DefaultPathMatcherFactory; +import org.apache.maven.internal.build.context.impl.DefaultBuildContext; +import org.apache.maven.internal.build.context.impl.FilesystemWorkspace; +import org.apache.maven.testing.plugin.Basedir; +import org.apache.maven.testing.plugin.InjectMojo; +import org.apache.maven.testing.plugin.MojoTest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -47,4 +56,19 @@ void jarTestEnvironment(JarMojo mojo) throws Exception { assertEquals("foo", mojo.getProject().getGroupId()); } + + @Provides + @Priority(10) + @SuppressWarnings("unused") + private static PathMatcherFactory pathMatcherFactory() { + return new DefaultPathMatcherFactory(); + } + + @Provides + @Priority(10) + @SuppressWarnings("unused") + private static BuildContext buildContext() { + return new DefaultBuildContext( + new FilesystemWorkspace(), null, new HashMap<>(), null, new DefaultPathMatcherFactory()); + } } From b632ea0fe558d2b841ac1138d359d8d8476aea23 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 29 Jul 2026 17:17:04 +0200 Subject: [PATCH 2/2] Use InputSet.aggregate() to properly associate JAR output with inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ad-hoc hasChangedInputs() check with the BuildContext InputSet aggregation pattern. This correctly registers all class files as inputs, associates them with the JAR output file, and lets the build context handle stale output cleanup when inputs are removed. Previously, inputs were registered via registerAndProcessInputs() but the JAR was never associated as an output — breaking the input→output tracking that enables BuildContext's stale output cleanup. The aggregate() pattern is the correct fit for many-inputs-to-one-output transformations like JAR packaging. Co-Authored-By: Claude Opus 4.6 --- .../maven/plugins/jar/AbstractJarMojo.java | 70 ++++++++----------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java index b935fea..1fa0a8c 100644 --- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java +++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java @@ -23,7 +23,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; @@ -34,8 +33,7 @@ import org.apache.maven.api.Project; import org.apache.maven.api.Session; import org.apache.maven.api.build.context.BuildContext; -import org.apache.maven.api.build.context.Input; -import org.apache.maven.api.build.context.Status; +import org.apache.maven.api.build.context.InputSet; import org.apache.maven.api.di.Inject; import org.apache.maven.api.plugin.Log; import org.apache.maven.api.plugin.MojoException; @@ -310,6 +308,11 @@ public Path createArchive() throws MojoException { /** * Generates the JAR. * + *

Uses the {@link BuildContext} aggregation pattern to register all class files as + * inputs and associate them with the JAR output. The JAR is only rebuilt when at least + * one input has changed since the last build (unless {@link #forceCreation} is set). + * When inputs are removed, the build context automatically handles stale output cleanup.

+ * * @throws MojoException in case of an error */ @Override @@ -320,28 +323,33 @@ public void execute() throws MojoException { return; } - // Check if any input files have changed since the last build. - // When forceCreation is false and the JAR already exists, skip creation - // if no inputs have been added, modified, or removed. - if (!forceCreation) { - Path jarFile = getJarFile( - outputDirectory != null - ? outputDirectory - : Path.of(project.getBuild().getDirectory()), - finalName != null ? finalName : project.getBuild().getFinalName(), - getClassifier()); - if (Files.isRegularFile(jarFile) && !hasChangedInputs()) { + Path basedir = outputDirectory != null + ? outputDirectory + : Path.of(project.getBuild().getDirectory()); + String resultFinalName = + finalName != null ? finalName : project.getBuild().getFinalName(); + Path jarFile = getJarFile(basedir, resultFinalName, getClassifier()); + + // Register all class files as inputs and aggregate them into the JAR output. + // The aggregate() callback is only invoked when at least one input has changed. + Path classesDir = getClassesDirectory(); + if (!forceCreation && Files.isDirectory(classesDir)) { + InputSet inputSet = buildContext.newInputSet(); + inputSet.registerInputs(classesDir, List.of("**/**"), List.of()); + + boolean rebuilt = inputSet.aggregate(jarFile, (output, inputs) -> { + createArchive(); + }); + + if (!rebuilt) { getLog().info("Nothing to package - all classes are up to date."); buildContext.markSkipExecution(); - if (attach) { - attachArtifact(jarFile); - } - return; } + } else { + // forceCreation is set or no classes directory — always create the JAR + jarFile = createArchive(); } - Path jarFile = createArchive(); - if (attach) { attachArtifact(jarFile); } else { @@ -349,28 +357,6 @@ public void execute() throws MojoException { } } - /** - * Checks whether any input files in the classes directory have changed since the last build. - * Uses the BuildContext to register and scan the classes directory, returning {@code true} - * if at least one file has been added, modified, or removed. - * - * @return {@code true} if any input files have changed, {@code false} if all are up to date - */ - private boolean hasChangedInputs() { - Path classesDir = getClassesDirectory(); - if (!Files.isDirectory(classesDir)) { - return false; - } - Collection inputs = - buildContext.registerAndProcessInputs(classesDir, List.of("**/**"), List.of()); - for (Input input : inputs) { - if (input.getStatus() != Status.UNMODIFIED) { - return true; - } - } - return false; - } - /** * Attaches the given JAR file as a project artifact. *