You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2022/11/22 19:49:10 UTC

[nifi-maven] branch main updated: NIFI-10457 Added new goal that checks dependency duplications in NARs

This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-maven.git


The following commit(s) were added to refs/heads/main by this push:
     new 57b3296  NIFI-10457 Added new goal that checks dependency duplications in NARs
57b3296 is described below

commit 57b3296deffc0ebb9d930aaaa7b9f88f9dc436eb
Author: Tamas Sudi <su...@pm.me>
AuthorDate: Fri Oct 7 15:38:04 2022 +0200

    NIFI-10457 Added new goal that checks dependency duplications in NARs
    
    This closes #23
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 pom.xml                                            |   2 +-
 .../apache/nifi/NarDuplicateDependenciesMojo.java  | 213 +++++++++++++++++++++
 src/main/java/org/apache/nifi/NarMojo.java         |   2 +-
 .../apache/nifi/NarProvidedDependenciesMojo.java   | 114 ++---------
 .../org/apache/nifi/utils/NarDependencyUtils.java  | 125 ++++++++++++
 5 files changed, 354 insertions(+), 102 deletions(-)

diff --git a/pom.xml b/pom.xml
index 64fe7c3..d849197 100644
--- a/pom.xml
+++ b/pom.xml
@@ -400,7 +400,7 @@
         <dependency>
             <groupId>org.apache.maven.shared</groupId>
             <artifactId>maven-dependency-tree</artifactId>
-            <version>3.0.1</version>
+            <version>3.2.0</version>
         </dependency>
         <dependency>
             <!-- No code from maven-jar-plugin is actually used; it's included
