From d079e8b844cc2e16165d389f716ca7d17d832a5a Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 9 Apr 2023 12:38:09 -0700 Subject: [PATCH] Add build-info --- api/build.gradle | 15 +- api/src/main/java/module-info.java | 7 +- build.gradle | 11 +- buildSrc/build.gradle | 21 +++ .../javamodules/ExtraModuleInfoPlugin.java | 54 ++++++ .../ExtraModuleInfoPluginExtension.java | 52 ++++++ .../javamodules/ExtraModuleInfoTransform.java | 164 ++++++++++++++++++ .../transform/javamodules/ModuleInfo.java | 53 ++++++ jackson/src/main/java/module-info.java | 1 + logstash/src/main/java/module-info.java | 3 + 10 files changed, 370 insertions(+), 11 deletions(-) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPlugin.java create mode 100644 buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPluginExtension.java create mode 100644 buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java create mode 100644 buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ModuleInfo.java diff --git a/api/build.gradle b/api/build.gradle index 7412bc44..948ec6c2 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id 'extra-java-module-info' } dependencies { @@ -7,8 +8,16 @@ dependencies { implementation "com.jayway.jsonpath:json-path:$jsonPathVersion" } -tasks.jar { - manifest { - attributes["Automatic-Module-Name"] = "com.tersesystems.echopraxia.api" +// https://docs.gradle.org/current/samples/sample_java_modules_with_transform.html +extraJavaModuleInfo { + module("json-path-$jsonPathVersion" + '.jar', 'com.jayway.jsonpath', jsonPathVersion) { + requires("org.slf4j.slf4j-api") + requires("net.minidev.json-smart") + + exports('com.jayway.jsonpath') + exports('com.jayway.jsonpath.spi.json') + exports('com.jayway.jsonpath.spi.mapper') } + module('slf4j-api-1.7.36.jar', 'org.slf4j.slf4j-api', '1.7.36') + module('json-smart-2.4.10.jar', 'net.minidev.json-smart', '2.4.10') } \ No newline at end of file diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index cc0661e5..9b0555b1 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -1,5 +1,6 @@ module com.tersesystems.echopraxia.api { - requires json.path; - requires org.jetbrains.annotations; + requires static transitive org.jetbrains.annotations; + requires com.jayway.jsonpath; + exports com.tersesystems.echopraxia.api; -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 80bda20e..55f46540 100644 --- a/build.gradle +++ b/build.gradle @@ -4,11 +4,12 @@ plugins { id 'com.diffplug.spotless' version '6.18.0' id "maven-publish" + //id 'org.javamodularity.moduleplugin' version '1.8.12' apply false // https://michaelbfullan.com/the-state-of-the-jpms-in-2022/ // https://github.com/gradlex-org/extra-java-module-info - id("org.gradlex.java-module-dependencies") - id("org.gradlex.java-module-testing") - id("org.gradlex.extra-java-module-info") + //id("org.gradlex.java-module-dependencies") + //id("org.gradlex.java-module-testing") + //id("org.gradlex.extra-java-module-info") } apply from: "gradle/release.gradle" @@ -75,10 +76,10 @@ subprojects { subproj -> dependencies { // https://docs.gradle.org/6.7.1/release-notes.html - compileOnlyApi 'org.jetbrains:annotations:23.0.0' + compileOnly 'org.jetbrains:annotations:24.0.1' // Fix a dependency in log4jbenchmarks - jmhImplementation 'org.jetbrains:annotations:23.0.0' + jmhImplementation 'org.jetbrains:annotations:24.0.1' jmhImplementation 'org.openjdk.jmh:jmh-core:1.35' jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.35' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 00000000..ebb5470f --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java-gradle-plugin' // so we can assign and ID to our plugin +} + +dependencies { + implementation 'org.ow2.asm:asm:8.0.1' +} + +repositories { + mavenCentral() +} + +gradlePlugin { + plugins { + // here we register our plugin with an ID + register("extra-java-module-info") { + id = "extra-java-module-info" + implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin" + } + } +} diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPlugin.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPlugin.java new file mode 100644 index 00000000..48d0b0b2 --- /dev/null +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPlugin.java @@ -0,0 +1,54 @@ +package org.gradle.sample.transform.javamodules; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.plugins.JavaPlugin; + +/** + * Entry point of our plugin that should be applied in the root project. + */ +public class ExtraModuleInfoPlugin implements Plugin { + + @Override + public void apply(Project project) { + // register the plugin extension as 'extraJavaModuleInfo {}' configuration block + ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class); + project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension); + + // setup the transform for all projects in the build + project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension)); + } + + private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) { + Attribute artifactType = Attribute.of("artifactType", String.class); + Attribute javaModule = Attribute.of("javaModule", Boolean.class); + + // compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute + project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all( + c -> c.getAttributes().attribute(javaModule, true)); + + // all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification + project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false); + + // register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter + project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> { + t.parameters(p -> { + p.setModuleInfo(extension.getModuleInfo()); + p.setAutomaticModules(extension.getAutomaticModules()); + }); + t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false); + t.getTo().attribute(artifactType, "jar").attribute(javaModule, true); + }); + } + + private boolean isResolvingJavaPluginConfiguration(Configuration configuration) { + if (!configuration.isCanBeResolved()) { + return false; + } + return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1)) + || configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1)) + || configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1)); + } +} diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPluginExtension.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPluginExtension.java new file mode 100644 index 00000000..d0d4e0fd --- /dev/null +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoPluginExtension.java @@ -0,0 +1,52 @@ +package org.gradle.sample.transform.javamodules; + + +import org.gradle.api.Action; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * A data class to collect all the module information we want to add. + * Here the class is used as extension that can be configured in the build script + * and as input to the ExtraModuleInfoTransform that add the information to Jars. + */ +public class ExtraModuleInfoPluginExtension { + + private final Map moduleInfo = new HashMap<>(); + private final Map automaticModules = new HashMap<>(); + + /** + * Add full module information for a given Jar file. + */ + public void module(String jarName, String moduleName, String moduleVersion) { + module(jarName, moduleName, moduleVersion, null); + } + + /** + * Add full module information, including exported packages and dependencies, for a given Jar file. + */ + public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action conf) { + ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion); + if (conf != null) { + conf.execute(moduleInfo); + } + this.moduleInfo.put(jarName, moduleInfo); + } + + /** + * Add only an automatic module name to a given jar file. + */ + public void automaticModule(String jarName, String moduleName) { + automaticModules.put(jarName, moduleName); + } + + protected Map getModuleInfo() { + return moduleInfo; + } + + protected Map getAutomaticModules() { + return automaticModules; + } +} diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java new file mode 100644 index 00000000..94e6922b --- /dev/null +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java @@ -0,0 +1,164 @@ +package org.gradle.sample.transform.javamodules; + +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ModuleVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.*; +import java.util.Collections; +import java.util.Map; +import java.util.jar.*; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; + +/** + * An artifact transform that applies additional information to Jars without module information. + * The transformation fails the build if a Jar does not contain information and no extra information + * was defined for it. This way we make sure that all Jars are turned into modules. + */ +abstract public class ExtraModuleInfoTransform implements TransformAction { + + public static class Parameter implements TransformParameters, Serializable { + private Map moduleInfo = Collections.emptyMap(); + private Map automaticModules = Collections.emptyMap(); + + @Input + public Map getModuleInfo() { + return moduleInfo; + } + + @Input + public Map getAutomaticModules() { + return automaticModules; + } + + public void setModuleInfo(Map moduleInfo) { + this.moduleInfo = moduleInfo; + } + + public void setAutomaticModules(Map automaticModules) { + this.automaticModules = automaticModules; + } + } + + @InputArtifact + protected abstract Provider getInputArtifact(); + + @Override + public void transform(TransformOutputs outputs) { + Map moduleInfo = getParameters().moduleInfo; + Map automaticModules = getParameters().automaticModules; + File originalJar = getInputArtifact().get().getAsFile(); + String originalJarName = originalJar.getName(); + + if (isModule(originalJar)) { + outputs.file(originalJar); + } else if (moduleInfo.containsKey(originalJarName)) { + addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName)); + } else if (isAutoModule(originalJar)) { + outputs.file(originalJar); + } else if (automaticModules.containsKey(originalJarName)) { + addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName)); + } else { + throw new RuntimeException("Not a module and no mapping defined: " + originalJarName); + } + } + + private boolean isModule(File jar) { + Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class"); + try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) { + boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream); + ZipEntry next = inputStream.getNextEntry(); + while (next != null) { + if ("module-info.class".equals(next.getName())) { + return true; + } + if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) { + return true; + } + next = inputStream.getNextEntry(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return false; + } + + private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) { + Manifest manifest = jarStream.getManifest(); + return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release")); + } + + private boolean isAutoModule(File jar) { + try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) { + return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private File getModuleJar(TransformOutputs outputs, File originalJar) { + return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar"); + } + + private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) { + try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) { + Manifest manifest = inputStream.getManifest(); + manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName); + try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) { + copyEntries(inputStream, outputStream); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) { + try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) { + try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) { + copyEntries(inputStream, outputStream); + outputStream.putNextEntry(new JarEntry("module-info.class")); + outputStream.write(addModuleInfo(moduleInfo)); + outputStream.closeEntry(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException { + JarEntry jarEntry = inputStream.getNextJarEntry(); + while (jarEntry != null) { + outputStream.putNextEntry(jarEntry); + outputStream.write(inputStream.readAllBytes()); + outputStream.closeEntry(); + jarEntry = inputStream.getNextJarEntry(); + } + } + + private static byte[] addModuleInfo(ModuleInfo moduleInfo) { + ClassWriter classWriter = new ClassWriter(0); + classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); + ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion()); + for (String packageName : moduleInfo.getExports()) { + moduleVisitor.visitExport(packageName.replace('.', '/'), 0); + } + moduleVisitor.visitRequire("java.base", 0, null); + for (String requireName : moduleInfo.getRequires()) { + moduleVisitor.visitRequire(requireName, 0, null); + } + for (String requireName : moduleInfo.getRequiresTransitive()) { + moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null); + } + moduleVisitor.visitEnd(); + classWriter.visitEnd(); + return classWriter.toByteArray(); + } +} diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ModuleInfo.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ModuleInfo.java new file mode 100644 index 00000000..9884a911 --- /dev/null +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ModuleInfo.java @@ -0,0 +1,53 @@ +package org.gradle.sample.transform.javamodules; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Data class to hold the information that should be added as module-info.class to an existing Jar file. + */ +public class ModuleInfo implements Serializable { + private String moduleName; + private String moduleVersion; + private List exports = new ArrayList<>(); + private List requires = new ArrayList<>(); + private List requiresTransitive = new ArrayList<>(); + + ModuleInfo(String moduleName, String moduleVersion) { + this.moduleName = moduleName; + this.moduleVersion = moduleVersion; + } + + public void exports(String exports) { + this.exports.add(exports); + } + + public void requires(String requires) { + this.requires.add(requires); + } + + public void requiresTransitive(String requiresTransitive) { + this.requiresTransitive.add(requiresTransitive); + } + + public String getModuleName() { + return moduleName; + } + + protected String getModuleVersion() { + return moduleVersion; + } + + protected List getExports() { + return exports; + } + + protected List getRequires() { + return requires; + } + + protected List getRequiresTransitive() { + return requiresTransitive; + } +} diff --git a/jackson/src/main/java/module-info.java b/jackson/src/main/java/module-info.java index 648dcc44..8c779b35 100644 --- a/jackson/src/main/java/module-info.java +++ b/jackson/src/main/java/module-info.java @@ -1,5 +1,6 @@ module com.tersesystems.echopraxia.jackson { requires com.tersesystems.echopraxia.api; + requires com.fasterxml.jackson.databind; exports com.tersesystems.echopraxia.jackson; } \ No newline at end of file diff --git a/logstash/src/main/java/module-info.java b/logstash/src/main/java/module-info.java index 21e87db4..c8ccdf11 100644 --- a/logstash/src/main/java/module-info.java +++ b/logstash/src/main/java/module-info.java @@ -4,4 +4,7 @@ requires com.tersesystems.echopraxia.jackson; exports com.tersesystems.echopraxia.logstash; + + provides com.tersesystems.echopraxia.api.CoreLoggerProvider + with com.tersesystems.echopraxia.logstash.LogstashLoggerProvider; } \ No newline at end of file