Skip to content

Commit 69c5a5d

Browse files
authored
[MNG-7619] Reverse Dependency Tree (#900)
Adds Maven feature that is able to explain why an artifact is present in local repository. Usable for diagnosing resolution issues. In local repository, for each artifact it records `.tracking` folder, containing farthest artifact that got to this artifact, and the list of graph nodes that lead to it. Note: this is based on by @grgrzybek proposal and reuses some code he provided. See apache/maven-resolver#182 --- https://issues.apache.org/jira/browse/MNG-7619
1 parent 2dc7a35 commit 69c5a5d

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed

maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.eclipse.aether.resolution.ResolutionErrorPolicy;
6060
import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
6161
import org.eclipse.aether.util.ConfigUtils;
62+
import org.eclipse.aether.util.listener.ChainedRepositoryListener;
6263
import org.eclipse.aether.util.repository.AuthenticationBuilder;
6364
import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
6465
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
@@ -89,6 +90,16 @@ public class DefaultRepositorySystemSessionFactory {
8990
*/
9091
private static final String MAVEN_REPO_LOCAL_TAIL_IGNORE_AVAILABILITY = "maven.repo.local.tail.ignoreAvailability";
9192

93+
/**
94+
* User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local
95+
* repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local
96+
* repository.
97+
* Default: {@code false}, will not record anything.
98+
*
99+
* @since 3.9.0
100+
*/
101+
private static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = "maven.repo.local.recordReverseTree";
102+
92103
private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport";
93104

94105
private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default";
@@ -351,6 +362,12 @@ public DefaultRepositorySystemSession newRepositorySession(MavenExecutionRequest
351362

352363
session.setRepositoryListener(eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger)));
353364

365+
boolean recordReverseTree = ConfigUtils.getBoolean(session, false, MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE);
366+
if (recordReverseTree) {
367+
session.setRepositoryListener(new ChainedRepositoryListener(
368+
session.getRepositoryListener(), new ReverseTreeRepositoryListener()));
369+
}
370+
354371
mavenRepositorySystem.injectMirror(request.getRemoteRepositories(), request.getMirrors());
355372
mavenRepositorySystem.injectProxy(session, request.getRemoteRepositories());
356373
mavenRepositorySystem.injectAuthentication(session, request.getRemoteRepositories());
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
package org.apache.maven.internal.aether;
20+
21+
import static java.util.Objects.requireNonNull;
22+
23+
import java.io.IOException;
24+
import java.io.UncheckedIOException;
25+
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.util.ArrayList;
29+
import java.util.ListIterator;
30+
import java.util.Objects;
31+
import org.eclipse.aether.AbstractRepositoryListener;
32+
import org.eclipse.aether.RepositoryEvent;
33+
import org.eclipse.aether.RepositorySystemSession;
34+
import org.eclipse.aether.RequestTrace;
35+
import org.eclipse.aether.artifact.Artifact;
36+
import org.eclipse.aether.collection.CollectStepData;
37+
import org.eclipse.aether.graph.Dependency;
38+
import org.eclipse.aether.graph.DependencyNode;
39+
40+
/**
41+
* A class building reverse tree using {@link CollectStepData} trace data provided in {@link RepositoryEvent}
42+
* events fired during collection.
43+
*
44+
* @since 3.9.0
45+
*/
46+
class ReverseTreeRepositoryListener extends AbstractRepositoryListener {
47+
@Override
48+
public void artifactResolved(RepositoryEvent event) {
49+
requireNonNull(event, "event cannot be null");
50+
51+
if (!isLocalRepositoryArtifact(event.getSession(), event.getArtifact())) {
52+
return;
53+
}
54+
55+
CollectStepData collectStepTrace = lookupCollectStepData(event.getTrace());
56+
if (collectStepTrace == null) {
57+
return;
58+
}
59+
60+
Artifact resolvedArtifact = event.getArtifact();
61+
Artifact nodeArtifact = collectStepTrace.getNode().getArtifact();
62+
63+
if (isInScope(resolvedArtifact, nodeArtifact)) {
64+
Dependency node = collectStepTrace.getNode();
65+
ArrayList<String> trackingData = new ArrayList<>();
66+
trackingData.add(node + " (" + collectStepTrace.getContext() + ")");
67+
String indent = "";
68+
ListIterator<DependencyNode> iter = collectStepTrace
69+
.getPath()
70+
.listIterator(collectStepTrace.getPath().size());
71+
while (iter.hasPrevious()) {
72+
DependencyNode curr = iter.previous();
73+
indent += " ";
74+
trackingData.add(indent + curr + " (" + collectStepTrace.getContext() + ")");
75+
}
76+
try {
77+
Path trackingDir =
78+
resolvedArtifact.getFile().getParentFile().toPath().resolve(".tracking");
79+
Files.createDirectories(trackingDir);
80+
Path trackingFile = trackingDir.resolve(collectStepTrace
81+
.getPath()
82+
.get(0)
83+
.getArtifact()
84+
.toString()
85+
.replace(":", "_"));
86+
Files.write(trackingFile, trackingData, StandardCharsets.UTF_8);
87+
} catch (IOException e) {
88+
throw new UncheckedIOException(e);
89+
}
90+
}
91+
}
92+
93+
/**
94+
* Returns {@code true} if passed in artifact is originating from local repository. In other words, we want
95+
* to process and store tracking information ONLY into local repository, not to any other place. This method
96+
* filters out currently built artifacts, as events are fired for them as well, but their resolved artifact
97+
* file would point to checked out source-tree, not the local repository.
98+
* <p>
99+
* Visible for testing.
100+
*/
101+
static boolean isLocalRepositoryArtifact(RepositorySystemSession session, Artifact artifact) {
102+
return artifact.getFile()
103+
.getPath()
104+
.startsWith(session.getLocalRepository().getBasedir().getPath());
105+
}
106+
107+
/**
108+
* Unravels trace tree (going upwards from current node), looking for {@link CollectStepData} trace data.
109+
* This method may return {@code null} if no collect step data found in passed trace data or it's parents.
110+
* <p>
111+
* Visible for testing.
112+
*/
113+
static CollectStepData lookupCollectStepData(RequestTrace trace) {
114+
CollectStepData collectStepTrace = null;
115+
while (trace != null) {
116+
if (trace.getData() instanceof CollectStepData) {
117+
collectStepTrace = (CollectStepData) trace.getData();
118+
break;
119+
}
120+
trace = trace.getParent();
121+
}
122+
return collectStepTrace;
123+
}
124+
125+
/**
126+
* The event "artifact resolved" if fired WHENEVER an artifact is resolved, BUT it happens also when an artifact
127+
* descriptor (model, the POM) is being built, and parent (and parent of parent...) is being asked for. Hence, this
128+
* method "filters" out in WHICH artifact are we interested in, but it intentionally neglects extension as
129+
* ArtifactDescriptorReader modifies extension to "pom" during collect. So all we have to rely on is GAV only.
130+
*/
131+
static boolean isInScope(Artifact artifact, Artifact nodeArtifact) {
132+
return Objects.equals(artifact.getGroupId(), nodeArtifact.getGroupId())
133+
&& Objects.equals(artifact.getArtifactId(), nodeArtifact.getArtifactId())
134+
&& Objects.equals(artifact.getVersion(), nodeArtifact.getVersion());
135+
}
136+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
package org.apache.maven.internal.aether;
20+
21+
import static org.hamcrest.CoreMatchers.equalTo;
22+
import static org.hamcrest.CoreMatchers.nullValue;
23+
import static org.hamcrest.CoreMatchers.sameInstance;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.mockito.Mockito.mock;
26+
import static org.mockito.Mockito.when;
27+
28+
import java.io.File;
29+
import org.eclipse.aether.RepositorySystemSession;
30+
import org.eclipse.aether.RequestTrace;
31+
import org.eclipse.aether.artifact.Artifact;
32+
import org.eclipse.aether.collection.CollectStepData;
33+
import org.eclipse.aether.repository.LocalRepository;
34+
import org.junit.Test;
35+
36+
/**
37+
* UT for {@link ReverseTreeRepositoryListener}.
38+
*/
39+
public class ReverseTreeRepositoryListenerTest {
40+
@Test
41+
public void isLocalRepositoryArtifactTest() {
42+
File baseDir = new File("local/repository");
43+
LocalRepository localRepository = new LocalRepository(baseDir);
44+
RepositorySystemSession session = mock(RepositorySystemSession.class);
45+
when(session.getLocalRepository()).thenReturn(localRepository);
46+
47+
Artifact localRepositoryArtifact = mock(Artifact.class);
48+
when(localRepositoryArtifact.getFile()).thenReturn(new File(baseDir, "some/path/within"));
49+
50+
Artifact nonLocalReposioryArtifact = mock(Artifact.class);
51+
when(nonLocalReposioryArtifact.getFile()).thenReturn(new File("something/completely/different"));
52+
53+
assertThat(
54+
ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, localRepositoryArtifact),
55+
equalTo(true));
56+
assertThat(
57+
ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, nonLocalReposioryArtifact),
58+
equalTo(false));
59+
}
60+
61+
@Test
62+
public void lookupCollectStepDataTest() {
63+
RequestTrace doesNotHaveIt =
64+
RequestTrace.newChild(null, "foo").newChild("bar").newChild("baz");
65+
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(doesNotHaveIt), nullValue());
66+
67+
final CollectStepData data = mock(CollectStepData.class);
68+
69+
RequestTrace haveItFirst = RequestTrace.newChild(null, data)
70+
.newChild("foo")
71+
.newChild("bar")
72+
.newChild("baz");
73+
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItFirst), sameInstance(data));
74+
75+
RequestTrace haveItLast = RequestTrace.newChild(null, "foo")
76+
.newChild("bar")
77+
.newChild("baz")
78+
.newChild(data);
79+
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItLast), sameInstance(data));
80+
81+
RequestTrace haveIt = RequestTrace.newChild(null, "foo")
82+
.newChild("bar")
83+
.newChild(data)
84+
.newChild("baz");
85+
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveIt), sameInstance(data));
86+
}
87+
}

0 commit comments

Comments
 (0)