Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ee7acf3
add task skeleton
jdconrad Apr 27, 2025
619fd63
generate file for test dependencies
jdconrad Apr 28, 2025
1de3d1d
fix policy file
jdconrad Apr 28, 2025
9841168
update
jdconrad Apr 29, 2025
a69872e
convert to json file isntead of properties file
jdconrad Apr 29, 2025
73af578
fix dependency link
jdconrad May 1, 2025
40cc22f
start of jar entry lookup
jdconrad May 4, 2025
06522bd
Merge remote-tracking branch 'upstream/main' into eut2
jdconrad May 4, 2025
65c7d72
generate file with class to module info
jdconrad May 4, 2025
a9984fe
add module info to build info file
jdconrad May 5, 2025
445d3bf
added files to resources
jdconrad May 5, 2025
2c1a15f
protect task for plugins with no source
jdconrad May 5, 2025
73e6379
separate into separate plugin
jdconrad May 6, 2025
036fed8
add output for server
jdconrad May 6, 2025
fdf526e
remove todo
jdconrad May 6, 2025
7d7f8cd
Merge branch 'main' into eut2
jdconrad May 6, 2025
5f4c870
clean up
jdconrad May 7, 2025
729559b
add module info for directory
jdconrad May 7, 2025
9867a39
add module inference when plugin is not modular
jdconrad May 7, 2025
c4764b9
Merge branch 'main' into eut2
jdconrad May 7, 2025
ff80fda
Merge branch 'main' into eut2
jdconrad May 7, 2025
f6585fc
[CI] Auto commit changes from spotless
May 7, 2025
927f9bf
Merge branch 'main' into eut2
jdconrad May 8, 2025
32cadde
Merge branch 'main' into eut2
jdconrad May 8, 2025
887e600
add comments
jdconrad May 8, 2025
9949f91
Merge remote-tracking branch 'upstream/main' into eut2
prdoyle May 13, 2025
e57cd37
First pass addressing PR comments
prdoyle May 13, 2025
444da3d
WIP non-working TestBuildInfoPluginFuncTest
prdoyle May 13, 2025
dd56d50
TestBuildInfoPluginFuncTest
prdoyle May 13, 2025
854df10
Tweaks
prdoyle May 14, 2025
931deb9
TestBuildInfoPluginFuncTest check the file's contents
prdoyle May 14, 2025
461af45
Merge branch 'main' into eut2
jdconrad May 20, 2025
33ce238
Use Files.walkFileTree
prdoyle May 21, 2025
460ea40
some response to pr comments
jdconrad May 21, 2025
8833e6e
Merge remote-tracking branch 'origin/eut2' into eut2
jdconrad May 21, 2025
1266847
update gradle values to use callable
jdconrad May 21, 2025
a38969c
Split out extractModuleNameFromJar into smaller steps
prdoyle May 21, 2025
0df276a
fix test
jdconrad May 21, 2025
69742cf
Merge remote-tracking branch 'origin/eut2' into eut2
jdconrad May 21, 2025
1d1c27d
Merge branch 'main' into eut2
jdconrad May 21, 2025
75496f4
remove extraneous callable
jdconrad May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-tools/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ gradlePlugin {
id = 'elasticsearch.stable-esplugin'
implementationClass = 'org.elasticsearch.gradle.plugin.StablePluginBuildPlugin'
}
testBuildInfo {
id = 'elasticsearch.test-build-info'
implementationClass = 'org.elasticsearch.gradle.test.TestBuildInfoPlugin'
}
javaRestTest {
id = 'elasticsearch.java-rest-test'
implementationClass = 'org.elasticsearch.gradle.test.JavaRestTestPlugin'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ private TaskProvider<Zip> createBundleTasks(final Project project, PluginPropert
task.getIsLicensed().set(providerFactory.provider(extension::isLicensed));

var mainSourceSet = project.getExtensions().getByType(SourceSetContainer.class).getByName(SourceSet.MAIN_SOURCE_SET_NAME);
FileCollection moduleInfoFile = mainSourceSet.getOutput().getAsFileTree().matching(p -> p.include("module-info.class"));
FileCollection moduleInfoFile = mainSourceSet.getOutput()
.getClassesDirs()
.getAsFileTree()
.matching(p -> p.include("module-info.class"));
task.getModuleInfoFile().setFrom(moduleInfoFile);

});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.gradle.plugin;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
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.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

