-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
JavaGradlePluginPlugin.java
486 lines (425 loc) · 24 KB
/
JavaGradlePluginPlugin.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/*
* 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 org.gradle.api.Action;
import org.gradle.api.JavaVersion;
import org.gradle.api.NonNullApi;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.ArtifactView;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ComponentIdentifier;
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.artifacts.dsl.dependencies.DependencyFactoryInternal;
import org.gradle.api.internal.artifacts.ivyservice.projectmodule.ProjectPublicationRegistry;
import org.gradle.api.internal.plugins.PluginDescriptor;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.internal.tasks.JvmConstants;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.internal.JavaPluginHelper;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.ClasspathNormalizer;
import org.gradle.api.tasks.Copy;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.testing.Test;
import org.gradle.initialization.buildsrc.GradlePluginApiVersionAttributeConfigurationAction;
import org.gradle.internal.Describables;
import org.gradle.internal.DisplayName;
import org.gradle.internal.component.local.model.OpaqueComponentIdentifier;
import org.gradle.jvm.toolchain.JavaLauncher;
import org.gradle.jvm.toolchain.JavaToolchainService;
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.ValidatePlugins;
import org.gradle.plugin.use.PluginId;
import org.gradle.plugin.use.internal.DefaultPluginId;
import org.gradle.plugin.use.resolve.internal.local.PluginPublication;
import org.gradle.process.CommandLineArgumentProvider;
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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import static org.gradle.api.internal.lambdas.SerializableLambdas.spec;
/**
* 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.
*
* @see <a href="https://docs.gradle.org/current/userguide/java_gradle_plugin.html">Gradle plugin development reference</a>
*/
@NonNullApi
public abstract class JavaGradlePluginPlugin implements Plugin<Project> {
private static final Logger LOGGER = Logging.getLogger(JavaGradlePluginPlugin.class);
static final String API_CONFIGURATION = JvmConstants.API_CONFIGURATION_NAME;
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_PLUGINS_TASK_NAME = "validatePlugins";
/**
* 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 the plugin.
*
* @since 6.0
*/
static final String VALIDATE_PLUGIN_TASK_DESCRIPTION = "Validates the plugin by checking parameter annotations on task and artifact transform types etc.";
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaLibraryPlugin.class);
applyDependencies(project);
GradlePluginDevelopmentExtension extension = createExtension(project);
configureJarTask(project, extension);
configureTestKit(project, extension);
configurePublishing(project);
registerPlugins(project, extension);
configureDescriptorGeneration(project, extension);
validatePluginDeclarations(project, extension);
configurePluginValidations(project, extension);
configureDependencyGradlePluginsResolution(project);
}
private void registerPlugins(Project project, GradlePluginDevelopmentExtension extension) {
ProjectInternal projectInternal = (ProjectInternal) project;
ProjectPublicationRegistry registry = projectInternal.getServices().get(ProjectPublicationRegistry.class);
extension.getPlugins().all(pluginDeclaration -> registry.registerPublication(projectInternal, new LocalPluginPublication(pluginDeclaration)));
}
private void applyDependencies(Project project) {
DependencyHandler dependencies = project.getDependencies();
dependencies.add(API_CONFIGURATION, dependencies.gradleApi());
}
private void configureJarTask(Project project, GradlePluginDevelopmentExtension extension) {
project.getTasks().named(JAR_TASK, Jar.class, jarTask -> {
PluginValidationActionsState actionsState = new PluginValidationActionsState();
PluginDescriptorCollectorAction pluginDescriptorCollector = new PluginDescriptorCollectorAction(actionsState);
ClassManifestCollectorAction classManifestCollector = new ClassManifestCollectorAction(actionsState);
Provider<Collection<PluginDeclaration>> pluginsProvider = project.provider(() -> extension.getPlugins().getAsMap().values());
PluginValidationAction pluginValidationAction = new PluginValidationAction(pluginsProvider, actionsState);
jarTask.filesMatching(PLUGIN_DESCRIPTOR_PATTERN, pluginDescriptorCollector);
jarTask.filesMatching(CLASSES_PATTERN, classManifestCollector);
jarTask.appendParallelSafeAction(pluginValidationAction);
});
}
private GradlePluginDevelopmentExtension createExtension(Project project) {
SourceSet defaultPluginSourceSet = JavaPluginHelper.getJavaComponent(project).getMainFeature().getSourceSet();
SourceSet defaultTestSourceSet = JavaPluginHelper.getDefaultTestSuite(project).getSources();
return project.getExtensions().create(EXTENSION_NAME, GradlePluginDevelopmentExtension.class, project, defaultPluginSourceSet, defaultTestSourceSet);
}
private void configureTestKit(Project project, GradlePluginDevelopmentExtension extension) {
TaskProvider<PluginUnderTestMetadata> pluginUnderTestMetadataTask = createAndConfigurePluginUnderTestMetadataTask(project, extension);
establishTestKitAndPluginClasspathDependencies(project, extension, pluginUnderTestMetadataTask);
}
private TaskProvider<PluginUnderTestMetadata> createAndConfigurePluginUnderTestMetadataTask(Project project, GradlePluginDevelopmentExtension extension) {
return project.getTasks().register(PLUGIN_UNDER_TEST_METADATA_TASK_NAME, PluginUnderTestMetadata.class, pluginUnderTestMetadataTask -> {
pluginUnderTestMetadataTask.setGroup(PLUGIN_DEVELOPMENT_GROUP);
pluginUnderTestMetadataTask.setDescription(PLUGIN_UNDER_TEST_METADATA_TASK_DESCRIPTION);
pluginUnderTestMetadataTask.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(pluginUnderTestMetadataTask.getName()));
pluginUnderTestMetadataTask.getPluginClasspath().from(classpathForPlugin(project, extension));
});
}
private static Callable<FileCollection> classpathForPlugin(Project project, GradlePluginDevelopmentExtension extension) {
return () -> {
SourceSet sourceSet = extension.getPluginSourceSet();
Configuration runtimeClasspath = project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName());
ArtifactView view = runtimeClasspath.getIncoming().artifactView(config ->
config.componentFilter(spec(JavaGradlePluginPlugin::excludeGradleApi))
);
return project.getObjects().fileCollection().from(
sourceSet.getOutput(),
view.getFiles().getElements()
);
};
}
private static boolean excludeGradleApi(ComponentIdentifier componentId) {
if (componentId instanceof OpaqueComponentIdentifier) {
DependencyFactoryInternal.ClassPathNotation classPathNotation = ((OpaqueComponentIdentifier) componentId).getClassPathNotation();
return classPathNotation != DependencyFactoryInternal.ClassPathNotation.GRADLE_API && classPathNotation != DependencyFactoryInternal.ClassPathNotation.LOCAL_GROOVY;
}
return true;
}
private void establishTestKitAndPluginClasspathDependencies(Project project, GradlePluginDevelopmentExtension extension, TaskProvider<PluginUnderTestMetadata> pluginClasspathTask) {
project.afterEvaluate(new TestKitAndPluginClasspathDependenciesAction(extension, pluginClasspathTask));
}
private void configurePublishing(Project project) {
project.getPluginManager().withPlugin("maven-publish", appliedPlugin -> project.getPluginManager().apply(MavenPluginPublishPlugin.class));
project.getPluginManager().withPlugin("ivy-publish", appliedPlugin -> project.getPluginManager().apply(IvyPluginPublishingPlugin.class));
}
private void configureDescriptorGeneration(Project project, GradlePluginDevelopmentExtension extension) {
TaskProvider<GeneratePluginDescriptors> generatePluginDescriptors = project.getTasks().register(GENERATE_PLUGIN_DESCRIPTORS_TASK_NAME, GeneratePluginDescriptors.class, task -> {
task.setGroup(PLUGIN_DEVELOPMENT_GROUP);
task.setDescription(GENERATE_PLUGIN_DESCRIPTORS_TASK_DESCRIPTION);
task.getDeclarations().set(extension.getPlugins());
task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(task.getName()));
});
project.getTasks().named(PROCESS_RESOURCES_TASK, Copy.class, task -> {
CopySpec copyPluginDescriptors = task.getRootSpec().addChild();
copyPluginDescriptors.into("META-INF/gradle-plugins");
copyPluginDescriptors.from(generatePluginDescriptors);
});
}
private void validatePluginDeclarations(Project project, GradlePluginDevelopmentExtension extension) {
project.afterEvaluate(evaluatedProject -> {
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 configurePluginValidations(Project project, GradlePluginDevelopmentExtension extension) {
TaskProvider<ValidatePlugins> validatorTask = project.getTasks().register(VALIDATE_PLUGINS_TASK_NAME, ValidatePlugins.class, task -> {
task.setGroup(PLUGIN_DEVELOPMENT_GROUP);
task.setDescription(VALIDATE_PLUGIN_TASK_DESCRIPTION);
task.getOutputFile().set(project.getLayout().getBuildDirectory().file("reports/plugin-development/validation-report.json"));
task.getClasses().setFrom((Callable<Object>) () -> extension.getPluginSourceSet().getOutput().getClassesDirs());
task.getClasspath().setFrom((Callable<Object>) () -> extension.getPluginSourceSet().getCompileClasspath());
task.getLauncher().convention(toolchainLauncher(project));
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, check -> check.dependsOn(validatorTask));
}
private void configureDependencyGradlePluginsResolution(Project project) {
new GradlePluginApiVersionAttributeConfigurationAction().execute((ProjectInternal) project);
}
private Provider<JavaLauncher> toolchainLauncher(Project project) {
JavaPluginExtension extension = project.getExtensions().findByType(JavaPluginExtension.class);
JavaToolchainService service = project.getExtensions().findByType(JavaToolchainService.class);
return service.launcherFor(extension.getToolchain());
}
/**
* A state shared by the validation process.
* <p>
* This separate class is required to ensure the shared state remains shared after deserialization of actions from the configuration cache.
*
* @see #configureJarTask(Project, GradlePluginDevelopmentExtension)
*/
static class PluginValidationActionsState {
private final List<PluginDescriptor> descriptors;
private final Set<String> classes;
public PluginValidationActionsState() {
this(new ArrayList<>(), new HashSet<>());
}
public PluginValidationActionsState(List<PluginDescriptor> descriptors, Set<String> classes) {
this.descriptors = descriptors;
this.classes = classes;
}
public void addPluginClass(String className) {
classes.add(className);
}
public void addPluginDescriptor(PluginDescriptor descriptor) {
descriptors.add(descriptor);
}
public Set<String> getCollectedClasses() {
return classes;
}
public List<PluginDescriptor> getCollectedDescriptors() {
return descriptors;
}
}
/**
* Implements plugin validation tasks to validate that a proper plugin jar is produced.
*/
static class PluginValidationAction implements Action<Task> {
private final Provider<Collection<PluginDeclaration>> plugins;
private final PluginValidationActionsState actionsState;
PluginValidationAction(Provider<Collection<PluginDeclaration>> plugins, PluginValidationActionsState actionsState) {
this.plugins = plugins;
this.actionsState = actionsState;
}
@Override
public void execute(Task task) {
List<PluginDescriptor> descriptors = actionsState.getCollectedDescriptors();
if (descriptors.isEmpty()) {
LOGGER.warn(String.format(NO_DESCRIPTOR_WARNING_MESSAGE, task.getPath()));
} else {
Set<String> pluginFileNames = new HashSet<>();
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.get()) {
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 actionsState.getCollectedClasses().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> {
private final PluginValidationActionsState actionsState;
PluginDescriptorCollectorAction(PluginValidationActionsState actionsState) {
this.actionsState = actionsState;
}
@Override
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) {
actionsState.addPluginDescriptor(descriptor);
}
}
}
/**
* A file copy action that collects class file paths as they are added to the jar.
*/
static class ClassManifestCollectorAction implements Action<FileCopyDetails> {
private final PluginValidationActionsState actionsState;
ClassManifestCollectorAction(PluginValidationActionsState actionsState) {
this.actionsState = actionsState;
}
@Override
public void execute(FileCopyDetails fileCopyDetails) {
actionsState.addPluginClass(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 TaskProvider<PluginUnderTestMetadata> pluginClasspathTask;
private TestKitAndPluginClasspathDependenciesAction(GradlePluginDevelopmentExtension extension, TaskProvider<PluginUnderTestMetadata> pluginClasspathTask) {
this.extension = extension;
this.pluginClasspathTask = pluginClasspathTask;
}
@Override
public void execute(Project project) {
DependencyHandler dependencies = project.getDependencies();
Set<SourceSet> testSourceSets = extension.getTestSourceSets();
project.getNormalization().getRuntimeClasspath().ignore(PluginUnderTestMetadata.METADATA_FILE_NAME);
project.getTasks().withType(Test.class).configureEach(test -> {
test.getInputs()
.files(pluginClasspathTask.get().getPluginClasspath())
.withPropertyName("pluginClasspath")
.withNormalizer(ClasspathNormalizer.class);
test.getJvmArgumentProviders().add(new AddOpensCommandLineArgumentProvider(test));
});
for (SourceSet testSourceSet : testSourceSets) {
String implementationConfigurationName = testSourceSet.getImplementationConfigurationName();
dependencies.add(implementationConfigurationName, dependencies.gradleTestKit());
String runtimeOnlyConfigurationName = testSourceSet.getRuntimeOnlyConfigurationName();
dependencies.add(runtimeOnlyConfigurationName, project.getLayout().files(pluginClasspathTask));
}
}
}
/**
* Provides an {@code --add-opens} flag for {@code java.base/java.lang} if the JVM version
* a given test task is running does not allow reflection of JDK internals by default.
* Needed when using ProjectBuilder in tests.
*/
private static class AddOpensCommandLineArgumentProvider implements CommandLineArgumentProvider {
private final Test test;
private AddOpensCommandLineArgumentProvider(Test test) {
this.test = test;
}
@Override
public Iterable<String> asArguments() {
return test.getJavaVersion().isCompatibleWith(JavaVersion.VERSION_1_9)
? Collections.singletonList("--add-opens=java.base/java.lang=ALL-UNNAMED")
: Collections.emptyList();
}
}
private static class LocalPluginPublication implements PluginPublication {
private final PluginDeclaration pluginDeclaration;
LocalPluginPublication(PluginDeclaration pluginDeclaration) {
this.pluginDeclaration = pluginDeclaration;
}
@Override
public DisplayName getDisplayName() {
return Describables.withTypeAndName("plugin", pluginDeclaration.getName());
}
@Override
public PluginId getPluginId() {
return DefaultPluginId.of(pluginDeclaration.getId());
}
}
}