From e4816809d4289d0eb358462c9bfed5c2d93d0f87 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 7 Jan 2021 15:11:22 +0100 Subject: [PATCH] Speed up parallel plugin setup #310 --- .../mvnd/plugin/CliMavenPluginManager.java | 779 ++++++++++++++++++ .../mvnd/plugin/CliPluginDescriptorCache.java | 240 ++++++ .../mvnd/plugin/CliPluginRealmCache.java | 78 +- .../plugin/CliPluginRealmCacheEventSpy.java | 6 +- .../ValidatingConfigurationListener.java | 88 ++ 5 files changed, 1171 insertions(+), 20 deletions(-) create mode 100644 daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliMavenPluginManager.java create mode 100644 daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginDescriptorCache.java create mode 100644 daemon/src/main/java/org/mvndaemon/mvnd/plugin/ValidatingConfigurationListener.java diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliMavenPluginManager.java b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliMavenPluginManager.java new file mode 100644 index 000000000..e6253806e --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliMavenPluginManager.java @@ -0,0 +1,779 @@ +/* + * 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.mvndaemon.mvnd.plugin; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.classrealm.ClassRealmManager; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.execution.scope.internal.MojoExecutionScopeModule; +import org.apache.maven.model.Plugin; +import org.apache.maven.monitor.logging.DefaultLog; +import org.apache.maven.plugin.ContextEnabled; +import org.apache.maven.plugin.DebugConfigurationListener; +import org.apache.maven.plugin.ExtensionRealmCache; +import org.apache.maven.plugin.InvalidPluginDescriptorException; +import org.apache.maven.plugin.MavenPluginManager; +import org.apache.maven.plugin.MavenPluginValidator; +import org.apache.maven.plugin.Mojo; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoNotFoundException; +import org.apache.maven.plugin.PluginArtifactsCache; +import org.apache.maven.plugin.PluginConfigurationException; +import org.apache.maven.plugin.PluginContainerException; +import org.apache.maven.plugin.PluginDescriptorCache; +import org.apache.maven.plugin.PluginDescriptorParsingException; +import org.apache.maven.plugin.PluginIncompatibleException; +import org.apache.maven.plugin.PluginManagerException; +import org.apache.maven.plugin.PluginParameterException; +import org.apache.maven.plugin.PluginParameterExpressionEvaluator; +import org.apache.maven.plugin.PluginRealmCache; +import org.apache.maven.plugin.PluginResolutionException; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.plugin.descriptor.Parameter; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder; +import org.apache.maven.plugin.internal.PluginDependenciesResolver; +import org.apache.maven.plugin.version.DefaultPluginVersionRequest; +import org.apache.maven.plugin.version.PluginVersionRequest; +import org.apache.maven.plugin.version.PluginVersionResolutionException; +import org.apache.maven.plugin.version.PluginVersionResolver; +import org.apache.maven.project.ExtensionDescriptor; +import org.apache.maven.project.ExtensionDescriptorBuilder; +import org.apache.maven.project.MavenProject; +import org.apache.maven.rtinfo.RuntimeInformation; +import org.apache.maven.session.scope.internal.SessionScopeModule; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.classworlds.realm.ClassRealm; +import org.codehaus.plexus.component.composition.CycleDetectedInComponentGraphException; +import org.codehaus.plexus.component.configurator.ComponentConfigurationException; +import org.codehaus.plexus.component.configurator.ComponentConfigurator; +import org.codehaus.plexus.component.configurator.ConfigurationListener; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator; +import org.codehaus.plexus.component.repository.ComponentDescriptor; +import org.codehaus.plexus.component.repository.exception.ComponentLifecycleException; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.codehaus.plexus.configuration.PlexusConfiguration; +import org.codehaus.plexus.configuration.PlexusConfigurationException; +import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration; +import org.codehaus.plexus.logging.Logger; +import org.codehaus.plexus.logging.LoggerManager; +import org.codehaus.plexus.util.ReaderFactory; +import org.codehaus.plexus.util.StringUtils; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.graph.DependencyFilter; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.filter.AndDependencyFilter; +import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; +import org.eclipse.sisu.Priority; +import org.eclipse.sisu.Typed; + +/* + * gnodet: This file is based on maven DefaultMavenPluginManager and changed in order + * to better support parallel builds. See https://github.com/mvndaemon/mvnd/issues/310 + */ +/** + * Provides basic services to manage Maven plugins and their mojos. This component is kept general in its design such + * that the plugins/mojos can be used in arbitrary contexts. In particular, the mojos can be used for ordinary build + * plugins as well as special purpose plugins like reports. + * + * @author Benjamin Bentmann + * @since 3.0 + */ +@Singleton +@Named +@Priority(10) +@Typed(MavenPluginManager.class) +public class CliMavenPluginManager + implements MavenPluginManager { + + /** + *

+ * PluginId => ExtensionRealmCache.CacheRecord map MavenProject context value key. The map is used to ensure the + * same class realm is used to load build extensions and load mojos for extensions=true plugins. + *

