Add plugin for creating MRJARs (#104883)

This commit adds an elasticsearch gradle plugin which sets up a project
to build an MRJAR. By applying the plugin, the src dir is checked for
any directories matching mainXX where XX is the java version number.
A source set is automatically setup, an appropriate compiler tied to
that source set, and it's output placed in the correct part of the final
jar. Additionally, the sourceset allows use of preview features in that
verison of Java, and the preview bits are stripped from the resulting
class files.
This commit is contained in:
Ryan Ernst 2024-01-29 21:06:19 -08:00 committed by GitHub
parent 9c086de30e
commit d5e727e362
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 133 additions and 1 deletions

View File

@ -139,6 +139,10 @@ gradlePlugin {
id = 'elasticsearch.java-module'
implementationClass = 'org.elasticsearch.gradle.internal.ElasticsearchJavaModulePathPlugin'
}
mrjar {
id = 'elasticsearch.mrjar'
implementationClass = 'org.elasticsearch.gradle.internal.MrjarPlugin'
}
releaseTools {
id = 'elasticsearch.release-tools'
implementationClass = 'org.elasticsearch.gradle.internal.release.ReleaseToolsPlugin'

View File

@ -0,0 +1,127 @@
/*
* 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.internal;
import org.elasticsearch.gradle.util.GradleUtils;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.jvm.tasks.Jar;
import org.gradle.jvm.toolchain.JavaLanguageVersion;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.inject.Inject;
import static org.objectweb.asm.Opcodes.V_PREVIEW;
public class MrjarPlugin implements Plugin<Project> {
private static final Pattern MRJAR_SOURCESET_PATTERN = Pattern.compile("main(\\d{2})");
private final JavaToolchainService javaToolchains;
@Inject
MrjarPlugin(JavaToolchainService javaToolchains) {
this.javaToolchains = javaToolchains;
}
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaLibraryPlugin.class);
var javaExtension = project.getExtensions().getByType(JavaPluginExtension.class);
var srcDir = project.getProjectDir().toPath().resolve("src");
try (var subdirStream = Files.list(srcDir)) {
for (Path sourceset : subdirStream.toList()) {
assert Files.isDirectory(sourceset);
String sourcesetName = sourceset.getFileName().toString();
Matcher sourcesetMatcher = MRJAR_SOURCESET_PATTERN.matcher(sourcesetName);
if (sourcesetMatcher.matches()) {
int javaVersion = Integer.parseInt(sourcesetMatcher.group(1));
addMrjarSourceset(project, javaExtension, sourcesetName, javaVersion);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void addMrjarSourceset(Project project, JavaPluginExtension javaExtension, String sourcesetName, int javaVersion) {
SourceSet sourceSet = javaExtension.getSourceSets().maybeCreate(sourcesetName);
GradleUtils.extendSourceSet(project, SourceSet.MAIN_SOURCE_SET_NAME, sourcesetName);
project.getTasks().withType(Jar.class).named(JavaPlugin.JAR_TASK_NAME).configure(jarTask -> {
jarTask.into("META-INF/versions/" + javaVersion, copySpec -> copySpec.from(sourceSet.getOutput()));
jarTask.manifest(manifest -> { manifest.attributes(Map.of("Multi-Release", "true")); });
});
project.getTasks().withType(JavaCompile.class).named(sourceSet.getCompileJavaTaskName()).configure(compileTask -> {
compileTask.getJavaCompiler()
.set(javaToolchains.compilerFor(spec -> { spec.getLanguageVersion().set(JavaLanguageVersion.of(javaVersion)); }));
compileTask.setSourceCompatibility(Integer.toString(javaVersion));
CompileOptions compileOptions = compileTask.getOptions();
compileOptions.getRelease().set(javaVersion);
compileOptions.getCompilerArgs().add("--enable-preview");
compileOptions.getCompilerArgs().add("-Xlint:-preview");
compileTask.doLast(t -> { stripPreviewFromFiles(compileTask.getDestinationDirectory().getAsFile().get().toPath()); });
});
}
private static void stripPreviewFromFiles(Path compileDir) {
try (Stream<Path> fileStream = Files.walk(compileDir)) {
fileStream.filter(p -> p.toString().endsWith(".class")).forEach(MrjarPlugin::maybeStripPreview);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static void maybeStripPreview(Path file) {
ClassWriter classWriter = null;
try (var in = Files.newInputStream(file)) {
ClassReader classReader = new ClassReader(in);
ClassNode classNode = new ClassNode();
classReader.accept(classNode, 0);
if ((classNode.version & V_PREVIEW) != 0) {
classNode.version = classNode.version & ~V_PREVIEW;
classWriter = new ClassWriter(0);
classNode.accept(classWriter);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (classWriter != null) {
try (var out = Files.newOutputStream(file)) {
out.write(classWriter.toByteArray());
} catch (IOException e) {
throw new org.gradle.api.UncheckedIOException(e);
}
}
}
}

View File

@ -1,5 +1,5 @@
[versions]
asm = "9.4"
asm = "9.6"
jackson = "2.15.0"
junit5 = "5.8.1"
spock = "2.1-groovy-3.0"

View File

@ -7,6 +7,7 @@
*/
apply plugin: 'elasticsearch.publish'
apply plugin: 'elasticsearch.mrjar'
dependencies {
// This dependency is used only by :libs:core for null-checking interop with other tools