diff --git a/gradle-plugin-jlink/build.gradle b/gradle-plugin-jlink/build.gradle index 2fe8fb5..f7bf38a 100755 --- a/gradle-plugin-jlink/build.gradle +++ b/gradle-plugin-jlink/build.gradle @@ -12,6 +12,8 @@ apply from: rootProject.file('gradle/test/junit5.gradle') dependencies { api gradleApi() testImplementation gradleTestKit() + testImplementation testLibs.spock.core + testImplementation testLibs.spock.junit4 } if (project.hasProperty('gradle.publish.key')) { diff --git a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkExtension.java b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkExtension.java index b39fd1a..36c02fe 100644 --- a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkExtension.java +++ b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkExtension.java @@ -1,25 +1,85 @@ package org.xbib.gradle.plugin.jlink; +import org.gradle.api.Project; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import java.util.List; + public class JLinkExtension { - ListProperty modules; + private final ListProperty modules; - Property compress; + private final Property compress; - Property stripDebug; + private final Property stripDebug; - Property noHeaderFiles; + private final Property noHeaderFiles; - Property noManPages; + private final Property noManPages; - Property endian; + private final Property endian; - enum Endian { + public enum Endian { LITTLE, BIG, NATIVE, } + + public JLinkExtension(Project project) { + this.modules = project.getObjects().listProperty(String.class); + this.compress = project.getObjects().property(Integer.class); + this.stripDebug = project.getObjects().property(Boolean.class); + this.noHeaderFiles = project.getObjects().property(Boolean.class); + this.noManPages = project.getObjects().property(Boolean.class); + this.endian = project.getObjects().property(Endian.class); + } + + public void setModules(List modules) { + this.modules.set(modules); + } + + public ListProperty getModules() { + return modules; + } + + public void setCompress(Integer compress) { + this.compress.set(compress); + } + + public Property getCompress() { + return compress; + } + + public void setStripDebug(Boolean stripDebug) { + this.stripDebug.set(stripDebug); + } + + public Property getStripDebug() { + return stripDebug; + } + + public void setNoHeaderFiles(Boolean noHeaderFiles) { + this.noHeaderFiles.set(noHeaderFiles); + } + + public Property getNoHeaderFiles() { + return noHeaderFiles; + } + + public void setNoManPages(Boolean noManPages) { + this.noManPages.set(noManPages); + } + + public Property getNoManPages() { + return noManPages; + } + + public void setEndian(Endian endian) { + this.endian.set(endian); + } + + public Property getEndian() { + return endian; + } } diff --git a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkPlugin.java b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkPlugin.java index ec9cad0..30a6dd3 100644 --- a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkPlugin.java +++ b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkPlugin.java @@ -6,8 +6,10 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.attributes.Usage; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; import java.util.List; +import java.util.Objects; public abstract class JLinkPlugin implements Plugin { @@ -15,30 +17,35 @@ public abstract class JLinkPlugin implements Plugin { public void apply(Project project) { project.getPluginManager().apply(JavaPlugin.class); JLinkExtension extension = project.getExtensions().create("jlink", JLinkExtension.class); - extension.modules.convention(List.of("java.base")); - extension.compress.convention(2); - extension.stripDebug.convention(true); - extension.noHeaderFiles.convention(true); - extension.noManPages.convention(true); - extension.endian.convention(JLinkExtension.Endian.NATIVE); - TaskProvider jlinkTask = project.getTasks().register("jlink", JLinkTask.class); + extension.getModules().convention(List.of("java.base")); + extension.getCompress().convention(6); // zip-6, default + extension.getStripDebug().convention(true); + extension.getNoHeaderFiles().convention(true); + extension.getNoManPages().convention(true); + extension.getEndian().convention(JLinkExtension.Endian.NATIVE); + TaskProvider jarTask = project.getTasks().named("jar", Jar.class); + Objects.requireNonNull(jarTask); + TaskProvider jModTask = project.getTasks().register("jmod", JModTask.class); + project.getTasks().withType(JModTask.class).forEach(it -> { + it.dependsOn(jarTask); + }); + Objects.requireNonNull(jModTask); + TaskProvider jLinkTask = project.getTasks().register("jlink", JLinkTask.class); project.getTasks().withType(JLinkTask.class).forEach(it -> { - it.modules.set(extension.modules); - it.compress.set(extension.compress); - it.stripDebug.set(extension.stripDebug); - it.noHeaderFiles.set(extension.noHeaderFiles); - it.noManPages.set(extension.noManPages); - it.endian.set(extension.endian); + it.dependsOn(jModTask); + it.modules.set(extension.getModules()); + it.compress.set(extension.getCompress()); + it.stripDebug.set(extension.getStripDebug()); + it.noHeaderFiles.set(extension.getNoHeaderFiles()); + it.noManPages.set(extension.getNoManPages()); + it.endian.set(extension.getEndian()); }); Configuration configuration = project.getConfigurations().create("jlink"); configuration.setCanBeConsumed(true); configuration.setCanBeResolved(false); configuration.setVisible(false); - configuration.attributes(a -> { - a.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, "jlink")); - }); - configuration.outgoing(o -> { - o.artifact(jlinkTask.flatMap(t -> t.outputDirectory)); - }); + configuration.attributes(a -> a.attribute(Usage.USAGE_ATTRIBUTE, + project.getObjects().named(Usage.class, "jlink"))); + configuration.outgoing(o -> o.artifact(jLinkTask.flatMap(t -> t.outputDirectory))); } } diff --git a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkTask.java b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkTask.java index 6a0311c..d703a96 100644 --- a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkTask.java +++ b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JLinkTask.java @@ -1,6 +1,8 @@ package org.xbib.gradle.plugin.jlink; +import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.logging.LogLevel; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; @@ -9,11 +11,14 @@ import org.gradle.api.tasks.AbstractExecTask; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.toolchain.JavaCompiler; import org.gradle.jvm.toolchain.JavaToolchainService; import org.gradle.jvm.toolchain.JavaToolchainSpec; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -26,25 +31,32 @@ import java.util.List; import java.util.Locale; public class JLinkTask extends AbstractExecTask { + @Nested Property javaCompiler; @Input + @Optional ListProperty modules; @Input + @Optional Property compress; @Input + @Optional Property stripDebug; @Input + @Optional Property noHeaderFiles; @Input + @Optional Property noManPages; @Input + @Optional Property endian; @InputDirectory @@ -53,33 +65,54 @@ public class JLinkTask extends AbstractExecTask { @OutputDirectory DirectoryProperty outputDirectory; - public JLinkTask(Class taskType) { - super(taskType); + @Inject + public JLinkTask() { + super(JLinkTask.class); + this.javaCompiler = getProject().getObjects().property(JavaCompiler.class); + this.modules = getProject().getObjects().listProperty(String.class); + this.compress = getProject().getObjects().property(Integer.class); + this.stripDebug = getProject().getObjects().property(Boolean.class); + this.noHeaderFiles = getProject().getObjects().property(Boolean.class); + this.noManPages = getProject().getObjects().property(Boolean.class); + this.endian = getProject().getObjects().property(JLinkExtension.Endian.class); + this.modulePath = getProject().getObjects().directoryProperty(); + this.outputDirectory = getProject().getObjects().directoryProperty(); JavaToolchainSpec toolchain = getProject().getExtensions().getByType(JavaPluginExtension.class).getToolchain(); JavaToolchainService service = getProject().getExtensions().getByType(JavaToolchainService.class); - Provider defaultJlinkTool = service.compilerFor(toolchain); - javaCompiler.convention(defaultJlinkTool); + javaCompiler.convention(service.compilerFor(toolchain)); modules.convention(List.of("java.base")); - modulePath.convention(javaCompiler.map(it -> it.getMetadata().getInstallationPath().dir("jmods"))); - outputDirectory.convention(getProject().getLayout().getBuildDirectory().dir("jlink-jre")); + Provider modulePathProvider = javaCompiler.map(it -> { + Directory jmods = it.getMetadata().getInstallationPath().dir("jmods"); + if (jmods.getAsFile().exists()) { + return jmods; + } else { + getLogger().log(LogLevel.WARN, "directory " + jmods + " does not exist! Is jmods package installed?"); + return null; + } + }); + modulePath.convention(modulePathProvider); + outputDirectory.convention(getProject().getLayout().getBuildDirectory().dir("jlink")); } @Override public void exec() { setExecutable(javaCompiler.get().getMetadata().getInstallationPath().file("bin/jlink")); - File jlinkOutput = outputDirectory.dir("jre").get().getAsFile(); + TaskProvider jmodTask = getProject().getTasks().named("jmod", JModTask.class); + File jlinkOutput = outputDirectory.dir("jlink-output").get().getAsFile(); try { - delete(jlinkOutput.toPath()); + createDirectory(jlinkOutput.toPath()); } catch (IOException e) { throw new RuntimeException(e); } List args = new ArrayList<>(List.of( "--module-path", modulePath.get().getAsFile().getAbsolutePath(), + "--module-path", + jmodTask.get().jmodFile.getAsFile().get().getAbsolutePath(), "--add-modules", String.join(",", modules.get()), "--compress", - "${compress.get()}", + "zip-" + compress.get(), "--output", jlinkOutput.getAbsolutePath() )); @@ -97,22 +130,100 @@ public class JLinkTask extends AbstractExecTask { args.add(endian.get().toString().toLowerCase(Locale.ROOT)); } setArgs(args); + System.err.println( "executing " + getExecutable() + " with " + args); super.exec(); } - private void delete(Path path) throws IOException { - Files.walkFileTree(path, new SimpleFileVisitor<>() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } + public void setJavaCompiler(JavaCompiler javaCompiler) { + this.javaCompiler.set(javaCompiler); + } - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - }); + public Property getJavaCompiler() { + return javaCompiler; + } + + public void setModules(List modules) { + this.modules.set(modules); + } + + public ListProperty getModules() { + return modules; + } + + public void setCompress(Integer compress) { + this.compress.set(compress); + } + + public Property getCompress() { + return compress; + } + + public void setStripDebug(Boolean stripDebug) { + this.stripDebug.set(stripDebug); + } + + public Property getStripDebug() { + return stripDebug; + } + + public void setNoHeaderFiles(Boolean noHeaderFiles) { + this.noHeaderFiles.set(noHeaderFiles); + } + + public Property getNoHeaderFiles() { + return noHeaderFiles; + } + + public void setNoManPages(Boolean noManPages) { + this.noManPages.set(noManPages); + } + + public Property getNoManPages() { + return noManPages; + } + + public void setEndian(JLinkExtension.Endian endian) { + this.endian.set(endian); + } + + public Property getEndian() { + return endian; + } + + public void setModulePath(Directory modulePath) { + this.modulePath.set(modulePath); + } + + public DirectoryProperty getModulePath() { + return modulePath; + } + + public void setOutputDirectory(Directory outputDirectory) { + this.outputDirectory.set(outputDirectory); + } + + public DirectoryProperty getOutputDirectory() { + return outputDirectory; + } + + private static void createDirectory(Path path) throws IOException { + if (Files.exists(path)) { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + Files.delete(path); + } + // create only parent, jlink aborts if output directory exists + Files.createDirectories(path.getParent()); } } diff --git a/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JModTask.java b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JModTask.java new file mode 100644 index 0000000..49bf8cb --- /dev/null +++ b/gradle-plugin-jlink/src/main/java/org/xbib/gradle/plugin/jlink/JModTask.java @@ -0,0 +1,118 @@ +package org.xbib.gradle.plugin.jlink; + +import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.AbstractExecTask; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; +import org.gradle.jvm.toolchain.JavaCompiler; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.toolchain.JavaToolchainSpec; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class JModTask extends AbstractExecTask { + + @Nested + Property javaCompiler; + + @InputFile + RegularFileProperty jarFile; + + @OutputFile + RegularFileProperty jmodFile; + + @Inject + public JModTask() { + super(JModTask.class); + javaCompiler = getProject().getObjects().property(JavaCompiler.class); + JavaToolchainSpec toolchain = getProject().getExtensions().getByType(JavaPluginExtension.class).getToolchain(); + JavaToolchainService service = getProject().getExtensions().getByType(JavaToolchainService.class); + javaCompiler.convention(service.compilerFor(toolchain)); + jarFile = getProject().getObjects().fileProperty(); + TaskProvider jarTask = getProject().getTasks().named("jar", Jar.class); + Objects.requireNonNull(jarTask); + jarFile.convention(jarTask.get().getArchiveFile()); + jmodFile = getProject().getObjects().fileProperty(); + jmodFile.convention(getProject().getLayout().getBuildDirectory() + .dir("jmod").get().file(getProject().getName() + ".jmod")); + } + + @Override + public void exec() { + setExecutable(javaCompiler.get().getMetadata().getInstallationPath().file("bin/jmod")); + try { + createDirectory(jmodFile.getAsFile().get().toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + List args = new ArrayList<>(List.of( + "create", + "--class-path", + jarFile.get().getAsFile().getAbsolutePath(), + jmodFile.get().getAsFile().getAbsolutePath() + )); + setArgs(args); + System.err.println( "executing " + getExecutable() + " with " + args); + super.exec(); + } + + public void setJavaCompiler(JavaCompiler javaCompiler) { + this.javaCompiler.set(javaCompiler); + } + + public Property getJavaCompiler() { + return javaCompiler; + } + + public void setJarFile(RegularFile jarFile) { + this.jarFile.set(jarFile); + } + + public RegularFileProperty getJarFile() { + return jarFile; + } + + public void setJmodFile(RegularFile jmodFile) { + this.jmodFile.set(jmodFile); + } + + public RegularFileProperty getJmodFile() { + return jmodFile; + } + + private static void createDirectory(Path path) throws IOException { + if (Files.exists(path)) { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + Files.delete(path); + } + // create only parent + Files.createDirectories(path.getParent()); + } +} diff --git a/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkGradleBuild.groovy b/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkGradleBuild.groovy new file mode 100644 index 0000000..999d0b7 --- /dev/null +++ b/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkGradleBuild.groovy @@ -0,0 +1,110 @@ +package org.xbib.gradle.plugin.jlink + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner + +class JLinkGradleBuild { + + final File projectDir + final File settingsFile + final File appBuildFile + final File appModuleInfoFile + final File libBuildFile + final File libModuleInfoFile + + final String gradleVersionUnderTest = System.getProperty('gradleVersionUnderTest') + + JLinkGradleBuild() { + this.projectDir = createDirectory('build/jlink-gradle-build') + this.settingsFile = tapFile('settings.gradle') + this.appBuildFile = tapFile('app/build.gradle') + this.appModuleInfoFile = tapFile('app/src/main/java/module-info.java') + this.libBuildFile = tapFile('lib/build.gradle') + this.libModuleInfoFile = tapFile('lib/src/main/java/module-info.java') + settingsFile << ''' + dependencyResolutionManagement { repositories.mavenCentral() } + includeBuild(".") + rootProject.name = "test-project" + include("app", "lib") + ''' + appBuildFile << ''' + plugins { + id("org.xbib.gradle.plugin.jlink") + } + group = "org.example" + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + ''' + tapFile("app/src/main/java/org/example/app/Main.java") << ''' + package org.example.app; + public class Main { + public static void main(String... args) { + System.out.println("Hello world"); + } + } + ''' + tapFile("app/src/test/java/org/example/app/test/MainTest.java") << ''' + package org.example.app.test; + import org.junit.jupiter.api.Test; + import org.example.app.Main; + public class MainTest { + @Test + void testApp() { + Main main = new Main(); + main.main(); + } + } + ''' + libBuildFile << ''' + plugins { + id("org.xbib.gradle.plugin.jlink") + } + group = "org.example" + ''' + } + + static File createDirectory(String path) { + File f = new File(path) + if (f.exists()) { + f.deleteDir() + } + f.mkdirs() + f + } + + File tapFile(String path) { + new File(projectDir, path).tap { + it.getParentFile().mkdirs() + } + } + + static boolean runsOnLinux() { + hostOs().contains('linux') + } + + static String hostOs() { + System.getProperty("os.name").replace(" ", "").toLowerCase() + } + + BuildResult build(taskToRun) { + runner(taskToRun).build() + } + + BuildResult fail(taskToRun) { + runner(taskToRun).buildAndFail() + } + + GradleRunner runner(String... args) { + GradleRunner.create() + .forwardOutput() + .withPluginClasspath() + .withProjectDir(projectDir) + .withArguments(Arrays.asList(args)) + .with { + gradleVersionUnderTest ? it.withGradleVersion(gradleVersionUnderTest) : it + } + } +} diff --git a/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkTest.groovy b/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkTest.groovy new file mode 100644 index 0000000..b3a67e2 --- /dev/null +++ b/gradle-plugin-jlink/src/test/groovy/org/xbib/gradle/plugin/jlink/JLinkTest.groovy @@ -0,0 +1,40 @@ +package org.xbib.gradle.plugin.jlink + +import spock.lang.Specification + +import static org.gradle.testkit.runner.TaskOutcome.FAILED +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS +import static org.xbib.gradle.plugin.jlink.JLinkGradleBuild.runsOnLinux + +class JLinkTest extends Specification { + + @Delegate + JLinkGradleBuild build = new JLinkGradleBuild() + + def "can use jlink plugin with success=#success"() { + given: + def taskToRun = ":app:jlink" + appBuildFile << """ + jlink { + modules.set(List.of("org.example.app")) + } + """ + appModuleInfoFile << """ + module org.example.app { + exports org.example.app; + } + """ + + when: + def result = success ? build(taskToRun) : fail(taskToRun) + + then: + println result.output + result.task(taskToRun).outcome == (success ? SUCCESS : FAILED) + success + + where: + label | os | success + 'fedora' | 'linux' | runsOnLinux() + } +}