/**
* This task generates a file with a class to module mapping
* used to imitate modular behavior during unit tests so
* entitlements can lookup correct policies.
*/
public abstract class GenerateTestBuildInfoTask extends DefaultTask {

public static final String DESCRIPTION = "generates plugin test dependencies file";

public GenerateTestBuildInfoTask() {
setDescription(DESCRIPTION);
}

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

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

@InputFiles
public abstract Property<FileCollection> getCodeLocations();

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

@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();

@TaskAction
public void generatePropertiesFile() throws IOException {
Map<String, String> classesToModules = buildClassesToModules();

Path outputDirectory = getOutputDirectory().get().getAsFile().toPath();
Files.createDirectories(outputDirectory);
Path outputFile = outputDirectory.resolve(getOutputFileName().get());

try (var writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) {
writer.write("{\n");

writer.write(" \"name\": \"");
writer.write(getComponentName().get());
writer.write("\",\n");

writer.write(" \"locations\": [\n");
if (classesToModules.isEmpty() == false) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : classesToModules.entrySet()) {
sb.append(" {\n");
sb.append(" \"class\": \"");
sb.append(entry.getKey());
sb.append("\",\n \"module\": \"");
sb.append(entry.getValue());
sb.append("\"\n },\n");
}
writer.write(sb.substring(0, sb.length() - 2));
}
writer.write("\n ]\n}\n");
}
}

private Map<String, String> buildClassesToModules() throws IOException {
Map<String, String> classesToModules = new HashMap<>();
for (File file : getCodeLocations().get().getFiles()) {
if (file.exists()) {
if (file.getName().endsWith(".jar")) {
extractFromJar(file, classesToModules);
} else if (file.isDirectory()) {
extractFromDirectory(file, classesToModules);
} else {
throw new IllegalArgumentException("unrecognized classpath entry: " + file);
}
}
}
return classesToModules;
}

private void extractFromJar(File file, Map<String, String> classesToModules) throws IOException {
try (JarFile jarFile = new JarFile(file)) {
String className = extractClassNameFromJar(jarFile);
String moduleName = extractModuleNameFromJar(file, jarFile);

if (className != null) {
classesToModules.put(className, moduleName);
}
}
}

private String extractClassNameFromJar(JarFile jarFile) {
return jarFile.stream()
.filter(
je -> je.getName().startsWith("META-INF") == false
&& je.getName().equals("module-info.class") == false
&& je.getName().endsWith(".class")
)
.findFirst()
.map(ZipEntry::getName)
.orElse(null);
}

private String extractModuleNameFromJar(File file, JarFile jarFile) throws IOException {
String moduleName = null;

if (jarFile.isMultiRelease()) {
List<Integer> versions = jarFile.stream()
.filter(je -> je.getName().startsWith("META-INF/versions/") && je.getName().endsWith("/module-info.class"))
.map(je -> Integer.parseInt(je.getName().substring(18, je.getName().length() - 18)))
.toList();
versions = new ArrayList<>(versions);
versions.sort(Integer::compareTo);
versions = versions.reversed();
int major = Runtime.version().version().get(0);
StringBuilder path = new StringBuilder("META-INF/versions/");
for (int version : versions) {
if (version <= major) {
path.append(version);
break;
}
}
if (path.length() > 18) {
path.append("/module-info.class");
JarEntry moduleEntry = jarFile.getJarEntry(path.toString());
if (moduleEntry != null) {
try (InputStream inputStream = jarFile.getInputStream(moduleEntry)) {
moduleName = extractModuleNameFromModuleInfo(inputStream);
}
}
}
}

if (moduleName == null) {
JarEntry moduleEntry = jarFile.getJarEntry("module-info.class");
if (moduleEntry != null) {
try (InputStream inputStream = jarFile.getInputStream(moduleEntry)) {
moduleName = extractModuleNameFromModuleInfo(inputStream);
}
}
}

if (moduleName == null) {
JarEntry manifestEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
if (manifestEntry != null) {
try (InputStream inputStream = jarFile.getInputStream(manifestEntry)) {
Manifest manifest = new Manifest(inputStream);
String amn = manifest.getMainAttributes().getValue("Automatic-Module-Name");
if (amn != null) {
moduleName = amn;
}
}
}
}

if (moduleName == null) {
String jn = file.getName().substring(0, file.getName().length() - 4);
Matcher matcher = Pattern.compile("-(\\d+(\\.|$))").matcher(jn);
if (matcher.find()) {
jn = jn.substring(0, matcher.start());
}
jn = jn.replaceAll("[^A-Za-z0-9]", ".");
moduleName = jn;
}

return moduleName;
}

private void extractFromDirectory(File file, Map<String, String> classesToModules) throws IOException {
String className = extractClassNameFromDirectory(file);
String moduleName = extractModuleNameFromDirectory(file);
if (className != null && moduleName != null) {
classesToModules.put(className, moduleName);
}
}

private String extractClassNameFromDirectory(File file) {
List<File> files = new ArrayList<>(List.of(file));
while (files.isEmpty() == false) {
File find = files.removeFirst();
if (find.exists()) {
if (find.getName().endsWith(".class")
&& find.getName().equals("module-info.class") == false
&& find.getName().contains("$") == false) {
return find.getAbsolutePath().substring(file.getAbsolutePath().length() + 1);
} else if (find.isDirectory()) {
files.addAll(Arrays.asList(find.listFiles()));
}
}

}
return null;
}