+ * Note: This is part of internal implementation and may be changed or removed without notice + * + * @since 3.3.0 + */ + public static final String KEY_EXTENSIONS_REALMS = CliMavenPluginManager.class.getName() + "/extensionsRealms"; + + @Inject + private Logger logger; + + @Inject + private LoggerManager loggerManager; + + @Inject + private PlexusContainer container; + + @Inject + private ClassRealmManager classRealmManager; + + @Inject + private CliPluginDescriptorCache pluginDescriptorCache; + + @Inject + private CliPluginRealmCache pluginRealmCache; + + @Inject + private PluginDependenciesResolver pluginDependenciesResolver; + + @Inject + private RuntimeInformation runtimeInformation; + + @Inject + private ExtensionRealmCache extensionRealmCache; + + @Inject + private PluginVersionResolver pluginVersionResolver; + + @Inject + private PluginArtifactsCache pluginArtifactsCache; + + private ExtensionDescriptorBuilder extensionDescriptorBuilder = new ExtensionDescriptorBuilder(); + + private PluginDescriptorBuilder builder = new PluginDescriptorBuilder(); + + public PluginDescriptor getPluginDescriptor(Plugin plugin, List repositories, + RepositorySystemSession session) + throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException { + PluginDescriptorCache.Key cacheKey = pluginDescriptorCache.createKey(plugin, repositories, session); + + PluginDescriptor pluginDescriptor = pluginDescriptorCache.get(cacheKey, () -> { + org.eclipse.aether.artifact.Artifact artifact = pluginDependenciesResolver.resolve(plugin, repositories, session); + + Artifact pluginArtifact = RepositoryUtils.toArtifact(artifact); + + PluginDescriptor descriptor = extractPluginDescriptor(pluginArtifact, plugin); + + descriptor.setRequiredMavenVersion(artifact.getProperty("requiredMavenVersion", null)); + + return descriptor; + }); + + pluginDescriptor.setPlugin(plugin); + + return pluginDescriptor; + } + + private PluginDescriptor extractPluginDescriptor(Artifact pluginArtifact, Plugin plugin) + throws PluginDescriptorParsingException, InvalidPluginDescriptorException { + PluginDescriptor pluginDescriptor = null; + + File pluginFile = pluginArtifact.getFile(); + + try { + if (pluginFile.isFile()) { + try (JarFile pluginJar = new JarFile(pluginFile, false)) { + ZipEntry pluginDescriptorEntry = pluginJar.getEntry(getPluginDescriptorLocation()); + + if (pluginDescriptorEntry != null) { + InputStream is = pluginJar.getInputStream(pluginDescriptorEntry); + + pluginDescriptor = parsePluginDescriptor(is, plugin, pluginFile.getAbsolutePath()); + } + } + } else { + File pluginXml = new File(pluginFile, getPluginDescriptorLocation()); + + if (pluginXml.isFile()) { + try (InputStream is = new BufferedInputStream(new FileInputStream(pluginXml))) { + pluginDescriptor = parsePluginDescriptor(is, plugin, pluginXml.getAbsolutePath()); + } + } + } + + if (pluginDescriptor == null) { + throw new IOException("No plugin descriptor found at " + getPluginDescriptorLocation()); + } + } catch (IOException e) { + throw new PluginDescriptorParsingException(plugin, pluginFile.getAbsolutePath(), e); + } + + MavenPluginValidator validator = new MavenPluginValidator(pluginArtifact); + + validator.validate(pluginDescriptor); + + if (validator.hasErrors()) { + throw new InvalidPluginDescriptorException( + "Invalid plugin descriptor for " + plugin.getId() + " (" + pluginFile + ")", validator.getErrors()); + } + + pluginDescriptor.setPluginArtifact(pluginArtifact); + + return pluginDescriptor; + } + + private String getPluginDescriptorLocation() { + return "META-INF/maven/plugin.xml"; + } + + private PluginDescriptor parsePluginDescriptor(InputStream is, Plugin plugin, String descriptorLocation) + throws PluginDescriptorParsingException { + try { + Reader reader = ReaderFactory.newXmlReader(is); + + PluginDescriptor pluginDescriptor = builder.build(reader, descriptorLocation); + + return pluginDescriptor; + } catch (IOException | PlexusConfigurationException e) { + throw new PluginDescriptorParsingException(plugin, descriptorLocation, e); + } + } + + public MojoDescriptor getMojoDescriptor(Plugin plugin, String goal, List repositories, + RepositorySystemSession session) + throws MojoNotFoundException, PluginResolutionException, PluginDescriptorParsingException, + InvalidPluginDescriptorException { + PluginDescriptor pluginDescriptor = getPluginDescriptor(plugin, repositories, session); + + MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo(goal); + + if (mojoDescriptor == null) { + throw new MojoNotFoundException(goal, pluginDescriptor); + } + + return mojoDescriptor; + } + + public void checkRequiredMavenVersion(PluginDescriptor pluginDescriptor) + throws PluginIncompatibleException { + String requiredMavenVersion = pluginDescriptor.getRequiredMavenVersion(); + if (StringUtils.isNotBlank(requiredMavenVersion)) { + try { + if (!runtimeInformation.isMavenVersion(requiredMavenVersion)) { + throw new PluginIncompatibleException(pluginDescriptor.getPlugin(), + "The plugin " + pluginDescriptor.getId() + + " requires Maven version " + requiredMavenVersion); + } + } catch (RuntimeException e) { + logger.warn("Could not verify plugin's Maven prerequisite: " + e.getMessage()); + } + } + } + + public void setupPluginRealm(PluginDescriptor pluginDescriptor, MavenSession session, + ClassLoader parent, List imports, DependencyFilter filter) + throws PluginResolutionException, PluginContainerException { + Plugin plugin = pluginDescriptor.getPlugin(); + MavenProject project = session.getCurrentProject(); + + if (plugin.isExtensions()) { + ExtensionRealmCache.CacheRecord extensionRecord; + try { + RepositorySystemSession repositorySession = session.getRepositorySession(); + extensionRecord = setupExtensionsRealm(project, plugin, repositorySession); + } catch (PluginManagerException e) { + // extensions realm is expected to be fully setup at this point + // any exception means a problem in maven code, not a user error + throw new IllegalStateException(e); + } + + ClassRealm pluginRealm = extensionRecord.getRealm(); + List pluginArtifacts = extensionRecord.getArtifacts(); + + for (ComponentDescriptor componentDescriptor : pluginDescriptor.getComponents()) { + componentDescriptor.setRealm(pluginRealm); + } + + pluginDescriptor.setClassRealm(pluginRealm); + pluginDescriptor.setArtifacts(pluginArtifacts); + } else { + Map foreignImports = calcImports(project, parent, imports); + + PluginRealmCache.Key cacheKey = pluginRealmCache.createKey(plugin, parent, foreignImports, filter, + project.getRemotePluginRepositories(), + session.getRepositorySession()); + + PluginRealmCache.CacheRecord cacheRecord = pluginRealmCache.get(cacheKey, () -> { + createPluginRealm(pluginDescriptor, session, parent, foreignImports, filter); + return new PluginRealmCache.CacheRecord(pluginDescriptor.getClassRealm(), pluginDescriptor.getArtifacts()); + }); + + if (cacheRecord != null) { + pluginDescriptor.setClassRealm(cacheRecord.getRealm()); + pluginDescriptor.setArtifacts(new ArrayList<>(cacheRecord.getArtifacts())); + for (ComponentDescriptor componentDescriptor : pluginDescriptor.getComponents()) { + componentDescriptor.setRealm(cacheRecord.getRealm()); + } + } + + pluginRealmCache.register(project, cacheKey, cacheRecord); + } + } + + private void createPluginRealm(PluginDescriptor pluginDescriptor, MavenSession session, ClassLoader parent, + Map foreignImports, DependencyFilter filter) + throws PluginResolutionException, PluginContainerException { + Plugin plugin = Objects.requireNonNull(pluginDescriptor.getPlugin(), "pluginDescriptor.plugin cannot be null"); + + Artifact pluginArtifact = Objects.requireNonNull(pluginDescriptor.getPluginArtifact(), + "pluginDescriptor.pluginArtifact cannot be null"); + + MavenProject project = session.getCurrentProject(); + + final ClassRealm pluginRealm; + final List pluginArtifacts; + + RepositorySystemSession repositorySession = session.getRepositorySession(); + DependencyFilter dependencyFilter = project.getExtensionDependencyFilter(); + dependencyFilter = AndDependencyFilter.newInstance(dependencyFilter, filter); + + DependencyNode root = pluginDependenciesResolver.resolve(plugin, RepositoryUtils.toArtifact(pluginArtifact), + dependencyFilter, + project.getRemotePluginRepositories(), repositorySession); + + PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); + root.accept(nlg); + + pluginArtifacts = toMavenArtifacts(root, nlg); + + pluginRealm = classRealmManager.createPluginRealm(plugin, parent, null, foreignImports, + toAetherArtifacts(pluginArtifacts)); + + discoverPluginComponents(pluginRealm, plugin, pluginDescriptor); + + pluginDescriptor.setClassRealm(pluginRealm); + pluginDescriptor.setArtifacts(pluginArtifacts); + } + + private void discoverPluginComponents(final ClassRealm pluginRealm, Plugin plugin, + PluginDescriptor pluginDescriptor) + throws PluginContainerException { + try { + if (pluginDescriptor != null) { + for (ComponentDescriptor componentDescriptor : pluginDescriptor.getComponents()) { + componentDescriptor.setRealm(pluginRealm); + container.addComponentDescriptor(componentDescriptor); + } + } + + ((DefaultPlexusContainer) container).discoverComponents(pluginRealm, new SessionScopeModule(container), + new MojoExecutionScopeModule(container)); + } catch (ComponentLookupException | CycleDetectedInComponentGraphException e) { + throw new PluginContainerException(plugin, pluginRealm, + "Error in component graph of plugin " + plugin.getId() + ": " + + e.getMessage(), + e); + } + } + + private List toAetherArtifacts(final List pluginArtifacts) { + return new ArrayList<>(RepositoryUtils.toArtifacts(pluginArtifacts)); + } + + private List toMavenArtifacts(DependencyNode root, PreorderNodeListGenerator nlg) { + List artifacts = new ArrayList<>(nlg.getNodes().size()); + RepositoryUtils.toArtifacts(artifacts, Collections.singleton(root), Collections. emptyList(), null); + for (Iterator it = artifacts.iterator(); it.hasNext();) { + Artifact artifact = it.next(); + if (artifact.getFile() == null) { + it.remove(); + } + } + return Collections.unmodifiableList(artifacts); + } + + private Map calcImports(MavenProject project, ClassLoader parent, List imports) { + Map foreignImports = new HashMap<>(); + + ClassLoader projectRealm = project.getClassRealm(); + if (projectRealm != null) { + foreignImports.put("", projectRealm); + } else { + foreignImports.put("", classRealmManager.getMavenApiRealm()); + } + + if (parent != null && imports != null) { + for (String parentImport : imports) { + foreignImports.put(parentImport, parent); + } + } + + return foreignImports; + } + + public T getConfiguredMojo(Class mojoInterface, MavenSession session, MojoExecution mojoExecution) + throws PluginConfigurationException, PluginContainerException { + MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor(); + + PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor(); + + ClassRealm pluginRealm = pluginDescriptor.getClassRealm(); + + if (logger.isDebugEnabled()) { + logger.debug("Configuring mojo " + mojoDescriptor.getId() + " from plugin realm " + pluginRealm); + } + + // We are forcing the use of the plugin realm for all lookups that might occur during + // the lifecycle that is part of the lookup. Here we are specifically trying to keep + // lookups that occur in contextualize calls in line with the right realm. + ClassRealm oldLookupRealm = container.setLookupRealm(pluginRealm); + + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(pluginRealm); + + try { + T mojo; + + try { + mojo = container.lookup(mojoInterface, mojoDescriptor.getRoleHint()); + } catch (ComponentLookupException e) { + Throwable cause = e.getCause(); + while (cause != null && !(cause instanceof LinkageError) + && !(cause instanceof ClassNotFoundException)) { + cause = cause.getCause(); + } + + if ((cause instanceof NoClassDefFoundError) || (cause instanceof ClassNotFoundException)) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" + + pluginDescriptor.getId() + "'. A required class is missing: " + + cause.getMessage()); + pluginRealm.display(ps); + + throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); + } else if (cause instanceof LinkageError) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println("Unable to load the mojo '" + mojoDescriptor.getGoal() + "' in the plugin '" + + pluginDescriptor.getId() + "' due to an API incompatibility: " + + e.getClass().getName() + ": " + cause.getMessage()); + pluginRealm.display(ps); + + throw new PluginContainerException(mojoDescriptor, pluginRealm, os.toString(), cause); + } + + throw new PluginContainerException(mojoDescriptor, pluginRealm, + "Unable to load the mojo '" + mojoDescriptor.getGoal() + + "' (or one of its required components) from the plugin '" + + pluginDescriptor.getId() + "'", + e); + } + + if (mojo instanceof ContextEnabled) { + MavenProject project = session.getCurrentProject(); + + Map pluginContext = session.getPluginContext(pluginDescriptor, project); + + if (pluginContext != null) { + pluginContext.put("project", project); + + pluginContext.put("pluginDescriptor", pluginDescriptor); + + ((ContextEnabled) mojo).setPluginContext(pluginContext); + } + } + + if (mojo instanceof Mojo) { + Logger mojoLogger = loggerManager.getLoggerForComponent(mojoDescriptor.getImplementation()); + ((Mojo) mojo).setLog(new DefaultLog(mojoLogger)); + } + + Xpp3Dom dom = mojoExecution.getConfiguration(); + + PlexusConfiguration pomConfiguration; + + if (dom == null) { + pomConfiguration = new XmlPlexusConfiguration("configuration"); + } else { + pomConfiguration = new XmlPlexusConfiguration(dom); + } + + ExpressionEvaluator expressionEvaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); + + populatePluginFields(mojo, mojoDescriptor, pluginRealm, pomConfiguration, expressionEvaluator); + + return mojo; + } finally { + Thread.currentThread().setContextClassLoader(oldClassLoader); + container.setLookupRealm(oldLookupRealm); + } + } + + private void populatePluginFields(Object mojo, MojoDescriptor mojoDescriptor, ClassRealm pluginRealm, + PlexusConfiguration configuration, ExpressionEvaluator expressionEvaluator) + throws PluginConfigurationException { + ComponentConfigurator configurator = null; + + String configuratorId = mojoDescriptor.getComponentConfigurator(); + + if (StringUtils.isEmpty(configuratorId)) { + configuratorId = "basic"; + } + + try { + // TODO could the configuration be passed to lookup and the configurator known to plexus via the descriptor + // so that this method could entirely be handled by a plexus lookup? + configurator = container.lookup(ComponentConfigurator.class, configuratorId); + + ConfigurationListener listener = new DebugConfigurationListener(logger); + + ValidatingConfigurationListener validator = new ValidatingConfigurationListener(mojo, mojoDescriptor, listener); + + logger.debug( + "Configuring mojo '" + mojoDescriptor.getId() + "' with " + configuratorId + " configurator -->"); + + configurator.configureComponent(mojo, configuration, expressionEvaluator, pluginRealm, validator); + + logger.debug("-- end configuration --"); + + Collection missingParameters = validator.getMissingParameters(); + if (!missingParameters.isEmpty()) { + if ("basic".equals(configuratorId)) { + throw new PluginParameterException(mojoDescriptor, new ArrayList<>(missingParameters)); + } else { + /* + * NOTE: Other configurators like the map-oriented one don't call into the listener, so do it the + * hard way. + */ + validateParameters(mojoDescriptor, configuration, expressionEvaluator); + } + } + } catch (ComponentConfigurationException e) { + String message = "Unable to parse configuration of mojo " + mojoDescriptor.getId(); + if (e.getFailedConfiguration() != null) { + message += " for parameter " + e.getFailedConfiguration().getName(); + } + message += ": " + e.getMessage(); + + throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), message, e); + } catch (ComponentLookupException e) { + throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), + "Unable to retrieve component configurator " + configuratorId + + " for configuration of mojo " + mojoDescriptor.getId(), + e); + } catch (NoClassDefFoundError e) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println("A required class was missing during configuration of mojo " + mojoDescriptor.getId() + ": " + + e.getMessage()); + pluginRealm.display(ps); + + throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), os.toString(), e); + } catch (LinkageError e) { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os); + ps.println( + "An API incompatibility was encountered during configuration of mojo " + mojoDescriptor.getId() + ": " + + e.getClass().getName() + ": " + e.getMessage()); + pluginRealm.display(ps); + + throw new PluginConfigurationException(mojoDescriptor.getPluginDescriptor(), os.toString(), e); + } finally { + if (configurator != null) { + try { + container.release(configurator); + } catch (ComponentLifecycleException e) { + logger.debug("Failed to release mojo configurator - ignoring."); + } + } + } + } + + private void validateParameters(MojoDescriptor mojoDescriptor, PlexusConfiguration configuration, + ExpressionEvaluator expressionEvaluator) + throws ComponentConfigurationException, PluginParameterException { + if (mojoDescriptor.getParameters() == null) { + return; + } + + List invalidParameters = new ArrayList<>(); + + for (Parameter parameter : mojoDescriptor.getParameters()) { + if (!parameter.isRequired()) { + continue; + } + + Object value = null; + + PlexusConfiguration config = configuration.getChild(parameter.getName(), false); + if (config != null) { + String expression = config.getValue(null); + + try { + value = expressionEvaluator.evaluate(expression); + + if (value == null) { + value = config.getAttribute("default-value", null); + } + } catch (ExpressionEvaluationException e) { + String msg = "Error evaluating the expression '" + expression + "' for configuration value '" + + configuration.getName() + "'"; + throw new ComponentConfigurationException(configuration, msg, e); + } + } + + if (value == null && (config == null || config.getChildCount() <= 0)) { + invalidParameters.add(parameter); + } + } + + if (!invalidParameters.isEmpty()) { + throw new PluginParameterException(mojoDescriptor, invalidParameters); + } + } + + public void releaseMojo(Object mojo, MojoExecution mojoExecution) { + if (mojo != null) { + try { + container.release(mojo); + } catch (ComponentLifecycleException e) { + String goalExecId = mojoExecution.getGoal(); + + if (mojoExecution.getExecutionId() != null) { + goalExecId += " {execution: " + mojoExecution.getExecutionId() + "}"; + } + + logger.debug("Error releasing mojo for " + goalExecId, e); + } + } + } + + public synchronized ExtensionRealmCache.CacheRecord setupExtensionsRealm(MavenProject project, Plugin plugin, + RepositorySystemSession session) + throws PluginManagerException { + @SuppressWarnings("unchecked") + Map pluginRealms = (Map) project + .getContextValue(KEY_EXTENSIONS_REALMS); + if (pluginRealms == null) { + pluginRealms = new HashMap<>(); + project.setContextValue(KEY_EXTENSIONS_REALMS, pluginRealms); + } + + final String pluginKey = plugin.getId(); + + ExtensionRealmCache.CacheRecord extensionRecord = pluginRealms.get(pluginKey); + if (extensionRecord != null) { + return extensionRecord; + } + + final List repositories = project.getRemotePluginRepositories(); + + // resolve plugin version as necessary + if (plugin.getVersion() == null) { + PluginVersionRequest versionRequest = new DefaultPluginVersionRequest(plugin, session, repositories); + try { + plugin.setVersion(pluginVersionResolver.resolve(versionRequest).getVersion()); + } catch (PluginVersionResolutionException e) { + throw new PluginManagerException(plugin, e.getMessage(), e); + } + } + + // resolve plugin artifacts + List artifacts; + PluginArtifactsCache.Key cacheKey = pluginArtifactsCache.createKey(plugin, null, repositories, session); + PluginArtifactsCache.CacheRecord recordArtifacts; + try { + recordArtifacts = pluginArtifactsCache.get(cacheKey); + } catch (PluginResolutionException e) { + throw new PluginManagerException(plugin, e.getMessage(), e); + } + if (recordArtifacts != null) { + artifacts = recordArtifacts.getArtifacts(); + } else { + try { + artifacts = resolveExtensionArtifacts(plugin, repositories, session); + recordArtifacts = pluginArtifactsCache.put(cacheKey, artifacts); + } catch (PluginResolutionException e) { + pluginArtifactsCache.put(cacheKey, e); + pluginArtifactsCache.register(project, cacheKey, recordArtifacts); + throw new PluginManagerException(plugin, e.getMessage(), e); + } + } + pluginArtifactsCache.register(project, cacheKey, recordArtifacts); + + // create and cache extensions realms + final ExtensionRealmCache.Key extensionKey = extensionRealmCache.createKey(artifacts); + extensionRecord = extensionRealmCache.get(extensionKey); + if (extensionRecord == null) { + ClassRealm extensionRealm = classRealmManager.createExtensionRealm(plugin, toAetherArtifacts(artifacts)); + + // TODO figure out how to use the same PluginDescriptor when running mojos + + PluginDescriptor pluginDescriptor = null; + if (plugin.isExtensions() && !artifacts.isEmpty()) { + // ignore plugin descriptor parsing errors at this point + // these errors will reported during calculation of project build execution plan + try { + pluginDescriptor = extractPluginDescriptor(artifacts.get(0), plugin); + } catch (PluginDescriptorParsingException | InvalidPluginDescriptorException e) { + // ignore, see above + } + } + + discoverPluginComponents(extensionRealm, plugin, pluginDescriptor); + + ExtensionDescriptor extensionDescriptor = null; + Artifact extensionArtifact = artifacts.get(0); + try { + extensionDescriptor = extensionDescriptorBuilder.build(extensionArtifact.getFile()); + } catch (IOException e) { + String message = "Invalid extension descriptor for " + plugin.getId() + ": " + e.getMessage(); + if (logger.isDebugEnabled()) { + logger.error(message, e); + } else { + logger.error(message); + } + } + extensionRecord = extensionRealmCache.put(extensionKey, extensionRealm, extensionDescriptor, artifacts); + } + extensionRealmCache.register(project, extensionKey, extensionRecord); + pluginRealms.put(pluginKey, extensionRecord); + + return extensionRecord; + } + + private List resolveExtensionArtifacts(Plugin extensionPlugin, List repositories, + RepositorySystemSession session) + throws PluginResolutionException { + DependencyNode root = pluginDependenciesResolver.resolve(extensionPlugin, null, null, repositories, session); + PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); + root.accept(nlg); + return toMavenArtifacts(root, nlg); + } + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginDescriptorCache.java b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginDescriptorCache.java new file mode 100644 index 000000000..01d65da6e --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginDescriptorCache.java @@ -0,0 +1,240 @@ +/* + * 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.mvndaemon.mvnd.plugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.ArtifactUtils; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.InvalidPluginDescriptorException; +import org.apache.maven.plugin.PluginDescriptorCache; +import org.apache.maven.plugin.PluginDescriptorParsingException; +import org.apache.maven.plugin.PluginResolutionException; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.codehaus.plexus.component.repository.ComponentDescriptor; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.WorkspaceRepository; +import org.eclipse.sisu.Priority; +import org.eclipse.sisu.Typed; + +/* + * gnodet: This file is based on maven DefaultPluginDescriptorCache and changed in order + * to better support parallel builds. See https://github.com/mvndaemon/mvnd/issues/310 + */ +/** + * Caches raw plugin descriptors. A raw plugin descriptor is a descriptor that has just been extracted from the plugin + * artifact and does not contain any runtime specific data. The cache must not be used for descriptors that hold runtime + * data like the plugin realm. Warning: This is an internal utility interface that is only public for + * technical reasons, it is not part of the public API. In particular, this interface can be changed or deleted without + * prior notice. + * + * @since 3.0 + * @author Benjamin Bentmann + */ +@Singleton +@Named +@Priority(10) +@Typed(CliPluginDescriptorCache.class) +public class CliPluginDescriptorCache + implements PluginDescriptorCache { + + @FunctionalInterface + public interface PluginDescriptorSupplier { + PluginDescriptor load() + throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException; + } + + private static class Holder { + + PluginDescriptor descriptor; + + public synchronized PluginDescriptor get() { + return descriptor; + } + + public synchronized void set(PluginDescriptor descriptor) { + this.descriptor = descriptor; + } + + public synchronized PluginDescriptor get(PluginDescriptorSupplier supplier) + throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException { + if (descriptor == null) { + descriptor = supplier.load(); + } + return descriptor; + } + } + + private ConcurrentMap descriptors = new ConcurrentHashMap<>(128); + + public void flush() { + descriptors.clear(); + } + + public Key createKey(Plugin plugin, List repositories, RepositorySystemSession session) { + return new CacheKey(plugin, repositories, session); + } + + public PluginDescriptor get(Key cacheKey) { + return clone(descriptors.computeIfAbsent(cacheKey, k -> new Holder()).get()); + } + + public PluginDescriptor get(Key cacheKey, PluginDescriptorSupplier supplier) + throws PluginResolutionException, PluginDescriptorParsingException, InvalidPluginDescriptorException { + return clone(descriptors.computeIfAbsent(cacheKey, k -> new Holder()).get(supplier)); + } + + public void put(Key cacheKey, PluginDescriptor pluginDescriptor) { + descriptors.computeIfAbsent(cacheKey, k -> new Holder()).set(clone(pluginDescriptor)); + } + + protected static PluginDescriptor clone(PluginDescriptor original) { + PluginDescriptor clone = null; + + if (original != null) { + clone = new PluginDescriptor(); + + clone.setGroupId(original.getGroupId()); + clone.setArtifactId(original.getArtifactId()); + clone.setVersion(original.getVersion()); + clone.setGoalPrefix(original.getGoalPrefix()); + clone.setInheritedByDefault(original.isInheritedByDefault()); + + clone.setName(original.getName()); + clone.setDescription(original.getDescription()); + clone.setRequiredMavenVersion(original.getRequiredMavenVersion()); + + clone.setPluginArtifact(ArtifactUtils.copyArtifactSafe(original.getPluginArtifact())); + + clone.setComponents(clone(original.getMojos(), clone)); + clone.setId(original.getId()); + clone.setIsolatedRealm(original.isIsolatedRealm()); + clone.setSource(original.getSource()); + + clone.setDependencies(original.getDependencies()); + } + + return clone; + } + + private static List> clone(List mojos, PluginDescriptor pluginDescriptor) { + List> clones = null; + + if (mojos != null) { + clones = new ArrayList<>(mojos.size()); + + for (MojoDescriptor mojo : mojos) { + MojoDescriptor clone = mojo.clone(); + clone.setPluginDescriptor(pluginDescriptor); + clones.add(clone); + } + } + + return clones; + } + + private static final class CacheKey + implements Key { + + private final String groupId; + + private final String artifactId; + + private final String version; + + private final WorkspaceRepository workspace; + + private final LocalRepository localRepo; + + private final List repositories; + + private final int hashCode; + + CacheKey(Plugin plugin, List repositories, RepositorySystemSession session) { + groupId = plugin.getGroupId(); + artifactId = plugin.getArtifactId(); + version = plugin.getVersion(); + + workspace = RepositoryUtils.getWorkspace(session); + localRepo = session.getLocalRepository(); + this.repositories = new ArrayList<>(repositories.size()); + for (RemoteRepository repository : repositories) { + if (repository.isRepositoryManager()) { + this.repositories.addAll(repository.getMirroredRepositories()); + } else { + this.repositories.add(repository); + } + } + + int hash = 17; + hash = hash * 31 + groupId.hashCode(); + hash = hash * 31 + artifactId.hashCode(); + hash = hash * 31 + version.hashCode(); + hash = hash * 31 + hash(workspace); + hash = hash * 31 + localRepo.hashCode(); + hash = hash * 31 + RepositoryUtils.repositoriesHashCode(repositories); + this.hashCode = hash; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof CacheKey)) { + return false; + } + + CacheKey that = (CacheKey) obj; + + return Objects.equals(this.artifactId, that.artifactId) + && Objects.equals(this.groupId, that.groupId) + && Objects.equals(this.version, that.version) + && Objects.equals(this.localRepo, that.localRepo) + && Objects.equals(this.workspace, that.workspace) + && RepositoryUtils.repositoriesEquals(this.repositories, that.repositories); + } + + @Override + public String toString() { + return groupId + ':' + artifactId + ':' + version; + } + + private static int hash(Object obj) { + return obj != null ? obj.hashCode() : 0; + } + + } + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCache.java b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCache.java index 7277a7296..facab020b 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCache.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCache.java @@ -40,7 +40,9 @@ import org.apache.maven.RepositoryUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.PluginContainerException; import org.apache.maven.plugin.PluginRealmCache; +import org.apache.maven.plugin.PluginResolutionException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.classworlds.realm.ClassRealm; import org.codehaus.plexus.classworlds.realm.NoSuchRealmException; @@ -67,6 +69,31 @@ @Typed(PluginRealmCache.class) public class CliPluginRealmCache implements PluginRealmCache, Disposable { + + @FunctionalInterface + public interface PluginRealmSupplier { + CacheRecord load() + throws PluginResolutionException, PluginContainerException; + } + + protected static class Holder { + + ValidableCacheRecord record; + + public synchronized ValidableCacheRecord get() { + if (record != null && !record.isValid()) { + record.dispose(); + record = null; + } + return record; + } + + public synchronized void set(ValidableCacheRecord record) { + this.record = record; + } + + } + /** * CacheKey */ @@ -324,17 +351,11 @@ public Registration(WatchKey watchKey) { } } - public ValidableCacheRecord newRecord(ClassRealm pluginRealm, List pluginArtifacts) { - final ValidableCacheRecord result = new ValidableCacheRecord(pluginRealm, pluginArtifacts); - add(result); - return result; - } - } private static final Logger LOG = LoggerFactory.getLogger(CliPluginRealmCache.class); - protected final Map cache = new ConcurrentHashMap<>(); + protected final Map cache = new ConcurrentHashMap<>(); private final RecordValidator watcher; public CliPluginRealmCache() { @@ -349,25 +370,48 @@ public Key createKey(Plugin plugin, ClassLoader parentRealm, Map { - if (!r.isValid()) { - r.dispose(); - return null; - } else { - return r; + Holder h = cache.get(key); + return h != null ? h.get() : null; + } + + public CacheRecord get(Key key, PluginRealmSupplier supplier) + throws PluginResolutionException, PluginContainerException { + watcher.validateRecords(); + Holder h = cache.computeIfAbsent(key, k -> new Holder()); + synchronized (h) { + ValidableCacheRecord vr = h.get(); + if (vr != null) { + if (vr.isValid()) { + return vr; + } + vr.dispose(); } - }); + CacheRecord r = supplier.load(); + vr = new ValidableCacheRecord(r.getRealm(), r.getArtifacts()); + watcher.add(vr); + h.set(vr); + return vr; + } } public CacheRecord put(Key key, ClassRealm pluginRealm, List pluginArtifacts) { Objects.requireNonNull(pluginRealm, "pluginRealm cannot be null"); Objects.requireNonNull(pluginArtifacts, "pluginArtifacts cannot be null"); - return cache.computeIfAbsent(key, k -> watcher.newRecord(pluginRealm, pluginArtifacts)); + Holder h = cache.computeIfAbsent(key, k -> new Holder()); + synchronized (h) { + ValidableCacheRecord r = new ValidableCacheRecord(pluginRealm, pluginArtifacts); + watcher.add(r); + h.set(r); + return r; + } } public void flush() { - for (ValidableCacheRecord record : cache.values()) { - record.dispose(); + for (Holder holder : cache.values()) { + ValidableCacheRecord record = holder.get(); + if (record != null) { + record.dispose(); + } } cache.clear(); } diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCacheEventSpy.java b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCacheEventSpy.java index d51b80443..6598d2688 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCacheEventSpy.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/CliPluginRealmCacheEventSpy.java @@ -55,11 +55,11 @@ public void onEvent(Object event) throws Exception { multiModuleProjectDirectory = ((MavenExecutionRequest) event).getMultiModuleProjectDirectory().toPath(); } else if (event instanceof MavenExecutionResult) { /* Evict the entries refering to jars under multiModuleProjectDirectory */ - final Iterator> i = cache.cache + final Iterator> i = cache.cache .entrySet().iterator(); while (i.hasNext()) { - final Map.Entry entry = i.next(); - final CliPluginRealmCache.ValidableCacheRecord record = entry.getValue(); + final Map.Entry entry = i.next(); + final CliPluginRealmCache.ValidableCacheRecord record = entry.getValue().get(); for (URL url : record.getRealm().getURLs()) { if (url.getProtocol().equals("file")) { final Path path = Paths.get(url.toURI()); diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/plugin/ValidatingConfigurationListener.java b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/ValidatingConfigurationListener.java new file mode 100644 index 000000000..fb0510324 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/plugin/ValidatingConfigurationListener.java @@ -0,0 +1,88 @@ +/* + * 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.mvndaemon.mvnd.plugin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.apache.maven.plugin.descriptor.MojoDescriptor; +import org.apache.maven.plugin.descriptor.Parameter; +import org.codehaus.plexus.component.configurator.ConfigurationListener; + +/* + * gnodet: This file is a copy of maven's ValidatingConfigurationListener because it's visibility + * is restricted to its defining package. + * See https://github.com/mvndaemon/mvnd/issues/310 + */ +/** + * A configuration listener to help validate the plugin configuration. For instance, check for required but missing + * parameters. + * + * @author Benjamin Bentmann + */ +class ValidatingConfigurationListener + implements ConfigurationListener { + + private final Object mojo; + + private final ConfigurationListener delegate; + + private final Map missingParameters; + + ValidatingConfigurationListener(Object mojo, MojoDescriptor mojoDescriptor, ConfigurationListener delegate) { + this.mojo = mojo; + this.delegate = delegate; + this.missingParameters = new HashMap<>(); + + if (mojoDescriptor.getParameters() != null) { + for (Parameter param : mojoDescriptor.getParameters()) { + if (param.isRequired()) { + missingParameters.put(param.getName(), param); + } + } + } + } + + public Collection getMissingParameters() { + return missingParameters.values(); + } + + public void notifyFieldChangeUsingSetter(String fieldName, Object value, Object target) { + delegate.notifyFieldChangeUsingSetter(fieldName, value, target); + + if (mojo == target) { + notify(fieldName, value); + } + } + + public void notifyFieldChangeUsingReflection(String fieldName, Object value, Object target) { + delegate.notifyFieldChangeUsingReflection(fieldName, value, target); + + if (mojo == target) { + notify(fieldName, value); + } + } + + private void notify(String fieldName, Object value) { + if (value != null) { + missingParameters.remove(fieldName); + } + } + +}