From ca1a66bcf53393d7888b0e683da84bd23a727bb2 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 25 Feb 2021 18:32:03 +0100 Subject: [PATCH] Enhanced extension dependency validation during the extension descriptor generation --- .../bootstrap/maven-plugin/pom.xml | 21 + .../maven/BootstrapWorkspaceProvider.java | 36 ++ .../maven/ExtensionDescriptorMojo.java | 425 ++++++++++++++---- 3 files changed, 386 insertions(+), 96 deletions(-) create mode 100644 independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/BootstrapWorkspaceProvider.java diff --git a/independent-projects/bootstrap/maven-plugin/pom.xml b/independent-projects/bootstrap/maven-plugin/pom.xml index 7ce3a1cc7c18d7..34112d1066ab16 100644 --- a/independent-projects/bootstrap/maven-plugin/pom.xml +++ b/independent-projects/bootstrap/maven-plugin/pom.xml @@ -15,6 +15,21 @@ + + org.codehaus.plexus + plexus-component-metadata + 2.1.0 + + ${basedir}/target/filtered-resources/META-INF/plexus + + + + + generate-metadata + + + + org.apache.maven.plugins maven-plugin-plugin @@ -41,6 +56,12 @@ io.quarkus quarkus-bootstrap-core + + + io.quarkus + quarkus-bootstrap-gradle-resolver + + io.quarkus diff --git a/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/BootstrapWorkspaceProvider.java b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/BootstrapWorkspaceProvider.java new file mode 100644 index 00000000000000..dc431e42efb597 --- /dev/null +++ b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/BootstrapWorkspaceProvider.java @@ -0,0 +1,36 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalWorkspace; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.codehaus.plexus.component.annotations.Component; + +@Component(role = BootstrapWorkspaceProvider.class, instantiationStrategy = "singleton") +public class BootstrapWorkspaceProvider { + + private final Path base; + private boolean loaded; + private LocalProject origin; + + public BootstrapWorkspaceProvider() { + // load the workspace lazily on request, in case the component is injected but the logic using it is skipped + base = Paths.get("").normalize().toAbsolutePath(); + } + + public LocalProject origin() { + if (!loaded) { + try { + origin = LocalProject.loadWorkspace(base); + } catch (BootstrapMavenException e) { + } + loaded = true; + } + return origin; + } + + public LocalWorkspace workspace() { + return origin().getWorkspace(); + } +} diff --git a/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java index 65cb1d31e47c25..34cc1ea03b25a2 100644 --- a/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java +++ b/independent-projects/bootstrap/maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java @@ -14,6 +14,9 @@ import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.AppModel; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -24,12 +27,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -48,6 +53,7 @@ import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.DependencyVisitor; +import org.eclipse.aether.impl.RemoteRepositoryManager; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactDescriptorException; import org.eclipse.aether.resolution.ArtifactDescriptorRequest; @@ -80,6 +86,12 @@ public class ExtensionDescriptorMojo extends AbstractMojo { @Component private RepositorySystem repoSystem; + @Component + RemoteRepositoryManager remoteRepoManager; + + @Component + BootstrapWorkspaceProvider workpaceProvider; + /** * The current repository/network configuration of Maven. * @@ -158,6 +170,8 @@ public class ExtensionDescriptorMojo extends AbstractMojo { AppArtifactCoords deploymentCoords; CollectResult collectedDeploymentDeps; + MavenArtifactResolver resolver; + @Override public void execute() throws MojoExecutionException { @@ -311,14 +325,13 @@ private void setBuiltWithQuarkusCoreVersion(ObjectMapper mapper, ObjectNode extO private void validateExtensionDeps() throws MojoExecutionException { final AppArtifactKey rootDeploymentGact = getDeploymentCoords().getKey(); - final Node rootDeployment = new Node(null, rootDeploymentGact, 2); + final RootNode rootDeployment = new RootNode(rootDeploymentGact, 2); final Artifact artifact = project.getArtifact(); final Node rootRuntime = rootDeployment.newChild(new AppArtifactKey(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier(), artifact.getType()), 1); - final Map expectedExtensionDeps = new HashMap<>(); - expectedExtensionDeps.put(rootDeploymentGact, rootDeployment); - expectedExtensionDeps.put(rootRuntime.gact, rootRuntime); + rootDeployment.expectedDeploymentNodes.put(rootDeployment.gact, rootDeployment); + rootDeployment.expectedDeploymentNodes.put(rootRuntime.gact, rootRuntime); // collect transitive extension deps final DependencyResult resolvedDeps; @@ -334,92 +347,231 @@ private void validateExtensionDeps() throws MojoExecutionException { throw new MojoExecutionException("Failed to resolve dependencies of " + project.getArtifact(), e); } - final AtomicInteger extDepsTotal = new AtomicInteger(2); - resolvedDeps.getRoot().accept(new DependencyVisitor() { - Node currentNode = rootDeployment; - int currentNodeId = rootDeployment.id; - - @Override - public boolean visitEnter(DependencyNode node) { - ++currentNodeId; - org.eclipse.aether.artifact.Artifact a = node.getArtifact(); - final File f = a.getFile(); - // if it hasn't been packaged yet, we skip it, we are not packaging yet - if (isAnalyzable(f)) { - try (FileSystem fs = FileSystems.newFileSystem(f.toPath(), (ClassLoader) null)) { - final Path extDescr = fs.getPath(BootstrapConstants.DESCRIPTOR_PATH); - if (Files.exists(extDescr)) { - final Properties props = new Properties(); - try (BufferedReader reader = Files.newBufferedReader(extDescr)) { - props.load(reader); - } - final String deploymentStr = props.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); - if (deploymentStr == null) { - throw new IllegalStateException("Quarkus extension runtime artifact " + a + " is missing " - + BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT + " property in its " - + BootstrapConstants.DESCRIPTOR_PATH); - } - currentNode = currentNode.newChild(AppArtifactCoords.fromString(deploymentStr).getKey(), - currentNodeId); - expectedExtensionDeps.put(currentNode.gact, currentNode); - extDepsTotal.incrementAndGet(); - } - } catch (Throwable e) { - throw new IllegalStateException("Failed to read " + f, e); - } + for (DependencyNode node : resolvedDeps.getRoot().getChildren()) { + rootDeployment.directRuntimeDeps.add(toKey(node.getArtifact())); + } + visitRuntimeDeps(rootDeployment, rootDeployment, rootDeployment.id, resolvedDeps.getRoot()); + + final DependencyNode deploymentNode = collectDeploymentDeps().getRoot(); + visitDeploymentDeps(rootDeployment, deploymentNode); + + if (rootDeployment.hasErrors()) { + final Log log = getLog(); + log.error("Quarkus Extension Dependency Verification Error"); + + final StringBuilder buf = new StringBuilder(); + + if (rootDeployment.deploymentDepsTotal != 0) { + log.error("Deployment artifact " + getDeploymentCoords() + + " was found to be missing dependencies on the Quarkus extension artifacts marked with '-' below:"); + final List missing = rootDeployment.collectMissingDeploymentDeps(log); + buf.append("Deployment artifact "); + buf.append(getDeploymentCoords()); + buf.append(" is missing the following dependencies from its configuration: "); + final Iterator i = missing.iterator(); + buf.append(i.next()); + while (i.hasNext()) { + buf.append(", ").append(i.next()); } - return true; } - @Override - public boolean visitLeave(DependencyNode node) { - if (currentNodeId == currentNode.id && currentNode.parent != null) { - currentNode = currentNode.parent; + if (!rootDeployment.deploymentsOnRtCp.isEmpty()) { + if (rootDeployment.runtimeCp > 0) { + log.error("The following deployment artifact(s) appear on the runtime classpath: "); + rootDeployment.collectDeploymentsOnRtCp(log); + } + if (buf.length() > 0) { + buf.append(System.lineSeparator()); + } + buf.append("The following deployment artifact(s) appear on the runtime classpath: "); + final Iterator i = rootDeployment.deploymentsOnRtCp.iterator(); + buf.append(i.next()); + while (i.hasNext()) { + buf.append(", ").append(i.next()); } - --currentNodeId; - return true; } - }); - - collectDeploymentDeps().getRoot().accept(new DependencyVisitor() { - @Override - public boolean visitEnter(DependencyNode dep) { - org.eclipse.aether.artifact.Artifact artifact = dep.getArtifact(); - if (artifact == null) { - return true; + + if (!rootDeployment.unexpectedDeploymentDeps.isEmpty()) { + final List unexpectedRtDeps = new ArrayList<>(0); + final List unexpectedDeploymentDeps = new ArrayList<>(0); + for (Map.Entry e : rootDeployment.unexpectedDeploymentDeps + .entrySet()) { + if (rootDeployment.allDeploymentDeps.contains(e.getKey())) { + unexpectedDeploymentDeps.add(e.getKey()); + } else { + unexpectedRtDeps.add(toKey(e.getValue())); + } + } + + if (!unexpectedRtDeps.isEmpty()) { + if (buf.length() > 0) { + buf.append(System.lineSeparator()); + } + buf.append("The deployment artifact " + rootDeploymentGact + + " depends on the following Quarkus extension runtime artifacts that weren't found among the dependencies of " + + project.getArtifact() + ":"); + for (AppArtifactKey a : unexpectedRtDeps) { + buf.append(' ').append(a); + } + log.error("The deployment artifact " + rootDeploymentGact + + " depends on the following Quarkus extension runtime artifacts that weren't found among the dependencies of " + + project.getArtifact() + ":"); + highlightInTree(deploymentNode, unexpectedRtDeps); } - final Node node = expectedExtensionDeps.get(new AppArtifactKey(artifact.getGroupId(), artifact.getArtifactId(), - artifact.getClassifier(), artifact.getExtension())); - if (node != null && !node.included) { - node.included = true; - extDepsTotal.decrementAndGet(); + + if (!unexpectedDeploymentDeps.isEmpty()) { + if (buf.length() > 0) { + buf.append(System.lineSeparator()); + } + buf.append("The deployment artifact " + rootDeploymentGact + + " depends on the following Quarkus extension deployment artifacts whose corresponding runtime artifacts were not found among the dependencies of " + + project.getArtifact() + ":"); + for (AppArtifactKey a : unexpectedDeploymentDeps) { + buf.append(' ').append(a); + } + log.error("The deployment artifact " + rootDeploymentGact + + " depends on the following Quarkus extension deployment artifacts whose corresponding runtime artifacts were not found among the dependencies of " + + project.getArtifact() + ":"); + highlightInTree(deploymentNode, unexpectedDeploymentDeps); } - return true; } - @Override - public boolean visitLeave(DependencyNode node) { - return true; + throw new MojoExecutionException(buf.toString()); + } + + } + + private void highlightInTree(DependencyNode node, Collection keys) { + highlightInTree(0, node, keys, new HashSet<>(), new StringBuilder(), new ArrayList<>()); + } + + private void highlightInTree(int depth, DependencyNode node, Collection keysToHighlight, + Set visited, StringBuilder buf, List branch) { + final AppArtifactKey key = toKey(node.getArtifact()); + if (!visited.add(key)) { + return; + } + buf.setLength(0); + final boolean highlighted = keysToHighlight.contains(key); + if (highlighted) { + buf.append('*'); + } else { + buf.append(' '); + } + for (int i = 0; i < depth; ++i) { + buf.append(" "); + } + buf.append(node.getArtifact()); + branch.add(buf.toString()); + if (!highlighted) { + for (DependencyNode child : node.getChildren()) { + highlightInTree(depth + 1, child, keysToHighlight, visited, buf, branch); } - }); + } else { + for (String line : branch) { + getLog().error(line); + } + } + branch.remove(branch.size() - 1); + } - if (extDepsTotal.intValue() != 0) { - final Log log = getLog(); - log.error("Quarkus Extension Dependency Verification Error"); - log.error("Deployment artifact " + getDeploymentCoords() + - " was found to be missing dependencies on Quarkus extension artifacts marked with '-' below:"); - final List missing = rootDeployment.collectMissing(log); - final StringBuilder buf = new StringBuilder(); - buf.append("Deployment artifact "); - buf.append(getDeploymentCoords()); - buf.append(" is missing the following dependencies from its configuration: "); - final Iterator i = missing.iterator(); - buf.append(i.next()); - while (i.hasNext()) { - buf.append(", ").append(i.next()); + private void visitDeploymentDeps(RootNode rootDeployment, DependencyNode dep) throws MojoExecutionException { + for (DependencyNode child : dep.getChildren()) { + visitDeploymentDep(rootDeployment, child); + } + } + + private void visitDeploymentDep(RootNode rootDeployment, DependencyNode dep) throws MojoExecutionException { + org.eclipse.aether.artifact.Artifact artifact = dep.getArtifact(); + if (artifact == null) { + return; + } + final AppArtifactKey key = toKey(artifact); + if (!rootDeployment.allDeploymentDeps.add(key)) { + return; + } + final Node node = rootDeployment.expectedDeploymentNodes.get(key); + if (node != null && !node.present) { + node.present = true; + --rootDeployment.deploymentDepsTotal; + } else if (!rootDeployment.allRtDeps.contains(key)) { + final AppArtifactKey deployment = getDeploymentKey(artifact); + if (deployment != null) { + rootDeployment.unexpectedDeploymentDeps.put(deployment, artifact); + } + } + visitDeploymentDeps(rootDeployment, dep); + } + + private void visitRuntimeDep(RootNode root, Node currentNode, int currentId, + DependencyNode node) throws MojoExecutionException { + final org.eclipse.aether.artifact.Artifact a = node.getArtifact(); + root.allRtDeps.add(toKey(a)); + final AppArtifactKey deployment = getDeploymentKey(a); + if (deployment != null) { + currentNode = currentNode.newChild(deployment, ++currentId); + root.expectedDeploymentNodes.put(currentNode.gact, currentNode); + ++root.deploymentDepsTotal; + if (root.allRtDeps.contains(deployment)) { + root.deploymentsOnRtCp.add(deployment); + if (root.directRuntimeDeps.contains(deployment)) { + currentNode.runtimeCp = 2; // actual rt dep + Node n = currentNode.parent; + while (n != null) { + if (n.runtimeCp != 0) { + break; + } else { + n.runtimeCp = 1; // path to the actual rt dep + } + n = n.parent; + } + } + } + } + visitRuntimeDeps(root, currentNode, currentId, node); + } + + private void visitRuntimeDeps(RootNode root, Node currentNode, int currentId, DependencyNode node) + throws MojoExecutionException { + for (DependencyNode child : node.getChildren()) { + visitRuntimeDep(root, currentNode, currentId, child); + } + } + + private AppArtifactKey getDeploymentKey(org.eclipse.aether.artifact.Artifact a) throws MojoExecutionException { + final File f; + try { + f = resolve(a); + } catch (Exception e) { + getLog().warn("Failed to resolve " + a); + return null; + } + // if it hasn't been packaged yet, we skip it, we are not packaging yet + if (isAnalyzable(f)) { + try (FileSystem fs = FileSystems.newFileSystem(f.toPath(), (ClassLoader) null)) { + final Path extDescr = fs.getPath(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(extDescr)) { + final Properties props = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(extDescr)) { + props.load(reader); + } + final String deploymentStr = props.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + if (deploymentStr == null) { + throw new IllegalStateException("Quarkus extension runtime artifact " + a + " is missing " + + BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT + " property in its " + + BootstrapConstants.DESCRIPTOR_PATH); + } + return AppArtifactCoords.fromString(deploymentStr).getKey(); + } + } catch (Throwable e) { + throw new IllegalStateException("Failed to read " + f, e); } - throw new MojoExecutionException(buf.toString()); } + return null; + } + + private static AppArtifactKey toKey(org.eclipse.aether.artifact.Artifact a) { + return new AppArtifactKey(a.getGroupId(), a.getArtifactId(), a.getClassifier(), a.getExtension()); } private CollectResult collectDeploymentDeps() throws MojoExecutionException { @@ -567,11 +719,32 @@ public boolean visitLeave(DependencyNode node) { } } + private static class RootNode extends Node { + + final Map expectedDeploymentNodes = new HashMap<>(); + final Set directRuntimeDeps = new HashSet<>(); + final Set allRtDeps = new HashSet<>(); + final Set allDeploymentDeps = new HashSet<>(); + final Map unexpectedDeploymentDeps = new HashMap<>(0); + + int deploymentDepsTotal = 1; + List deploymentsOnRtCp = new ArrayList<>(0); + + RootNode(AppArtifactKey gact, int id) { + super(null, gact, id); + } + + boolean hasErrors() { + return deploymentDepsTotal != 0 || runtimeCp != 0 || !unexpectedDeploymentDeps.isEmpty(); + } + } + private static class Node { final Node parent; final AppArtifactKey gact; final int id; - boolean included; + boolean present; + int runtimeCp; List children = new ArrayList<>(0); Node(Node parent, AppArtifactKey gact, int id) { @@ -586,29 +759,89 @@ Node newChild(AppArtifactKey gact, int id) { return child; } - List collectMissing(Log log) { + List collectMissingDeploymentDeps(Log log) { final List missing = new ArrayList<>(); - collectMissing(log, 0, missing); + handleChildren(log, 0, missing, (log1, depth, n, collected) -> { + final StringBuilder buf = new StringBuilder(); + if (n.present) { + buf.append('+'); + } else { + buf.append('-'); + collected.add(n.gact); + } + buf.append(' '); + for (int i = 0; i < depth; ++i) { + buf.append(" "); + } + buf.append(n.gact); + log1.error(buf.toString()); + }); return missing; } - private void collectMissing(Log log, int depth, List missing) { - final StringBuilder buf = new StringBuilder(); - if (included) { - buf.append('+'); - } else { - buf.append('-'); - missing.add(gact); - } - buf.append(' '); - for (int i = 0; i < depth; ++i) { - buf.append(" "); - } - buf.append(gact); - log.error(buf.toString()); + List collectDeploymentsOnRtCp(Log log) { + final List missing = new ArrayList<>(); + handleChildren(log, 0, missing, (log1, depth, n, collected) -> { + if (n.runtimeCp == 0) { + return; + } + final StringBuilder buf = new StringBuilder(); + if (n.runtimeCp == 1) { + buf.append(' '); + } else { + buf.append('*'); + collected.add(n.gact); + } + buf.append(' '); + for (int i = 0; i < depth; ++i) { + buf.append(" "); + } + buf.append(n.gact); + log1.error(buf.toString()); + }); + return missing; + } + + private void handle(Log log, int depth, List collected, NodeHandler handler) { + handler.handle(log, depth, this, collected); + handleChildren(log, depth, collected, handler); + } + + private void handleChildren(Log log, int depth, List collected, NodeHandler handler) { for (Node child : children) { - child.collectMissing(log, depth + 1, missing); + child.handle(log, depth + 1, collected, handler); } } } + + private static interface NodeHandler { + void handle(Log log, int depth, Node n, List collected); + } + + private MavenArtifactResolver resolver() throws MojoExecutionException { + if (resolver == null) { + try { + final BootstrapMavenContext ctx = new BootstrapMavenContext(BootstrapMavenContext.config() + .setRepositorySystem(repoSystem) + .setRemoteRepositoryManager(remoteRepoManager) + .setRepositorySystemSession(repoSession) + .setRemoteRepositories(repos) + .setCurrentProject(workpaceProvider.origin())); + resolver = new MavenArtifactResolver(ctx); + } catch (BootstrapMavenException e) { + throw new MojoExecutionException("Failed to initialize Maven artifact resolver", e); + } + } + return resolver; + } + + private File resolve(org.eclipse.aether.artifact.Artifact a) throws MojoExecutionException { + try { + return resolver().resolve(a).getArtifact().getFile(); + } catch (MojoExecutionException e) { + throw e; + } catch (Exception e) { + throw new MojoExecutionException("Failed to resolve " + a, e); + } + } }