private String extractModuleNameFromDirectory(File file) throws IOException {
List<File> files = new ArrayList<>(List.of(file));
while (files.isEmpty() == false) {
File find = files.removeFirst();
if (find.exists()) {
if (find.getName().equals("module-info.class")) {
try (InputStream inputStream = new FileInputStream(find)) {
return extractModuleNameFromModuleInfo(inputStream);
}
} else if (find.isDirectory()) {
files.addAll(Arrays.asList(find.listFiles()));
}
}
}
return getModuleName().isPresent() ? getModuleName().get() : null;
}

private String extractModuleNameFromModuleInfo(InputStream inputStream) throws IOException {
String[] moduleName = new String[1];
ClassReader cr = new ClassReader(inputStream);
cr.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public ModuleVisitor visitModule(String name, int access, String version) {
moduleName[0] = name;
return super.visitModule(name, access, version);
}
}, Opcodes.ASM9);
return moduleName[0];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
package org.elasticsearch.gradle.plugin;

import org.elasticsearch.gradle.VersionProperties;
import org.elasticsearch.gradle.test.TestBuildInfoPlugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.jvm.tasks.Jar;
import org.gradle.language.jvm.tasks.ProcessResources;

import javax.inject.Inject;

Expand All @@ -33,6 +36,7 @@ public PluginBuildPlugin(ProviderFactory providerFactory) {
@Override
public void apply(final Project project) {
project.getPluginManager().apply(BasePluginBuildPlugin.class);
project.getPluginManager().apply(TestBuildInfoPlugin.class);

var dependencies = project.getDependencies();
dependencies.add("compileOnly", "org.elasticsearch:elasticsearch:" + VersionProperties.getElasticsearch());
Expand All @@ -51,6 +55,27 @@ public void apply(final Project project) {
task.getOutputFile().set(file);
});

}
project.getTasks().withType(GenerateTestBuildInfoTask.class).named("generateTestBuildInfo").configure(task -> {
var jarTask = project.getTasks().withType(Jar.class).named("jar").get();
String moduleName = (String) jarTask.getManifest().getAttributes().get("Automatic-Module-Name");
if (moduleName == null) {
moduleName = jarTask.getArchiveBaseName().getOrNull();
}
if (moduleName != null) {
task.getModuleName().set(moduleName);
}
var propertiesExtension = project.getExtensions().getByType(PluginPropertiesExtension.class);
task.getComponentName().set(providerFactory.provider(propertiesExtension::getName));
task.getOutputFileName().set("plugin-test-build-info.json");
});

project.getTasks().withType(ProcessResources.class).named("processResources").configure(task -> {
var componentName = project.getExtensions().getByType(PluginPropertiesExtension.class).getName();
var pluginProperties = project.getTasks().withType(GeneratePluginPropertiesTask.class).named("pluginProperties");
task.into("META-INF/es-plugins/" + componentName + "/", copy -> {
copy.from(pluginProperties);
copy.from(project.getLayout().getProjectDirectory().file("src/main/plugin-metadata/entitlement-policy.yaml"));
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.gradle.test;

import org.elasticsearch.gradle.dependencies.CompileOnlyResolvePlugin;
import org.elasticsearch.gradle.plugin.GenerateTestBuildInfoTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.language.jvm.tasks.ProcessResources;

import javax.inject.Inject;

/**
* This plugin configures the {@link GenerateTestBuildInfoTask} task
* with customizations for component name and output file name coming
* from the source using the plugin (server or ES plugin).
*/
public class TestBuildInfoPlugin implements Plugin<Project> {

protected final ProviderFactory providerFactory;

@Inject
public TestBuildInfoPlugin(ProviderFactory providerFactory) {
this.providerFactory = providerFactory;
}

@Override
public void apply(Project project) {
var testBuildInfoTask = project.getTasks().register("generateTestBuildInfo", GenerateTestBuildInfoTask.class, task -> {
var sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
task.getCodeLocations()
.set(
project.getConfigurations()
.getByName("runtimeClasspath")
.minus(project.getConfigurations().getByName(CompileOnlyResolvePlugin.RESOLVEABLE_COMPILE_ONLY_CONFIGURATION_NAME))
.plus(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs())
);
Provider<Directory> directory = project.getLayout().getBuildDirectory().dir("generated-build-info");
task.getOutputDirectory().set(directory);
});

project.getTasks().withType(ProcessResources.class).named("processResources").configure(task -> {
task.into("META-INF", copy -> copy.from(testBuildInfoTask));
});
}
}
Loading