You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2022/12/05 15:19:36 UTC
[maven] branch master updated: [MNG-7619] Reverse Dependency Tree (#902)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new 6773c0512 [MNG-7619] Reverse Dependency Tree (#902)
6773c0512 is described below
commit 6773c051285c7734e6148dc6d3c265c4d18a1675
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Dec 5 16:19:31 2022 +0100
[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
---
.../DefaultRepositorySystemSessionFactory.java | 17 +++
.../aether/ReverseTreeRepositoryListener.java | 136 +++++++++++++++++++++
.../aether/ReverseTreeRepositoryListenerTest.java | 87 +++++++++++++
3 files changed, 240 insertions(+)
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
index 73b351458..276476616 100644
--- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
@@ -72,6 +72,7 @@ import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
import org.eclipse.aether.transform.FileTransformer;
import org.eclipse.aether.transform.TransformException;
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.ChainedLocalRepositoryManager;
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";
+ /**
+ * 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_DEFAULT = "default";
@@ -348,6 +359,12 @@ public class DefaultRepositorySystemSessionFactory {
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.injectProxy(session, request.getRemoteRepositories());
mavenRepositorySystem.injectAuthentication(session, request.getRemoteRepositories());
diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java b/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java
new file mode 100644
index 000000000..773d2e16f
--- /dev/null
+++ b/maven-core/src/main/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListener.java
@@ -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());
+ }
+}
diff --git a/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java b/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java
new file mode 100644
index 000000000..f7e68beba
--- /dev/null
+++ b/maven-core/src/test/java/org/apache/maven/internal/aether/ReverseTreeRepositoryListenerTest.java
@@ -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));
+ }
+}