From 1ed4d8946645b3d93b1c7c92e2f7f2485ec5b6d8 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 16 Sep 2022 12:26:24 -0500 Subject: [PATCH] Copy native reachability metadata to jar file in Gradle plugin When the Spring Boot Gradle plugin builds a fat jar and the Native Build Tools Gradle plugin is applied to the build, any configuration files from the GraalVM reachability metadata repository that match project dependencies are copied to a `META-INF/native-image` directory in the fat jar. Closes gh-32408 --- .../plugin/NativeImagePluginAction.java | 64 +++++++++++++++---- ...tiveImagePluginActionIntegrationTests.java | 61 ++++++++++++++++++ ...ataConfigurationFilesAreCopiedToJar.gradle | 23 +++++++ 3 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-reachabilityMetadataConfigurationFilesAreCopiedToJar.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index d00b86b960..28aa38ba76 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -16,24 +16,32 @@ package org.springframework.boot.gradle.plugin; +import java.io.File; +import java.nio.file.Path; + import org.graalvm.buildtools.gradle.NativeImagePlugin; import org.graalvm.buildtools.gradle.dsl.GraalVMExtension; import org.graalvm.buildtools.gradle.dsl.GraalVMReachabilityMetadataRepositoryExtension; +import org.graalvm.buildtools.gradle.dsl.NativeImageOptions; import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask; import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.file.FileCopyDetails; import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; +import org.springframework.boot.gradle.tasks.bundling.BootJar; + /** * {@link Action} that is executed in response to the {@link NativeImagePlugin} being * applied. * * @author Andy Wilkinson + * @author Scott Frederick */ class NativeImagePluginAction implements PluginApplicationAction { @@ -49,23 +57,53 @@ class NativeImagePluginAction implements PluginApplicationAction { project.getPlugins().withType(JavaPlugin.class).all((plugin) -> { JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); SourceSetContainer sourceSets = javaPluginExtension.getSourceSets(); - SourceSet aotSourceSet = sourceSets.getByName(SpringBootAotPlugin.AOT_SOURCE_SET_NAME); - project.getTasks().named(NativeImagePlugin.NATIVE_COMPILE_TASK_NAME, BuildNativeImageTask.class, - (nativeCompile) -> nativeCompile.getOptions().get().classpath(aotSourceSet.getOutput())); - SourceSet aotTestSourceSet = sourceSets.getByName(SpringBootAotPlugin.AOT_TEST_SOURCE_SET_NAME); - project.getTasks().named("nativeTestCompile", BuildNativeImageTask.class, - (nativeTestCompile) -> nativeTestCompile.getOptions().get() - .classpath(aotTestSourceSet.getOutput())); + configureTaskClasspath(project, NativeImagePlugin.NATIVE_COMPILE_TASK_NAME, + sourceSets.getByName(SpringBootAotPlugin.AOT_SOURCE_SET_NAME)); + configureTaskClasspath(project, NativeImagePlugin.NATIVE_TEST_COMPILE_TASK_NAME, + sourceSets.getByName(SpringBootAotPlugin.AOT_TEST_SOURCE_SET_NAME)); + GraalVMExtension graalVmExtension = configureGraalVmExtension(project); + configureGraalVmReachabilityExtension(graalVmExtension); + copyReachabilityMetadataToBootJar(project, graalVmExtension); }); - GraalVMExtension graalVmExtension = project.getExtensions().getByType(GraalVMExtension.class); - graalVmExtension.getToolchainDetection().set(false); - reachabilityExtensionOn(graalVmExtension).getEnabled().set(true); } - private static GraalVMReachabilityMetadataRepositoryExtension reachabilityExtensionOn( - GraalVMExtension graalVmExtension) { - return ((ExtensionAware) graalVmExtension).getExtensions() + private void configureTaskClasspath(Project project, String taskName, SourceSet sourceSet) { + project.getTasks().named(taskName, BuildNativeImageTask.class, + (nativeCompile) -> nativeCompile.getOptions().get().classpath(sourceSet.getOutput())); + } + + private GraalVMExtension configureGraalVmExtension(Project project) { + GraalVMExtension extension = project.getExtensions().getByType(GraalVMExtension.class); + extension.getToolchainDetection().set(false); + return extension; + } + + private void configureGraalVmReachabilityExtension(GraalVMExtension graalVmExtension) { + GraalVMReachabilityMetadataRepositoryExtension extension = ((ExtensionAware) graalVmExtension).getExtensions() .getByType(GraalVMReachabilityMetadataRepositoryExtension.class); + extension.getEnabled().set(true); + } + + private void copyReachabilityMetadataToBootJar(Project project, GraalVMExtension graalVmExtension) { + Path repositoryCacheDir = new File(project.getGradle().getGradleUserHomeDir(), + "native-build-tools/repositories").toPath(); + project.getTasks().named(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class).configure((bootJar) -> { + NativeImageOptions options = graalVmExtension.getBinaries().named(NativeImagePlugin.NATIVE_MAIN_EXTENSION) + .get(); + bootJar.from(options.getConfigurationFileDirectories()) + .eachFile((file) -> normalizePathIfNecessary(repositoryCacheDir, file)); + }); + } + + private void normalizePathIfNecessary(Path repositoryCacheDir, FileCopyDetails configurationFile) { + Path configurationFilePath = configurationFile.getFile().toPath(); + if (configurationFilePath.startsWith(repositoryCacheDir)) { + Path versionDir = configurationFilePath.getParent(); + Path artifactDir = versionDir.getParent(); + Path groupDir = artifactDir.getParent(); + Path gavParentDir = groupDir.getParent(); + configurationFile.setPath("/META-INF/native-image/" + gavParentDir.relativize(configurationFilePath)); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 2a3d685443..14f1f3dda6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -16,6 +16,18 @@ package org.springframework.boot.gradle.plugin; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; import org.springframework.boot.gradle.junit.GradleCompatibility; @@ -27,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Integration tests for {@link NativeImagePluginAction}. * * @author Andy Wilkinson + * @author Scott Frederick */ @GradleCompatibility(configurationCache = true) class NativeImagePluginActionIntegrationTests { @@ -39,4 +52,52 @@ class NativeImagePluginActionIntegrationTests { .contains("org.springframework.boot.aot applied = true"); } + @TestTemplate + void reachabilityMetadataConfigurationFilesAreCopiedToJar() throws IOException { + writeDummyAotProcessorMainClass(); + BuildResult result = this.gradleBuild.build("bootJar"); + assertThat(result.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + File jarFile = new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"); + assertThat(buildLibs.listFiles()).contains(jarFile); + assertThat(getEntryNames(jarFile)).contains( + "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reflect-config.json", + "META-INF/native-image/org.jline/jline/3.21.0/jni-config.json", + "META-INF/native-image/org.jline/jline/3.21.0/proxy-config.json", + "META-INF/native-image/org.jline/jline/3.21.0/reflect-config.json", + "META-INF/native-image/org.jline/jline/3.21.0/resource-config.json"); + } + + private void writeDummyAotProcessorMainClass() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/org/springframework/boot"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "AotProcessor.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package org.springframework.boot;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class AotProcessor {"); + writer.println(); + writer.println(" public static void main(String[] args) {"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + protected List getEntryNames(File file) throws IOException { + List entryNames = new ArrayList<>(); + try (JarFile jarFile = new JarFile(file)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + entryNames.add(entries.nextElement().getName()); + } + } + return entryNames; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-reachabilityMetadataConfigurationFilesAreCopiedToJar.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-reachabilityMetadataConfigurationFilesAreCopiedToJar.gradle new file mode 100644 index 0000000000..9f904e3339 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-reachabilityMetadataConfigurationFilesAreCopiedToJar.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'org.springframework.boot.aot' +} + +apply plugin: 'org.graalvm.buildtools.native' + +repositories { + mavenCentral() +} + +dependencies { + implementation "ch.qos.logback:logback-classic:1.2.11" + implementation "org.jline:jline:3.21.0" +} + +// see https://github.com/graalvm/native-build-tools/issues/302 +graalvmNative { + agent { + tasksToInstrumentPredicate = { t -> false } as java.util.function.Predicate + } +} \ No newline at end of file