modernized JPMS check and dependency export plugins
All checks were successful
CI / build (push) Successful in 1m57s
All checks were successful
CI / build (push) Successful in 1m57s
This commit is contained in:
@@ -8,6 +8,9 @@ import org.gradle.api.plugins.ObjectConfigurationAction;
|
|||||||
import org.gradle.api.provider.Provider;
|
import org.gradle.api.provider.Provider;
|
||||||
|
|
||||||
public class DependencyExportPlugin implements Plugin<Project> {
|
public class DependencyExportPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
public static final String DEPENDENCY_EXPORT_GROUP = "dependency-export";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(Project project) {
|
public void apply(Project project) {
|
||||||
project.apply(new Action<ObjectConfigurationAction>() {
|
project.apply(new Action<ObjectConfigurationAction>() {
|
||||||
|
@@ -24,6 +24,7 @@ import org.gradle.api.plugins.JavaPlugin;
|
|||||||
import org.gradle.api.plugins.JavaPluginConvention;
|
import org.gradle.api.plugins.JavaPluginConvention;
|
||||||
import org.gradle.api.provider.Property;
|
import org.gradle.api.provider.Property;
|
||||||
import org.gradle.api.provider.Provider;
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.Classpath;
|
||||||
import org.gradle.api.tasks.Input;
|
import org.gradle.api.tasks.Input;
|
||||||
import org.gradle.api.tasks.InputFiles;
|
import org.gradle.api.tasks.InputFiles;
|
||||||
import org.gradle.api.tasks.Internal;
|
import org.gradle.api.tasks.Internal;
|
||||||
@@ -49,6 +50,8 @@ import java.util.stream.Collector;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static net.woggioni.gradle.dependency.export.DependencyExportPlugin.DEPENDENCY_EXPORT_GROUP;
|
||||||
|
|
||||||
public class ExportDependencies extends DefaultTask {
|
public class ExportDependencies extends DefaultTask {
|
||||||
|
|
||||||
@Getter(onMethod_ = { @Input })
|
@Getter(onMethod_ = { @Input })
|
||||||
@@ -74,6 +77,7 @@ public class ExportDependencies extends DefaultTask {
|
|||||||
private final JavaPluginConvention javaPluginConvention;
|
private final JavaPluginConvention javaPluginConvention;
|
||||||
|
|
||||||
@InputFiles
|
@InputFiles
|
||||||
|
@Classpath
|
||||||
public Provider<FileCollection> getConfigurationFiles() {
|
public Provider<FileCollection> getConfigurationFiles() {
|
||||||
return configurationName.map(this::fetchConfiguration);
|
return configurationName.map(this::fetchConfiguration);
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,7 @@ public class ExportDependencies extends DefaultTask {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ExportDependencies(ObjectFactory objects) {
|
public ExportDependencies(ObjectFactory objects) {
|
||||||
|
setGroup(DEPENDENCY_EXPORT_GROUP);
|
||||||
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
|
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
|
||||||
configurationName = objects.property(String.class).convention(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
|
configurationName = objects.property(String.class).convention(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
|
||||||
Provider<File> defaultOutputFileProvider =
|
Provider<File> defaultOutputFileProvider =
|
||||||
|
@@ -10,29 +10,37 @@ import org.gradle.api.model.ObjectFactory;
|
|||||||
import org.gradle.api.plugins.JavaPluginConvention;
|
import org.gradle.api.plugins.JavaPluginConvention;
|
||||||
import org.gradle.api.provider.Property;
|
import org.gradle.api.provider.Property;
|
||||||
import org.gradle.api.provider.Provider;
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.CacheableTask;
|
||||||
import org.gradle.api.tasks.Input;
|
import org.gradle.api.tasks.Input;
|
||||||
import org.gradle.api.tasks.InputFile;
|
import org.gradle.api.tasks.InputFile;
|
||||||
import org.gradle.api.tasks.Internal;
|
import org.gradle.api.tasks.Internal;
|
||||||
import org.gradle.api.tasks.Optional;
|
import org.gradle.api.tasks.Optional;
|
||||||
import org.gradle.api.tasks.OutputFile;
|
import org.gradle.api.tasks.OutputFile;
|
||||||
|
import org.gradle.api.tasks.PathSensitive;
|
||||||
|
import org.gradle.api.tasks.PathSensitivity;
|
||||||
import org.gradle.api.tasks.TaskAction;
|
import org.gradle.api.tasks.TaskAction;
|
||||||
import org.gradle.api.tasks.options.Option;
|
import org.gradle.api.tasks.options.Option;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static net.woggioni.gradle.dependency.export.DependencyExportPlugin.DEPENDENCY_EXPORT_GROUP;
|
||||||
|
|
||||||
|
@CacheableTask
|
||||||
public class RenderDependencies extends DefaultTask {
|
public class RenderDependencies extends DefaultTask {
|
||||||
|
|
||||||
@Getter(onMethod_ = { @InputFile })
|
@Getter(onMethod_ = {@InputFile, @PathSensitive(PathSensitivity.NONE)})
|
||||||
private Provider<File> sourceFile;
|
private Provider<File> sourceFile;
|
||||||
|
|
||||||
@Getter(onMethod_ = { @Input})
|
@Getter(onMethod_ = {@Input})
|
||||||
private final Property<String> format;
|
private final Property<String> format;
|
||||||
|
|
||||||
@Getter(onMethod_ = { @Input })
|
@Getter(onMethod_ = {@Input})
|
||||||
private final Property<String> graphvizExecutable;
|
private final Property<String> graphvizExecutable;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -45,6 +53,7 @@ public class RenderDependencies extends DefaultTask {
|
|||||||
return outputFile.map(RegularFile::getAsFile).map(File::getAbsolutePath).getOrNull();
|
return outputFile.map(RegularFile::getAsFile).map(File::getAbsolutePath).getOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Optional
|
||||||
@OutputFile
|
@OutputFile
|
||||||
public Provider<File> getResult() {
|
public Provider<File> getResult() {
|
||||||
return outputFile.map(RegularFile::getAsFile);
|
return outputFile.map(RegularFile::getAsFile);
|
||||||
@@ -64,33 +73,46 @@ public class RenderDependencies extends DefaultTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setExportTask(Provider<ExportDependencies> taskProvider) {
|
public void setExportTask(Provider<ExportDependencies> taskProvider) {
|
||||||
|
dependsOn(taskProvider);
|
||||||
sourceFile = taskProvider.flatMap(ExportDependencies::getResult);
|
sourceFile = taskProvider.flatMap(ExportDependencies::getResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RenderDependencies(ObjectFactory objects) {
|
public RenderDependencies(ObjectFactory objects) {
|
||||||
|
setGroup(DEPENDENCY_EXPORT_GROUP);
|
||||||
sourceFile = objects.property(File.class);
|
sourceFile = objects.property(File.class);
|
||||||
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
|
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
|
||||||
format = objects.property(String.class).convention("xlib");
|
format = objects.property(String.class).convention("xlib");
|
||||||
graphvizExecutable = objects.property(String.class).convention("dot");
|
graphvizExecutable = objects.property(String.class).convention("dot");
|
||||||
Provider<File> defaultOutputFileProvider =
|
Provider<File> defaultOutputFileProvider =
|
||||||
getProject().provider(() -> new File(javaPluginConvention.getDocsDir(), "renderedDependencies"));
|
getProject().provider(() -> new File(javaPluginConvention.getDocsDir(), "renderedDependencies"));
|
||||||
outputFile = objects.fileProperty().convention(getProject().getLayout().file(defaultOutputFileProvider));
|
outputFile = objects.fileProperty().convention(getProject().getLayout().file(defaultOutputFileProvider)
|
||||||
|
.zip(format, (file, type) -> Objects.equals("xlib", type) ? null : file));
|
||||||
|
getOutputs().upToDateWhen(t -> outputFile.isPresent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@TaskAction
|
@TaskAction
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
void run() {
|
void run() {
|
||||||
Path destination = outputFile
|
java.util.Optional<Path> destination = java.util.Optional.of(
|
||||||
.map(RegularFile::getAsFile)
|
outputFile
|
||||||
.map(File::toPath)
|
.map(RegularFile::getAsFile)
|
||||||
.get();
|
.map(File::toPath)
|
||||||
List<String> cmd = Arrays.asList(
|
)
|
||||||
|
.filter(Provider::isPresent)
|
||||||
|
.map(Provider::get);
|
||||||
|
|
||||||
|
List<String> cmd = new ArrayList<>(Arrays.asList(
|
||||||
graphvizExecutable.get(),
|
graphvizExecutable.get(),
|
||||||
"-T" + format.get(),
|
"-T" + format.get()
|
||||||
"-o" + destination,
|
));
|
||||||
sourceFile.get().toString()
|
|
||||||
);
|
if (destination.isPresent()) {
|
||||||
|
cmd.add("-o");
|
||||||
|
cmd.add(destination.get().toString());
|
||||||
|
}
|
||||||
|
cmd.add(sourceFile.get().toString());
|
||||||
|
|
||||||
int returnCode = new ProcessBuilder(cmd).inheritIO().start().waitFor();
|
int returnCode = new ProcessBuilder(cmd).inheritIO().start().waitFor();
|
||||||
if (returnCode != 0) {
|
if (returnCode != 0) {
|
||||||
throw new GradleException("Error invoking graphviz");
|
throw new GradleException("Error invoking graphviz");
|
||||||
|
@@ -7,6 +7,7 @@ import org.gradle.api.file.FileCollection;
|
|||||||
import org.gradle.api.file.ProjectLayout;
|
import org.gradle.api.file.ProjectLayout;
|
||||||
import org.gradle.api.file.RegularFileProperty;
|
import org.gradle.api.file.RegularFileProperty;
|
||||||
import org.gradle.api.logging.Logger;
|
import org.gradle.api.logging.Logger;
|
||||||
|
import org.gradle.api.logging.Logging;
|
||||||
import org.gradle.api.plugins.BasePluginExtension;
|
import org.gradle.api.plugins.BasePluginExtension;
|
||||||
import org.gradle.api.plugins.ExtensionContainer;
|
import org.gradle.api.plugins.ExtensionContainer;
|
||||||
import org.gradle.api.plugins.JavaApplication;
|
import org.gradle.api.plugins.JavaApplication;
|
||||||
@@ -48,6 +49,8 @@ public abstract class NativeImageTask extends Exec {
|
|||||||
public abstract Property<Boolean> getBuildStaticImage();
|
public abstract Property<Boolean> getBuildStaticImage();
|
||||||
@Input
|
@Input
|
||||||
public abstract Property<Boolean> getEnableFallback();
|
public abstract Property<Boolean> getEnableFallback();
|
||||||
|
@Input
|
||||||
|
public abstract Property<Boolean> getLinkAtBuildTime();
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
public abstract Property<String> getMainClass();
|
public abstract Property<String> getMainClass();
|
||||||
@@ -61,15 +64,17 @@ public abstract class NativeImageTask extends Exec {
|
|||||||
|
|
||||||
@OutputFile
|
@OutputFile
|
||||||
protected abstract RegularFileProperty getOutputFile();
|
protected abstract RegularFileProperty getOutputFile();
|
||||||
private final Logger logger;
|
|
||||||
|
private static final Logger log = Logging.getLogger(NativeImageTask.class);
|
||||||
|
|
||||||
public NativeImageTask() {
|
public NativeImageTask() {
|
||||||
Project project = getProject();
|
Project project = getProject();
|
||||||
logger = project.getLogger();
|
|
||||||
setGroup(GRAALVM_TASK_GROUP);
|
setGroup(GRAALVM_TASK_GROUP);
|
||||||
setDescription("Create a native image of the application using GraalVM");
|
setDescription("Create a native image of the application using GraalVM");
|
||||||
getUseMusl().convention(false);
|
getUseMusl().convention(false);
|
||||||
getBuildStaticImage().convention(false);
|
getBuildStaticImage().convention(false);
|
||||||
getEnableFallback().convention(false);
|
getEnableFallback().convention(false);
|
||||||
|
getLinkAtBuildTime().convention(false);
|
||||||
ExtensionContainer ext = project.getExtensions();
|
ExtensionContainer ext = project.getExtensions();
|
||||||
JavaApplication javaApplication = ext.findByType(JavaApplication.class);
|
JavaApplication javaApplication = ext.findByType(JavaApplication.class);
|
||||||
if(!Objects.isNull(javaApplication)) {
|
if(!Objects.isNull(javaApplication)) {
|
||||||
@@ -111,6 +116,9 @@ public abstract class NativeImageTask extends Exec {
|
|||||||
if(getUseMusl().get()) {
|
if(getUseMusl().get()) {
|
||||||
result.add("--libc=musl");
|
result.add("--libc=musl");
|
||||||
}
|
}
|
||||||
|
if(getLinkAtBuildTime().get()) {
|
||||||
|
result.add("--link-at-build-time");
|
||||||
|
}
|
||||||
JavaModuleDetector javaModuleDetector = getJavaModuleDetector();
|
JavaModuleDetector javaModuleDetector = getJavaModuleDetector();
|
||||||
boolean useJpms = getMainModule().isPresent();
|
boolean useJpms = getMainModule().isPresent();
|
||||||
FileCollection classpath = getClasspath().get();
|
FileCollection classpath = getClasspath().get();
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
lys.catalog.version=2025.01.31
|
lys.catalog.version=2025.02.05
|
||||||
version.myGradlePlugins=2025.02.05
|
version.myGradlePlugins=2025.02.08
|
||||||
version.gradle=8.12
|
version.gradle=8.12
|
||||||
|
|
||||||
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||||
|
@@ -1,216 +1,43 @@
|
|||||||
package net.woggioni.gradle.jpms.check
|
package net.woggioni.gradle.jpms.check
|
||||||
|
|
||||||
import groovy.json.JsonBuilder
|
|
||||||
import groovy.transform.Canonical
|
|
||||||
import groovy.transform.CompileStatic
|
import groovy.transform.CompileStatic
|
||||||
import groovy.xml.MarkupBuilder
|
|
||||||
import org.gradle.api.GradleException
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.artifacts.Configuration
|
import org.gradle.api.file.RegularFile
|
||||||
import org.gradle.api.artifacts.result.ResolvedArtifactResult
|
import org.gradle.api.plugins.JavaPlugin
|
||||||
|
import org.gradle.api.plugins.ReportingBasePlugin
|
||||||
import java.nio.file.Files
|
import org.gradle.api.reporting.ReportingExtension
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.jar.JarFile
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
import java.util.stream.Stream
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
class JPMSCheckPlugin implements Plugin<Project> {
|
class JPMSCheckPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
@Canonical
|
|
||||||
@CompileStatic
|
|
||||||
private class CheckResult {
|
|
||||||
ResolvedArtifactResult dep
|
|
||||||
String automaticModuleName
|
|
||||||
boolean multiReleaseJar
|
|
||||||
boolean moduleInfo
|
|
||||||
|
|
||||||
boolean getJpmsFriendly() {
|
|
||||||
return automaticModuleName != null || moduleInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean equals(Object other) {
|
|
||||||
if(other == null) {
|
|
||||||
return false
|
|
||||||
} else if(other.class == CheckResult.class) {
|
|
||||||
return dep?.id?.componentIdentifier == ((CheckResult) other).dep?.id?.componentIdentifier
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int hashCode() {
|
|
||||||
return dep.id.componentIdentifier.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CompileStatic
|
|
||||||
private Stream<CheckResult> computeResults(Stream<ResolvedArtifactResult> artifacts) {
|
|
||||||
return artifacts.filter { ResolvedArtifactResult res ->
|
|
||||||
res.file.exists() && res.file.name.endsWith(".jar")
|
|
||||||
}.<CheckResult>map { resolvedArtifact ->
|
|
||||||
JarFile jarFile = new JarFile(resolvedArtifact.file).with {
|
|
||||||
if (it.isMultiRelease()) {
|
|
||||||
new JarFile(
|
|
||||||
resolvedArtifact.file,
|
|
||||||
false,
|
|
||||||
ZipFile.OPEN_READ,
|
|
||||||
Runtime.version()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String automaticModuleName = jarFile.manifest?.with {it.mainAttributes.getValue("Automatic-Module-Name") }
|
|
||||||
def moduleInfoEntry = jarFile.getJarEntry("module-info.class")
|
|
||||||
new CheckResult(
|
|
||||||
resolvedArtifact,
|
|
||||||
automaticModuleName,
|
|
||||||
jarFile.isMultiRelease(),
|
|
||||||
moduleInfoEntry != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createHtmlReport(Project project, Stream<CheckResult> checkResults, Writer writer) {
|
|
||||||
def builder = new MarkupBuilder(writer)
|
|
||||||
int friendly = 0
|
|
||||||
int total = 0
|
|
||||||
def results = checkResults.peek { CheckResult res ->
|
|
||||||
total += 1
|
|
||||||
if(res.jpmsFriendly) friendly += 1
|
|
||||||
}.collect(Collectors.toList())
|
|
||||||
builder.html {
|
|
||||||
head {
|
|
||||||
meta name: "viewport", content: "width=device-width, initial-scale=1"
|
|
||||||
InputStream resourceStream = getClass().classLoader.getResourceAsStream('net/woggioni/plugins/jpms/check/github-markdown.css')
|
|
||||||
resourceStream.withReader { Reader reader ->
|
|
||||||
style reader.text
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
article(class: 'markdown-body') {
|
|
||||||
h1 "Project ${project.group}:${project.name}:${project.version}", style: "text-align: center;"
|
|
||||||
div {
|
|
||||||
table {
|
|
||||||
thead {
|
|
||||||
tr {
|
|
||||||
th "JPMS friendly"
|
|
||||||
th "Not JPMS friendly", colspan: 2
|
|
||||||
th "Total", colspan: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
tr {
|
|
||||||
td friendly, style: "text-align: center;"
|
|
||||||
td total - friendly, style: "text-align: center;", colspan: 2
|
|
||||||
td total, style: "text-align: center;", colspan: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thead {
|
|
||||||
th "Name"
|
|
||||||
th "Multi-release jar"
|
|
||||||
th "Automatic-Module-Name"
|
|
||||||
th "Module descriptor"
|
|
||||||
th "JPMS friendly"
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
results.forEach {res ->
|
|
||||||
String color = res.jpmsFriendly ? "#dfd" : "fdd"
|
|
||||||
tr(style: "background-color:$color;") {
|
|
||||||
td res.dep.id.displayName
|
|
||||||
td style: "text-align: center;", res.multiReleaseJar ? "✓" : "✕"
|
|
||||||
td style: "text-align: center;", res.automaticModuleName ?: "n/a"
|
|
||||||
td style: "text-align: center;", res.moduleInfo ? "✓" : "✕"
|
|
||||||
td style: "text-align: center;", res.jpmsFriendly ? "✓" : "✕"
|
|
||||||
}
|
|
||||||
total += 1
|
|
||||||
if(res.jpmsFriendly) friendly += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CompileStatic
|
|
||||||
private createJsonReport(Stream<CheckResult> checkResults, Writer writer) {
|
|
||||||
def builder = new JsonBuilder()
|
|
||||||
builder (checkResults.map {
|
|
||||||
[
|
|
||||||
name: it.dep.id.componentIdentifier.displayName,
|
|
||||||
automaticModuleName: it.automaticModuleName,
|
|
||||||
isMultiReleaseJar: it.multiReleaseJar,
|
|
||||||
hasModuleInfo: it.moduleInfo,
|
|
||||||
jpmsFriendly: it.jpmsFriendly
|
|
||||||
]
|
|
||||||
}.collect(Collectors.toList()))
|
|
||||||
builder.writeTo(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@CompileStatic
|
@CompileStatic
|
||||||
void apply(Project project) {
|
void apply(Project project) {
|
||||||
project.tasks.register("jpms-check") {task ->
|
project.pluginManager.apply(ReportingBasePlugin.class)
|
||||||
boolean recursive = project.properties["jpms-check.recursive"]?.with(Boolean.&parseBoolean) ?: false
|
project.tasks.register("jpms-check", JPMSCheckTask) {task ->
|
||||||
String cfgName = project.properties["jpms-check.configurationName"] ?: "default"
|
ReportingExtension reporting = project.extensions.getByType(ReportingExtension.class)
|
||||||
String outputFormat = project.properties["jpms-check.outputFormat"] ?: "html"
|
boolean recursive = project.properties["jpms-check.recursive"]?.with(Object.&toString)?.with(Boolean.&parseBoolean) ?: false
|
||||||
Path outputFile = project.properties["jpms-check.outputFile"]?.with {
|
String cfgName = project.properties["jpms-check.configurationName"] ?: JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME
|
||||||
Paths.get(it as String)
|
OutputFormat defaultOutputFormat = (project.properties["jpms-check.outputFormat"]
|
||||||
} ?: with {
|
?.with(Object.&toString)
|
||||||
|
?.with(OutputFormat.&valueOf)
|
||||||
|
?: OutputFormat.html)
|
||||||
|
task.getConfigurationName().convention(cfgName)
|
||||||
|
task.getRecursive().convention(recursive)
|
||||||
|
task.outputFormat.convention(defaultOutputFormat)
|
||||||
|
task.getOutputFile().convention(reporting.baseDirectory.zip(task.getOutputFormat(), { dir, outputFormat ->
|
||||||
|
RegularFile result = null
|
||||||
switch(outputFormat) {
|
switch(outputFormat) {
|
||||||
case "html":
|
case OutputFormat.html:
|
||||||
Paths.get(project.buildDir.path, "jpms-report.html")
|
result = dir.file( "jpms-report.html")
|
||||||
break
|
break
|
||||||
case "json":
|
case OutputFormat.json:
|
||||||
Paths.get(project.buildDir.path, "jpms-report.json")
|
result = dir.file( "jpms-report.json")
|
||||||
break
|
break
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported output format: $outputFormat")
|
|
||||||
}
|
}
|
||||||
}
|
result
|
||||||
task.doLast {
|
}))
|
||||||
Stream<Project> projects = Stream.of(project)
|
|
||||||
if(recursive) {
|
|
||||||
projects = Stream.concat(projects, project.subprojects.stream())
|
|
||||||
}
|
|
||||||
Set<CheckResult> results = projects.flatMap {
|
|
||||||
Configuration requestedConfiguration = (project.configurations.<Configuration>find { Configuration cfg ->
|
|
||||||
cfg.canBeResolved && cfg.name == cfgName
|
|
||||||
} ?: {
|
|
||||||
def resolvableConfigurations = "[" + project.configurations
|
|
||||||
.grep { Configuration cfg -> cfg.canBeResolved }
|
|
||||||
.collect { "'${it.name}'" }
|
|
||||||
.join(",") + "]"
|
|
||||||
throw new GradleException("Configuration '$cfgName' doesn't exist or cannot be resolved, " +
|
|
||||||
"resolvable configurations in this project are " + resolvableConfigurations)
|
|
||||||
}) as Configuration
|
|
||||||
computeResults(requestedConfiguration.incoming.artifacts.artifacts.stream())
|
|
||||||
}.collect(Collectors.toSet())
|
|
||||||
Files.createDirectories(outputFile.parent)
|
|
||||||
Files.newBufferedWriter(outputFile).withWriter {
|
|
||||||
Stream<CheckResult> resultStream = results.stream().sorted(Comparator.<CheckResult, String>comparing { CheckResult res ->
|
|
||||||
res.dep.id.componentIdentifier.displayName
|
|
||||||
})
|
|
||||||
switch(outputFormat) {
|
|
||||||
case "html":
|
|
||||||
createHtmlReport(project, resultStream, it)
|
|
||||||
break
|
|
||||||
case "json":
|
|
||||||
createJsonReport(resultStream, it)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported output format: $outputFormat")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,213 @@
|
|||||||
|
package net.woggioni.gradle.jpms.check
|
||||||
|
|
||||||
|
import groovy.json.JsonBuilder
|
||||||
|
import groovy.transform.Canonical
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import groovy.xml.MarkupBuilder
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.Configuration
|
||||||
|
import org.gradle.api.artifacts.result.ResolvedArtifactResult
|
||||||
|
import org.gradle.api.file.RegularFileProperty
|
||||||
|
import org.gradle.api.provider.Property
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.OutputFile
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import java.util.stream.Stream
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
abstract class JPMSCheckTask extends DefaultTask {
|
||||||
|
|
||||||
|
@Input
|
||||||
|
abstract Property<String> getConfigurationName()
|
||||||
|
|
||||||
|
@Input
|
||||||
|
abstract Property<Boolean> getRecursive()
|
||||||
|
|
||||||
|
@Input
|
||||||
|
abstract Property<OutputFormat> getOutputFormat()
|
||||||
|
|
||||||
|
@OutputFile
|
||||||
|
abstract RegularFileProperty getOutputFile()
|
||||||
|
|
||||||
|
@Canonical
|
||||||
|
@CompileStatic
|
||||||
|
private class CheckResult {
|
||||||
|
ResolvedArtifactResult dep
|
||||||
|
String automaticModuleName
|
||||||
|
boolean multiReleaseJar
|
||||||
|
boolean moduleInfo
|
||||||
|
|
||||||
|
boolean getJpmsFriendly() {
|
||||||
|
return automaticModuleName != null || moduleInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean equals(Object other) {
|
||||||
|
if(other == null) {
|
||||||
|
return false
|
||||||
|
} else if(other.class == CheckResult.class) {
|
||||||
|
return dep?.id?.componentIdentifier == ((CheckResult) other).dep?.id?.componentIdentifier
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int hashCode() {
|
||||||
|
return dep.id.componentIdentifier.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
private Stream<CheckResult> computeResults(Stream<ResolvedArtifactResult> artifacts) {
|
||||||
|
return artifacts.filter { ResolvedArtifactResult res ->
|
||||||
|
res.file.exists() && res.file.name.endsWith(".jar")
|
||||||
|
}.<CheckResult>map { resolvedArtifact ->
|
||||||
|
JarFile jarFile = new JarFile(resolvedArtifact.file).with {
|
||||||
|
if (it.isMultiRelease()) {
|
||||||
|
new JarFile(
|
||||||
|
resolvedArtifact.file,
|
||||||
|
false,
|
||||||
|
ZipFile.OPEN_READ,
|
||||||
|
Runtime.version()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String automaticModuleName = jarFile.manifest?.with {it.mainAttributes.getValue("Automatic-Module-Name") }
|
||||||
|
def moduleInfoEntry = jarFile.getJarEntry("module-info.class")
|
||||||
|
new CheckResult(
|
||||||
|
resolvedArtifact,
|
||||||
|
automaticModuleName,
|
||||||
|
jarFile.isMultiRelease(),
|
||||||
|
moduleInfoEntry != null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createHtmlReport(Project project, Stream<CheckResult> checkResults, Writer writer) {
|
||||||
|
def builder = new MarkupBuilder(writer)
|
||||||
|
int friendly = 0
|
||||||
|
int total = 0
|
||||||
|
def results = checkResults.peek { CheckResult res ->
|
||||||
|
total += 1
|
||||||
|
if(res.jpmsFriendly) friendly += 1
|
||||||
|
}.collect(Collectors.toList())
|
||||||
|
builder.html {
|
||||||
|
head {
|
||||||
|
meta name: "viewport", content: "width=device-width, initial-scale=1"
|
||||||
|
InputStream resourceStream = getClass().classLoader.getResourceAsStream('net/woggioni/plugins/jpms/check/github-markdown.css')
|
||||||
|
resourceStream.withReader { Reader reader ->
|
||||||
|
style reader.text
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
article(class: 'markdown-body') {
|
||||||
|
h1 "Project ${project.group}:${project.name}:${project.version}", style: "text-align: center;"
|
||||||
|
div {
|
||||||
|
table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th "JPMS friendly"
|
||||||
|
th "Not JPMS friendly", colspan: 2
|
||||||
|
th "Total", colspan: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
td friendly, style: "text-align: center;"
|
||||||
|
td total - friendly, style: "text-align: center;", colspan: 2
|
||||||
|
td total, style: "text-align: center;", colspan: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
th "Name"
|
||||||
|
th "Multi-release jar"
|
||||||
|
th "Automatic-Module-Name"
|
||||||
|
th "Module descriptor"
|
||||||
|
th "JPMS friendly"
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
results.forEach {res ->
|
||||||
|
String color = res.jpmsFriendly ? "#dfd" : "fdd"
|
||||||
|
tr(style: "background-color:$color;") {
|
||||||
|
td res.dep.id.displayName
|
||||||
|
td style: "text-align: center;", res.multiReleaseJar ? "✓" : "✕"
|
||||||
|
td style: "text-align: center;", res.automaticModuleName ?: "n/a"
|
||||||
|
td style: "text-align: center;", res.moduleInfo ? "✓" : "✕"
|
||||||
|
td style: "text-align: center;", res.jpmsFriendly ? "✓" : "✕"
|
||||||
|
}
|
||||||
|
total += 1
|
||||||
|
if(res.jpmsFriendly) friendly += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
private createJsonReport(Stream<CheckResult> checkResults, Writer writer) {
|
||||||
|
def builder = new JsonBuilder()
|
||||||
|
builder (checkResults.map {
|
||||||
|
[
|
||||||
|
name: it.dep.id.componentIdentifier.displayName,
|
||||||
|
automaticModuleName: it.automaticModuleName,
|
||||||
|
isMultiReleaseJar: it.multiReleaseJar,
|
||||||
|
hasModuleInfo: it.moduleInfo,
|
||||||
|
jpmsFriendly: it.jpmsFriendly
|
||||||
|
]
|
||||||
|
}.collect(Collectors.toList()))
|
||||||
|
builder.writeTo(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
@CompileStatic
|
||||||
|
def createReport() {
|
||||||
|
String cfgName = configurationName.get()
|
||||||
|
Path outputFile = outputFile.get().asFile.toPath()
|
||||||
|
Stream<Project> projects = Stream.of(project)
|
||||||
|
if(recursive.get()) {
|
||||||
|
projects = Stream.concat(projects, project.subprojects.stream())
|
||||||
|
}
|
||||||
|
Set<CheckResult> results = projects.flatMap {
|
||||||
|
Configuration requestedConfiguration = (project.configurations.<Configuration>find { Configuration cfg ->
|
||||||
|
cfg.canBeResolved && cfg.name == cfgName
|
||||||
|
} ?: {
|
||||||
|
def resolvableConfigurations = "[" + project.configurations
|
||||||
|
.grep { Configuration cfg -> cfg.canBeResolved }
|
||||||
|
.collect { "'${it.name}'" }
|
||||||
|
.join(",") + "]"
|
||||||
|
throw new GradleException("Configuration '$cfgName' doesn't exist or cannot be resolved, " +
|
||||||
|
"resolvable configurations in this project are " + resolvableConfigurations)
|
||||||
|
}) as Configuration
|
||||||
|
computeResults(requestedConfiguration.incoming.artifacts.artifacts.stream())
|
||||||
|
}.collect(Collectors.toSet())
|
||||||
|
Files.createDirectories(outputFile.parent)
|
||||||
|
Files.newBufferedWriter(outputFile).withWriter {
|
||||||
|
Stream<CheckResult> resultStream = results.stream().sorted(Comparator.<CheckResult, String>comparing { CheckResult res ->
|
||||||
|
res.dep.id.componentIdentifier.displayName
|
||||||
|
})
|
||||||
|
switch(outputFormat.get()) {
|
||||||
|
case OutputFormat.html:
|
||||||
|
createHtmlReport(project, resultStream, it)
|
||||||
|
break
|
||||||
|
case OutputFormat.json:
|
||||||
|
createJsonReport(resultStream, it)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported output format: $outputFormat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package net.woggioni.gradle.jpms.check
|
||||||
|
|
||||||
|
enum OutputFormat {
|
||||||
|
html, json
|
||||||
|
}
|
Reference in New Issue
Block a user