From d2b34c5954008539ca5d78ce7c127ad5d26fb8fc Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 10 Oct 2026 08:35:12 +0000 Subject: [PATCH 1/2] Auto-detect module source hierarchy and skip main artifact at deploy 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 deploy plugin then fails because the main artifact path is a directory. Auto-detect this case by checking whether any enabled source root declares a module (via ProjectManager.getSourceRoots() / SourceRoot.module()), and skip the main artifact from the deploy request — deploying only the POM and the per-module JARs. The existing allowIncompleteProjects fallback is preserved unchanged for unrelated incomplete-project scenarios. This is a workaround for Maven 4.0. The proper fix is tracked in apache/maven#13396 (Packaging.producesMainArtifact() API for 4.1.0). --- .../maven/plugins/deploy/DeployMojo.java | 35 ++++++++++------ .../maven/plugins/deploy/DeployMojoTest.java | 42 +++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 3bd0627..9b04439 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -33,6 +33,7 @@ import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.SourceRoot; import org.apache.maven.api.di.Inject; import org.apache.maven.api.model.DistributionManagement; import org.apache.maven.api.model.Plugin; @@ -436,6 +437,7 @@ private ArtifactDeployerRequest createDeployerRequest() { + ":" + project.getVersion() + " are not valid: they use invalid characters."); } + List toDeploy = new ArrayList<>(deployables); for (Artifact deployable : deployables) { if (!isValidClassifier(deployable.getClassifier())) { throw new MojoException("The classifier of attached artifact " + deployable @@ -446,19 +448,20 @@ private ArtifactDeployerRequest createDeployerRequest() { 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;" + + " deploying POM and per-module artifacts only."); + toDeploy.remove(deployable); + } 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 { - 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 " @@ -470,7 +473,7 @@ private ArtifactDeployerRequest createDeployerRequest() { ArtifactDeployerRequest request = ArtifactDeployerRequest.builder() .session(session) .repository(getDeploymentRepository(session.isVersionSnapshot(project.getVersion()))) - .artifacts(deployables) + .artifacts(toDeploy) .retryFailedDeploymentCount(Math.max(1, Math.min(10, getRetryFailedDeploymentCount()))) .build(); @@ -681,6 +684,12 @@ private void warnIfPolicyMismatch(org.apache.maven.api.model.DeploymentRepositor } } + private static boolean usesModuleSourceHierarchy(Project project, ProjectManager projectManager) { + return projectManager.getSourceRoots(project).stream() + .filter(SourceRoot::enabled) + .anyMatch(sr -> sr.module().isPresent()); + } + private boolean isValidPath(Artifact a) { return getArtifactManager().getPath(a).filter(Files::isRegularFile).isPresent(); } diff --git a/src/test/java/org/apache/maven/plugins/deploy/DeployMojoTest.java b/src/test/java/org/apache/maven/plugins/deploy/DeployMojoTest.java index 58175e6..725ffc4 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/DeployMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/DeployMojoTest.java @@ -25,12 +25,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.maven.api.Artifact; import org.apache.maven.api.ProducedArtifact; import org.apache.maven.api.Project; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.SourceRoot; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Priority; import org.apache.maven.api.di.Provides; @@ -68,6 +70,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** @@ -569,6 +572,45 @@ void ambiguousLegacyAltDeploymentRepositoryRefused(DeployMojo mojo) throws Excep assertEquals("Ambiguous syntax for alternative repository.", e.getMessage()); } + @Test + @InjectMojo(goal = "deploy") + @MojoParameter(name = "deployAtEnd", value = "false") + void moduleSourceHierarchySkipsMainArtifactAtDeploy(DeployMojo mojo) throws Exception { + assertNotNull(mojo); + Project project = (Project) getVariableValueFromObject(mojo, "project"); + + // Simulate module source hierarchy: main artifact path is a directory (the build output dir) + artifactManager.setPath(project.getMainArtifact().get(), Paths.get(getBasedir(), "target/test-classes")); + + // Attach a per-module JAR as the real deployable artifact + ProducedArtifactStub moduleJar = new ProducedArtifactStub( + "org.apache.maven.test", "maven-deploy-test", "module.one", "1.0-SNAPSHOT", "jar"); + projectManager.attachArtifact( + project, + moduleJar, + Paths.get(getBasedir(), "target/test-classes/unit/maven-deploy-test-1.0-SNAPSHOT.jar")); + + // Stub a module source root on the projectManager + 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)); + + ArtifactDeployerRequest request = execute(mojo); + + assertNotNull(request); + Collection artifacts = request.getArtifacts(); + // Main artifact (directory) must be excluded; POM and per-module JAR must be deployed + List keys = artifacts.stream().map(Artifact::key).collect(Collectors.toList()); + assertFalse( + keys.contains("org.apache.maven.test:maven-deploy-test:jar:1.0-SNAPSHOT"), + "directory main artifact must be excluded from deploy request"); + assertTrue(keys.contains("org.apache.maven.test:maven-deploy-test:pom:1.0-SNAPSHOT"), "POM must be deployed"); + assertTrue( + keys.contains("org.apache.maven.test:maven-deploy-test:jar:module.one:1.0-SNAPSHOT"), + "per-module JAR must be deployed"); + } + private ArtifactDeployerRequest execute(DeployMojo mojo) { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ArtifactDeployerRequest.class); doNothing().when(artifactDeployer).deploy(requestCaptor.capture()); From fadc35356816e347263b443d2a9a991eb25c73c9 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 10 Oct 2026 12:28:53 +0000 Subject: [PATCH 2/2] Fix allowIncompleteProjects branch: remove artifact and consolidate warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues in the allowIncompleteProjects branch: - The main artifact was not removed from toDeploy, causing the deployer to receive a directory path and fail — defeating the purpose of the warn-and-continue path - The warning was split across multiple getLog().warn() calls; consolidate into a single call matching the install plugin style --- .../org/apache/maven/plugins/deploy/DeployMojo.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 9b04439..e330535 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -453,12 +453,11 @@ private ArtifactDeployerRequest createDeployerRequest() { + " deploying POM and per-module artifacts only."); toDeploy.remove(deployable); } 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!"); + toDeploy.remove(deployable); } 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'.");