From 2d15715266751421b7958c374be898f90c63b887 Mon Sep 17 00:00:00 2001 From: Tomo Suzuki Date: Tue, 9 Aug 2022 15:43:51 -0400 Subject: [PATCH] Exclude direct test-scope dependencies when building dependency graph Fixes #185 --- pom.xml | 2 +- .../pom.xml | 35 +++++++++ .../verify.groovy | 45 +++++++++++ .../parent/pom.xml | 6 +- .../pom.xml | 4 +- .../codehaus/mojo/flatten/FlattenMojo.java | 76 ++++++++++++++----- 6 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 src/it/projects/flatten-dependency-all-both-test-and-transitive/pom.xml create mode 100644 src/it/projects/flatten-dependency-all-both-test-and-transitive/verify.groovy diff --git a/pom.xml b/pom.xml index 44501fcf..fb52bfb3 100644 --- a/pom.xml +++ b/pom.xml @@ -162,7 +162,7 @@ org.apache.maven.shared maven-dependency-tree - 2.2 + 3.1.1 org.codehaus.plexus diff --git a/src/it/projects/flatten-dependency-all-both-test-and-transitive/pom.xml b/src/it/projects/flatten-dependency-all-both-test-and-transitive/pom.xml new file mode 100644 index 00000000..8ca9f3a6 --- /dev/null +++ b/src/it/projects/flatten-dependency-all-both-test-and-transitive/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + org.codehaus.mojo.flatten.its + flatten-dependency-all-both-test-and-transitive + 0.0.1-SNAPSHOT + + + verify + + + org.codehaus.mojo + flatten-maven-plugin + + oss + all + + + + + + + + org.codehaus.mojo.flatten.its + core + + 3.2.1 + + + org.codehaus.mojo.flatten.its + dep + 3.2.1 + test + + + \ No newline at end of file diff --git a/src/it/projects/flatten-dependency-all-both-test-and-transitive/verify.groovy b/src/it/projects/flatten-dependency-all-both-test-and-transitive/verify.groovy new file mode 100644 index 00000000..8a1426c4 --- /dev/null +++ b/src/it/projects/flatten-dependency-all-both-test-and-transitive/verify.groovy @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +File originalPom = new File( basedir, 'pom.xml' ) +assert originalPom.exists() + +def originalProject = new XmlSlurper().parse( originalPom ) +assert 2 == originalProject.dependencies.dependency.size() +assert "dep" == originalProject.dependencies.dependency[1].artifactId.text() +assert "3.2.1" == originalProject.dependencies.dependency[1].version.text() +assert "test" == originalProject.dependencies.dependency[1].scope.text() + +File flattenedPom = new File( basedir, '.flattened-pom.xml' ) +assert flattenedPom.exists() + +def flattenedProject = new XmlSlurper().parse( flattenedPom ) + +// core and dep should be there. It's because while the test-scope dep (the +// direct dependency), core declares dep as compile-scope (default) dependency. +assert 2 == flattenedProject.dependencies.dependency.size() + +assert "core" == flattenedProject.dependencies.dependency[0].artifactId.text() +assert "3.2.1" == flattenedProject.dependencies.dependency[0].version.text() +assert "compile" == flattenedProject.dependencies.dependency[0].scope.text() + +// The flattened pom.xml should declare the dep under core as compile scope. +// It's ok to ignore the one in the test-scope dependency. +assert "dep" == flattenedProject.dependencies.dependency[1].artifactId.text() +assert "3.2.1" == flattenedProject.dependencies.dependency[1].version.text() +assert "compile" == flattenedProject.dependencies.dependency[1].scope.text() diff --git a/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/parent/pom.xml b/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/parent/pom.xml index fccfa0a7..18083c5f 100644 --- a/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/parent/pom.xml +++ b/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/parent/pom.xml @@ -3,14 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.codehaus.mojo.flatten.its - profile-with-deps-inherit-parent-depMgmt-jdk_parent + profile-with-deps-inherit-parent-depMgmt-test_parent 0.1-SNAPSHOT pom - - 1.3.2 - - diff --git a/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/pom.xml b/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/pom.xml index 69df2f23..bdd0f72f 100644 --- a/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/pom.xml +++ b/src/it/projects/profile-with-deps-inherit-parent-depMgmt-test/pom.xml @@ -3,12 +3,12 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.codehaus.mojo.flatten.its - profile-with-deps-inherit-parent-depMgmt-jdk + profile-with-deps-inherit-parent-depMgmt-test 0.1-SNAPSHOT jar org.codehaus.mojo.flatten.its - profile-with-deps-inherit-parent-depMgmt-jdk_parent + profile-with-deps-inherit-parent-depMgmt-test_parent 0.1-SNAPSHOT parent diff --git a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java index bf764dce..af6b9a1a 100644 --- a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java +++ b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java @@ -19,7 +19,6 @@ * under the License. */ -import java.util.Queue; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; @@ -51,11 +50,13 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.MavenProject; -import org.apache.maven.shared.dependency.tree.DependencyNode; -import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder; -import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException; -import org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; +import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException; +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver; import org.codehaus.mojo.flatten.cifriendly.CiInterpolator; import org.codehaus.mojo.flatten.model.resolution.FlattenModelResolver; @@ -86,9 +87,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Properties; +import java.util.Queue; import java.util.Set; /** @@ -224,7 +227,7 @@ public class FlattenMojo @Parameter( property = "updatePomFile" ) private Boolean updatePomFile; - /** The {@link ArtifactRepository} required to resolve POM using {@link #modelBuilder}. */ + /** The {@link ArtifactRepository} required to resolve POM. */ @Parameter( defaultValue = "${localRepository}", readonly = true, required = true ) private ArtifactRepository localRepository; @@ -284,7 +287,7 @@ public class FlattenMojo * * * bom - * Like {@link #ossrh} but additionally keeps {@link Model#getDependencyManagement() dependencyManagement} and + * Like ossrh but additionally keeps {@link Model#getDependencyManagement() dependencyManagement} and * {@link Model#getProperties() properties}. Especially it will keep the {@link Model#getDependencyManagement() * dependencyManagement} as-is without resolving parent influences and import-scoped dependencies. This is * useful if your POM represents a projectDependen * The collected dependencies are stored in order, so that the leaf dependencies are prioritized in front of direct dependencies. * In addition, every non-leaf dependencies will exclude its own direct dependency, since all transitive dependencies * will be collected. - * + * * Transitive dependencies are all going to be collected and become a direct dependency. Maven should already resolve * versions properly because now the transitive dependencies are closer to the artifact. However, when this artifact is * being consumed, Maven Enforcer Convergence rule will fail because there may be multiple versions for the same transitive dependency. - * + * * Typically, exclusion can be done by using the wildcard. However, a known Maven issue prevents convergence enforcer from * working properly w/ wildcard exclusions. Thus, this will exclude each dependencies explicitly rather than using the wildcard. * * @param projectDependencies is the effective POM {@link Model}'s current dependencies * @param flattenedDependencies is the {@link List} where to add the collected {@link Dependency dependencies}. - * @throws DependencyTreeBuilderException + * @throws DependencyGraphBuilderException * @throws ArtifactDescriptorException */ private void createFlattenedDependenciesAll( List projectDependencies, List flattenedDependencies ) - throws DependencyTreeBuilderException, ArtifactDescriptorException + throws ArtifactDescriptorException, DependencyGraphBuilderException { final Queue dependencyNodeLinkedList = new LinkedList<>(); final Set processedDependencies = new HashSet<>(); final Artifact projectArtifact = this.project.getArtifact(); - final DependencyNode dependencyNode = this.dependencyTreeBuilder.buildDependencyTree(this.project, - this.localRepository, null); + ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() ); + buildingRequest.setProject( cloneProjectWithoutTestDependencies( project ) ); + + final DependencyNode dependencyNode = this.dependencyGraphBuilder.buildDependencyGraph( buildingRequest, null); dependencyNode.accept(new DependencyNodeVisitor() { @@ -1110,10 +1115,6 @@ private void createFlattenedDependenciesAll( List projectDependencie return false; } } - if (node.getState() != DependencyNode.INCLUDED) - { - return false; - } if (node.getArtifact().isOptional()) { return false; @@ -1183,6 +1184,43 @@ private void createFlattenedDependenciesAll( List projectDependencie } } + /** + * Returns a cloned project that does not have direct test-scope dependencies. + * + * Test-scope project dependencies may hinder transitive dependencies by marking them as 'omitted for duplicate' when + * building dependency tree. This was a problem when the transitive dependency is actually needed by another non-test dependency + * of the project (See https://github.com/mojohaus/flatten-maven-plugin/issues/185). To avoid this interference of + * test-scope project dependencies, this plugin builds a dependency tree of the project without direct, test-scope dependencies. + * + * Removal of test scope dependencies is safe because these dependencies do not appear in library users' class path in + * any case. + * + * @param project is the original project to clone. + * @return a cloned project without direct test-scope dependencies. + */ + private static MavenProject cloneProjectWithoutTestDependencies( MavenProject project ) + { + final Set testScopeProjectDependencyKeys = new HashSet<>(); + for ( Dependency projectDependency : project.getDependencies() ) + { + if ( "test".equals( projectDependency.getScope() ) ) + { + testScopeProjectDependencyKeys.add( projectDependency.getManagementKey() ); + } + } + // LinkedHashSet preserves the order. + final Set dependencyArtifactsWithoutTest = new LinkedHashSet<>( project.getDependencyArtifacts() ); + dependencyArtifactsWithoutTest.removeIf(artifact -> { + // The same logic as org.apache.maven.model.Dependency.getManagementKey() + String managementKey = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getType() + + ( artifact.getClassifier() != null ? ":" + artifact.getClassifier() : "" ); + return testScopeProjectDependencyKeys.contains( managementKey ); + }); + final MavenProject projectWithoutTestScopeDeps = project.clone(); + projectWithoutTestScopeDeps.setDependencyArtifacts( dependencyArtifactsWithoutTest ); + return projectWithoutTestScopeDeps; + } + /** * Collects the resolved {@link Dependency dependencies} from the given effectiveModel. *