mirror of
https://github.com/apache/maven.git
synced 2025-02-08 02:59:22 +00:00
[MNG-7619] Reverse Dependency Tree (#902)
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 https://github.com/apache/maven-resolver/pull/182 --- https://issues.apache.org/jira/browse/MNG-7619
This commit is contained in:
parent
0d4a1a1257
commit
6773c05128
@ -72,6 +72,7 @@
|
|||||||
import org.eclipse.aether.transform.FileTransformer;
|
import org.eclipse.aether.transform.FileTransformer;
|
||||||
import org.eclipse.aether.transform.TransformException;
|
import org.eclipse.aether.transform.TransformException;
|
||||||
import org.eclipse.aether.util.ConfigUtils;
|
import org.eclipse.aether.util.ConfigUtils;
|
||||||
|
import org.eclipse.aether.util.listener.ChainedRepositoryListener;
|
||||||
import org.eclipse.aether.util.repository.AuthenticationBuilder;
|
import org.eclipse.aether.util.repository.AuthenticationBuilder;
|
||||||
import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
|
import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
|
||||||
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
|
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
|
||||||
@ -104,6 +105,16 @@ public class DefaultRepositorySystemSessionFactory {
|
|||||||
*/
|
*/
|
||||||
private static final String MAVEN_REPO_LOCAL_TAIL_IGNORE_AVAILABILITY = "maven.repo.local.tail.ignoreAvailability";
|
private static final String MAVEN_REPO_LOCAL_TAIL_IGNORE_AVAILABILITY = "maven.repo.local.tail.ignoreAvailability";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local
|
||||||
|
* repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local
|
||||||
|
* repository.
|
||||||
|
* Default: {@code false}, will not record anything.
|
||||||
|
*
|
||||||
|
* @since 3.9.0
|
||||||
|
*/
|
||||||
|
private static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = "maven.repo.local.recordReverseTree";
|
||||||
|
|
||||||
private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport";
|
private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport";
|
||||||
|
|
||||||
private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default";
|
private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default";
|
||||||
@ -348,6 +359,12 @@ public DefaultRepositorySystemSession newRepositorySession(MavenExecutionRequest
|
|||||||
|
|
||||||
session.setRepositoryListener(eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger)));
|
session.setRepositoryListener(eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger)));
|
||||||
|
|
||||||
|
boolean recordReverseTree = ConfigUtils.getBoolean(session, false, MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE);
|
||||||
|
if (recordReverseTree) {
|
||||||
|
session.setRepositoryListener(new ChainedRepositoryListener(
|
||||||
|
session.getRepositoryListener(), new ReverseTreeRepositoryListener()));
|
||||||
|
}
|
||||||
|
|
||||||
mavenRepositorySystem.injectMirror(request.getRemoteRepositories(), request.getMirrors());
|
mavenRepositorySystem.injectMirror(request.getRemoteRepositories(), request.getMirrors());
|
||||||
mavenRepositorySystem.injectProxy(session, request.getRemoteRepositories());
|
mavenRepositorySystem.injectProxy(session, request.getRemoteRepositories());
|
||||||
mavenRepositorySystem.injectAuthentication(session, request.getRemoteRepositories());
|
mavenRepositorySystem.injectAuthentication(session, request.getRemoteRepositories());
|
||||||
|
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package org.apache.maven.internal.aether;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.eclipse.aether.AbstractRepositoryListener;
|
||||||
|
import org.eclipse.aether.RepositoryEvent;
|
||||||
|
import org.eclipse.aether.RepositorySystemSession;
|
||||||
|
import org.eclipse.aether.RequestTrace;
|
||||||
|
import org.eclipse.aether.artifact.Artifact;
|
||||||
|
import org.eclipse.aether.collection.CollectStepData;
|
||||||
|
import org.eclipse.aether.graph.Dependency;
|
||||||
|
import org.eclipse.aether.graph.DependencyNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class building reverse tree using {@link CollectStepData} trace data provided in {@link RepositoryEvent}
|
||||||
|
* events fired during collection.
|
||||||
|
*
|
||||||
|
* @since 3.9.0
|
||||||
|
*/
|
||||||
|
class ReverseTreeRepositoryListener extends AbstractRepositoryListener {
|
||||||
|
@Override
|
||||||
|
public void artifactResolved(RepositoryEvent event) {
|
||||||
|
requireNonNull(event, "event cannot be null");
|
||||||
|
|
||||||
|
if (!isLocalRepositoryArtifact(event.getSession(), event.getArtifact())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectStepData collectStepTrace = lookupCollectStepData(event.getTrace());
|
||||||
|
if (collectStepTrace == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Artifact resolvedArtifact = event.getArtifact();
|
||||||
|
Artifact nodeArtifact = collectStepTrace.getNode().getArtifact();
|
||||||
|
|
||||||
|
if (isInScope(resolvedArtifact, nodeArtifact)) {
|
||||||
|
Dependency node = collectStepTrace.getNode();
|
||||||
|
ArrayList<String> trackingData = new ArrayList<>();
|
||||||
|
trackingData.add(node + " (" + collectStepTrace.getContext() + ")");
|
||||||
|
String indent = "";
|
||||||
|
ListIterator<DependencyNode> iter = collectStepTrace
|
||||||
|
.getPath()
|
||||||
|
.listIterator(collectStepTrace.getPath().size());
|
||||||
|
while (iter.hasPrevious()) {
|
||||||
|
DependencyNode curr = iter.previous();
|
||||||
|
indent += " ";
|
||||||
|
trackingData.add(indent + curr + " (" + collectStepTrace.getContext() + ")");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path trackingDir =
|
||||||
|
resolvedArtifact.getFile().getParentFile().toPath().resolve(".tracking");
|
||||||
|
Files.createDirectories(trackingDir);
|
||||||
|
Path trackingFile = trackingDir.resolve(collectStepTrace
|
||||||
|
.getPath()
|
||||||
|
.get(0)
|
||||||
|
.getArtifact()
|
||||||
|
.toString()
|
||||||
|
.replace(":", "_"));
|
||||||
|
Files.write(trackingFile, trackingData, StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if passed in artifact is originating from local repository. In other words, we want
|
||||||
|
* to process and store tracking information ONLY into local repository, not to any other place. This method
|
||||||
|
* filters out currently built artifacts, as events are fired for them as well, but their resolved artifact
|
||||||
|
* file would point to checked out source-tree, not the local repository.
|
||||||
|
* <p>
|
||||||
|
* Visible for testing.
|
||||||
|
*/
|
||||||
|
static boolean isLocalRepositoryArtifact(RepositorySystemSession session, Artifact artifact) {
|
||||||
|
return artifact.getFile()
|
||||||
|
.getPath()
|
||||||
|
.startsWith(session.getLocalRepository().getBasedir().getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unravels trace tree (going upwards from current node), looking for {@link CollectStepData} trace data.
|
||||||
|
* This method may return {@code null} if no collect step data found in passed trace data or it's parents.
|
||||||
|
* <p>
|
||||||
|
* Visible for testing.
|
||||||
|
*/
|
||||||
|
static CollectStepData lookupCollectStepData(RequestTrace trace) {
|
||||||
|
CollectStepData collectStepTrace = null;
|
||||||
|
while (trace != null) {
|
||||||
|
if (trace.getData() instanceof CollectStepData) {
|
||||||
|
collectStepTrace = (CollectStepData) trace.getData();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
trace = trace.getParent();
|
||||||
|
}
|
||||||
|
return collectStepTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event "artifact resolved" if fired WHENEVER an artifact is resolved, BUT it happens also when an artifact
|
||||||
|
* descriptor (model, the POM) is being built, and parent (and parent of parent...) is being asked for. Hence, this
|
||||||
|
* method "filters" out in WHICH artifact are we interested in, but it intentionally neglects extension as
|
||||||
|
* ArtifactDescriptorReader modifies extension to "pom" during collect. So all we have to rely on is GAV only.
|
||||||
|
*/
|
||||||
|
static boolean isInScope(Artifact artifact, Artifact nodeArtifact) {
|
||||||
|
return Objects.equals(artifact.getGroupId(), nodeArtifact.getGroupId())
|
||||||
|
&& Objects.equals(artifact.getArtifactId(), nodeArtifact.getArtifactId())
|
||||||
|
&& Objects.equals(artifact.getVersion(), nodeArtifact.getVersion());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package org.apache.maven.internal.aether;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
|
import static org.hamcrest.CoreMatchers.sameInstance;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import org.eclipse.aether.RepositorySystemSession;
|
||||||
|
import org.eclipse.aether.RequestTrace;
|
||||||
|
import org.eclipse.aether.artifact.Artifact;
|
||||||
|
import org.eclipse.aether.collection.CollectStepData;
|
||||||
|
import org.eclipse.aether.repository.LocalRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UT for {@link ReverseTreeRepositoryListener}.
|
||||||
|
*/
|
||||||
|
public class ReverseTreeRepositoryListenerTest {
|
||||||
|
@Test
|
||||||
|
public void isLocalRepositoryArtifactTest() {
|
||||||
|
File baseDir = new File("local/repository");
|
||||||
|
LocalRepository localRepository = new LocalRepository(baseDir);
|
||||||
|
RepositorySystemSession session = mock(RepositorySystemSession.class);
|
||||||
|
when(session.getLocalRepository()).thenReturn(localRepository);
|
||||||
|
|
||||||
|
Artifact localRepositoryArtifact = mock(Artifact.class);
|
||||||
|
when(localRepositoryArtifact.getFile()).thenReturn(new File(baseDir, "some/path/within"));
|
||||||
|
|
||||||
|
Artifact nonLocalReposioryArtifact = mock(Artifact.class);
|
||||||
|
when(nonLocalReposioryArtifact.getFile()).thenReturn(new File("something/completely/different"));
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, localRepositoryArtifact),
|
||||||
|
equalTo(true));
|
||||||
|
assertThat(
|
||||||
|
ReverseTreeRepositoryListener.isLocalRepositoryArtifact(session, nonLocalReposioryArtifact),
|
||||||
|
equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lookupCollectStepDataTest() {
|
||||||
|
RequestTrace doesNotHaveIt =
|
||||||
|
RequestTrace.newChild(null, "foo").newChild("bar").newChild("baz");
|
||||||
|
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(doesNotHaveIt), nullValue());
|
||||||
|
|
||||||
|
final CollectStepData data = mock(CollectStepData.class);
|
||||||
|
|
||||||
|
RequestTrace haveItFirst = RequestTrace.newChild(null, data)
|
||||||
|
.newChild("foo")
|
||||||
|
.newChild("bar")
|
||||||
|
.newChild("baz");
|
||||||
|
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItFirst), sameInstance(data));
|
||||||
|
|
||||||
|
RequestTrace haveItLast = RequestTrace.newChild(null, "foo")
|
||||||
|
.newChild("bar")
|
||||||
|
.newChild("baz")
|
||||||
|
.newChild(data);
|
||||||
|
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveItLast), sameInstance(data));
|
||||||
|
|
||||||
|
RequestTrace haveIt = RequestTrace.newChild(null, "foo")
|
||||||
|
.newChild("bar")
|
||||||
|
.newChild(data)
|
||||||
|
.newChild("baz");
|
||||||
|
assertThat(ReverseTreeRepositoryListener.lookupCollectStepData(haveIt), sameInstance(data));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user