diff --git a/src/main/java/org/apache/nifi/NarDuplicateDependenciesMojo.java b/src/main/java/org/apache/nifi/NarDuplicateDependenciesMojo.java
new file mode 100644
index 0000000..008ca9f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/NarDuplicateDependenciesMojo.java
@@ -0,0 +1,213 @@
+/*
+ * 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.nifi;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.handler.ArtifactHandler;
+import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.DefaultProjectBuildingRequest;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuilder;
+import org.apache.maven.project.ProjectBuildingException;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
+import org.apache.maven.shared.dependency.graph.DependencyNode;
+import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
+import org.apache.nifi.utils.NarDependencyUtils;
+import org.eclipse.aether.RepositorySystemSession;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.HashMap;
+
+/**
+ * Generates a list of duplicate dependencies with compile scope in the nar.
+ */
+@Mojo(name = "duplicate-nar-dependencies", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true, requiresDependencyResolution = ResolutionScope.RUNTIME)
+public class NarDuplicateDependenciesMojo extends AbstractMojo {
+
+    /**
+     * The Maven project.
+     */
+    @Parameter(defaultValue = "${project}", readonly = true, required = true)
+    private MavenProject project;
+
+
+    /**
+     * The {@link RepositorySystemSession} used for obtaining the local and remote artifact repositories.
+     */
+    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
+    private RepositorySystemSession repoSession;
+
+
+    /**
+     * The dependency tree builder to use for verbose output.
+     */
+    @Component
+    private DependencyCollectorBuilder dependencyCollectorBuilder;
+
+    /**
+     * *
+     * The {@link ArtifactHandlerManager} into which any extension {@link ArtifactHandler} instances should have been injected when the extensions were loaded.
+     */
+    @Component
+    private ArtifactHandlerManager artifactHandlerManager;
+
+    /**
+     * The {@link ProjectBuilder} used to generate the {@link MavenProject} for the nar artifact the dependency tree is being generated for.
+     */
+    @Component
+    private ProjectBuilder projectBuilder;
+
+    /*
+     * @see org.apache.maven.plugin.Mojo#execute()
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        try {
+            NarDependencyUtils.ensureSingleNarDependencyExists(project);
+            // build the project for the nar artifact
+            final ProjectBuildingRequest narRequest = new DefaultProjectBuildingRequest();
+            narRequest.setRepositorySession(repoSession);
+            narRequest.setSystemProperties(System.getProperties());
+
+            artifactHandlerManager.addHandlers(NarDependencyUtils.createNarHandlerMap(narRequest, project, projectBuilder));
+
+            // get the dependency tree
+            final DependencyNode root = dependencyCollectorBuilder.collectDependencyGraph(narRequest, null);
+
+            DependencyNode narParent = root.getChildren()
+                    .stream()
+                    .filter(child -> NarDependencyUtils.NAR.equals(child.getArtifact().getType()))
+                    .findFirst()
+                    .orElseThrow(() -> new MojoExecutionException("Project does not have any NAR dependencies."));
+
+            getLog().info("Analyzing dependencies of " + narRequest.getProject().getFile().getPath());
+
+            // all compiled dependencies except inherited from parent
+            Map<String, List<Artifact>> directDependencies = new HashMap<>();
+
+            root.accept(new DependencyNodeVisitor() {
+                final Stack<Artifact> hierarchy = new Stack<>();
+
+                @Override
+                public boolean visit(DependencyNode node) {
+                    if (node == root) {
+                        return true;
+                    }
+                    Artifact artifact = node.getArtifact();
+                    hierarchy.push(artifact);
+                    if (NarDependencyUtils.COMPILE_STRING.equals(artifact.getScope()) && !NarDependencyUtils.NAR.equals(artifact.getType())) {
+                        directDependencies.put(artifact.toString(), new ArrayList<>(hierarchy));
+                        return true;
+                    }
+                    return false;
+                }
+
+                @Override
+                public boolean endVisit(DependencyNode node) {
+                    if (node != root) {
+                        hierarchy.pop();
+                    }
+                    return true;
+                }
+            });
+
+            Map<String, List<String>> errors = new HashMap<>();
+
+            narParent.accept(new DependencyNodeVisitor() {
+                final Stack<Artifact> hierarchy = new Stack<>();
+
+                @Override
+                public boolean visit(DependencyNode node) {
+                    Artifact artifact = node.getArtifact();
+                    hierarchy.push(artifact);
+                    if (NarDependencyUtils.COMPILE_STRING.equals(artifact.getScope()) && directDependencies.containsKey(artifact.toString())) {
+                        StringBuilder sb = new StringBuilder().append(root.getArtifact()).append(" (this nar)").append(System.lineSeparator());
+                        List<Artifact> otherHierarchy = directDependencies.get(artifact.toString());
+                        // print other hierarchy
+                        for (int i = 0; i < otherHierarchy.size(); i++) {
+                            sb.append(indent(i)).append(otherHierarchy.get(i));
+                            // print the last artifact in the hierarchy
+                            if (i == otherHierarchy.size() - 1) {
+                                sb.append(" (duplicate)");
+                            }
+                            sb.append(System.lineSeparator());
+                        }
+                        // print this hierarchy
+                        for (int i = 0; i < hierarchy.size(); i++) {
+                            sb.append(indent(i)).append(hierarchy.get(i));
+                            // print the last artifact in the hierarchy
+                            if (i == hierarchy.size() - 1) {
+                                sb.append(" (already included here)");
+                            }
+                            sb.append(System.lineSeparator());
+                        }
+                        errors.computeIfAbsent(artifact.toString(), k -> new ArrayList<>()).add(sb.toString());
+                    }
+                    return true;
+                }
+
+                @Override
+                public boolean endVisit(DependencyNode node) {
+                    hierarchy.pop();
+                    return true;
+                }
+            });
+
+            for (Map.Entry<String, List<String>> entry : errors.entrySet()) {
+                StringBuilder sb = new StringBuilder().append(entry.getKey()).append(" is already included in the nar");
+                if (entry.getValue().size() > 1) {
+                    sb.append(" multiple times");
+                }
+                sb.append(":");
+                for (String error : entry.getValue()) {
+                    sb.append(System.lineSeparator()).append(error);
+                }
+                getLog().error(sb.toString());
+            }
+
+            if (!errors.isEmpty()) {
+                getLog().info("Consider changing the scope from \"compile\" to \"provided\" or exclude it in case it's a transitive dependency.");
+                throw new MojoFailureException("Found duplicate dependencies");
+            }
+
+        } catch (ProjectBuildingException | DependencyCollectorBuilderException e) {
+            throw new MojoExecutionException("Cannot build project dependency tree", e);
+        }
+    }
+
+    private String indent(int indent) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < indent; i++) {
+            sb.append("|  ");
+        }
+        sb.append("+- ");
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/org/apache/nifi/NarMojo.java b/src/main/java/org/apache/nifi/NarMojo.java
index ee0133a..d355232 100644
--- a/src/main/java/org/apache/nifi/NarMojo.java
+++ b/src/main/java/org/apache/nifi/NarMojo.java
@@ -676,7 +676,7 @@ public class NarMojo extends AbstractMojo {
                                     final Class<?> docWriterClass, final XMLStreamWriter xmlWriter)
         throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, IOException {
 
-        getLog().debug("Generating documentation for " + extensionDefinition.getExtensionName() + " using ClassLoader:\n" + classLoader.toTree());
+        getLog().debug("Generating documentation for " + extensionDefinition.getExtensionName() + " using ClassLoader:" + System.lineSeparator() + classLoader.toTree());
         final Object docWriter = docWriterClass.getConstructor(XMLStreamWriter.class).newInstance(xmlWriter);
         final Class<?> configurableComponentClass = Class.forName("org.apache.nifi.components.ConfigurableComponent", false, classLoader);
 
diff --git a/src/main/java/org/apache/nifi/NarProvidedDependenciesMojo.java b/src/main/java/org/apache/nifi/NarProvidedDependenciesMojo.java
index cf43619..06553eb 100644
--- a/src/main/java/org/apache/nifi/NarProvidedDependenciesMojo.java
+++ b/src/main/java/org/apache/nifi/NarProvidedDependenciesMojo.java
@@ -33,17 +33,15 @@ import org.apache.maven.project.MavenProject;
 import org.apache.maven.project.ProjectBuilder;
 import org.apache.maven.project.ProjectBuildingException;
 import org.apache.maven.project.ProjectBuildingRequest;
-import org.apache.maven.project.ProjectBuildingResult;
 import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
 import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
 import org.apache.maven.shared.dependency.graph.DependencyNode;
 import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
+import org.apache.nifi.utils.NarDependencyUtils;
 import org.eclipse.aether.RepositorySystemSession;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * Generates the listing of dependencies that is provided by the NAR dependency of the current NAR. This is important as artifacts that bundle dependencies will
@@ -53,8 +51,6 @@ import java.util.Map;
 @Mojo(name = "provided-nar-dependencies", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true, requiresDependencyResolution = ResolutionScope.RUNTIME)
 public class NarProvidedDependenciesMojo extends AbstractMojo {
 
-    private static final String NAR = "nar";
-
     /**
      * The Maven project.
      */
@@ -105,46 +101,13 @@ public class NarProvidedDependenciesMojo extends AbstractMojo {
     @Override
     public void execute() throws MojoExecutionException, MojoFailureException {
         try {
-            // find the nar dependency
-            Artifact narArtifact = null;
-            for (final Artifact artifact : project.getDependencyArtifacts()) {
-                if (NAR.equals(artifact.getType())) {
-                    // ensure the project doesn't have two nar dependencies
-                    if (narArtifact != null) {
-                        throw new MojoExecutionException("Project can only have one NAR dependency.");
-                    }
-
-                    // record the nar dependency
-                    narArtifact = artifact;
-                }
-            }
-
-            // ensure there is a nar dependency
-            if (narArtifact == null) {
-                throw new MojoExecutionException("Project does not have any NAR dependencies.");
-            }
-
+            NarDependencyUtils.ensureSingleNarDependencyExists(project);
             // build the project for the nar artifact
             final ProjectBuildingRequest narRequest = new DefaultProjectBuildingRequest();
             narRequest.setRepositorySession(repoSession);
             narRequest.setSystemProperties(System.getProperties());
 
-            final ProjectBuildingResult narResult = projectBuilder.build(narArtifact, narRequest);
-            narRequest.setProject(narResult.getProject());
-
-            // get the artifact handler for excluding dependencies
-            final ArtifactHandler narHandler = excludesDependencies(narArtifact);
-            narArtifact.setArtifactHandler(narHandler);
-
-            // nar artifacts by nature includes dependencies, however this prevents the
-            // transitive dependencies from printing using tools like dependency:tree.
-            // here we are overriding the artifact handler for all nars so the
-            // dependencies can be listed. this is important because nar dependencies
-            // will be used as the parent classloader for this nar and seeing what
-            // dependencies are provided is critical.
-            final Map<String, ArtifactHandler> narHandlerMap = new HashMap<>();
-            narHandlerMap.put(NAR, narHandler);
-            artifactHandlerManager.addHandlers(narHandlerMap);
+            artifactHandlerManager.addHandlers(NarDependencyUtils.createNarHandlerMap(narRequest, project, projectBuilder));
 
             // get the dependency tree
             final DependencyNode root = dependencyGraphBuilder.buildDependencyGraph(narRequest, null);
@@ -164,7 +127,7 @@ public class NarProvidedDependenciesMojo extends AbstractMojo {
 
             // visit and print the results
             root.accept(visitor);
-            getLog().info("--- Provided NAR Dependencies ---\n\n" + visitor.toString());
+            getLog().info("--- Provided NAR Dependencies ---" + System.lineSeparator() + System.lineSeparator() + visitor);
         } catch (ProjectBuildingException | DependencyGraphBuilderException e) {
             throw new MojoExecutionException("Cannot build project dependency tree", e);
         }
@@ -179,60 +142,11 @@ public class NarProvidedDependenciesMojo extends AbstractMojo {
         return project;
     }
 
-    /**
-     * Creates a new ArtifactHandler for the specified Artifact that overrides the includeDependencies flag. When set, this flag prevents transitive
-     * dependencies from being printed in dependencies plugin.
-     *
-     * @param artifact  The artifact
-     * @return          The handler for the artifact
-     */
-    private ArtifactHandler excludesDependencies(final Artifact artifact) {
-        final ArtifactHandler orig = artifact.getArtifactHandler();
-
-        return new ArtifactHandler() {
-            @Override
-            public String getExtension() {
-                return orig.getExtension();
-            }
-
-            @Override
-            public String getDirectory() {
-                return orig.getDirectory();
-            }
-
-            @Override
-            public String getClassifier() {
-                return orig.getClassifier();
-            }
-
-            @Override
-            public String getPackaging() {
-                return orig.getPackaging();
-            }
-
-            // mark dependencies has excluded so they will appear in tree listing
-            @Override
-            public boolean isIncludesDependencies() {
-                return false;
-            }
-
-            @Override
-            public String getLanguage() {
-                return orig.getLanguage();
-            }
-
-            @Override
-            public boolean isAddedToClasspath() {
-                return orig.isAddedToClasspath();
-            }
-        };
-    }
-
     /**
      * Returns whether the specified dependency has test scope.
      *
-     * @param node  The dependency
-     * @return      What the dependency is a test scoped dep
+     * @param node The dependency
+     * @return What the dependency is a test scoped dep
      */
     private boolean isTest(final DependencyNode node) {
         return "test".equals(node.getArtifact().getScope());
@@ -265,7 +179,7 @@ public class NarProvidedDependenciesMojo extends AbstractMojo {
             pad.append("+- ");
 
             // log it
-            output.append(pad).append(node.toNodeString()).append("\n");
+            output.append(pad).append(node.toNodeString()).append(System.lineSeparator());
 
             return true;
         }
@@ -296,13 +210,13 @@ public class NarProvidedDependenciesMojo extends AbstractMojo {
             }
 
             final Artifact artifact = node.getArtifact();
-            if (!NAR.equals(artifact.getType())) {
-                output.append("<dependency>\n");
-                output.append("    <groupId>").append(artifact.getGroupId()).append("</groupId>\n");
-                output.append("    <artifactId>").append(artifact.getArtifactId()).append("</artifactId>\n");
-                output.append("    <version>").append(artifact.getVersion()).append("</version>\n");
-                output.append("    <scope>provided</scope>\n");
-                output.append("</dependency>\n");
+            if (!NarDependencyUtils.NAR.equals(artifact.getType())) {
+                output.append("<dependency>").append(System.lineSeparator());
+                output.append("    <groupId>").append(artifact.getGroupId()).append("</groupId>").append(System.lineSeparator());
+                output.append("    <artifactId>").append(artifact.getArtifactId()).append("</artifactId>").append(System.lineSeparator());
+                output.append("    <version>").append(artifact.getVersion()).append("</version>").append(System.lineSeparator());
+                output.append("    <scope>provided</scope>").append(System.lineSeparator());
+                output.append("</dependency>").append(System.lineSeparator());
             }
 
             return true;
diff --git a/src/main/java/org/apache/nifi/utils/NarDependencyUtils.java b/src/main/java/org/apache/nifi/utils/NarDependencyUtils.java
new file mode 100644
index 0000000..d98e75f
--- /dev/null
+++ b/src/main/java/org/apache/nifi/utils/NarDependencyUtils.java
@@ -0,0 +1,125 @@
+/*
+ * 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.nifi.utils;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.handler.ArtifactHandler;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuilder;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.project.ProjectBuildingResult;
+import org.apache.maven.project.ProjectBuildingException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NarDependencyUtils {
+    public static final String NAR = "nar";
+    public static final String COMPILE_STRING = "compile";
+
+    public static Map<String, ArtifactHandler> createNarHandlerMap(ProjectBuildingRequest narRequest, MavenProject project, ProjectBuilder projectBuilder) throws ProjectBuildingException {
+
+        final Artifact projectArtifact = project.getArtifact();
+        final ProjectBuildingResult narResult = projectBuilder.build(projectArtifact, narRequest);
+        narRequest.setProject(narResult.getProject());
+
+        // get the artifact handler for excluding dependencies
+        final ArtifactHandler narHandler = excludesDependencies(projectArtifact);
+        projectArtifact.setArtifactHandler(narHandler);
+
+        // nar artifacts by nature includes dependencies, however this prevents the
+        // transitive dependencies from printing using tools like dependency:tree.
+        // here we are overriding the artifact handler for all nars so the
+        // dependencies can be listed. this is important because nar dependencies
+        // will be used as the parent classloader for this nar and seeing what
+        // dependencies are provided is critical.
+        final Map<String, ArtifactHandler> narHandlerMap = new HashMap<>();
+        narHandlerMap.put(NAR, narHandler);
+        return narHandlerMap;
+    }
+
+    public static void ensureSingleNarDependencyExists(MavenProject project) throws MojoExecutionException {
+        // find the nar dependency
+        boolean found = false;
+        for (final Artifact artifact : project.getDependencyArtifacts()) {
+            if (NAR.equals(artifact.getType())) {
+                // ensure the project doesn't have two nar dependencies
+                if (found) {
+                    throw new MojoExecutionException("Project can only have one NAR dependency.");
+                }
+
+                // record the nar dependency
+                found = true;
+            }
+        }
+
+        // ensure there is a nar dependency
+        if (!found) {
+            throw new MojoExecutionException("Project does not have any NAR dependencies.");
+        }
+    }
+
+    /**
+     * Creates a new ArtifactHandler for the specified Artifact that overrides the includeDependencies flag. When set, this flag prevents transitive
+     * dependencies from being printed in dependencies plugin.
+     *
+     * @param artifact The artifact
+     * @return The handler for the artifact
+     */
+    private static ArtifactHandler excludesDependencies(final Artifact artifact) {
+        final ArtifactHandler orig = artifact.getArtifactHandler();
+
+        return new ArtifactHandler() {
+            @Override
+            public String getExtension() {
+                return orig.getExtension();
+            }
+
+            @Override
+            public String getDirectory() {
+                return orig.getDirectory();
+            }
+
+            @Override
+            public String getClassifier() {
+                return orig.getClassifier();
+            }
+
+            @Override
+            public String getPackaging() {
+                return orig.getPackaging();
+            }
+
+            // mark dependencies as excluded, so they will appear in tree listing
+            @Override
+            public boolean isIncludesDependencies() {
+                return false;
+            }
+
+            @Override
+            public String getLanguage() {
+                return orig.getLanguage();
+            }
+
+            @Override
+            public boolean isAddedToClasspath() {
+                return orig.isAddedToClasspath();
+            }
+        };
+    }
+}