first jmod/jlink Hello world project, successful test

This commit is contained in:
Jörg Prante 2024-10-30 17:42:21 +01:00
parent 20f5a45db9
commit e91f776db2
7 changed files with 496 additions and 48 deletions

View file

@ -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')) {

View file

@ -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<String> modules;
private final ListProperty<String> modules;
Property<Integer> compress;
private final Property<Integer> compress;
Property<Boolean> stripDebug;
private final Property<Boolean> stripDebug;
Property<Boolean> noHeaderFiles;
private final Property<Boolean> noHeaderFiles;
Property<Boolean> noManPages;
private final Property<Boolean> noManPages;
Property<Endian> endian;
private final Property<Endian> 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<String> modules) {
this.modules.set(modules);
}
public ListProperty<String> getModules() {
return modules;
}
public void setCompress(Integer compress) {
this.compress.set(compress);
}
public Property<Integer> getCompress() {
return compress;
}
public void setStripDebug(Boolean stripDebug) {
this.stripDebug.set(stripDebug);
}
public Property<Boolean> getStripDebug() {
return stripDebug;
}
public void setNoHeaderFiles(Boolean noHeaderFiles) {
this.noHeaderFiles.set(noHeaderFiles);
}
public Property<Boolean> getNoHeaderFiles() {
return noHeaderFiles;
}
public void setNoManPages(Boolean noManPages) {
this.noManPages.set(noManPages);
}
public Property<Boolean> getNoManPages() {
return noManPages;
}
public void setEndian(Endian endian) {
this.endian.set(endian);
}
public Property<Endian> getEndian() {
return endian;
}
}

View file

@ -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<Project> {
@ -15,30 +17,35 @@ public abstract class JLinkPlugin implements Plugin<Project> {
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> 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<Jar> jarTask = project.getTasks().named("jar", Jar.class);
Objects.requireNonNull(jarTask);
TaskProvider<JModTask> jModTask = project.getTasks().register("jmod", JModTask.class);
project.getTasks().withType(JModTask.class).forEach(it -> {
it.dependsOn(jarTask);
});
Objects.requireNonNull(jModTask);
TaskProvider<JLinkTask> 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)));
}
}

View file

@ -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<JLinkTask> {
@Nested
Property<JavaCompiler> javaCompiler;
@Input
@Optional
ListProperty<String> modules;
@Input
@Optional
Property<Integer> compress;
@Input
@Optional
Property<Boolean> stripDebug;
@Input
@Optional
Property<Boolean> noHeaderFiles;
@Input
@Optional
Property<Boolean> noManPages;
@Input
@Optional
Property<JLinkExtension.Endian> endian;
@InputDirectory
@ -53,33 +65,54 @@ public class JLinkTask extends AbstractExecTask<JLinkTask> {
@OutputDirectory
DirectoryProperty outputDirectory;
public JLinkTask(Class<JLinkTask> 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<JavaCompiler> 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<Directory> 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> 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<String> 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,10 +130,84 @@ public class JLinkTask extends AbstractExecTask<JLinkTask> {
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 {
public void setJavaCompiler(JavaCompiler javaCompiler) {
this.javaCompiler.set(javaCompiler);
}
public Property<JavaCompiler> getJavaCompiler() {
return javaCompiler;
}
public void setModules(List<String> modules) {
this.modules.set(modules);
}
public ListProperty<String> getModules() {
return modules;
}
public void setCompress(Integer compress) {
this.compress.set(compress);
}
public Property<Integer> getCompress() {
return compress;
}
public void setStripDebug(Boolean stripDebug) {
this.stripDebug.set(stripDebug);
}
public Property<Boolean> getStripDebug() {
return stripDebug;
}
public void setNoHeaderFiles(Boolean noHeaderFiles) {
this.noHeaderFiles.set(noHeaderFiles);
}
public Property<Boolean> getNoHeaderFiles() {
return noHeaderFiles;
}
public void setNoManPages(Boolean noManPages) {
this.noManPages.set(noManPages);
}
public Property<Boolean> getNoManPages() {
return noManPages;
}
public void setEndian(JLinkExtension.Endian endian) {
this.endian.set(endian);
}
public Property<JLinkExtension.Endian> 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 {
@ -114,5 +221,9 @@ public class JLinkTask extends AbstractExecTask<JLinkTask> {
return FileVisitResult.CONTINUE;
}
});
Files.delete(path);
}
// create only parent, jlink aborts if output directory exists
Files.createDirectories(path.getParent());
}
}

View file

@ -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<JModTask> {
@Nested
Property<JavaCompiler> 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<Jar> 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<String> 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<JavaCompiler> 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());
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}