Skip to content

Commit 36ebace

Browse files
[MASSEMBLY-1008] Fix transitive dependencies resolving with required scope (#166)
This IT demonstrates how assembly plugin (among others) does wrongly, the code probably originates from Maven2 times. What happens: * "single" mojo resolves test project dependencies * then it reads assembly descriptor * then uses project dependencies to deliver descriptor contents But, here is a problem that IT demonstrates: the delivered list of files are NOT runtime scoped, they are "test scope filtered for runtime leaves" which is not the same thing. By the way, dependency plugin demonstrates same behaviour, wrongly assumes that "test" graph is larger "runtime" tree, but that is not true nor was never true since Maven3 (resolver), it was true ONLY in Maven2 times. --------- Co-authored-by: Slawomir Jaranowski <[email protected]>
1 parent 091e87f commit 36ebace

File tree

6 files changed

+335
-34
lines changed

6 files changed

+335
-34
lines changed

pom.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,9 @@ under the License.
108108
</dependency>
109109
<dependency>
110110
<groupId>org.eclipse.aether</groupId>
111-
<artifactId>aether-api</artifactId>
111+
<artifactId>aether-util</artifactId>
112112
<!-- the same version as in Maven 3.2.5 -->
113113
<version>1.0.0.v20140518</version>
114-
<scope>provided</scope>
115114
</dependency>
116115
<dependency>
117116
<groupId>org.slf4j</groupId>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>org.apache.maven.plugin.assembly.test</groupId>
24+
<artifactId>it-project-parent</artifactId>
25+
<version>1</version>
26+
</parent>
27+
28+
<groupId>test</groupId>
29+
<artifactId>massembly-1008</artifactId>
30+
<version>1</version>
31+
32+
<url>https://issues.apache.org/jira/browse/MASSEMBLY-1008</url>
33+
34+
<properties>
35+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
36+
</properties>
37+
<dependencies>
38+
<dependency>
39+
<groupId>com.google.inject</groupId>
40+
<artifactId>guice</artifactId>
41+
<version>6.0.0</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>com.google.guava</groupId>
45+
<artifactId>guava</artifactId>
46+
<version>31.0.1-jre</version>
47+
<scope>test</scope>
48+
</dependency>
49+
</dependencies>
50+
<build>
51+
<plugins>
52+
<plugin>
53+
<artifactId>maven-assembly-plugin</artifactId>
54+
<executions>
55+
<execution>
56+
<id>assembly</id>
57+
<phase>package</phase>
58+
<goals>
59+
<goal>single</goal>
60+
</goals>
61+
<configuration>
62+
<descriptors>
63+
<descriptor>src/main/assembly/bin.xml</descriptor>
64+
</descriptors>
65+
</configuration>
66+
</execution>
67+
</executions>
68+
</plugin>
69+
</plugins>
70+
</build>
71+
</project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<assembly>
20+
<id>bin</id>
21+
<formats>
22+
<format>dir</format>
23+
</formats>
24+
<includeBaseDirectory>false</includeBaseDirectory>
25+
<dependencySets>
26+
<dependencySet>
27+
<unpack>false</unpack>
28+
<scope>runtime</scope>
29+
<outputDirectory></outputDirectory>
30+
<useProjectArtifact>false</useProjectArtifact>
31+
</dependencySet>
32+
</dependencySets>
33+
</assembly>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.*;
21+
22+
def expectedFilenames = [
23+
"aopalliance-1.0.jar",
24+
"checker-qual-3.12.0.jar",
25+
"error_prone_annotations-2.7.1.jar",
26+
"failureaccess-1.0.1.jar",
27+
"guava-31.0.1-jre.jar",
28+
"guice-6.0.0.jar",
29+
"j2objc-annotations-1.3.jar",
30+
"jakarta.inject-api-2.0.1.jar",
31+
"javax.inject-1.jar",
32+
"jsr305-3.0.2.jar",
33+
"listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"
34+
]
35+
36+
File assemblyBasedir = new File( basedir, "target/massembly-1008-1-bin/" )
37+
38+
assert assemblyBasedir.listFiles().length == expectedFilenames.size()
39+
40+
for ( fileName in expectedFilenames )
41+
{
42+
File file = new File( assemblyBasedir, fileName )
43+
assert file.isFile() // exists and is file
44+
}
45+
46+
// defined set vs listed set: same cardinality and all present: OK
47+
48+
return true

src/main/java/org/apache/maven/plugins/assembly/artifact/DefaultDependencyResolver.java

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,24 @@
2222
import javax.inject.Named;
2323
import javax.inject.Singleton;
2424

25+
import java.util.ArrayDeque;
26+
import java.util.ArrayList;
27+
import java.util.Deque;
28+
import java.util.HashMap;
29+
import java.util.HashSet;
2530
import java.util.LinkedHashMap;
2631
import java.util.List;
2732
import java.util.Map;
33+
import java.util.Optional;
2834
import java.util.Set;
35+
import java.util.stream.Collectors;
2936

37+
import org.apache.maven.RepositoryUtils;
3038
import org.apache.maven.artifact.Artifact;
3139
import org.apache.maven.artifact.DefaultArtifact;
3240
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
3341
import org.apache.maven.artifact.versioning.VersionRange;
42+
import org.apache.maven.model.DependencyManagement;
3443
import org.apache.maven.plugins.assembly.AssemblerConfigurationSource;
3544
import org.apache.maven.plugins.assembly.archive.ArchiveCreationException;
3645
import org.apache.maven.plugins.assembly.archive.phase.ModuleSetAssemblyPhase;
@@ -40,14 +49,24 @@
4049
import org.apache.maven.plugins.assembly.model.ModuleSet;
4150
import org.apache.maven.project.MavenProject;
4251
import org.codehaus.plexus.util.StringUtils;
52+
import org.eclipse.aether.RepositorySystem;
53+
import org.eclipse.aether.RepositorySystemSession;
54+
import org.eclipse.aether.collection.CollectRequest;
55+
import org.eclipse.aether.graph.DefaultDependencyNode;
56+
import org.eclipse.aether.graph.Dependency;
57+
import org.eclipse.aether.graph.DependencyFilter;
58+
import org.eclipse.aether.graph.DependencyNode;
59+
import org.eclipse.aether.graph.DependencyVisitor;
60+
import org.eclipse.aether.resolution.DependencyRequest;
61+
import org.eclipse.aether.resolution.DependencyResult;
62+
import org.eclipse.aether.util.filter.DependencyFilterUtils;
4363
import org.slf4j.Logger;
4464
import org.slf4j.LoggerFactory;
4565

4666
import static java.util.Objects.requireNonNull;
4767

4868
/**
4969
* @author jdcasey
50-
*
5170
*/
5271
@Singleton
5372
@Named
@@ -56,9 +75,12 @@ public class DefaultDependencyResolver implements DependencyResolver {
5675

5776
private final ArtifactHandlerManager artifactHandlerManager;
5877

78+
private final RepositorySystem repositorySystem;
79+
5980
@Inject
60-
public DefaultDependencyResolver(ArtifactHandlerManager artifactHandlerManager) {
81+
public DefaultDependencyResolver(ArtifactHandlerManager artifactHandlerManager, RepositorySystem repositorySystem) {
6182
this.artifactHandlerManager = requireNonNull(artifactHandlerManager);
83+
this.repositorySystem = requireNonNull(repositorySystem);
6284
}
6385

6486
@Override
@@ -75,7 +97,8 @@ public Map<DependencySet, Set<Artifact>> resolveDependencySets(
7597
final MavenProject currentProject = configSource.getProject();
7698

7799
final ResolutionManagementInfo info = new ResolutionManagementInfo();
78-
updateDependencySetResolutionRequirements(dependencySet, info, currentProject);
100+
updateDependencySetResolutionRequirements(
101+
configSource.getMavenSession().getRepositorySession(), dependencySet, info, currentProject);
79102
updateModuleSetResolutionRequirements(moduleSet, dependencySet, info, configSource);
80103

81104
result.put(dependencySet, info.getArtifacts());
@@ -96,7 +119,8 @@ public Map<DependencySet, Set<Artifact>> resolveDependencySets(
96119
final MavenProject currentProject = configSource.getProject();
97120

98121
final ResolutionManagementInfo info = new ResolutionManagementInfo();
99-
updateDependencySetResolutionRequirements(dependencySet, info, currentProject);
122+
updateDependencySetResolutionRequirements(
123+
configSource.getMavenSession().getRepositorySession(), dependencySet, info, currentProject);
100124

101125
result.put(dependencySet, info.getArtifacts());
102126
}
@@ -127,7 +151,10 @@ void updateModuleSetResolutionRequirements(
127151

128152
if (binaries.isIncludeDependencies()) {
129153
updateDependencySetResolutionRequirements(
130-
dependencySet, requirements, projects.toArray(new MavenProject[0]));
154+
configSource.getMavenSession().getRepositorySession(),
155+
dependencySet,
156+
requirements,
157+
projects.toArray(new MavenProject[0]));
131158
}
132159
}
133160
}
@@ -149,7 +176,10 @@ private Artifact createArtifact(String groupId, String artifactId, String versio
149176
}
150177

151178
void updateDependencySetResolutionRequirements(
152-
final DependencySet set, final ResolutionManagementInfo requirements, final MavenProject... projects)
179+
RepositorySystemSession systemSession,
180+
final DependencySet set,
181+
final ResolutionManagementInfo requirements,
182+
final MavenProject... projects)
153183
throws DependencyResolutionException {
154184
for (final MavenProject project : projects) {
155185
if (project == null) {
@@ -158,14 +188,91 @@ void updateDependencySetResolutionRequirements(
158188

159189
Set<Artifact> dependencyArtifacts = null;
160190
if (set.isUseTransitiveDependencies()) {
161-
dependencyArtifacts = project.getArtifacts();
191+
try {
192+
// we need resolve project again according to requested scope
193+
dependencyArtifacts = resolveTransitive(systemSession, set.getScope(), project);
194+
} catch (org.eclipse.aether.resolution.DependencyResolutionException e) {
195+
throw new DependencyResolutionException(e.getMessage(), e);
196+
}
162197
} else {
198+
// FIXME remove using deprecated method
163199
dependencyArtifacts = project.getDependencyArtifacts();
164200
}
165201

166202
requirements.addArtifacts(dependencyArtifacts);
167-
LOGGER.debug("Dependencies for project: " + project.getId() + " are:\n"
168-
+ StringUtils.join(dependencyArtifacts.iterator(), "\n"));
203+
if (LOGGER.isDebugEnabled()) {
204+
LOGGER.debug(
205+
"Dependencies for project: {} are:\n{}",
206+
project.getId(),
207+
StringUtils.join(dependencyArtifacts.iterator(), "\n"));
208+
}
169209
}
170210
}
211+
212+
private Set<Artifact> resolveTransitive(
213+
RepositorySystemSession repositorySession, String scope, MavenProject project)
214+
throws org.eclipse.aether.resolution.DependencyResolutionException {
215+
216+
// scope dependency filter
217+
DependencyFilter scoopeDependencyFilter = DependencyFilterUtils.classpathFilter(scope);
218+
219+
// get project dependencies filtered by requested scope
220+
List<Dependency> dependencies = project.getDependencies().stream()
221+
.map(d -> RepositoryUtils.toDependency(d, repositorySession.getArtifactTypeRegistry()))
222+
.filter(d -> scoopeDependencyFilter.accept(new DefaultDependencyNode(d), null))
223+
.collect(Collectors.toList());
224+
225+
List<Dependency> managedDependencies = Optional.ofNullable(project.getDependencyManagement())
226+
.map(DependencyManagement::getDependencies)
227+
.map(list -> list.stream()
228+
.map(d -> RepositoryUtils.toDependency(d, repositorySession.getArtifactTypeRegistry()))
229+
.collect(Collectors.toList()))
230+
.orElse(null);
231+
232+
CollectRequest collectRequest = new CollectRequest();
233+
collectRequest.setManagedDependencies(managedDependencies);
234+
collectRequest.setRepositories(project.getRemoteProjectRepositories());
235+
collectRequest.setDependencies(dependencies);
236+
collectRequest.setRootArtifact(RepositoryUtils.toArtifact(project.getArtifact()));
237+
238+
DependencyRequest request = new DependencyRequest(collectRequest, scoopeDependencyFilter);
239+
240+
DependencyResult dependencyResult = repositorySystem.resolveDependencies(repositorySession, request);
241+
242+
// cache for artifact mapping
243+
Map<org.eclipse.aether.artifact.Artifact, Artifact> aetherToMavenArtifacts = new HashMap<>();
244+
Deque<String> stack = new ArrayDeque<>();
245+
stack.push(project.getArtifact().getId());
246+
247+
Set<Artifact> artifacts = new HashSet<>();
248+
249+
// we need rebuild artifact dependencyTrail - it is used by useTransitiveFiltering
250+
dependencyResult.getRoot().accept(new DependencyVisitor() {
251+
@Override
252+
public boolean visitEnter(DependencyNode node) {
253+
if (node.getDependency() != null) {
254+
stack.push(aetherToMavenArtifacts
255+
.computeIfAbsent(node.getDependency().getArtifact(), RepositoryUtils::toArtifact)
256+
.getId());
257+
}
258+
return true;
259+
}
260+
261+
@Override
262+
public boolean visitLeave(DependencyNode node) {
263+
if (node.getDependency() != null) {
264+
Artifact artifact = aetherToMavenArtifacts.computeIfAbsent(
265+
node.getDependency().getArtifact(), RepositoryUtils::toArtifact);
266+
List<String> depTrail = new ArrayList<>();
267+
stack.descendingIterator().forEachRemaining(depTrail::add);
268+
stack.pop();
269+
artifact.setDependencyTrail(depTrail);
270+
artifacts.add(artifact);
271+
}
272+
return true;
273+
}
274+
});
275+
276+
return artifacts;
277+
}
171278
}

0 commit comments

Comments
 (0)