Compare commits

...
30 Commits
Author SHA1 Message Date
woggioni c1e3e960b1 tmp 2026-06-05 09:27:32 +08:00
woggioni 85ca793893 removed "setup Gradle" build step
CI / build (push) Successful in 3m56s
2026-02-23 18:55:40 +08:00
woggioni 0ccffb0642 fixed default finalguard exclusion prefix
CI / build (push) Has been cancelled
2026-02-23 18:53:21 +08:00
woggioni 1fe7ce65f6 Excluded generated sources from finalguard plugin
CI / build (push) Successful in 5m58s
Also added optional list of exclusion paths
2026-02-19 22:32:14 +08:00
woggioni c5dcee554e updated finalguard to use plugin arguments instead of system properties 2026-02-18 08:31:09 +08:00
woggioni 91cf489630 updated build to Gradle 9 and JDK 25 2026-02-17 14:54:01 +08:00
woggioni b6c4cca2e7 added special handling for abstract method parameters in finalguard plugin
CI / build (push) Successful in 3m31s
2025-11-28 21:07:21 +08:00
woggioni a6e3d692a0 fixed finalguard when Gradle uses java compiler API directly without forking
CI / build (push) Successful in 3m50s
2025-11-28 17:05:42 +08:00
woggioni 8f5c21a03e temporarily downgraded Junit version to keep suporting Java 8
CI / build (push) Successful in 3m4s
2025-11-19 01:12:38 +08:00
woggioni 9dcadec69f added record support to finalguard plugin
CI / build (push) Failing after 1m18s
2025-11-19 00:50:23 +08:00
woggioni d78b8d443e Finalguard promoted to beta
CI / build (push) Successful in 3m39s
2025-11-18 20:33:59 +08:00
woggioni c7d96f3d83 added finalguard plugin in aplha state
CI / build (push) Successful in 3m17s
2025-11-18 12:47:57 +08:00
woggioni 3f6627465c added jdeps plugin
CI / build (push) Successful in 2m26s
2025-04-15 22:04:50 +08:00
woggioni 0d32fb5ba9 added tar distribution to jlink plugin
CI / build (push) Successful in 2m45s
2025-04-15 14:12:20 +08:00
woggioni 8d9861ae75 added toolchain support to NativeImageconfigurationTask
CI / build (push) Successful in 1m32s
2025-03-08 14:36:47 +08:00
woggioni 97df729677 added Gradle toolchain support to NativeImage task
CI / build (push) Successful in 1m39s
2025-03-03 17:57:57 +08:00
woggioni d580161d01 added nativeCompilerPath property to GraalVM NativeImage task
CI / build (push) Successful in 1m49s
2025-03-03 13:29:16 +08:00
woggioni 2b978ddf1c made NativeImageTask cacheable in native image plugin
CI / build (push) Successful in 1m27s
2025-02-28 09:26:50 +08:00
woggioni 0d5865e638 improved graalvm plugin
CI / build (push) Successful in 1m26s
2025-02-25 16:39:14 +08:00
woggioni 04d50e5a52 modernized JPMS check and dependency export plugins
CI / build (push) Successful in 1m57s
2025-02-08 00:48:20 +08:00
woggioni e0d8628188 added compressionLevel and bindServices properties to the jlink task
CI / build (push) Successful in 2m22s
2025-02-05 21:37:05 +08:00
woggioni 4565a626d6 fixed gitea build
CI / build (push) Successful in 1m48s
2025-01-24 16:47:26 +08:00
woggioni 99f1fd16ab added JPMS support to native image plugin
CI / build (push) Has been cancelled
2025-01-24 16:45:52 +08:00
woggioni 5c16f1bc13 updated Gradle to 8.12
CI / build (push) Successful in 1m32s
2025-01-09 16:25:59 +08:00
woggioni 0c3f08bfd5 made Sambal plugin compatible with Gradle configuration cache
CI / build (push) Successful in 2m12s
2024-12-29 22:09:39 +08:00
woggioni b6fa9f2948 fixed configuration cache issue in Sambal plugin
CI / build (push) Successful in 1m53s
2024-12-28 15:04:59 +08:00
woggioni d1af8ef942 fixed gitea build
CI / build (push) Successful in 2m32s
2024-12-08 21:58:22 +08:00
woggioni 6146868f37 update gradle version
CI / build (push) Waiting to run
2024-12-08 21:57:09 +08:00
woggioni 9c361d0419 fully migrated to gitea
CI / build (push) Successful in 38s
2024-04-04 07:04:27 +08:00
woggioni bffa4f1f56 migrated to gitea
CI / build (push) Successful in 38s
2024-04-04 06:12:23 +08:00
57 changed files with 2052 additions and 436 deletions
+14
View File
@@ -0,0 +1,14 @@
name: CI
on:
push:
branches: [ master ]
jobs:
build:
runs-on: woryzen
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Execute Gradle build
env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
run: ./gradlew build publish
+30 -20
View File
@@ -1,23 +1,35 @@
subprojects { subproject ->
apply plugin: 'java-library'
apply plugin: 'maven-publish'
group = "net.woggioni.gradle"
version = getProperty('version.myGradlePlugins')
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.named(JavaPlugin.JAR_TASK_NAME, Jar.class, {
manifest {
attributes.put(java.util.jar.Attributes.Name.SPECIFICATION_TITLE.toString(), subproject.getName());
attributes.put(java.util.jar.Attributes.Name.SPECIFICATION_VERSION.toString(), subproject.getVersion());
}
})
int javaVersion
if(subproject.path == ':osgi-app' || subproject.path == ':multi-release-jar') {
javaVersion = 11
} else {
javaVersion = 8
}
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.release = javaVersion
options.compilerArgs << '-parameters'
if(subproject.path != ':finalguard:finalguard-javac-plugin') {
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.release = javaVersion
options.compilerArgs << '-parameters'
}
}
pluginManager.withPlugin('groovy') {
@@ -27,12 +39,6 @@ subprojects { subproject ->
}
repositories {
maven {
url = woggioniMavenRepositoryUrl
content {
includeGroup 'net.woggioni'
}
}
mavenCentral()
}
@@ -41,25 +47,29 @@ subprojects { subproject ->
add(conf, [group: "org.projectlombok", name: "lombok", version: catalog.versions.lombok.get()])
}
add("testImplementation", catalog.junit.jupiter.api)
add("testImplementation", catalog.junit.jupiter.params)
add("testRuntimeOnly", catalog.junit.jupiter.engine)
add("testRuntimeOnly", catalog.junit.platform.launcher)
add("testImplementation", gradleTestKit())
}
tasks.named("test", Test) {
tasks.withType(Test) {
useJUnitPlatform()
}
}
childProjects.forEach { name, child ->
child.with {
apply plugin: 'maven-publish'
publishing {
repositories {
maven {
name = "Gitea"
url = uri("https://gitea.woggioni.net/api/packages/woggioni/maven")
group = "net.woggioni.gradle"
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "token ${System.getenv()["PUBLISHER_TOKEN"]}"
}
publishing {
repositories {
maven {
url = woggioniMavenRepositoryUrl
authentication {
header(HttpHeaderAuthentication)
}
}
}
@@ -8,6 +8,9 @@ import org.gradle.api.plugins.ObjectConfigurationAction;
import org.gradle.api.provider.Provider;
public class DependencyExportPlugin implements Plugin<Project> {
public static final String DEPENDENCY_EXPORT_GROUP = "dependency-export";
@Override
public void apply(Project project) {
project.apply(new Action<ObjectConfigurationAction>() {
@@ -20,10 +20,13 @@ import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.ReportingBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
@@ -49,6 +52,8 @@ import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.woggioni.gradle.dependency.export.DependencyExportPlugin.DEPENDENCY_EXPORT_GROUP;
public class ExportDependencies extends DefaultTask {
@Getter(onMethod_ = { @Input })
@@ -71,9 +76,8 @@ public class ExportDependencies extends DefaultTask {
@Getter(onMethod_ = { @Input })
private final Property<Boolean> showArtifacts;
private final JavaPluginConvention javaPluginConvention;
@InputFiles
@Classpath
public Provider<FileCollection> getConfigurationFiles() {
return configurationName.map(this::fetchConfiguration);
}
@@ -96,11 +100,11 @@ public class ExportDependencies extends DefaultTask {
@Inject
public ExportDependencies(ObjectFactory objects) {
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
setGroup(DEPENDENCY_EXPORT_GROUP);
configurationName = objects.property(String.class).convention(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
Provider<File> defaultOutputFileProvider =
getProject().provider(() -> new File(javaPluginConvention.getDocsDir(), "dependencies.dot"));
outputFile = objects.fileProperty().convention(getProject().getLayout().file(defaultOutputFileProvider));
final JavaPluginExtension javaPluginExtension = getProject().getExtensions().findByType(JavaPluginExtension.class);
final Provider<RegularFile> defaultOutputFileProvider = javaPluginExtension.getDocsDir().file("dependencies.dot");
outputFile = objects.fileProperty().convention(defaultOutputFileProvider);
showArtifacts = objects.property(Boolean.class).convention(false);
}
@@ -7,32 +7,40 @@ import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
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.options.Option;
import javax.inject.Inject;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
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 {
@Getter(onMethod_ = { @InputFile })
@Getter(onMethod_ = {@InputFile, @PathSensitive(PathSensitivity.NONE)})
private Provider<File> sourceFile;
@Getter(onMethod_ = { @Input})
@Getter(onMethod_ = {@Input})
private final Property<String> format;
@Getter(onMethod_ = { @Input })
@Getter(onMethod_ = {@Input})
private final Property<String> graphvizExecutable;
@Getter
@@ -45,13 +53,12 @@ public class RenderDependencies extends DefaultTask {
return outputFile.map(RegularFile::getAsFile).map(File::getAbsolutePath).getOrNull();
}
@Optional
@OutputFile
public Provider<File> getResult() {
return outputFile.map(RegularFile::getAsFile);
}
private final JavaPluginConvention javaPluginConvention;
@Option(option = "output", description = "Set the output file name")
public void setOutputCli(String outputFile) {
Provider<File> fileProvider = getProject().provider(() -> new File(outputFile));
@@ -64,33 +71,45 @@ public class RenderDependencies extends DefaultTask {
}
public void setExportTask(Provider<ExportDependencies> taskProvider) {
dependsOn(taskProvider);
sourceFile = taskProvider.flatMap(ExportDependencies::getResult);
}
@Inject
public RenderDependencies(ObjectFactory objects) {
setGroup(DEPENDENCY_EXPORT_GROUP);
sourceFile = objects.property(File.class);
javaPluginConvention = getProject().getConvention().getPlugin(JavaPluginConvention.class);
format = objects.property(String.class).convention("xlib");
graphvizExecutable = objects.property(String.class).convention("dot");
Provider<File> defaultOutputFileProvider =
getProject().provider(() -> new File(javaPluginConvention.getDocsDir(), "renderedDependencies"));
outputFile = objects.fileProperty().convention(getProject().getLayout().file(defaultOutputFileProvider));
final JavaPluginExtension javaPluginExtension = getProject().getExtensions().findByType(JavaPluginExtension.class);
final Provider<RegularFile> defaultOutputFileProvider = javaPluginExtension.getDocsDir().file("renderedDependencies");
outputFile = objects.fileProperty().convention(defaultOutputFileProvider
.zip(format, (file, type) -> Objects.equals("xlib", type) ? null : file));
getOutputs().upToDateWhen(t -> outputFile.isPresent());
}
@TaskAction
@SneakyThrows
void run() {
Path destination = outputFile
.map(RegularFile::getAsFile)
.map(File::toPath)
.get();
List<String> cmd = Arrays.asList(
java.util.Optional<Path> destination = java.util.Optional.of(
outputFile
.map(RegularFile::getAsFile)
.map(File::toPath)
)
.filter(Provider::isPresent)
.map(Provider::get);
List<String> cmd = new ArrayList<>(Arrays.asList(
graphvizExecutable.get(),
"-T" + format.get(),
"-o" + destination,
sourceFile.get().toString()
);
"-T" + format.get()
));
if (destination.isPresent()) {
cmd.add("-o");
cmd.add(destination.get().toString());
}
cmd.add(sourceFile.get().toString());
int returnCode = new ProcessBuilder(cmd).inheritIO().start().waitFor();
if (returnCode != 0) {
throw new GradleException("Error invoking graphviz");
@@ -4,7 +4,7 @@ plugins {
}
repositories {
jcenter()
mavenCentral()
mavenLocal()
}
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
create("FinalGuardPlugin") {
id = 'net.woggioni.gradle.finalguard'
implementationClass = "net.woggioni.gradle.finalguard.FinalGuardPlugin"
}
}
}
@@ -0,0 +1,51 @@
plugins {
id 'java-library'
}
group = "net.woggioni.finalguard"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
java {
sourceCompatibility(JavaVersion.VERSION_1_8.toString())
targetCompatibility(JavaVersion.VERSION_1_8.toString())
modularity.inferModulePath = false
}
tasks.named(org.gradle.api.plugins.JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile.class) {
options.compilerArgs << '-parameters'
}
configurations {
testCompileClasspath {
attributes {
attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
}
testRuntimeClasspath {
attributes {
attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25)
}
}
}
test {
def testCompilationClassPath = sourceSets["main"].output.classesDirs.files +
sourceSets["main"].runtimeClasspath.files +
sourceSets["test"].resources.srcDirs
systemProperty("test.compilation.classpath",
String.join(File.pathSeparator, testCompilationClassPath.collect { it.toString() }))
}
publishing {
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}
@@ -0,0 +1,311 @@
package net.woggioni.finalguard;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class FinalGuardPlugin implements Plugin {
public static final String DEFAULT_LEVEL_KEY = "default.level";
public static final String EXCLUDE_KEY = "exclude";
enum VariableType {
LOCAL_VAR("local.variable.level"),
METHOD_PARAM("method.param.level"),
LOOP_PARAM("for.param.level"),
TRY_WITH_PARAM("try.param.level"),
CATCH_PARAM("catch.param.level"),
LAMBDA_PARAM("lambda.param.level"),
ABSTRACT_METHOD_PARAM("abstract.method.param.level");
private final String argKey;
VariableType(final String argKey) {
this.argKey = argKey;
}
public String getArgKey() {
return argKey;
}
public String getMessage(final String variableName) {
switch (this) {
case LOCAL_VAR:
return "Local variable '" + variableName + "' is never reassigned, so it should be declared final";
case METHOD_PARAM:
return "Method parameter '" + variableName + "' is never reassigned, so it should be declared final";
case LOOP_PARAM:
return "Loop parameter '" + variableName + "' is never reassigned, so it should be declared final";
case TRY_WITH_PARAM:
return "Try-with-resources parameter '" + variableName + "' is never reassigned, so it should be declared final";
case CATCH_PARAM:
return "Catch parameter '" + variableName + "' is never reassigned, so it should be declared final";
case LAMBDA_PARAM:
return "Lambda parameter '" + variableName + "' is never reassigned, so it should be declared final";
case ABSTRACT_METHOD_PARAM:
return "Abstract method parameter '" + variableName + "' is never reassigned, so it should be declared final";
default:
throw new UnsupportedOperationException();
}
}
}
private static class VariableInfo {
final VariableTree variableTree;
final VariableType variableType;
VariableInfo(VariableTree variableTree, VariableType variableType) {
this.variableTree = variableTree;
this.variableType = variableType;
}
}
private static final class Configuration {
private final Map<VariableType, Diagnostic.Kind> levels;
private final List<String> excludedPaths;
public Configuration(final String... args) {
final Map<String, String> props = new HashMap<>();
final List<String> excluded = new ArrayList<>();
for (final String arg : args) {
final String[] parts = arg.split("=", 2);
if (parts.length == 2) {
if (EXCLUDE_KEY.equals(parts[0])) {
final Path path = Paths.get(parts[1]);
excluded.add(path.toAbsolutePath().toString());
} else {
props.put(parts[0], parts[1]);
}
}
}
this.excludedPaths = Collections.unmodifiableList(excluded);
final Diagnostic.Kind defaultLevel =
Optional.ofNullable(props.get(DEFAULT_LEVEL_KEY)).map(Diagnostic.Kind::valueOf).orElse(null);
this.levels = Arrays.stream(VariableType.values()).map(vt -> {
final Diagnostic.Kind level = Optional.ofNullable(props.get(vt.getArgKey())).map(Diagnostic.Kind::valueOf).orElse(defaultLevel);
if (level != null) {
return new AbstractMap.SimpleEntry<>(vt, level);
} else {
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public boolean isExcluded(final String sourcePath) {
for (final String excludedPath : excludedPaths) {
if (sourcePath.startsWith(excludedPath)) {
return true;
}
}
return false;
}
}
private static boolean isJava17OrHigher() {
return System.getProperty("java.version").compareTo("17") >= 0;
}
private static final boolean isJava17OrHigher = isJava17OrHigher();
@Override
public String getName() {
return getClass().getName();
}
@Override
public void init(JavacTask task, String... args) {
final Configuration configuration = new Configuration(args);
task.addTaskListener(new TaskListener() {
@Override
public void started(TaskEvent e) {
}
@Override
public void finished(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.ANALYZE) {
analyzeFinalVariables(e.getCompilationUnit(), task, e.getTypeElement(), configuration);
}
}
});
}
private void analyzeFinalVariables(CompilationUnitTree compilationUnit, JavacTask task, Element typeElement, Configuration configuration) {
final String sourcePath = compilationUnit.getSourceFile().toUri().getPath();
if (sourcePath != null && configuration.isExcluded(sourcePath)) {
return;
}
FinalVariableAnalyzer analyzer = new FinalVariableAnalyzer(compilationUnit, task, configuration);
TreePath path = Trees.instance(task).getPath(typeElement);
if (path != null) {
analyzer.scan(path, null);
}
}
private static class FinalVariableAnalyzer extends TreePathScanner<Void, Void> {
private final Configuration configuration;
private final CompilationUnitTree compilationUnit;
private final Trees trees;
private final Map<String, VariableInfo> variableInfoMap = new LinkedHashMap<>();
private final Set<String> reassignedVariables = new HashSet<>();
public FinalVariableAnalyzer(CompilationUnitTree compilationUnit, JavacTask task, Configuration configuration) {
this.configuration = configuration;
this.compilationUnit = compilationUnit;
this.trees = Trees.instance(task);
}
@Override
public Void visitMethod(MethodTree node, Void p) {
variableInfoMap.clear();
reassignedVariables.clear();
super.visitMethod(node, p);
// Check for variables that could be final
checkForFinalCandidates();
return null;
}
@Override
public Void visitVariable(VariableTree node, Void p) {
final String varName = node.getName().toString();
final TreePath currentPath = getCurrentPath();
final TreePath parentPath = currentPath.getParentPath();
final Tree parent = parentPath.getLeaf();
final VariableType type;
if (parent instanceof LambdaExpressionTree) {
type = VariableType.LAMBDA_PARAM;
} else if (parent instanceof ForLoopTree || parent instanceof EnhancedForLoopTree) {
type = VariableType.LOOP_PARAM;
} else if (parent instanceof CatchTree) {
type = VariableType.CATCH_PARAM;
} else if (parent instanceof TryTree) {
type = VariableType.TRY_WITH_PARAM;
} else if (parent instanceof MethodTree) {
if (isAbstractMethodParameter(node, (MethodTree) parent)) {
type = VariableType.ABSTRACT_METHOD_PARAM;
} else {
type = VariableType.METHOD_PARAM;
if (isJava17OrHigher && ((MethodTree) parent).getName().contentEquals("<init>")) {
final TreePath grandParentPath = parentPath.getParentPath();
if (grandParentPath.getLeaf().getKind() == Tree.Kind.RECORD) {
return super.visitVariable(node, p);
}
}
}
} else if (parent instanceof BlockTree) {
type = VariableType.LOCAL_VAR;
} else {
type = VariableType.LOCAL_VAR;
}
variableInfoMap.put(varName, new VariableInfo(node, type));
return super.visitVariable(node, p);
}
private boolean isAbstractMethodParameter(VariableTree variableTree, MethodTree methodTree) {
// Get the element for the method
Element methodElement = trees.getElement(getCurrentPath().getParentPath());
if (methodElement instanceof ExecutableElement) {
ExecutableElement executableElement = (ExecutableElement) methodElement;
return executableElement.getModifiers().contains(Modifier.ABSTRACT);
}
return false;
}
@Override
public Void visitAssignment(AssignmentTree node, Void p) {
if (node.getVariable() instanceof IdentifierTree) {
final IdentifierTree ident = (IdentifierTree) node.getVariable();
reassignedVariables.add(ident.getName().toString());
}
return super.visitAssignment(node, p);
}
@Override
public Void visitUnary(UnaryTree node, Void p) {
if ((node.getKind() == Tree.Kind.PREFIX_INCREMENT ||
node.getKind() == Tree.Kind.PREFIX_DECREMENT ||
node.getKind() == Tree.Kind.POSTFIX_INCREMENT ||
node.getKind() == Tree.Kind.POSTFIX_DECREMENT) &&
node.getExpression() instanceof IdentifierTree) {
IdentifierTree ident = (IdentifierTree) node.getExpression();
reassignedVariables.add(ident.getName().toString());
}
return super.visitUnary(node, p);
}
@Override
public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) {
if (node.getVariable() instanceof IdentifierTree) {
final IdentifierTree ident = (IdentifierTree) node.getVariable();
reassignedVariables.add(ident.getName().toString());
}
return super.visitCompoundAssignment(node, p);
}
private void checkForFinalCandidates() {
for (final Map.Entry<String, VariableInfo> entry : variableInfoMap.entrySet()) {
final String varName = entry.getKey();
final VariableInfo info = entry.getValue();
Diagnostic.Kind level = configuration.levels.get(info.variableType);
// Skip if level is not configured
if (level == null) {
continue;
}
// Skip if already final
if (isFinal(info.variableTree)) {
continue;
}
// Skip if reassigned
if (reassignedVariables.contains(varName)) {
continue;
}
trees.printMessage(level,
info.variableType.getMessage(varName),
info.variableTree,
compilationUnit);
}
}
private static boolean isFinal(VariableTree variableTree) {
final Set<Modifier> modifiers = variableTree.getModifiers().getFlags();
return modifiers.contains(Modifier.FINAL);
}
}
}
@@ -0,0 +1 @@
net.woggioni.finalguard.FinalGuardPlugin
@@ -0,0 +1,221 @@
package net.woggioni.finalguard;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static net.woggioni.finalguard.FinalGuardPlugin.VariableType.*;
public class PluginTest {
private static class ClassFile extends SimpleJavaFileObject {
private ByteArrayOutputStream out;
public ClassFile(URI uri) {
super(uri, Kind.CLASS);
}
@Override
public OutputStream openOutputStream() {
return out = new ByteArrayOutputStream();
}
public byte[] getCompiledBinaries() {
return out.toByteArray();
}
}
private static class SourceFile extends SimpleJavaFileObject {
public SourceFile(URI uri) {
super(uri, Kind.SOURCE);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
Reader r = new InputStreamReader(uri.toURL().openStream());
StringBuilder sb = new StringBuilder();
char[] buffer = new char[0x1000];
while (true) {
int read = r.read(buffer);
if (read < 0) break;
sb.append(buffer, 0, read);
}
return sb.toString();
}
}
private static class FileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final List<ClassFile> compiled = new ArrayList<>();
protected FileManager(StandardJavaFileManager fileManager) {
super(fileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling) {
ClassFile result = new ClassFile(URI.create("string://" + className));
compiled.add(result);
return result;
}
public List<ClassFile> getCompiled() {
return compiled;
}
}
private Optional<Iterable<Diagnostic<? extends JavaFileObject>>> compile(Iterable<URI> sources) {
return compile(sources, "");
}
private Optional<Iterable<Diagnostic<? extends JavaFileObject>>> compile(Iterable<URI> sources, String extraPluginArgs) {
StringWriter output = new StringWriter();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
FileManager fileManager =
new FileManager(compiler.getStandardFileManager(null, null, null));
List<JavaFileObject> compilationUnits = StreamSupport.stream(sources.spliterator(), false)
.map(SourceFile::new).collect(Collectors.toList());
String pluginArg = "-Xplugin:" + FinalGuardPlugin.class.getName() + " " + FinalGuardPlugin.DEFAULT_LEVEL_KEY + "=ERROR";
if (!extraPluginArgs.isEmpty()) {
pluginArg += " " + extraPluginArgs;
}
List<String> arguments = Arrays.asList(
"-classpath", System.getProperty("test.compilation.classpath"),
pluginArg
);
final ArrayList<Diagnostic<? extends JavaFileObject>> compilerMessages = new ArrayList<>();
JavaCompiler.CompilationTask task = compiler.getTask(
output,
fileManager,
compilerMessages::add,
arguments,
null,
compilationUnits
);
if (task.call()) return Optional.empty();
else return Optional.of(compilerMessages);
}
private enum CompilationResult {
SUCCESS, FAILURE
}
private static class TestCaseProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
String prefix = "net/woggioni/finalguard/test/";
return Stream.of(
Arguments.of(prefix + "TestCase1.java", Arrays.asList(
LOOP_PARAM.getMessage("item"),
LAMBDA_PARAM.getMessage("s"),
LOCAL_VAR.getMessage("f"),
LOCAL_VAR.getMessage("localVar"),
LOCAL_VAR.getMessage("loopVar"),
LOOP_PARAM.getMessage("i"),
TRY_WITH_PARAM.getMessage("is"),
METHOD_PARAM.getMessage("param1"),
CATCH_PARAM.getMessage("ioe"),
LOCAL_VAR.getMessage("anotherVar"),
METHOD_PARAM.getMessage("param2")
)),
Arguments.of(prefix + "TestCase2.java", Collections.emptyList()),
Arguments.of(prefix + "TestCase3.java",
Arrays.asList(LOCAL_VAR.getMessage("n"))),
Arguments.of(prefix + "TestCase4.java",
Arrays.asList(LOOP_PARAM.getMessage("i"))),
Arguments.of(prefix + "TestCase5.java", Arrays.asList(LOCAL_VAR.getMessage("loopVar"))),
Arguments.of(prefix + "TestCase6.java", Arrays.asList(LOOP_PARAM.getMessage("item"))),
Arguments.of(prefix + "TestCase7.java", Arrays.asList(CATCH_PARAM.getMessage("re"))),
Arguments.of(prefix + "TestCase8.java", Arrays.asList(TRY_WITH_PARAM.getMessage("is"))),
Arguments.of(prefix + "TestCase9.java", Arrays.asList(LAMBDA_PARAM.getMessage("s"))),
Arguments.of(prefix + "TestCase10.java", Arrays.asList(METHOD_PARAM.getMessage("n"))),
Arguments.of(prefix + "TestCase11.java", Arrays.asList(
ABSTRACT_METHOD_PARAM.getMessage("n"),
LOCAL_VAR.getMessage("result"),
LOCAL_VAR.getMessage("size"),
METHOD_PARAM.getMessage("t1s")
)),
Arguments.of(prefix + "TestCase12.java", Collections.emptyList()),
Arguments.of(prefix + "TestCase13.java", Arrays.asList(ABSTRACT_METHOD_PARAM.getMessage("x"), ABSTRACT_METHOD_PARAM.getMessage("y"))),
Arguments.of(prefix + "TestCase14.java", Arrays.asList(ABSTRACT_METHOD_PARAM.getMessage("x"), ABSTRACT_METHOD_PARAM.getMessage("y"))),
Arguments.of(prefix + "TestCase15.java",
Arrays.asList(
LOCAL_VAR.getMessage("a"),
METHOD_PARAM.getMessage("source")))
);
}
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(TestCaseProvider.class)
public void test(String sourceFilePath, List<String> expectedErrorMessages) {
Optional<Iterable<Diagnostic<? extends JavaFileObject>>> result;
try {
ClassLoader cl = getClass().getClassLoader();
result = compile(Collections.singletonList(cl.getResource(sourceFilePath).toURI()));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
result.ifPresent(diagnostics -> {
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics) {
System.err.printf("%s:%s %s\n",
diagnostic.getSource().getName(),
diagnostic.getLineNumber(),
diagnostic.getMessage(Locale.getDefault()));
}
});
if (expectedErrorMessages.isEmpty()) {
Assertions.assertFalse(result.isPresent(), "Compilation was expected to succeed but it has failed");
} else {
final List<String> compilationErrors = result
.map(it -> StreamSupport.stream(it.spliterator(), false))
.orElse(Stream.empty())
.map(it -> it.getMessage(Locale.ENGLISH))
.collect(Collectors.toList());
for (String expectedErrorMessage : expectedErrorMessages) {
int index = compilationErrors.indexOf(expectedErrorMessage);
Assertions.assertTrue(index >= 0, String.format("Expected compilation error `%s` not found in output", expectedErrorMessage));
compilationErrors.remove(index);
}
Assertions.assertTrue(compilationErrors.isEmpty(), "Unexpected compilation errors found in the output");
}
}
@Test
public void testExcludedSourceIsSkipped() throws Exception {
// TestCase3 normally produces an error (local var 'n' not final).
// When we exclude the directory containing TestCase3, it should compile without errors.
ClassLoader cl = getClass().getClassLoader();
URI sourceUri = cl.getResource("net/woggioni/finalguard/test/TestCase3.java").toURI();
String sourcePath = new File(sourceUri).getParent();
// Compile WITH exclude — should succeed (no errors)
Optional<Iterable<Diagnostic<? extends JavaFileObject>>> resultWithExclude =
compile(Collections.singletonList(sourceUri), FinalGuardPlugin.EXCLUDE_KEY + "=" + sourcePath);
Assertions.assertFalse(resultWithExclude.isPresent(),
"Compilation should succeed when source directory is excluded");
// Compile WITHOUT exclude — should fail (TestCase3 has errors)
Optional<Iterable<Diagnostic<? extends JavaFileObject>>> resultWithoutExclude =
compile(Collections.singletonList(sourceUri));
Assertions.assertTrue(resultWithoutExclude.isPresent(),
"Compilation should fail when source directory is NOT excluded");
}
}
@@ -0,0 +1,40 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.function.Function;
public class TestCase1 {
public void testMethod(String param1, String param2) { // Warning: param1 could be final
String localVar = "hello"; // Warning: localVar could be final
String reassignedVar = "initial"; // No warning (reassigned below)
reassignedVar = "changed";
param2 = "modified"; // No warning for param2 (reassigned)
for (int i = 0; i < 10; ) { // Warning: i could be final
String loopVar = "constant"; // Warning: loopVar could be final
}
// Enhanced for loop - no warning for loop variable
for (String item : Arrays.asList("a", "b")) {
// item is effectively final in each iteration
}
try (InputStream is = Files.newInputStream(Path.of("/tmp/file.txt"))) {
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
Function<String, String> f = s -> s.toLowerCase();
}
public void finalMethod(final String param1, String param2) { // Warning only for param2
final String localVar = "hello"; // No warning (already final)
String anotherVar = "world"; // Warning: anotherVar could be final
}
}
@@ -0,0 +1,5 @@
public class TestCase10 {
public void testMethod(int n) {
}
}
@@ -0,0 +1,15 @@
import java.util.Arrays;
public abstract class TestCase11<T1> {
abstract T1 get(int n);
public T1[] toArray(T1[] t1s) {
int size = 42;
T1[] result = Arrays.copyOf(t1s, size);
for (int i = 0; i < size; i++) {
result[i] = get(i);
}
return result;
}
}
@@ -0,0 +1,2 @@
public record TestCase12<T, U>(double x, double y) {
}
@@ -0,0 +1,3 @@
public interface TestCase13 {
void foo(double x, double y);
}
@@ -0,0 +1,3 @@
public abstract class TestCase14 {
abstract public void foo(double x, double y);
}
@@ -0,0 +1,24 @@
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class TestCase15 {
public static void foo() {
final InputStream source = null;
final InputStream is = new FilterInputStream(source) {
@Override
public int read() throws IOException {
int a = 5;
return a;
}
};
}
static class Bar extends FilterInputStream {
public Bar(InputStream source) {
super(source);
}
};
}
@@ -0,0 +1,18 @@
public class TestCase2 {
public void testMethod(int a, int b, int c, int d, int e, int f, int g, int h, int j, int k, int l, int m, int n) {
a++;
b--;
++c;
--d;
e |= 2;
f &= 1;
g <<= 1;
h >>= 1;
j ^= 1;
k += 1;
l -= 1;
m *= 1;
n /= 1;
}
}
@@ -0,0 +1,6 @@
public class TestCase3 {
public void testMethod() {
int n = 5; // Error: n could be final
}
}
@@ -0,0 +1,7 @@
public class TestCase4 {
public void testMethod() {
for (int i = 0; i < 10; ) { // Error: i could be final
}
}
}
@@ -0,0 +1,8 @@
public class TestCase5 {
public void testMethod() {
for (int i = 0; i < 10; i++) {
String loopVar = "constant"; // Error: loopVar should be final
}
}
}
@@ -0,0 +1,9 @@
import java.util.Arrays;
public class TestCase6 {
public void testMethod() {
for (String item : Arrays.asList("a", "b")) { // Error: item should be final
}
}
}
@@ -0,0 +1,10 @@
public class TestCase7 {
public void testMethod() {
try {
} catch (RuntimeException re) { // Error: re should be final
}
}
}
@@ -0,0 +1,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class TestCase8 {
public void testMethod() throws IOException {
try (InputStream is = Files.newInputStream(Path.of("some-path"))) { // Error: is could be final
}
}
}
@@ -0,0 +1,8 @@
import java.util.function.Function;
public class TestCase9 {
public void testMethod() {
final Function<String, String> d = s -> s.toLowerCase();
}
}
@@ -0,0 +1,29 @@
package net.woggioni.gradle.finalguard;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import javax.tools.Diagnostic;
public interface FinalGuardExtension {
Property<Diagnostic.Kind> getDefaultLevel();
Property<Diagnostic.Kind> getLocalVariableLevel();
Property<Diagnostic.Kind> getLambdaParameterLevel();
Property<Diagnostic.Kind> getForLoopParameterLevel();
Property<Diagnostic.Kind> getTryWithResourceLevel();
Property<Diagnostic.Kind> getMethodParameterLevel();
Property<Diagnostic.Kind> getAbstractMethodParameterLevel();
Property<Diagnostic.Kind> getCatchParameterLevel();
Property<Boolean> getSkipGeneratedSources();
ListProperty<String> getExcludedPrefixes();
}
@@ -0,0 +1,92 @@
package net.woggioni.gradle.finalguard;
import lombok.SneakyThrows;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.JavaCompile;
import javax.tools.Diagnostic;
import java.io.File;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
public class FinalGuardPlugin implements Plugin<Project> {
private static final String FINALGUARD_PLUGIN_CONFIGURATION = "finalguard_plugin";
private static final String JAVAC_PLUGIN_NAME = "net.woggioni.finalguard.FinalGuardPlugin";
private static final String EXCLUDE_KEY = "exclude";
@Override
public void apply(final Project project) {
final ExtensionContainer extensionContainer = project.getExtensions();
final TaskContainer tasks = project.getTasks();
final ObjectFactory objects = project.getObjects();
Configuration javacPluginConfiguration = project.getConfigurations().create(FINALGUARD_PLUGIN_CONFIGURATION);
javacPluginConfiguration.withDependencies(new Action<DependencySet>() {
@Override
@SneakyThrows
public void execute(DependencySet dependencies) {
final Class<?> cls = getClass();
final String resourceName = cls.getName().replace('.', '/') + ".class";
final URL classUrl = cls.getClassLoader().getResource(resourceName);
if (classUrl.getProtocol().startsWith("jar")) {
final String path = classUrl.toString();
String manifestPath = path.substring(0, path.lastIndexOf("!") + 1) +
"/META-INF/MANIFEST.MF";
final Manifest manifest = new Manifest(new URL(manifestPath).openStream());
final Attributes attr = manifest.getMainAttributes();
final String version = attr.getValue(Attributes.Name.SPECIFICATION_VERSION);
dependencies.add(project.getDependencies().create("net.woggioni.finalguard:finalguard-javac-plugin:" + version));
}
}
});
final FinalGuardExtension finalGuardExtension = objects.newInstance(FinalGuardExtension.class);
finalGuardExtension.getSkipGeneratedSources().convention(true);
extensionContainer.add("finalguard", finalGuardExtension);
tasks.withType(JavaCompile.class, javaCompileTask -> {
javaCompileTask.doFirst(t -> {
final CompileOptions options = javaCompileTask.getOptions();
options.setAnnotationProcessorPath(options.getAnnotationProcessorPath().plus(javacPluginConfiguration));
final StringBuilder xpluginArg = new StringBuilder("-Xplugin:").append(JAVAC_PLUGIN_NAME);
appendOption(xpluginArg, "default.level", finalGuardExtension.getDefaultLevel());
appendOption(xpluginArg, "local.variable.level", finalGuardExtension.getLocalVariableLevel());
appendOption(xpluginArg, "method.param.level", finalGuardExtension.getMethodParameterLevel());
appendOption(xpluginArg, "abstract.method.param.level", finalGuardExtension.getAbstractMethodParameterLevel());
appendOption(xpluginArg, "for.param.level", finalGuardExtension.getForLoopParameterLevel());
appendOption(xpluginArg, "try.param.level", finalGuardExtension.getTryWithResourceLevel());
appendOption(xpluginArg, "catch.param.level", finalGuardExtension.getCatchParameterLevel());
appendOption(xpluginArg, "lambda.param.level", finalGuardExtension.getLambdaParameterLevel());
if (finalGuardExtension.getSkipGeneratedSources().getOrElse(true)) {
final File generatedSourceDir = project.getLayout().getBuildDirectory().getAsFile().get();
if (generatedSourceDir != null) {
appendOption(xpluginArg, EXCLUDE_KEY, generatedSourceDir.getAbsolutePath());
}
}
for(final String excludedPrefix : finalGuardExtension.getExcludedPrefixes().get()) {
appendOption(xpluginArg, EXCLUDE_KEY, excludedPrefix);
}
options.getCompilerArgs().add(xpluginArg.toString());
});
});
}
private static void appendOption(StringBuilder sb, String key, String value) {
sb.append(' ').append(key).append('=').append(value);
}
private static void appendOption(StringBuilder sb, String key, Property<Diagnostic.Kind> property) {
if (property.isPresent()) {
appendOption(sb, key, property.get().toString());
}
}
}
@@ -0,0 +1,22 @@
package net.woggioni.gradle.graalvm;
import org.gradle.api.Action;
import org.gradle.api.model.ObjectFactory;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import javax.inject.Inject;
abstract class DefaultNativeImageExtension implements NativeImageExtension {
@Inject
public DefaultNativeImageExtension(ObjectFactory objects) {
}
@Override
public JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action) {
JavaToolchainSpec jts = getToolchain();
action.execute(getToolchain());
return jts;
}
}
@@ -2,11 +2,8 @@ package net.woggioni.gradle.graalvm;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaApplication;
@@ -14,23 +11,22 @@ import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.bundling.Tar;
import org.gradle.api.tasks.bundling.Zip;
import org.gradle.jvm.tasks.Jar;
import java.util.Optional;
public class JlinkPlugin implements Plugin<Project> {
public static final String JLINK_TASK_NAME = "jlink";
public static final String JLINK_DIST_TASK_NAME = "jlinkDist";
public static final String JLINK_DIST_ZIP_TASK_NAME = "jlinkDistZip";
public static final String JLINK_DIST_TAR_TASK_NAME = "jlinkDistTar";
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaLibraryPlugin.class);
ExtensionContainer extensionContainer = project.getExtensions();
BasePluginExtension basePluginExtension = extensionContainer.getByType(BasePluginExtension.class);
JavaApplication javaApplicationExtension =
Optional.ofNullable(extensionContainer.findByType(JavaApplication.class))
.orElseGet(() -> extensionContainer.create("application", JavaApplication.class));
TaskContainer tasks = project.getTasks();
Provider<JlinkTask> jlinTaskProvider = tasks.register(JLINK_TASK_NAME, JlinkTask.class, jlinkTask -> {
@@ -40,7 +36,7 @@ public class JlinkPlugin implements Plugin<Project> {
jlinkTask.getClasspath().set(classpath);
});
Provider<Zip> jlinkZipTaskProvider = tasks.register(JLINK_DIST_TASK_NAME, Zip.class, zip -> {
Provider<Zip> jlinkZipTaskProvider = tasks.register(JLINK_DIST_ZIP_TASK_NAME, Zip.class, zip -> {
zip.getArchiveBaseName().set(project.getName());
if(project.getVersion() != null) {
zip.getArchiveVersion().set(project.getVersion().toString());
@@ -49,5 +45,19 @@ public class JlinkPlugin implements Plugin<Project> {
zip.from(jlinTaskProvider);
});
Provider<Tar> jlinkTarTaskProvider = tasks.register(JLINK_DIST_TAR_TASK_NAME, Tar.class, zip -> {
zip.getArchiveBaseName().set(project.getName());
if(project.getVersion() != null) {
zip.getArchiveVersion().set(project.getVersion().toString());
}
zip.getDestinationDirectory().set(basePluginExtension.getDistsDirectory());
zip.from(jlinTaskProvider);
});
tasks.named(JLINK_TASK_NAME, JlinkTask.class, jlinkTask -> {
jlinkTask.finalizedBy(jlinkZipTaskProvider);
jlinkTask.finalizedBy(jlinkTarTaskProvider);
});
}
}
@@ -1,13 +1,15 @@
package net.woggioni.gradle.graalvm;
import lombok.SneakyThrows;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaApplication;
@@ -21,11 +23,12 @@ import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.internal.jvm.JavaModuleDetector;
import org.gradle.jvm.toolchain.JavaInstallationMetadata;
import org.gradle.jvm.toolchain.JavaLauncher;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import org.gradle.jvm.toolchain.internal.DefaultToolchainSpec;
import org.gradle.process.CommandLineArgumentProvider;
import javax.inject.Inject;
@@ -43,6 +46,13 @@ import static net.woggioni.gradle.graalvm.Constants.GRAALVM_TASK_GROUP;
public abstract class JlinkTask extends Exec {
private final JavaToolchainSpec toolchain;
public JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action) {
action.execute(toolchain);
return toolchain;
}
@Classpath
public abstract Property<FileCollection> getClasspath();
@@ -60,15 +70,39 @@ public abstract class JlinkTask extends Exec {
@Input
public abstract ListProperty<String> getAdditionalModules();
@Input
public abstract ListProperty<String> getLimitModules();
@Input
public abstract Property<Boolean> getBindServices();
@Input
public abstract Property<Boolean> getIncludeHeaderFiles();
@Input
public abstract Property<Boolean> getIncludeManPages();
@Input
public abstract Property<Boolean> getStripDebug();
@Input
public abstract Property<Boolean> getGenerateCdsArchive();
@Input
@Optional
public abstract Property<Integer> getCompressionLevel();
@Inject
protected abstract JavaModuleDetector getJavaModuleDetector();
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
private final Logger logger;
public JlinkTask() {
private static final Logger log = Logging.getLogger(JlinkTask.class);
@Inject
public JlinkTask(ObjectFactory objects) {
Project project = getProject();
logger = project.getLogger();
setGroup(GRAALVM_TASK_GROUP);
setDescription(
"Generates a custom Java runtime image that contains only the platform modules" +
@@ -79,17 +113,27 @@ public abstract class JlinkTask extends Exec {
getMainClass().convention(javaApplication.getMainClass());
getMainModule().convention(javaApplication.getMainModule());
}
getIncludeManPages().convention(false);
getIncludeHeaderFiles().convention(false);
getGenerateCdsArchive().convention(true);
getStripDebug().convention(true);
getClasspath().convention(project.files());
ProjectLayout layout = project.getLayout();
toolchain = getObjectFactory().newInstance(DefaultToolchainSpec.class);
JavaToolchainService javaToolchainService = ext.findByType(JavaToolchainService.class);
JavaPluginExtension javaPluginExtension = ext.findByType(JavaPluginExtension.class);
Provider<Directory> graalHomeDirectoryProvider = ofNullable(javaPluginExtension.getToolchain()).map(javaToolchainSpec ->
javaToolchainService.launcherFor(javaToolchainSpec)
).map(javaLauncher ->
javaLauncher.map(JavaLauncher::getMetadata).map(JavaInstallationMetadata::getInstallationPath)
).orElseGet(() -> layout.dir(project.provider(() ->project.file(System.getProperty("java.home")))));
Provider<Directory> graalHomeDirectoryProvider = javaToolchainService.launcherFor(it -> {
it.getLanguageVersion().set(toolchain.getLanguageVersion());
it.getVendor().set(toolchain.getVendor());
it.getImplementation().set(toolchain.getImplementation());
}).map(javaLauncher ->
javaLauncher.getMetadata().getInstallationPath()
).orElse(layout.dir(project.provider(() -> project.file(System.getProperty("java.home")))));
getGraalVmHome().convention(graalHomeDirectoryProvider);
getGraalVmHome().convention(graalHomeDirectoryProvider);
getAdditionalModules().convention(new ArrayList<>());
getLimitModules().convention(new ArrayList<>());
getBindServices().convention(false);
BasePluginExtension basePluginExtension =
ext.getByType(BasePluginExtension.class);
@@ -110,7 +154,13 @@ public abstract class JlinkTask extends Exec {
@SneakyThrows
public Iterable<String> asArguments() {
List<String> result = new ArrayList<>();
result.add("--compress=2");
final Property<Integer> compressionLevelProperty = getCompressionLevel();
if(compressionLevelProperty.isPresent()) {
result.add(String.format("--compress=zip-%d", compressionLevelProperty.get()));
}
if(getBindServices().get()) {
result.add("--bind-services");
}
JavaModuleDetector javaModuleDetector = getJavaModuleDetector();
FileCollection classpath = getClasspath().get();
FileCollection mp = javaModuleDetector.inferModulePath(true, classpath);
@@ -131,8 +181,34 @@ public abstract class JlinkTask extends Exec {
List<String> additionalModules = getAdditionalModules().get();
if(getMainModule().isPresent() || !additionalModules.isEmpty()) {
result.add("--add-modules");
ofNullable(getMainModule().getOrElse(null)).ifPresent(result::add);
additionalModules.forEach(result::add);
final List<String> modules2BeAdded = new ArrayList<>();
ofNullable(getMainModule().getOrElse(null)).ifPresent(modules2BeAdded::add);
modules2BeAdded.addAll(additionalModules);
if(!modules2BeAdded.isEmpty()) {
result.add(String.join(",", modules2BeAdded));
}
}
List<String> limitModules = getLimitModules().get();
if(!limitModules.isEmpty()) {
result.add("--limit-modules");
final List<String> modules2BeAdded = new ArrayList<>();
modules2BeAdded.addAll(limitModules);
if(!modules2BeAdded.isEmpty()) {
result.add(String.join(",", modules2BeAdded));
}
}
if(getStripDebug().getOrElse(false)) {
result.add("--strip-debug");
}
if(getGenerateCdsArchive().getOrElse(false)) {
result.add("--generate-cds-archive");
}
if(!getIncludeHeaderFiles().getOrElse(true)) {
result.add("--no-header-files");
}
if(!getIncludeManPages().getOrElse(true)) {
result.add("--no-man-pages");
}
return Collections.unmodifiableList(result);
}
@@ -1,5 +1,6 @@
package net.woggioni.gradle.graalvm;
import org.gradle.api.Action;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.DirectoryProperty;
@@ -17,6 +18,8 @@ import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.jvm.toolchain.JavaLauncher;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import org.gradle.jvm.toolchain.internal.DefaultToolchainSpec;
import org.gradle.process.CommandLineArgumentProvider;
import java.util.ArrayList;
@@ -35,10 +38,18 @@ public abstract class NativeImageConfigurationTask extends JavaExec {
@OutputDirectory
public abstract DirectoryProperty getConfigurationDir();
private final JavaToolchainSpec toolchain;
public JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action) {
action.execute(toolchain);
return toolchain;
}
public NativeImageConfigurationTask() {
setGroup(GRAALVM_TASK_GROUP);
setDescription("Run the application with the native-image-agent " +
"to create a configuration for native image creation");
toolchain = getProject().getObjects().newInstance(DefaultToolchainSpec.class);
ProjectLayout layout = getProject().getLayout();
TaskContainer taskContainer = getProject().getTasks();
JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class);
@@ -46,8 +57,8 @@ public abstract class NativeImageConfigurationTask extends JavaExec {
ExtensionContainer ext = getProject().getExtensions();
Property<JavaLauncher> javaLauncherProperty = getJavaLauncher();
Optional.ofNullable(ext.findByType(JavaToolchainService.class))
.flatMap(ts -> Optional.ofNullable(javaExtension.getToolchain()).map(ts::launcherFor))
.ifPresent(javaLauncherProperty::set);
.flatMap(ts -> Optional.of(toolchain).map(ts::launcherFor))
.ifPresent(javaLauncherProperty::convention);
if(!Objects.isNull(javaApplication)) {
getMainClass().convention(javaApplication.getMainClass());
getMainModule().convention(javaApplication.getMainModule());
@@ -74,7 +85,9 @@ public abstract class NativeImageConfigurationTask extends JavaExec {
} else {
jvmArgs.add("-agentlib:native-image-agent=config-output-dir=" + getConfigurationDir().get());
}
for(String jvmArg : Optional.ofNullable(javaApplication.getApplicationDefaultJvmArgs()).orElse(Collections.emptyList())) {
for(String jvmArg : Optional.ofNullable(javaApplication)
.map(JavaApplication::getApplicationDefaultJvmArgs)
.orElse(Collections.emptyList())) {
jvmArgs.add(jvmArg);
}
return jvmArgs;
@@ -0,0 +1,32 @@
package net.woggioni.gradle.graalvm;
import org.gradle.api.Action;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Nested;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
public interface NativeImageExtension {
Property<FileCollection> getClasspath();
@Nested
JavaToolchainSpec getToolchain();
JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action);
Property<Boolean> getUseMusl();
Property<Boolean> getBuildStaticImage();
Property<Boolean> getEnableFallback();
Property<Boolean> getLinkAtBuildTime();
Property<String> getMainClass();
Property<String> getMainModule();
Property<Boolean> getCompressExecutable();
Property<Boolean> getUseLZMA();
Property<Integer> getCompressionLevel();
}
@@ -2,58 +2,117 @@ package net.woggioni.gradle.graalvm;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.jvm.tasks.Jar;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import org.gradle.jvm.toolchain.internal.DefaultToolchainSpec;
import java.util.Optional;
@CacheableTask
public class NativeImagePlugin implements Plugin<Project> {
public static final String NATIVE_IMAGE_TASK_NAME = "nativeImage";
public static final String UPX_TASK_NAME = "upx";
public static final String CONFIGURE_NATIVE_IMAGE_TASK_NAME = "configureNativeImage";
public static final String NATIVE_IMAGE_CONFIGURATION_FOLDER_NAME = "native-image";
private static <T> void setIfPresent(Property<T> p1, Provider<T> provider) {
if (provider.isPresent()) {
p1.set(provider);
}
}
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaLibraryPlugin.class);
ProjectLayout layout = project.getLayout();
ExtensionContainer extensionContainer = project.getExtensions();
JavaApplication javaApplicationExtension =
Optional.ofNullable(extensionContainer.findByType(JavaApplication.class))
.orElseGet(() -> extensionContainer.create("application", JavaApplication.class));
TaskContainer tasks = project.getTasks();
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME, Jar.class, jar -> {
jar.from(layout.getProjectDirectory().dir(NATIVE_IMAGE_CONFIGURATION_FOLDER_NAME), copySpec -> {
copySpec.into(
String.format("META-INF/native-image/%s/%s/",
project.getName(),
project.getGroup()
)
String.format("META-INF/native-image/%s/%s/",
project.getName(),
project.getGroup()
)
);
});
});
ExtensionContainer ext = project.getExtensions();
JavaPluginExtension javaPluginExtension = ext.findByType(JavaPluginExtension.class);
ObjectFactory objects = project.getObjects();
NativeImageExtension nativeImageExtension = objects.newInstance(DefaultNativeImageExtension.class);
extensionContainer.add("nativeImage", nativeImageExtension);
Provider<NativeImageConfigurationTask> nativeImageConfigurationTaskProvider =
tasks.register(CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask.class);
nativeImageExtension.toolchain(jts -> {
jts.getImplementation().convention(javaPluginExtension.getToolchain().getImplementation());
jts.getVendor().convention(javaPluginExtension.getToolchain().getVendor());
jts.getLanguageVersion().convention(javaPluginExtension.getToolchain().getLanguageVersion());
});
nativeImageExtension.getUseMusl().convention(false);
nativeImageExtension.getEnableFallback().convention(false);
nativeImageExtension.getLinkAtBuildTime().convention(false);
nativeImageExtension.getBuildStaticImage().convention(false);
nativeImageExtension.getCompressExecutable().convention(false);
nativeImageExtension.getUseLZMA().convention(false);
nativeImageExtension.getCompressionLevel().convention(6);
ConfigurationContainer configurations = project.getConfigurations();
FileCollection classpath = project.files(jarTaskProvider,
configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
nativeImageExtension.getClasspath().convention(classpath);
Provider<NativeImageConfigurationTask> nativeImageConfigurationTaskProvider = tasks.register(
CONFIGURE_NATIVE_IMAGE_TASK_NAME,
NativeImageConfigurationTask.class,
nativeImageConfigurationTask -> {
nativeImageConfigurationTask.toolchain(jts -> {
jts.getImplementation().convention(nativeImageExtension.getToolchain().getImplementation());
jts.getVendor().convention(nativeImageExtension.getToolchain().getVendor());
jts.getLanguageVersion().convention(nativeImageExtension.getToolchain().getLanguageVersion());
});
});
Provider<NativeImageTask> nativeImageTaskProvider = tasks.register(NATIVE_IMAGE_TASK_NAME, NativeImageTask.class, nativeImageTask -> {
nativeImageTask.getInputs().files(nativeImageConfigurationTaskProvider);
ConfigurationContainer configurations = project.getConfigurations();
FileCollection classpath = project.files(jarTaskProvider,
configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
nativeImageTask.getClasspath().set(classpath);
nativeImageTask.getClasspath().set(nativeImageExtension.getClasspath());
nativeImageTask.toolchain(jts -> {
jts.getImplementation().convention(nativeImageExtension.getToolchain().getImplementation());
jts.getVendor().convention(nativeImageExtension.getToolchain().getVendor());
jts.getLanguageVersion().convention(nativeImageExtension.getToolchain().getLanguageVersion());
});
nativeImageTask.getBuildStaticImage().set(nativeImageExtension.getBuildStaticImage());
nativeImageTask.getUseMusl().set(nativeImageExtension.getUseMusl());
nativeImageTask.getLinkAtBuildTime().set(nativeImageExtension.getLinkAtBuildTime());
nativeImageTask.getMainClass().set(nativeImageExtension.getMainClass());
nativeImageTask.getMainModule().set(nativeImageExtension.getMainModule());
nativeImageTask.getEnableFallback().set(nativeImageExtension.getEnableFallback());
});
Provider<UpxTask> upxTaskProvider = tasks.register(UPX_TASK_NAME, UpxTask.class, t -> {
t.getInputFile().set(nativeImageTaskProvider.flatMap(NativeImageTask::getOutputFile));
setIfPresent(t.getUseLZMA(), nativeImageExtension.getUseLZMA());
setIfPresent(t.getCompressionLevel(), nativeImageExtension.getCompressionLevel());
setIfPresent(t.getCompressionLevel(), nativeImageExtension.getCompressionLevel());
});
tasks.named(NATIVE_IMAGE_TASK_NAME, NativeImageTask.class, t -> {
if (nativeImageExtension.getCompressExecutable().getOrElse(false)) {
t.finalizedBy(upxTaskProvider);
}
});
}
}
@@ -1,5 +1,6 @@
package net.woggioni.gradle.graalvm;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
@@ -7,49 +8,71 @@ import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Exec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.internal.jvm.JavaModuleDetector;
import org.gradle.jvm.toolchain.JavaInstallationMetadata;
import org.gradle.jvm.toolchain.JavaLauncher;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import org.gradle.jvm.toolchain.internal.DefaultToolchainSpec;
import org.gradle.process.CommandLineArgumentProvider;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static java.util.Optional.ofNullable;
import static net.woggioni.gradle.graalvm.Constants.GRAALVM_TASK_GROUP;
@CacheableTask
public abstract class NativeImageTask extends Exec {
public static final String NATIVE_COMPILER_PATH_ENV_VARIABLE = "GRAAL_NATIVE_COMPILER_PATH";
public static final String NATIVE_COMPILER_PATH_PROPERTY_KEY = "graal.native.compiler.path";
@Classpath
public abstract Property<FileCollection> getClasspath();
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getGraalVmHome();
@Input
public abstract Property<Boolean> getUseJpms();
private final JavaToolchainSpec toolchain;
public JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action) {
action.execute(toolchain);
return toolchain;
}
@Optional
@InputFile
@PathSensitive(PathSensitivity.ABSOLUTE)
public abstract RegularFileProperty getNativeCompilerPath();
@Input
public abstract Property<Boolean> getUseMusl();
@Input
public abstract Property<Boolean> getBuildStaticImage();
@Input
public abstract Property<Boolean> getEnableFallback();
@Input
public abstract Property<Boolean> getLinkAtBuildTime();
@Input
public abstract Property<String> getMainClass();
@@ -63,16 +86,30 @@ public abstract class NativeImageTask extends Exec {
@OutputFile
protected abstract RegularFileProperty getOutputFile();
private final Logger logger;
public NativeImageTask() {
private static final Logger log = Logging.getLogger(NativeImageTask.class);
@Inject
public NativeImageTask(ObjectFactory objects) {
Project project = getProject();
logger = project.getLogger();
setGroup(GRAALVM_TASK_GROUP);
setDescription("Create a native image of the application using GraalVM");
getUseJpms().convention(false);
toolchain = objects.newInstance(DefaultToolchainSpec.class);
getUseMusl().convention(false);
getBuildStaticImage().convention(false);
getEnableFallback().convention(false);
getLinkAtBuildTime().convention(false);
Provider<File> nativeComnpilerProvider = project.provider(() -> {
String envVar;
File compilerPath = null;
if(project.hasProperty(NATIVE_COMPILER_PATH_PROPERTY_KEY)) {
compilerPath = new File(project.property(NATIVE_COMPILER_PATH_PROPERTY_KEY).toString());
} else if((envVar = System.getenv(NATIVE_COMPILER_PATH_ENV_VARIABLE)) != null) {
compilerPath = new File(envVar);
}
return compilerPath;
});
getNativeCompilerPath().convention(project.getLayout().file(nativeComnpilerProvider));
ExtensionContainer ext = project.getExtensions();
JavaApplication javaApplication = ext.findByType(JavaApplication.class);
if(!Objects.isNull(javaApplication)) {
@@ -81,15 +118,16 @@ public abstract class NativeImageTask extends Exec {
}
getClasspath().convention(project.files());
ProjectLayout layout = project.getLayout();
JavaToolchainService javaToolchainService = ext.findByType(JavaToolchainService.class);
JavaPluginExtension javaPluginExtension = ext.findByType(JavaPluginExtension.class);
Provider<Directory> graalHomeDirectoryProvider = ofNullable(javaPluginExtension.getToolchain()).map(javaToolchainSpec ->
javaToolchainService.launcherFor(javaToolchainSpec)
).map(javaLauncher ->
javaLauncher.map(JavaLauncher::getMetadata).map(JavaInstallationMetadata::getInstallationPath)
).orElseGet(() -> layout.dir(project.provider(() -> project.file(System.getProperty("java.home")))));
getGraalVmHome().convention(graalHomeDirectoryProvider);
JavaToolchainService javaToolchainService = ext.findByType(JavaToolchainService.class);
Provider<Directory> graalHomeDirectoryProvider = javaToolchainService.launcherFor(it -> {
it.getLanguageVersion().set(toolchain.getLanguageVersion());
it.getVendor().set(toolchain.getVendor());
it.getImplementation().set(toolchain.getImplementation());
}).map(javaLauncher ->
javaLauncher.getMetadata().getInstallationPath()
).orElse(layout.dir(project.provider(() -> project.file(System.getProperty("java.home")))));
getGraalVmHome().convention(graalHomeDirectoryProvider);
BasePluginExtension basePluginExtension =
ext.getByType(BasePluginExtension.class);
getOutputFile().convention(basePluginExtension.getLibsDirectory().file(project.getName()));
@@ -114,8 +152,14 @@ public abstract class NativeImageTask extends Exec {
if(getUseMusl().get()) {
result.add("--libc=musl");
}
if(getLinkAtBuildTime().get()) {
result.add("--link-at-build-time");
}
if(getNativeCompilerPath().isPresent()) {
result.add("--native-compiler-path=" + getNativeCompilerPath().getAsFile().get());
}
JavaModuleDetector javaModuleDetector = getJavaModuleDetector();
boolean useJpms = getUseJpms().get();
boolean useJpms = getMainModule().isPresent();
FileCollection classpath = getClasspath().get();
FileCollection cp = javaModuleDetector.inferClasspath(useJpms, classpath);
FileCollection mp = javaModuleDetector.inferModulePath(useJpms, classpath);
@@ -129,7 +173,15 @@ public abstract class NativeImageTask extends Exec {
}
result.add("-o");
result.add(getOutputFile().get().getAsFile().toString());
result.add(getMainClass().get());
if(getMainModule().isPresent()) {
result.add("--module");
String mainModule = getMainModule().get();
result.add(getMainClass()
.map(mainClass -> String.format("%s/%s", mainModule, mainClass))
.getOrElse(mainModule));
} else {
result.add(getMainClass().get());
}
return Collections.unmodifiableList(result);
}
};
@@ -0,0 +1,108 @@
package net.woggioni.gradle.graalvm;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Exec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.process.CommandLineArgumentProvider;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@CacheableTask
public abstract class UpxTask extends Exec {
@InputFile
@PathSensitive(PathSensitivity.NONE)
public abstract RegularFileProperty getInputFile();
@OutputFile
public abstract RegularFileProperty getOutputFile();
@Input
public abstract Property<Boolean> getUseLZMA();
@Input
public abstract Property<Integer> getCompressionLevel();
@Inject
public UpxTask(Project project) {
BasePluginExtension be = project.getExtensions().findByType(BasePluginExtension.class);
getOutputFile().convention(
be.getDistsDirectory().file(String.format("%s.upx", project.getName()))
);
getUseLZMA().convention(false);
getCompressionLevel().convention(10);
executable("upx");
getArgumentProviders().add(new CommandLineArgumentProvider() {
@Override
public Iterable<String> asArguments() {
List<String> result = new ArrayList<String>();
if(getUseLZMA().get()) {
result.add("--lzma");
} else {
result.add("--no-lzma");
}
String compressionLevel;
int cl = getCompressionLevel().get();
switch (cl) {
case 1:
compressionLevel = "-1";
break;
case 2:
compressionLevel = "-2";
break;
case 3:
compressionLevel = "-3";
break;
case 4:
compressionLevel = "-4";
break;
case 5:
compressionLevel = "-5";
break;
case 6:
compressionLevel = "-6";
break;
case 7:
compressionLevel = "-7";
break;
case 8:
compressionLevel = "-8";
break;
case 9:
compressionLevel = "-9";
break;
case 10:
compressionLevel = "--best";
break;
case 11:
compressionLevel = "--brute";
break;
case 12:
compressionLevel = "--ultra-brute";
break;
default:
throw new IllegalArgumentException(String.format("Unsupported compression level %d", cl));
}
result.add(compressionLevel);
result.add(getInputFile().getAsFile().get().toString());
result.add("-f");
result.add("-o");
result.add(getOutputFile().getAsFile().get().toString());
return Collections.unmodifiableList(result);
}
});
}
}
+4 -14
View File
@@ -1,15 +1,5 @@
woggioniMavenRepositoryUrl=https://mvn.woggioni.net/
lys.catalog.version=2025.12.27
version.myGradlePlugins=2026.02.23
version.gradle=9.3.1
lys.catalog.version=2024.02.12
version.myGradlePlugins=2024.03.11
version.gradle=7.6
version.felix.config.admin=1.9.26
version.felix=7.0.5
version.felix.scr=2.2.4
version.felix.security=2.8.2
version.osgi=8.0.0
version.osgi.cm=1.6.0
version.osgi.service.component=1.5.1
version.osgi.function=1.2.0
version.osgi.promise=1.3.0
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
Binary file not shown.
+3 -1
View File
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+32 -18
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,22 +132,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -165,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+22 -18
View File
@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
+13
View File
@@ -0,0 +1,13 @@
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
create("JdepsPlugin") {
id = 'net.woggioni.gradle.jdeps'
implementationClass = "net.woggioni.gradle.jdeps.JdepsPlugin"
}
}
}
@@ -0,0 +1,5 @@
package net.woggioni.gradle.jdeps;
public class Constants {
public static final String JDEPS_TASK_GROUP = "jdeps";
}
@@ -0,0 +1,26 @@
package net.woggioni.gradle.jdeps;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.FileCollection;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskContainer;
public class JdepsPlugin implements Plugin<Project> {
public static final String JDEPS_TASK_NAME = "jdeps";
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaLibraryPlugin.class);
TaskContainer tasks = project.getTasks();
Provider<JdepsTask> jdepsTaskProvider = tasks.register(JDEPS_TASK_NAME, JdepsTask.class, jdepsTask -> {
ConfigurationContainer configurations = project.getConfigurations();
FileCollection classpath = project.files(configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
jdepsTask.getClasspath().set(classpath);
jdepsTask.getArchives().set(project.files(tasks.named(JavaPlugin.JAR_TASK_NAME)));
});
}
}
@@ -0,0 +1,209 @@
package net.woggioni.gradle.jdeps;
import lombok.SneakyThrows;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.reporting.ReportingExtension;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Exec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.internal.jvm.JavaModuleDetector;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.jvm.toolchain.JavaToolchainSpec;
import org.gradle.jvm.toolchain.internal.DefaultToolchainSpec;
import org.gradle.process.CommandLineArgumentProvider;
import javax.inject.Inject;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import static java.util.Optional.ofNullable;
import static net.woggioni.gradle.jdeps.Constants.JDEPS_TASK_GROUP;
public abstract class JdepsTask extends Exec {
private final JavaToolchainSpec toolchain;
public JavaToolchainSpec toolchain(Action<? super JavaToolchainSpec> action) {
action.execute(toolchain);
return toolchain;
}
@Classpath
public abstract Property<FileCollection> getClasspath();
@Classpath
public abstract Property<FileCollection> getArchives();
@InputDirectory
public abstract DirectoryProperty getJavaHome();
@Input
@Optional
public abstract Property<String> getMainClass();
@Input
@Optional
public abstract Property<String> getMainModule();
@Input
public abstract ListProperty<String> getAdditionalModules();
@Inject
protected abstract JavaModuleDetector getJavaModuleDetector();
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@Optional
@OutputDirectory
public abstract DirectoryProperty getDotOutput();
@Input
@Optional
public abstract Property<Integer> getJavaRelease();
@Input
public abstract Property<Boolean> getRecursive();
private static final Logger log = Logging.getLogger(JdepsTask.class);
public JdepsTask() {
Project project = getProject();
setGroup(JDEPS_TASK_GROUP);
setDescription(
"Generates a custom Java runtime image that contains only the platform modules" +
" that are required for a given application");
ExtensionContainer ext = project.getExtensions();
JavaApplication javaApplication = ext.findByType(JavaApplication.class);
if (!Objects.isNull(javaApplication)) {
getMainClass().convention(javaApplication.getMainClass());
getMainModule().convention(javaApplication.getMainModule());
}
getClasspath().convention(project.files());
getRecursive().convention(true);
ProjectLayout layout = project.getLayout();
toolchain = getObjectFactory().newInstance(DefaultToolchainSpec.class);
JavaToolchainService javaToolchainService = ext.findByType(JavaToolchainService.class);
Provider<Directory> graalHomeDirectoryProvider = javaToolchainService.launcherFor(it -> {
it.getLanguageVersion().set(toolchain.getLanguageVersion());
it.getVendor().set(toolchain.getVendor());
it.getImplementation().set(toolchain.getImplementation());
}).map(javaLauncher ->
javaLauncher.getMetadata().getInstallationPath()
).orElse(layout.dir(project.provider(() -> project.file(System.getProperty("java.home")))));
getJavaHome().convention(graalHomeDirectoryProvider);
getJavaHome().convention(graalHomeDirectoryProvider);
getAdditionalModules().convention(new ArrayList<>());
ReportingExtension reporting = ext.getByType(ReportingExtension.class);
getOutputDir().convention(
reporting.getBaseDirectory()
.dir(project.getName() +
ofNullable(project.getVersion()).map(it -> "-" + it).orElse(""))
);
getDotOutput().convention(getOutputDir().dir("graphviz"));
Object executableProvider = new Object() {
@Override
public String toString() {
return getJavaHome().get() + "/bin/jdeps";
}
};
executable(executableProvider);
CommandLineArgumentProvider argumentProvider = new CommandLineArgumentProvider() {
@Override
@SneakyThrows
public Iterable<String> asArguments() {
List<String> result = new ArrayList<>();
JavaModuleDetector javaModuleDetector = getJavaModuleDetector();
FileCollection classpath = getClasspath().get();
FileCollection mp = javaModuleDetector.inferModulePath(true, classpath);
if (!mp.isEmpty()) {
result.add("--module-path");
result.add(mp.getAsPath());
}
FileCollection cp = classpath.minus(mp);
if(!cp.isEmpty()) {
result.add("-cp");
result.add(cp.getAsPath());
}
List<String> additionalModules = getAdditionalModules().get();
if (!additionalModules.isEmpty()) {
result.add("--add-modules");
final List<String> modules2BeAdded = new ArrayList<>();
modules2BeAdded.addAll(additionalModules);
if (!modules2BeAdded.isEmpty()) {
result.add(String.join(",", modules2BeAdded));
}
}
if (getDotOutput().isPresent()) {
result.add("-dotoutput");
result.add(getDotOutput().get().getAsFile().toString());
}
if(getRecursive().get()) {
result.add("--recursive");
} else {
result.add("--no-recursive");
}
if (getMainModule().isPresent()) {
result.add("-m");
result.add(getMainModule().get());
}
if (getJavaRelease().isPresent()) {
result.add("--multi-release");
result.add(getJavaRelease().get().toString());
}
for (File archive : getArchives().get()) {
result.add(archive.toString());
}
return Collections.unmodifiableList(result);
}
};
getArgumentProviders().add(argumentProvider);
}
@Override
@SneakyThrows
protected void exec() {
Files.walk(getOutputDir().get().getAsFile().toPath())
.sorted(Comparator.reverseOrder())
.forEach(new Consumer<Path>() {
@Override
@SneakyThrows
public void accept(Path path) {
Files.delete(path);
}
});
super.exec();
}
}
+6
View File
@@ -2,6 +2,12 @@ plugins {
id 'groovy-gradle-plugin'
}
java {
sourceCompatibility(JavaVersion.VERSION_1_8.toString())
targetCompatibility(JavaVersion.VERSION_1_8.toString())
modularity.inferModulePath = false
}
gradlePlugin {
plugins {
create("JPMSCheckPlugin") {
@@ -1,216 +1,43 @@
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.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import java.nio.file.Files
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
import org.gradle.api.file.RegularFile
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.ReportingBasePlugin
import org.gradle.api.reporting.ReportingExtension
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
@CompileStatic
void apply(Project project) {
project.tasks.register("jpms-check") {task ->
boolean recursive = project.properties["jpms-check.recursive"]?.with(Boolean.&parseBoolean) ?: false
String cfgName = project.properties["jpms-check.configurationName"] ?: "default"
String outputFormat = project.properties["jpms-check.outputFormat"] ?: "html"
Path outputFile = project.properties["jpms-check.outputFile"]?.with {
Paths.get(it as String)
} ?: with {
project.pluginManager.apply(ReportingBasePlugin.class)
project.tasks.register("jpms-check", JPMSCheckTask) {task ->
ReportingExtension reporting = project.extensions.getByType(ReportingExtension.class)
boolean recursive = project.properties["jpms-check.recursive"]?.with(Object.&toString)?.with(Boolean.&parseBoolean) ?: false
String cfgName = project.properties["jpms-check.configurationName"] ?: JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME
OutputFormat defaultOutputFormat = (project.properties["jpms-check.outputFormat"]
?.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) {
case "html":
Paths.get(project.buildDir.path, "jpms-report.html")
case OutputFormat.html:
result = dir.file( "jpms-report.html")
break
case "json":
Paths.get(project.buildDir.path, "jpms-report.json")
case OutputFormat.json:
result = dir.file( "jpms-report.json")
break
default:
throw new IllegalArgumentException("Unsupported output format: $outputFormat")
}
}
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")
}
}
}
result
}))
}
}
}
@@ -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
}
@@ -6,7 +6,6 @@ import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
@@ -28,7 +27,6 @@ public class LombokPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPluginManager().apply(JavaPlugin.class);
ObjectFactory objectFactory = project.getObjects();
LombokExtension ext = project.getExtensions().create("lombok", LombokExtension.class);
ExtraPropertiesExtension epe = project.getExtensions().getExtraProperties();
if(epe.has("version.lombok")) {
@@ -75,7 +73,9 @@ public class LombokPlugin implements Plugin<Project> {
delombok.getSourceSet().set(ss);
delombok.getOutputDir().set(outputDir);
delombok.getLombokJar().set(lombokConfiguration);
delombok.getInferModulePath().set(javaPluginExtension.getModularity().getInferModulePath());
// Disabled for now due to https://github.com/projectlombok/lombok/issues/2829
//delombok.getInferModulePath().set(javaPluginExtension.getModularity().getInferModulePath());
delombok.getInferModulePath().set(false);
}));
javadoc.setSource(outputDir);
javadoc.getInputs().files(delombokTaskProvider);
@@ -10,7 +10,6 @@ import org.gradle.api.attributes.LibraryElements;
import org.gradle.api.attributes.java.TargetJvmVersion;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.internal.plugins.DslObject;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.MapProperty;
@@ -167,7 +166,6 @@ public class MultiReleaseJarPlugin implements Plugin<Project> {
"classes/" + mainSourceSet.getName() + "/" + sourceDirectorySet.getName())
);
sourcePaths.add(sourceDirectorySet.getSourceDirectories());
new DslObject(mainSourceSet).getConvention().getPlugins().put(sourceDirectorySet.getName(), sourceDirectorySet);
mainSourceSet.getExtensions().add(SourceDirectorySet.class, sourceDirectorySet.getName(), sourceDirectorySet);
TaskProvider<JavaCompile> compileTask =
project.getTasks().register(JavaPlugin.COMPILE_JAVA_TASK_NAME + javaVersion.getMajorVersion(), JavaCompile.class,
-19
View File
@@ -2,25 +2,6 @@ plugins {
id "java-gradle-plugin"
}
childProjects.forEach {name, child ->
child.with {
apply plugin: 'maven-publish'
publishing {
repositories {
maven {
url = woggioniMavenRepositoryUrl
}
}
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}
}
}
evaluationDependsOnChildren()
configurations {
@@ -1,5 +1,7 @@
package net.woggioni.gradle.sambal;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.codehaus.groovy.runtime.MethodClosure;
import org.eclipse.jgit.api.Git;
@@ -13,11 +15,13 @@ import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeContainer;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.logging.Logger;
import org.gradle.api.plugins.AppliedPlugin;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.PluginManager;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ValueSource;
import org.gradle.api.provider.ValueSourceParameters;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.bundling.Jar;
@@ -27,6 +31,7 @@ import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -45,9 +50,6 @@ public class SambalPlugin implements Plugin<Project> {
private static Pattern tagPattern = Pattern.compile("^refs/tags/v?(\\d+\\.\\d+.*)");
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static final String currentTagCachedKey = "CURRENT_TAG_CACHED";
private static final String currentRevCachedKey = "CURRENT_REV_CACHED";
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
@@ -131,7 +133,7 @@ public class SambalPlugin implements Plugin<Project> {
}
@SneakyThrows
private static String getCurrentTag(Git git, Logger logger) {
private static List<String> getCurrentTag(Git git) {
List<Ref> tags = git.tagList().call();
Ref currentRef = git.getRepository().findRef("HEAD");
List<String> currentTag = tags.stream()
@@ -141,43 +143,10 @@ public class SambalPlugin implements Plugin<Project> {
.collect(Collectors.toList());
if (currentTag.isEmpty()) return null;
else {
if (currentTag.size() > 1) {
logger.warn("Found more than one tag in correct format for HEAD.");
}
return currentTag.get(0);
return currentTag;
}
}
@SneakyThrows
private static String getCurrentTag(Project project) {
ExtraPropertiesExtension ext = project.getRootProject().getExtensions().getExtraProperties();
if (!ext.has(currentTagCachedKey)) {
Git git = Git.open(project.getRootDir());
Status status = git.status().call();
String currentTag;
if (status.isClean() && (currentTag = getCurrentTag(git, project.getLogger())) != null) {
ext.set(currentTagCachedKey, currentTag);
} else {
ext.set(currentTagCachedKey, null);
}
}
return Optional.ofNullable(ext.get(currentTagCachedKey))
.map(Object::toString)
.orElse(null);
}
@SneakyThrows
private static String getGitRevision(Project project) {
ExtraPropertiesExtension ext = project.getRootProject().getExtensions().getExtraProperties();
if (!ext.has(currentRevCachedKey)) {
Git git = Git.open(project.getRootDir());
ext.set(currentRevCachedKey, git.getRepository().findRef("HEAD").getObjectId().name());
}
return Optional.ofNullable(ext.get(currentRevCachedKey))
.map(Object::toString)
.orElse(null);
}
private static String resolveProperty(Project project, String key, String defaultValue) {
if (project.hasProperty(key)) return project.property(key).toString();
else {
@@ -204,15 +173,58 @@ public class SambalPlugin implements Plugin<Project> {
}
}
@Getter
@Setter
public static class ProjectParameters implements ValueSourceParameters, Serializable {
private File rootDirectory;
}
public abstract static class GitTagValueSource implements ValueSource<List<String>, ProjectParameters> {
@Override
@SneakyThrows
public List<String> obtain() {
File rootDirectory = getParameters().getRootDirectory();
try(Git git = Git.open(rootDirectory)) {
Status status = git.status().call();
if (status.isClean()) {
return getCurrentTag(git);
} else {
return null;
}
}
}
}
public abstract static class GitRevisionValueSource implements ValueSource<String, ProjectParameters> {
@Override
@SneakyThrows
public String obtain() {
File rootDirectory = getParameters().getRootDirectory();
try (Git git = Git.open(rootDirectory)) {
return git.getRepository().findRef("HEAD").getObjectId().name();
}
}
}
@Override
public void apply(Project project) {
ExtraPropertiesExtension ext = project.getRootProject().getExtensions().getExtraProperties();
ext.set("getIntegerVersion", new MethodClosure(this, "getVersionInt").curry(project));
ext.set("currentTag", getCurrentTag(project));
final Provider<List<String>> gitTagProvider = project.getProviders().of(GitTagValueSource.class, it -> {
it.parameters( params -> params.setRootDirectory(project.getRootDir()));
});
ext.set("currentTag", gitTagProvider);
ext.set("resolveProperty", new MethodClosure(this, "resolveProperty").curry(project));
ext.set("copyConfigurationAttributes", new MethodClosure(this, "copyConfigurationAttributes"));
final String gitRevision = getGitRevision(project);
ext.set("gitRevision", gitRevision);
final Provider<String> gitRevisionProvider = project.getProviders().of(GitRevisionValueSource.class, it -> {
it.parameters( params -> params.setRootDirectory(project.getRootDir()));
});
ext.set("gitRevision", gitRevisionProvider);
ext.set("which", new MethodClosure(this, "which").curry(project));
PluginManager pluginManager = project.getPluginManager();
pluginManager.withPlugin("java-library", (AppliedPlugin plugin) -> {
@@ -223,7 +235,7 @@ public class SambalPlugin implements Plugin<Project> {
Attributes attrs = mf.getAttributes();
attrs.put(java.util.jar.Attributes.Name.SPECIFICATION_TITLE.toString(), project.getName());
attrs.put(java.util.jar.Attributes.Name.SPECIFICATION_VERSION.toString(), project.getVersion());
attrs.put(java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString(), gitRevision);
attrs.put(java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString(), gitRevisionProvider);
});
});
});
@@ -6,7 +6,6 @@ import org.gradle.api.attributes.AttributeCompatibilityRule;
import org.gradle.api.attributes.AttributeDisambiguationRule;
import org.gradle.api.attributes.CompatibilityCheckDetails;
import org.gradle.api.attributes.MultipleCandidatesDetails;
import org.gradle.api.internal.ReusableAction;
import java.util.Arrays;
import java.util.Comparator;
@@ -19,7 +18,7 @@ public interface Sealing extends Named {
String sealed = "sealed";
String open = "open";
class CompatibilityRules implements AttributeCompatibilityRule<Sealing>, ReusableAction {
class CompatibilityRules implements AttributeCompatibilityRule<Sealing> {
public void execute(CompatibilityCheckDetails<Sealing> details) {
Sealing consumerValue = details.getConsumerValue();
Sealing producerValue = details.getProducerValue();
@@ -35,7 +34,7 @@ public interface Sealing extends Named {
}
}
class DisambiguationRules implements AttributeDisambiguationRule<Sealing>, ReusableAction {
class DisambiguationRules implements AttributeDisambiguationRule<Sealing> {
private static final List<String> ORDER = Arrays.asList(open, sealed);
private static final Comparator<Sealing> comparator =
Comparator.comparingInt(sealing -> ORDER.indexOf(sealing.getName()));
@@ -6,7 +6,6 @@ import org.gradle.api.attributes.AttributeCompatibilityRule;
import org.gradle.api.attributes.AttributeDisambiguationRule;
import org.gradle.api.attributes.CompatibilityCheckDetails;
import org.gradle.api.attributes.MultipleCandidatesDetails;
import org.gradle.api.internal.ReusableAction;
import java.util.Arrays;
import java.util.Comparator;
@@ -19,7 +18,7 @@ public interface Signing extends Named {
String signed = "signed";
String unsigned = "unsigned";
class CompatibilityRules implements AttributeCompatibilityRule<Signing>, ReusableAction {
class CompatibilityRules implements AttributeCompatibilityRule<Signing> {
public void execute(CompatibilityCheckDetails<Signing> details) {
Signing consumerValue = details.getConsumerValue();
Signing producerValue = details.getProducerValue();
@@ -35,7 +34,7 @@ public interface Signing extends Named {
}
}
class DisambiguationRules implements AttributeDisambiguationRule<Signing>, ReusableAction {
class DisambiguationRules implements AttributeDisambiguationRule<Signing> {
private static final List<String> ORDER = Arrays.asList(unsigned, signed);
private static final Comparator<Signing> comparator =
Comparator.comparingInt(signing -> ORDER.indexOf(signing.getName()));
+4 -1
View File
@@ -1,7 +1,7 @@
dependencyResolutionManagement {
repositories {
maven {
url = 'https://woggioni.net/mvn/'
url = getProperty('gitea.maven.url')
content {
includeGroup 'com.lys'
}
@@ -27,3 +27,6 @@ include 'osgi-app:osgi-simple-bootstrapper-application'
include 'wildfly'
include 'sambal'
include 'graalvm'
include 'jdeps'
include 'finalguard'
include 'finalguard:finalguard-javac-plugin'