From 8722da09a6b9ed33747cdeca65cc06788e744b66 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 10 Oct 2026 02:04:28 +0200 Subject: [PATCH 1/3] Auto-detect module source hierarchy and skip main artifact at install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a project uses module source hierarchy (…), the JAR plugin produces one JAR per JPMS module as attached artifacts and never assigns a file to the main artifact. The install plugin then fails because the main artifact path is a directory. Auto-detect this case by checking whether any source root declares a module (via ProjectManager.getSourceRoots() / SourceRoot.module()), and skip the main artifact from the install request — installing only the POM and the per-module JARs. This is a workaround for Maven 4.0. The proper fix is tracked in apache/maven#13396 (Packaging.producesMainArtifact() API for 4.1.0). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../maven/plugins/install/InstallMojo.java | 34 ++++++++++++------- .../plugins/install/InstallMojoTest.java | 30 ++++++++++++++++ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java index 1eda589..9120c35 100644 --- a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java +++ b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java @@ -249,6 +249,7 @@ private ArtifactInstallerRequest processProject(Project project) { ProjectManager projectManager = getProjectManager(); Collection installables = projectManager.getAllArtifacts(project); Collection attachedArtifacts = projectManager.getAttachedArtifacts(project); + List toInstall = new ArrayList<>(installables); getArtifactManager().setPath(project.getPomArtifact(), project.getPomPath()); @@ -258,19 +259,21 @@ private ArtifactInstallerRequest processProject(Project project) { if (attachedArtifacts.isEmpty()) { throw new MojoException( "The packaging for this project did not assign a file to the build artifact"); + } else if (usesModuleSourceHierarchy(project, projectManager)) { + getLog().info("Module source hierarchy: no main artifact produced;" + + " installing POM and per-module artifacts only."); + toInstall.remove(installable); + } else if (allowIncompleteProjects) { + getLog().warn(""); + getLog().warn("The packaging plugin for this project did not assign"); + getLog().warn("a main file to the project but it has attachments. Change packaging to 'pom'."); + getLog().warn(""); + getLog().warn("Incomplete projects like this will fail in future Maven versions!"); + getLog().warn(""); + toInstall.remove(installable); } else { - if (allowIncompleteProjects) { - getLog().warn(""); - getLog().warn("The packaging plugin for this project did not assign"); - getLog().warn( - "a main file to the project but it has attachments. Change packaging to 'pom'."); - getLog().warn(""); - getLog().warn("Incomplete projects like this will fail in future Maven versions!"); - getLog().warn(""); - } else { - throw new MojoException("The packaging plugin for this project did not assign " - + "a main file to the project but it has attachments. Change packaging to 'pom'."); - } + throw new MojoException("The packaging plugin for this project did not assign " + + "a main file to the project but it has attachments. Change packaging to 'pom'."); } } else { throw new MojoException("The packaging for this project did not assign " @@ -279,13 +282,18 @@ private ArtifactInstallerRequest processProject(Project project) { } } - return ArtifactInstallerRequest.build(session, installables); + return ArtifactInstallerRequest.build(session, toInstall); } private boolean isValidPath(Artifact a) { return getArtifactManager().getPath(a).filter(Files::isRegularFile).isPresent(); } + private static boolean usesModuleSourceHierarchy(Project project, ProjectManager projectManager) { + return projectManager.getSourceRoots(project).stream() + .anyMatch(sr -> sr.module().isPresent()); + } + void setSkip(boolean skip) { this.skip = skip; } diff --git a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java index df6a474..85cc253 100644 --- a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java @@ -27,12 +27,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.apache.maven.api.Artifact; import org.apache.maven.api.MojoExecution; import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; +import org.apache.maven.api.SourceRoot; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Priority; @@ -154,6 +156,34 @@ void basicInstallWithAttachedArtifacts(InstallMojo mojo) throws Exception { artifacts.stream().map(Artifact::key).toList()); } + @Test + @InjectMojo(goal = "install") + @MojoParameter(name = "installAtEnd", value = "false") + void moduleSourceHierarchySkipsMainArtifact(InstallMojo mojo) throws Exception { + assertNotNull(mojo); + Project project = (Project) getVariableValueFromObject(mojo, "project"); + + SourceRoot moduleRoot = mock(SourceRoot.class); + when(moduleRoot.module()).thenReturn(Optional.of("org.example.module")); + when(projectManager.getSourceRoots(project)).thenReturn(List.of(moduleRoot)); + + projectManager.attachArtifact( + project, + new ProducedArtifactStub("org.apache.maven.test", "org.example.module", "", "1.0-SNAPSHOT", "jar"), + Paths.get(getBasedir(), "target/test-classes/unit/attached-artifact-test-1.0-SNAPSHOT.jar")); + artifactManager.setPath(project.getMainArtifact().get(), Paths.get(getBasedir(), "target/test-classes/unit")); + + ArtifactInstallerRequest request = execute(mojo); + + assertNotNull(request); + Collection artifacts = request.getArtifacts(); + assertEquals( + Arrays.asList( + "org.apache.maven.test:maven-install-test:pom:1.0-SNAPSHOT", + "org.apache.maven.test:org.example.module:jar:1.0-SNAPSHOT"), + artifacts.stream().map(Artifact::key).toList()); + } + @Test @InjectMojo(goal = "install") void installIfArtifactFileIsNull(InstallMojo mojo) throws Exception { From 3ffdfff70e5a12e2955933d15e32be3032c1c8bb Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 10 Oct 2026 02:07:08 +0200 Subject: [PATCH 2/3] Consolidate allowIncompleteProjects warning into a single log call Co-Authored-By: Claude Opus 4.6 (1M context) --- .../org/apache/maven/plugins/install/InstallMojo.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java index 9120c35..460895c 100644 --- a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java +++ b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java @@ -264,12 +264,10 @@ private ArtifactInstallerRequest processProject(Project project) { + " installing POM and per-module artifacts only."); toInstall.remove(installable); } else if (allowIncompleteProjects) { - getLog().warn(""); - getLog().warn("The packaging plugin for this project did not assign"); - getLog().warn("a main file to the project but it has attachments. Change packaging to 'pom'."); - getLog().warn(""); - getLog().warn("Incomplete projects like this will fail in future Maven versions!"); - getLog().warn(""); + getLog().warn("The packaging plugin for this project did not assign" + + " a main file to the project but it has attachments." + + " Change packaging to 'pom'." + + " Incomplete projects like this will fail in future Maven versions!"); toInstall.remove(installable); } else { throw new MojoException("The packaging plugin for this project did not assign " From 502a31c1f7d1bbc6e2908b03f354504accbc7b02 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 10 Oct 2026 06:31:56 +0000 Subject: [PATCH 3/3] Filter disabled source roots in usesModuleSourceHierarchy --- src/main/java/org/apache/maven/plugins/install/InstallMojo.java | 2 ++ .../java/org/apache/maven/plugins/install/InstallMojoTest.java | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java index 460895c..fb4cd54 100644 --- a/src/main/java/org/apache/maven/plugins/install/InstallMojo.java +++ b/src/main/java/org/apache/maven/plugins/install/InstallMojo.java @@ -29,6 +29,7 @@ import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; import org.apache.maven.api.Session; +import org.apache.maven.api.SourceRoot; import org.apache.maven.api.di.Inject; import org.apache.maven.api.model.Plugin; import org.apache.maven.api.plugin.Log; @@ -289,6 +290,7 @@ private boolean isValidPath(Artifact a) { private static boolean usesModuleSourceHierarchy(Project project, ProjectManager projectManager) { return projectManager.getSourceRoots(project).stream() + .filter(SourceRoot::enabled) .anyMatch(sr -> sr.module().isPresent()); } diff --git a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java index 85cc253..231ff6e 100644 --- a/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/install/InstallMojoTest.java @@ -164,6 +164,7 @@ void moduleSourceHierarchySkipsMainArtifact(InstallMojo mojo) throws Exception { Project project = (Project) getVariableValueFromObject(mojo, "project"); SourceRoot moduleRoot = mock(SourceRoot.class); + when(moduleRoot.enabled()).thenReturn(true); when(moduleRoot.module()).thenReturn(Optional.of("org.example.module")); when(projectManager.getSourceRoots(project)).thenReturn(List.of(moduleRoot));