Skip to content

Commit d1f2b5d

Browse files
authored
Create gradle plugin for ES stable plugins (#90355)
New ES stable plugins when built should have a stable-plugin-descriptor.properties file instead of plugin-descriptor.properties. New plugins also do not use classname property in the plugin descriptor new plugin will also scan classes and libraries for @NamedComponents and will create named_components.json file. That file contains a map of Extensible interface (like TokenizerFactory) to a map of "component name" to "className" This commit extracts common logic from PluginBuildPlugin into BasePluginBuildPLugin so that it can also be used by StableBuildPlugin the differences are: classaname - used in old plugin, but not in the new one (stable) the plugin descriptor file name - the new one has stable-plugin-descriptor.properties dependencies - the new plugin does not need elasticsearch as a dependency. We might want to consider if we want to add test framework dependency in the future. relates #88980
1 parent 41ede4e commit d1f2b5d

33 files changed

+1792
-160
lines changed

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/distribution/ElasticsearchDistributionExtension.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.concurrent.Callable;
2020
import java.util.regex.Pattern;
2121

22-
import static org.elasticsearch.gradle.plugin.PluginBuildPlugin.EXPLODED_BUNDLE_CONFIG;
22+
import static org.elasticsearch.gradle.plugin.BasePluginBuildPlugin.EXPLODED_BUNDLE_CONFIG;
2323

2424
public class ElasticsearchDistributionExtension {
2525
private static final Pattern CONFIG_BIN_REGEX_PATTERN = Pattern.compile("([^\\/]+\\/)?(config|bin)\\/.*");

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/RestTestBasePlugin.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
import javax.inject.Inject;
3030

31-
import static org.elasticsearch.gradle.plugin.PluginBuildPlugin.BUNDLE_PLUGIN_TASK_NAME;
32-
import static org.elasticsearch.gradle.plugin.PluginBuildPlugin.EXPLODED_BUNDLE_PLUGIN_TASK_NAME;
31+
import static org.elasticsearch.gradle.plugin.BasePluginBuildPlugin.BUNDLE_PLUGIN_TASK_NAME;
32+
import static org.elasticsearch.gradle.plugin.BasePluginBuildPlugin.EXPLODED_BUNDLE_PLUGIN_TASK_NAME;
3333

3434
public class RestTestBasePlugin implements Plugin<Project> {
3535
private static final String TESTS_REST_CLUSTER = "tests.rest.cluster";

build-tools/build.gradle

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ gradlePlugin {
3737
id = 'elasticsearch.esplugin'
3838
implementationClass = 'org.elasticsearch.gradle.plugin.PluginBuildPlugin'
3939
}
40+
stableEsPlugin {
41+
id = 'elasticsearch.stable-esplugin'
42+
implementationClass = 'org.elasticsearch.gradle.plugin.StablePluginBuildPlugin'
43+
}
4044
javaRestTest {
4145
id = 'elasticsearch.java-rest-test'
4246
implementationClass = 'org.elasticsearch.gradle.test.JavaRestTestPlugin'
@@ -119,6 +123,8 @@ dependencies {
119123
api buildLibs.commmons.io
120124
implementation buildLibs.asm.tree
121125
implementation buildLibs.asm
126+
implementation buildLibs.jackson.core
127+
implementation buildLibs.jackson.databind
122128

123129
testFixturesApi gradleApi()
124130
testFixturesApi gradleTestKit()
@@ -128,7 +134,9 @@ dependencies {
128134
testFixturesApi(buildLibs.spock.core) {
129135
exclude module: "groovy"
130136
}
131-
137+
testFixturesApi(buildLibs.bytebuddy) {
138+
because 'Generating dynamic plugin apis'
139+
}
132140
testImplementation(buildLibs.spock.junit4) {
133141
because 'required as we rely on junit4 rules'
134142
}
@@ -144,6 +152,7 @@ dependencies {
144152
testRuntimeOnly(buildLibs.junit5.platform.launcher) {
145153
because 'allows tests to run from IDEs that bundle older version of launcher'
146154
}
155+
147156
}
148157

149158
tasks.named('test').configure {

build-tools/src/integTest/groovy/org/elasticsearch/gradle/plugin/PluginBuildPluginFuncTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class PluginBuildPluginFuncTest extends AbstractGradleFuncTest {
7575
}
7676
7777
dependencies {
78-
consume project(path:':', configuration:'${PluginBuildPlugin.EXPLODED_BUNDLE_CONFIG}')
78+
consume project(path:':', configuration:'${BasePluginBuildPlugin.EXPLODED_BUNDLE_CONFIG}')
7979
}
8080
8181
tasks.register("resolveModule", Copy) {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.gradle.plugin
10+
11+
import groovy.json.JsonSlurper
12+
13+
import org.elasticsearch.gradle.VersionProperties
14+
import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest
15+
import org.elasticsearch.gradle.internal.test.StableApiJarMocks
16+
import org.gradle.testkit.runner.TaskOutcome
17+
18+
import java.nio.file.Files
19+
import java.nio.file.Path
20+
import java.util.stream.Collectors
21+
22+
class StableBuildPluginPluginFuncTest extends AbstractGradleFuncTest {
23+
24+
def setup() {
25+
// underlaying TestClusterPlugin and StandaloneRestIntegTestTask are not cc compatible
26+
configurationCacheCompatible = false
27+
}
28+
29+
def "can build stable plugin properties"() {
30+
given:
31+
buildFile << """plugins {
32+
id 'elasticsearch.stable-esplugin'
33+
}
34+
35+
version = '1.2.3'
36+
37+
esplugin {
38+
name = 'myplugin'
39+
description = 'test plugin'
40+
}
41+
"""
42+
43+
when:
44+
def result = gradleRunner(":pluginProperties").build()
45+
def props = getPluginProperties()
46+
47+
then:
48+
result.task(":pluginProperties").outcome == TaskOutcome.SUCCESS
49+
props.get("classname") == null
50+
51+
props.get("name") == "myplugin"
52+
props.get("version") == "1.2.3"
53+
props.get("description") == "test plugin"
54+
props.get("modulename") == ""
55+
props.get("java.version") == Integer.toString(Runtime.version().feature())
56+
props.get("elasticsearch.version") == VersionProperties.elasticsearchVersion.toString()
57+
props.get("extended.plugins") == ""
58+
props.get("has.native.controller") == "false"
59+
props.size() == 8
60+
61+
}
62+
63+
def "can scan and create named components file"() {
64+
given:
65+
File jarFolder = new File(testProjectDir.root, "jars")
66+
jarFolder.mkdirs()
67+
68+
buildFile << """plugins {
69+
id 'elasticsearch.stable-esplugin'
70+
}
71+
72+
version = '1.2.3'
73+
74+
esplugin {
75+
name = 'myplugin'
76+
description = 'test plugin'
77+
}
78+
79+
dependencies {
80+
implementation files('${StableApiJarMocks.createPluginApiJar(jarFolder.toPath()).toAbsolutePath()}')
81+
implementation files('${StableApiJarMocks.createExtensibleApiJar(jarFolder.toPath()).toAbsolutePath()}')
82+
}
83+
84+
"""
85+
86+
file("src/main/java/org/acme/A.java") << """
87+
package org.acme;
88+
89+
import org.elasticsearch.plugin.api.NamedComponent;
90+
import org.elasticsearch.plugin.scanner.test_classes.ExtensibleClass;
91+
92+
@NamedComponent(name = "componentA")
93+
public class A extends ExtensibleClass {
94+
}
95+
"""
96+
97+
98+
when:
99+
def result = gradleRunner(":assemble").build()
100+
Path namedComponents = file("build/generated-named-components/named_components.json").toPath();
101+
def map = new JsonSlurper().parse(namedComponents.toFile())
102+
then:
103+
result.task(":assemble").outcome == TaskOutcome.SUCCESS
104+
105+
map == ["org.elasticsearch.plugin.scanner.test_classes.ExtensibleClass" : (["componentA" : "org.acme.A"]) ]
106+
}
107+
108+
109+
Map<String, String> getPluginProperties() {
110+
Path propsFile = file("build/generated-descriptor/stable-plugin-descriptor.properties").toPath();
111+
Properties rawProps = new Properties()
112+
try (var inputStream = Files.newInputStream(propsFile)) {
113+
rawProps.load(inputStream)
114+
}
115+
return rawProps.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()))
116+
}
117+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.gradle.plugin;
10+
11+
import org.elasticsearch.gradle.Version;
12+
import org.elasticsearch.gradle.VersionProperties;
13+
import org.elasticsearch.gradle.dependencies.CompileOnlyResolvePlugin;
14+
import org.elasticsearch.gradle.jarhell.JarHellPlugin;
15+
import org.elasticsearch.gradle.test.GradleTestPolicySetupPlugin;
16+
import org.elasticsearch.gradle.testclusters.ElasticsearchCluster;
17+
import org.elasticsearch.gradle.testclusters.RunTask;
18+
import org.elasticsearch.gradle.testclusters.TestClustersPlugin;
19+
import org.elasticsearch.gradle.util.GradleUtils;
20+
import org.gradle.api.NamedDomainObjectContainer;
21+
import org.gradle.api.Plugin;
22+
import org.gradle.api.Project;
23+
import org.gradle.api.Task;
24+
import org.gradle.api.Transformer;
25+
import org.gradle.api.artifacts.type.ArtifactTypeDefinition;
26+
import org.gradle.api.file.CopySpec;
27+
import org.gradle.api.file.FileCollection;
28+
import org.gradle.api.file.RegularFile;
29+
import org.gradle.api.plugins.BasePlugin;
30+
import org.gradle.api.plugins.JavaPlugin;
31+
import org.gradle.api.plugins.JavaPluginExtension;
32+
import org.gradle.api.provider.Provider;
33+
import org.gradle.api.provider.ProviderFactory;
34+
import org.gradle.api.tasks.SourceSet;
35+
import org.gradle.api.tasks.SourceSetContainer;
36+
import org.gradle.api.tasks.Sync;
37+
import org.gradle.api.tasks.TaskProvider;
38+
import org.gradle.api.tasks.bundling.Zip;
39+
40+
import java.io.File;
41+
import java.util.Map;
42+
import java.util.concurrent.Callable;
43+
44+
import javax.inject.Inject;
45+
46+
/**
47+
* Common logic for building ES plugins.
48+
* Requires plugin extension to be created before applying
49+
*/
50+
public class BasePluginBuildPlugin implements Plugin<Project> {
51+
52+
public static final String PLUGIN_EXTENSION_NAME = "esplugin";
53+
public static final String BUNDLE_PLUGIN_TASK_NAME = "bundlePlugin";
54+
public static final String EXPLODED_BUNDLE_PLUGIN_TASK_NAME = "explodedBundlePlugin";
55+
public static final String EXPLODED_BUNDLE_CONFIG = "explodedBundleZip";
56+
57+
protected final ProviderFactory providerFactory;
58+
59+
@Inject
60+
public BasePluginBuildPlugin(ProviderFactory providerFactory) {
61+
this.providerFactory = providerFactory;
62+
}
63+
64+
@Override
65+
public void apply(final Project project) {
66+
project.getPluginManager().apply(JavaPlugin.class);
67+
project.getPluginManager().apply(TestClustersPlugin.class);
68+
project.getPluginManager().apply(CompileOnlyResolvePlugin.class);
69+
project.getPluginManager().apply(JarHellPlugin.class);
70+
project.getPluginManager().apply(GradleTestPolicySetupPlugin.class);
71+
72+
var extension = project.getExtensions()
73+
.create(BasePluginBuildPlugin.PLUGIN_EXTENSION_NAME, PluginPropertiesExtension.class, project);
74+
75+
final var bundleTask = createBundleTasks(project, extension);
76+
project.getConfigurations().getByName("default").extendsFrom(project.getConfigurations().getByName("runtimeClasspath"));
77+
78+
// allow running ES with this plugin in the foreground of a build
79+
var testClusters = testClusters(project, TestClustersPlugin.EXTENSION_NAME);
80+
var runCluster = testClusters.register("runTask", c -> {
81+
// TODO: use explodedPlugin here for modules
82+
if (GradleUtils.isModuleProject(project.getPath())) {
83+
c.module(bundleTask.flatMap((Transformer<Provider<RegularFile>, Zip>) zip -> zip.getArchiveFile()));
84+
} else {
85+
c.plugin(bundleTask.flatMap((Transformer<Provider<RegularFile>, Zip>) zip -> zip.getArchiveFile()));
86+
}
87+
});
88+
89+
project.getTasks().register("run", RunTask.class, r -> {
90+
r.useCluster(runCluster);
91+
r.dependsOn(project.getTasks().named(BUNDLE_PLUGIN_TASK_NAME));
92+
});
93+
}
94+
95+
@SuppressWarnings("unchecked")
96+
private static NamedDomainObjectContainer<ElasticsearchCluster> testClusters(Project project, String extensionName) {
97+
return (NamedDomainObjectContainer<ElasticsearchCluster>) project.getExtensions().getByName(extensionName);
98+
}
99+
100+
/**
101+
* Adds bundle tasks which builds the dir and zip containing the plugin jars,
102+
* metadata, properties, and packaging files
103+
*/
104+
private TaskProvider<Zip> createBundleTasks(final Project project, PluginPropertiesExtension extension) {
105+
final var pluginMetadata = project.file("src/main/plugin-metadata");
106+
107+
final var buildProperties = project.getTasks().register("pluginProperties", GeneratePluginPropertiesTask.class, task -> {
108+
task.getPluginName().set(providerFactory.provider(extension::getName));
109+
task.getPluginDescription().set(providerFactory.provider(extension::getDescription));
110+
task.getPluginVersion().set(providerFactory.provider(extension::getVersion));
111+
task.getElasticsearchVersion().set(Version.fromString(VersionProperties.getElasticsearch()).toString());
112+
var javaExtension = project.getExtensions().getByType(JavaPluginExtension.class);
113+
task.getJavaVersion().set(providerFactory.provider(() -> javaExtension.getTargetCompatibility().toString()));
114+
task.getExtendedPlugins().set(providerFactory.provider(extension::getExtendedPlugins));
115+
task.getHasNativeController().set(providerFactory.provider(extension::isHasNativeController));
116+
task.getRequiresKeystore().set(providerFactory.provider(extension::isRequiresKeystore));
117+
task.getIsLicensed().set(providerFactory.provider(extension::isLicensed));
118+
119+
var mainSourceSet = project.getExtensions().getByType(SourceSetContainer.class).getByName(SourceSet.MAIN_SOURCE_SET_NAME);
120+
FileCollection moduleInfoFile = mainSourceSet.getOutput().getAsFileTree().matching(p -> p.include("module-info.class"));
121+
task.getModuleInfoFile().setFrom(moduleInfoFile);
122+
123+
});
124+
// add the plugin properties and metadata to test resources, so unit tests can
125+
// know about the plugin (used by test security code to statically initialize the plugin in unit tests)
126+
var testSourceSet = project.getExtensions().getByType(SourceSetContainer.class).getByName("test");
127+
Map<String, Object> map = Map.of("builtBy", buildProperties);
128+
testSourceSet.getOutput().dir(map, new File(project.getBuildDir(), "generated-resources"));
129+
testSourceSet.getResources().srcDir(pluginMetadata);
130+
131+
var bundleSpec = createBundleSpec(project, pluginMetadata, buildProperties);
132+
extension.setBundleSpec(bundleSpec);
133+
// create the actual bundle task, which zips up all the files for the plugin
134+
final var bundle = project.getTasks().register("bundlePlugin", Zip.class, zip -> zip.with(bundleSpec));
135+
project.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME).configure(task -> task.dependsOn(bundle));
136+
137+
// also make the zip available as a configuration (used when depending on this project)
138+
var configuration = project.getConfigurations().create("zip");
139+
configuration.getAttributes().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.ZIP_TYPE);
140+
project.getArtifacts().add("zip", bundle);
141+
142+
var explodedBundle = project.getTasks().register(EXPLODED_BUNDLE_PLUGIN_TASK_NAME, Sync.class, sync -> {
143+
sync.with(bundleSpec);
144+
sync.into(new File(project.getBuildDir(), "explodedBundle/" + extension.getName()));
145+
});
146+
147+
// also make the exploded bundle available as a configuration (used when depending on this project)
148+
var explodedBundleZip = project.getConfigurations().create(EXPLODED_BUNDLE_CONFIG);
149+
explodedBundleZip.setCanBeResolved(false);
150+
explodedBundleZip.setCanBeConsumed(true);
151+
explodedBundleZip.getAttributes().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE);
152+
project.getArtifacts().add(EXPLODED_BUNDLE_CONFIG, explodedBundle);
153+
return bundle;
154+
}
155+
156+
private static CopySpec createBundleSpec(
157+
Project project,
158+
File pluginMetadata,
159+
TaskProvider<GeneratePluginPropertiesTask> buildProperties
160+
) {
161+
var bundleSpec = project.copySpec();
162+
bundleSpec.from(buildProperties);
163+
bundleSpec.from(pluginMetadata, copySpec -> {
164+
// metadata (eg custom security policy)
165+
// the codebases properties file is only for tests and not needed in production
166+
copySpec.exclude("plugin-security.codebases");
167+
});
168+
bundleSpec.from(
169+
(Callable<TaskProvider<Task>>) () -> project.getPluginManager().hasPlugin("com.github.johnrengelman.shadow")
170+
? project.getTasks().named("shadowJar")
171+
: project.getTasks().named("jar")
172+
);
173+
bundleSpec.from(
174+
project.getConfigurations()
175+
.getByName("runtimeClasspath")
176+
.minus(project.getConfigurations().getByName(CompileOnlyResolvePlugin.RESOLVEABLE_COMPILE_ONLY_CONFIGURATION_NAME))
177+
);
178+
179+
// extra files for the plugin to go into the zip
180+
bundleSpec.from("src/main/packaging");// TODO: move all config/bin/_size/etc into packaging
181+
bundleSpec.from("src/main", copySpec -> {
182+
copySpec.include("config/**");
183+
copySpec.include("bin/**");
184+
});
185+
return bundleSpec;
186+
}
187+
}

0 commit comments

Comments
 (0)