Skip to content

Commit

Permalink
Add build-info
Browse files Browse the repository at this point in the history
  • Loading branch information
wsargent committed Apr 9, 2023
1 parent 505dcbf commit d079e8b
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 11 deletions.
15 changes: 12 additions & 3 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
plugins {
id 'java-library'
id 'extra-java-module-info'
}

dependencies {
// https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path
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')
}
7 changes: 4 additions & 3 deletions api/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'
Expand Down
21 changes: 21 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Project> {

@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<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> 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<? super ModuleInfo> 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<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}

protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}
Original file line number Diff line number Diff line change
@@ -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<ExtraModuleInfoTransform.Parameter> {

public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();

@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}

@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}

public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}

public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}

@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();

@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> 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();
}
}
Loading

0 comments on commit d079e8b

Please sign in to comment.