Skip to content

Commit

Permalink
Add optional module name to plugin properties (#86016)
Browse files Browse the repository at this point in the history
In preparation for modularizing Elasticsearch server, each plugin needs
a way to declare itself as modular. When modularized, the main class of
a plugin must be found in a named module. This commit adds a module name
property to the plugin properties template and gradle support for
setting it, as well as automatically inferring the module name when a
module-info.java exists in the source a plugin.
  • Loading branch information
rjernst committed Apr 25, 2022
1 parent 773d62c commit 0131d9f
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 81 deletions.
2 changes: 2 additions & 0 deletions build-tools/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ dependencies {
api 'org.apache.commons:commons-compress:1.21'
api 'org.apache.ant:ant:1.10.8'
api 'commons-io:commons-io:2.2'
implementation 'org.ow2.asm:asm-tree:9.2'
implementation 'org.ow2.asm:asm:9.2'

testFixturesApi "junit:junit:${versions.getProperty('junit')}"
testFixturesApi "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.getProperty('randomizedrunner')}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@

package org.elasticsearch.gradle.plugin

import org.elasticsearch.gradle.VersionProperties
import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest
import org.gradle.testkit.runner.TaskOutcome

import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors

class PluginBuildPluginFuncTest extends AbstractGradleFuncTest {

def "can assemble plugin via #taskName"() {
given:
buildFile << """plugins {
id 'elasticsearch.esplugin'
}
esplugin {
esplugin {
description = 'test plugin'
classname = 'com.acme.plugin.TestPlugin'
}
// for testing purposes only
configurations.compileOnly.dependencies.clear()
"""
Expand All @@ -47,13 +52,13 @@ class PluginBuildPluginFuncTest extends AbstractGradleFuncTest {
buildFile << """plugins {
id 'elasticsearch.esplugin'
}
esplugin {
esplugin {
name = 'sample-plugin'
description = 'test plugin'
classname = 'com.acme.plugin.TestPlugin'
}
// for testing purposes only
configurations.compileOnly.dependencies.clear()
"""
Expand All @@ -63,11 +68,11 @@ class PluginBuildPluginFuncTest extends AbstractGradleFuncTest {
configurations {
consume
}
dependencies {
consume project(path:':', configuration:'${PluginBuildPlugin.EXPLODED_BUNDLE_CONFIG}')
}
tasks.register("resolveModule", Copy) {
from configurations.consume
into "build/resolved"
Expand All @@ -82,4 +87,77 @@ class PluginBuildPluginFuncTest extends AbstractGradleFuncTest {
file("module-consumer/build/resolved/sample-plugin.jar").exists()
file("module-consumer/build/resolved/plugin-descriptor.properties").exists()
}

def "can build plugin properties"() {
given:
buildFile << """plugins {
id 'elasticsearch.esplugin'
}
version = '1.2.3'
esplugin {
name = 'myplugin'
description = 'test plugin'
classname = 'com.acme.plugin.TestPlugin'
}
"""


when:
def result = gradleRunner(":pluginProperties").build()
def props = getPluginProperties()

then:
result.task(":pluginProperties").outcome == TaskOutcome.SUCCESS
props.get("name") == "myplugin"
props.get("version") == "1.2.3"
props.get("description") == "test plugin"
props.get("classname") == "com.acme.plugin.TestPlugin"
props.get("modulename") == ""
props.get("type") == "isolated"
props.get("java.version") == Integer.toString(Runtime.version().feature())
props.get("elasticsearch.version") == VersionProperties.elasticsearchVersion.toString()
props.get("extended.plugins") == ""
props.get("has.native.controller") == "false"
props.size() == 10
}

def "module name is inferred by plugin properties"() {
given:
buildFile << """plugins {
id 'elasticsearch.esplugin'
}
esplugin {
name = 'myplugin'
description = 'test plugin'
classname = 'com.acme.plugin.TestPlugin'
}
// for testing purposes only
configurations.compileOnly.dependencies.clear()
"""
file('src/main/java/module-info.java') << """
module org.test.plugin {
}
"""

when:
def result = gradleRunner(":pluginProperties").build()
def props = getPluginProperties()

then:
result.task(":pluginProperties").outcome == TaskOutcome.SUCCESS
props.get("modulename") == "org.test.plugin"
}

Map<String, String> getPluginProperties() {
Path propsFile = file("build/generated-descriptor/plugin-descriptor.properties").toPath();
Properties rawProps = new Properties()
try (var inputStream = Files.newInputStream(propsFile)) {
rawProps.load(inputStream)
}
return rawProps.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.gradle.plugin;

import groovy.text.SimpleTemplateEngine;
import groovy.text.Template;

import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

import javax.inject.Inject;

public abstract class GeneratePluginPropertiesTask extends DefaultTask {

private static final String PROPERTIES_FILENAME = "plugin-descriptor.properties";

@Inject
public GeneratePluginPropertiesTask(ProjectLayout projectLayout) {
setDescription("Generate " + PROPERTIES_FILENAME);
getOutputFile().convention(projectLayout.getBuildDirectory().file("generated-descriptor/" + PROPERTIES_FILENAME));
}

@Input
public abstract Property<String> getPluginName();

@Input
public abstract Property<String> getPluginDescription();

@Input
public abstract Property<String> getPluginVersion();

@Input
public abstract Property<String> getElasticsearchVersion();

@Input
public abstract Property<String> getJavaVersion();

@Optional
@Input
public abstract Property<String> getClassname();

@Input
public abstract ListProperty<String> getExtendedPlugins();

@Input
public abstract Property<Boolean> getHasNativeController();

@Input
public abstract Property<Boolean> getRequiresKeystore();

@Input
public abstract Property<PluginType> getPluginType();

@Input
public abstract Property<String> getJavaOpts();

@Input
public abstract Property<Boolean> getIsLicensed();

@InputFiles
public abstract ConfigurableFileCollection getModuleInfoFile();

@OutputFile
public abstract RegularFileProperty getOutputFile();

@TaskAction
public void generatePropertiesFile() throws IOException {
PluginType pluginType = getPluginType().get();
String classname = getClassname().getOrElse("");
if (pluginType.equals(PluginType.BOOTSTRAP) == false && classname.isEmpty()) {
throw new InvalidUserDataException("classname is a required setting for esplugin");
}

Map<String, Object> props = new HashMap<>();
props.put("name", getPluginName().get());
props.put("description", getPluginDescription().get());
props.put("version", getPluginVersion().get());
props.put("elasticsearchVersion", getElasticsearchVersion().get());
props.put("javaVersion", getJavaVersion().get());
props.put("classname", classname);
props.put("extendedPlugins", String.join(",", getExtendedPlugins().get()));
props.put("hasNativeController", getHasNativeController().get());
props.put("requiresKeystore", getRequiresKeystore().get());
props.put("type", pluginType.toString());
props.put("javaOpts", getJavaOpts().get());
props.put("licensed", getIsLicensed().get());
props.put("modulename", findModuleName());

SimpleTemplateEngine engine = new SimpleTemplateEngine();
Path outputFile = getOutputFile().get().getAsFile().toPath();
Files.createDirectories(outputFile.getParent());
try (
var inputStream = GeneratePluginPropertiesTask.class.getResourceAsStream("/" + PROPERTIES_FILENAME);
var reader = new BufferedReader(new InputStreamReader(inputStream));
var writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)
) {
Template template = engine.createTemplate(reader);
template.make(props).writeTo(writer);
}
}

private String findModuleName() {
if (getModuleInfoFile().isEmpty()) {
return "";
}
Path moduleInfoSource = getModuleInfoFile().getSingleFile().toPath();
ClassNode visitor = new ClassNode();
try (var inputStream = Files.newInputStream(moduleInfoSource)) {
new ClassReader(inputStream).accept(visitor, 0);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return visitor.module.name;
}
}

0 comments on commit 0131d9f

Please sign in to comment.