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));
+    }
+}