/* * Copyright 2014 the original author or authors. * * Licensed 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.gradle.plugin.devel.plugins; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.gradle.api.Action; import org.gradle.api.Incubating; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.internal.ConventionMapping; import org.gradle.api.internal.plugins.DslObject; import org.gradle.api.internal.plugins.PluginDescriptor; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.plugins.AppliedPlugin; import org.gradle.api.plugins.ExtensionContainer; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.bundling.Jar; import org.gradle.model.Model; import org.gradle.model.RuleSource; import org.gradle.plugin.devel.GradlePluginDevelopmentExtension; import org.gradle.plugin.devel.PluginDeclaration; import org.gradle.plugin.devel.tasks.GeneratePluginDescriptors; import org.gradle.plugin.devel.tasks.PluginUnderTestMetadata; import org.gradle.plugin.devel.tasks.ValidateTaskProperties; import java.io.File; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; /** * A plugin for building java gradle plugins. Automatically generates plugin descriptors. Emits warnings for common error conditions. <p> Provides a direct integration with TestKit by declaring the * {@code gradleTestKit()} dependency for the test compile configuration and a dependency on the plugin classpath manifest generation task for the test runtime configuration. Default conventions can * be customized with the help of {@link GradlePluginDevelopmentExtension}. * * Integrates with the 'maven-publish' and 'ivy-publish' plugins to automatically publish the plugins so they can be resolved using the `pluginRepositories` and `plugins` DSL. */ @Incubating public class JavaGradlePluginPlugin implements Plugin<Project> { private static final Logger LOGGER = Logging.getLogger(JavaGradlePluginPlugin.class); static final String COMPILE_CONFIGURATION = "compile"; static final String JAR_TASK = "jar"; static final String PROCESS_RESOURCES_TASK = "processResources"; static final String GRADLE_PLUGINS = "gradle-plugins"; static final String PLUGIN_DESCRIPTOR_PATTERN = "META-INF/" + GRADLE_PLUGINS + "/*.properties"; static final String CLASSES_PATTERN = "**/*.class"; static final String BAD_IMPL_CLASS_WARNING_MESSAGE = "%s: A valid plugin descriptor was found for %s but the implementation class %s was not found in the jar."; static final String INVALID_DESCRIPTOR_WARNING_MESSAGE = "%s: A plugin descriptor was found for %s but it was invalid."; static final String NO_DESCRIPTOR_WARNING_MESSAGE = "%s: No valid plugin descriptors were found in META-INF/" + GRADLE_PLUGINS + ""; static final String DECLARED_PLUGIN_MISSING_MESSAGE = "%s: Could not find plugin descriptor of %s at META-INF/" + GRADLE_PLUGINS + "/%s.properties"; static final String DECLARATION_MISSING_ID_MESSAGE = "Missing id for %s"; static final String DECLARATION_MISSING_IMPLEMENTATION_MESSAGE = "Missing implementationClass for %s"; static final String EXTENSION_NAME = "gradlePlugin"; static final String PLUGIN_UNDER_TEST_METADATA_TASK_NAME = "pluginUnderTestMetadata"; static final String GENERATE_PLUGIN_DESCRIPTORS_TASK_NAME = "pluginDescriptors"; static final String VALIDATE_TASK_PROPERTIES_TASK_NAME = "validateTaskProperties"; /** * The task group used for tasks created by the Java Gradle plugin development plugin. * * @since 4.0 */ static final String PLUGIN_DEVELOPMENT_GROUP = "Plugin development"; /** * The description for the task generating metadata for plugin functional tests. * * @since 4.0 */ static final String PLUGIN_UNDER_TEST_METADATA_TASK_DESCRIPTION = "Generates the metadata for plugin functional tests."; /** * The description for the task generating plugin descriptors from plugin declarations. * * @since 4.0 */ static final String GENERATE_PLUGIN_DESCRIPTORS_TASK_DESCRIPTION = "Generates plugin descriptors from plugin declarations."; /** * The description for the task validating task property annotations for the plugin. * * @since 4.0 */ static final String VALIDATE_TASK_PROPERTIES_TASK_DESCRIPTION = "Validates task property annotations for the plugin."; public void apply(Project project) { project.getPluginManager().apply(JavaPlugin.class); applyDependencies(project); GradlePluginDevelopmentExtension extension = createExtension(project); configureJarTask(project, extension); configureTestKit(project, extension); configurePublishing(project); configureDescriptorGeneration(project, extension); validatePluginDeclarations(project, extension); configureTaskPropertiesValidation(project); } private void applyDependencies(Project project) { DependencyHandler dependencies = project.getDependencies(); dependencies.add(COMPILE_CONFIGURATION, dependencies.gradleApi()); } private void configureJarTask(Project project, GradlePluginDevelopmentExtension extension) { Jar jarTask = (Jar) project.getTasks().findByName(JAR_TASK); List<PluginDescriptor> descriptors = new ArrayList<PluginDescriptor>(); Set<String> classList = new HashSet<String>(); PluginDescriptorCollectorAction pluginDescriptorCollector = new PluginDescriptorCollectorAction(descriptors); ClassManifestCollectorAction classManifestCollector = new ClassManifestCollectorAction(classList); PluginValidationAction pluginValidationAction = new PluginValidationAction(extension.getPlugins(), descriptors, classList); jarTask.filesMatching(PLUGIN_DESCRIPTOR_PATTERN, pluginDescriptorCollector); jarTask.filesMatching(CLASSES_PATTERN, classManifestCollector); jarTask.appendParallelSafeAction(pluginValidationAction); } private GradlePluginDevelopmentExtension createExtension(Project project) { JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class); SourceSet defaultPluginSourceSet = javaConvention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); SourceSet defaultTestSourceSet = javaConvention.getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME); return project.getExtensions().create(EXTENSION_NAME, GradlePluginDevelopmentExtension.class, project, defaultPluginSourceSet, defaultTestSourceSet); } private void configureTestKit(Project project, GradlePluginDevelopmentExtension extension) { PluginUnderTestMetadata pluginUnderTestMetadataTask = createAndConfigurePluginUnderTestMetadataTask(project, extension); establishTestKitAndPluginClasspathDependencies(project, extension, pluginUnderTestMetadataTask); } private PluginUnderTestMetadata createAndConfigurePluginUnderTestMetadataTask(final Project project, final GradlePluginDevelopmentExtension extension) { final PluginUnderTestMetadata pluginUnderTestMetadataTask = project.getTasks().create(PLUGIN_UNDER_TEST_METADATA_TASK_NAME, PluginUnderTestMetadata.class); pluginUnderTestMetadataTask.setGroup(PLUGIN_DEVELOPMENT_GROUP); pluginUnderTestMetadataTask.setDescription(PLUGIN_UNDER_TEST_METADATA_TASK_DESCRIPTION); final Configuration gradlePluginConfiguration = project.getConfigurations().detachedConfiguration(project.getDependencies().gradleApi()); ConventionMapping conventionMapping = new DslObject(pluginUnderTestMetadataTask).getConventionMapping(); conventionMapping.map("pluginClasspath", new Callable<Object>() { public Object call() throws Exception { FileCollection gradleApi = gradlePluginConfiguration.getIncoming().getFiles(); return extension.getPluginSourceSet().getRuntimeClasspath().minus(gradleApi); } }); conventionMapping.map("outputDirectory", new Callable<Object>() { public Object call() throws Exception { return new File(project.getBuildDir(), pluginUnderTestMetadataTask.getName()); } }); return pluginUnderTestMetadataTask; } private void establishTestKitAndPluginClasspathDependencies(Project project, GradlePluginDevelopmentExtension extension, PluginUnderTestMetadata pluginClasspathTask) { project.afterEvaluate(new TestKitAndPluginClasspathDependenciesAction(extension, pluginClasspathTask)); } private void configurePublishing(final Project project) { project.getPluginManager().withPlugin("maven-publish", new Action<AppliedPlugin>() { @Override public void execute(AppliedPlugin appliedPlugin) { project.getPluginManager().apply(MavenPluginPublishingRules.class); } }); project.getPluginManager().withPlugin("ivy-publish", new Action<AppliedPlugin>() { @Override public void execute(AppliedPlugin appliedPlugin) { project.getPluginManager().apply(IvyPluginPublishingRules.class); } }); } private void configureDescriptorGeneration(final Project project, final GradlePluginDevelopmentExtension extension) { final GeneratePluginDescriptors generatePluginDescriptors = project.getTasks().create(GENERATE_PLUGIN_DESCRIPTORS_TASK_NAME, GeneratePluginDescriptors.class); generatePluginDescriptors.setGroup(PLUGIN_DEVELOPMENT_GROUP); generatePluginDescriptors.setDescription(GENERATE_PLUGIN_DESCRIPTORS_TASK_DESCRIPTION); generatePluginDescriptors.conventionMapping("declarations", new Callable<List<PluginDeclaration>>() { @Override public List<PluginDeclaration> call() throws Exception { return Lists.newArrayList(extension.getPlugins()); } }); generatePluginDescriptors.conventionMapping("outputDirectory", new Callable<File>() { @Override public File call() throws Exception { return new File(project.getBuildDir(), generatePluginDescriptors.getName()); } }); Copy processResources = (Copy) project.getTasks().getByName(PROCESS_RESOURCES_TASK); CopySpec copyPluginDescriptors = processResources.getRootSpec().addChild(); copyPluginDescriptors.into("META-INF/gradle-plugins"); copyPluginDescriptors.from(new Callable<File>() { @Override public File call() throws Exception { return generatePluginDescriptors.getOutputDirectory(); } }); processResources.dependsOn(generatePluginDescriptors); } private void validatePluginDeclarations(Project project, final GradlePluginDevelopmentExtension extension) { project.afterEvaluate(new Action<Project>() { @Override public void execute(Project project) { for (PluginDeclaration declaration : extension.getPlugins()) { if (declaration.getId() == null) { throw new IllegalArgumentException(String.format(DECLARATION_MISSING_ID_MESSAGE, declaration.getName())); } if (declaration.getImplementationClass() == null) { throw new IllegalArgumentException(String.format(DECLARATION_MISSING_IMPLEMENTATION_MESSAGE, declaration.getName())); } } } }); } private void configureTaskPropertiesValidation(Project project) { ValidateTaskProperties validator = project.getTasks().create(VALIDATE_TASK_PROPERTIES_TASK_NAME, ValidateTaskProperties.class); validator.setGroup(PLUGIN_DEVELOPMENT_GROUP); validator.setDescription(VALIDATE_TASK_PROPERTIES_TASK_DESCRIPTION); File reportsDir = new File(project.getBuildDir(), "reports"); File validatorReportsDir = new File(reportsDir, "task-properties"); validator.setOutputFile(new File(validatorReportsDir, "report.txt")); final SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); validator.setClasses(mainSourceSet.getOutput().getClassesDirs()); validator.setClasspath(mainSourceSet.getCompileClasspath()); validator.dependsOn(mainSourceSet.getOutput()); project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(validator); } /** * Implements plugin validation tasks to validate that a proper plugin jar is produced. */ static class PluginValidationAction implements Action<Task> { private final Collection<PluginDeclaration> plugins; private final Collection<PluginDescriptor> descriptors; private final Set<String> classes; PluginValidationAction(Collection<PluginDeclaration> plugins, Collection<PluginDescriptor> descriptors, Set<String> classes) { this.plugins = plugins; this.descriptors = descriptors; this.classes = classes; } public void execute(Task task) { if (descriptors == null || descriptors.isEmpty()) { LOGGER.warn(String.format(NO_DESCRIPTOR_WARNING_MESSAGE, task.getPath())); } else { Set<String> pluginFileNames = Sets.newHashSet(); for (PluginDescriptor descriptor : descriptors) { URI descriptorURI = null; try { descriptorURI = descriptor.getPropertiesFileUrl().toURI(); } catch (URISyntaxException e) { // Do nothing since the only side effect is that we wouldn't // be able to log the plugin descriptor file name. Shouldn't // be a reasonable scenario where this occurs since these // descriptors should be generated from real files. } String pluginFileName = descriptorURI != null ? new File(descriptorURI).getName() : "UNKNOWN"; pluginFileNames.add(pluginFileName); String pluginImplementation = descriptor.getImplementationClassName(); if (pluginImplementation.length() == 0) { LOGGER.warn(String.format(INVALID_DESCRIPTOR_WARNING_MESSAGE, task.getPath(), pluginFileName)); } else if (!hasFullyQualifiedClass(pluginImplementation)) { LOGGER.warn(String.format(BAD_IMPL_CLASS_WARNING_MESSAGE, task.getPath(), pluginFileName, pluginImplementation)); } } for (PluginDeclaration declaration : plugins) { if (!pluginFileNames.contains(declaration.getId() + ".properties")) { LOGGER.warn(String.format(DECLARED_PLUGIN_MISSING_MESSAGE, task.getPath(), declaration.getName(), declaration.getId())); } } } } boolean hasFullyQualifiedClass(String fqClass) { return classes.contains(fqClass.replaceAll("\\.", "/") + ".class"); } } /** * A file copy action that collects plugin descriptors as they are added to the jar. */ static class PluginDescriptorCollectorAction implements Action<FileCopyDetails> { List<PluginDescriptor> descriptors; PluginDescriptorCollectorAction(List<PluginDescriptor> descriptors) { this.descriptors = descriptors; } public void execute(FileCopyDetails fileCopyDetails) { PluginDescriptor descriptor; try { descriptor = new PluginDescriptor(fileCopyDetails.getFile().toURI().toURL()); } catch (MalformedURLException e) { // Not sure under what scenario (if any) this would occur, // but there's no sense in collecting the descriptor if it does. return; } if (descriptor.getImplementationClassName() != null) { descriptors.add(descriptor); } } } /** * A file copy action that collects class file paths as they are added to the jar. */ static class ClassManifestCollectorAction implements Action<FileCopyDetails> { Set<String> classList; ClassManifestCollectorAction(Set<String> classList) { this.classList = classList; } public void execute(FileCopyDetails fileCopyDetails) { classList.add(fileCopyDetails.getRelativePath().toString()); } } /** * An action that automatically declares the TestKit dependency for the test compile configuration and a dependency * on the plugin classpath manifest generation task for the test runtime configuration. */ static class TestKitAndPluginClasspathDependenciesAction implements Action<Project> { private final GradlePluginDevelopmentExtension extension; private final PluginUnderTestMetadata pluginClasspathTask; private TestKitAndPluginClasspathDependenciesAction(GradlePluginDevelopmentExtension extension, PluginUnderTestMetadata pluginClasspathTask) { this.extension = extension; this.pluginClasspathTask = pluginClasspathTask; } @Override public void execute(Project project) { DependencyHandler dependencies = project.getDependencies(); Set<SourceSet> testSourceSets = extension.getTestSourceSets(); for (SourceSet testSourceSet : testSourceSets) { String compileConfigurationName = testSourceSet.getCompileConfigurationName(); dependencies.add(compileConfigurationName, dependencies.gradleTestKit()); String runtimeConfigurationName = testSourceSet.getRuntimeConfigurationName(); dependencies.add(runtimeConfigurationName, project.files(pluginClasspathTask)); } } } static class Rules extends RuleSource { @Model public GradlePluginDevelopmentExtension gradlePluginDevelopmentExtension(ExtensionContainer extensionContainer) { return extensionContainer.getByType(GradlePluginDevelopmentExtension.class); } } }