Skip to content

Commit 0716493

Browse files
committed
Generate 'META-INF/native-image/argfile' file for buildpack use
Update the Maven and Gradle plugin to generate an `argfile` file file under `META-INF/native-image` that contains `--exclude-config` arguments that should be passed when generating a native image. The contents of the file is generated for each nested jar that has a `reachability-metadata.properties` file containing 'override=true'. The `reachability-metadata.properties` file is expected to be generated by the Graal native build tools plugin. Closes spring-projectsgh-32738
1 parent 430c6b7 commit 0716493

File tree

10 files changed

+227
-34
lines changed

10 files changed

+227
-34
lines changed

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,12 @@ private String determineSpringBootVersion() {
105105
return (version != null) ? version : "unknown";
106106
}
107107

108-
CopyAction createCopyAction(Jar jar) {
109-
return createCopyAction(jar, null, null);
108+
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies) {
109+
return createCopyAction(jar, resolvedDependencies, null, null);
110110
}
111111

112-
CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, String layerToolsLocation) {
112+
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, LayerResolver layerResolver,
113+
String layerToolsLocation) {
113114
File output = jar.getArchiveFile().get().getAsFile();
114115
Manifest manifest = jar.getManifest();
115116
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
@@ -122,7 +123,7 @@ CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, String layerTo
122123
String encoding = jar.getMetadataCharset();
123124
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, includeDefaultLoader,
124125
layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver,
125-
encoding, layerResolver);
126+
encoding, resolvedDependencies, layerResolver);
126127
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
127128
}
128129

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ protected CopyAction createCopyAction() {
136136
if (!isLayeredDisabled()) {
137137
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
138138
String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null;
139-
return this.support.createCopyAction(this, layerResolver, layerToolsLocation);
139+
return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation);
140140
}
141-
return this.support.createCopyAction(this);
141+
return this.support.createCopyAction(this, this.resolvedDependencies);
142142
}
143143

144144
@Override

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ protected CopyAction createCopyAction() {
111111
if (!isLayeredDisabled()) {
112112
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
113113
String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null;
114-
return this.support.createCopyAction(this, layerResolver, layerToolsLocation);
114+
return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation);
115115
}
116-
return this.support.createCopyAction(this);
116+
return this.support.createCopyAction(this, this.resolvedDependencies);
117117
}
118118

119119
@Override

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

+63-6
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@
2222
import java.io.InputStream;
2323
import java.io.OutputStream;
2424
import java.io.OutputStreamWriter;
25+
import java.util.ArrayList;
2526
import java.util.Calendar;
2627
import java.util.Collection;
2728
import java.util.GregorianCalendar;
29+
import java.util.HashMap;
30+
import java.util.LinkedHashMap;
2831
import java.util.LinkedHashSet;
2932
import java.util.List;
3033
import java.util.Map;
34+
import java.util.Properties;
3135
import java.util.Set;
3236
import java.util.function.Function;
37+
import java.util.regex.Pattern;
3338
import java.util.zip.CRC32;
3439
import java.util.zip.ZipEntry;
3540

@@ -47,11 +52,13 @@
4752
import org.gradle.api.tasks.WorkResult;
4853
import org.gradle.api.tasks.WorkResults;
4954

55+
import org.springframework.boot.gradle.tasks.bundling.ResolvedDependencies.DependencyDescriptor;
5056
import org.springframework.boot.loader.tools.DefaultLaunchScript;
5157
import org.springframework.boot.loader.tools.FileUtils;
5258
import org.springframework.boot.loader.tools.JarModeLibrary;
5359
import org.springframework.boot.loader.tools.Layer;
5460
import org.springframework.boot.loader.tools.LayersIndex;
61+
import org.springframework.boot.loader.tools.LibraryCoordinates;
5562
import org.springframework.util.Assert;
5663
import org.springframework.util.StreamUtils;
5764
import org.springframework.util.StringUtils;
@@ -69,6 +76,11 @@ class BootZipCopyAction implements CopyAction {
6976
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0)
7077
.getTimeInMillis();
7178

79+
private static final String REACHABILITY_METADATA_PROPERTIES_LOCATION = "META-INF/native-image/%s/%s/reachability-metadata.properties";
80+
81+
private static final Pattern REACHABILITY_METADATA_PROPERTIES_LOCATION_PATTERN = Pattern
82+
.compile(REACHABILITY_METADATA_PROPERTIES_LOCATION.formatted(".*", ".*"));
83+
7284
private final File output;
7385

7486
private final Manifest manifest;
@@ -91,13 +103,15 @@ class BootZipCopyAction implements CopyAction {
91103

92104
private final String encoding;
93105

106+
private final ResolvedDependencies resolvedDependencies;
107+
94108
private final LayerResolver layerResolver;
95109

96110
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader,
97111
String layerToolsLocation, Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
98112
LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
99113
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
100-
LayerResolver layerResolver) {
114+
ResolvedDependencies resolvedDependencies, LayerResolver layerResolver) {
101115
this.output = output;
102116
this.manifest = manifest;
103117
this.preserveFileTimestamps = preserveFileTimestamps;
@@ -109,6 +123,7 @@ class BootZipCopyAction implements CopyAction {
109123
this.librarySpec = librarySpec;
110124
this.compressionResolver = compressionResolver;
111125
this.encoding = encoding;
126+
this.resolvedDependencies = resolvedDependencies;
112127
this.layerResolver = layerResolver;
113128
}
114129

@@ -189,7 +204,9 @@ private class Processor {
189204

190205
private final Set<String> writtenDirectories = new LinkedHashSet<>();
191206

192-
private final Set<String> writtenLibraries = new LinkedHashSet<>();
207+
private final Map<String, FileCopyDetails> writtenLibraries = new LinkedHashMap<>();
208+
209+
private final Map<String, FileCopyDetails> reachabilityMetadataProperties = new HashMap<>();
193210

194211
Processor(ZipArchiveOutputStream out) {
195212
this.out = out;
@@ -241,7 +258,10 @@ private void processFile(FileCopyDetails details) throws IOException {
241258
details.copyTo(this.out);
242259
this.out.closeArchiveEntry();
243260
if (BootZipCopyAction.this.librarySpec.isSatisfiedBy(details)) {
244-
this.writtenLibraries.add(name);
261+
this.writtenLibraries.put(name, details);
262+
}
263+
if (REACHABILITY_METADATA_PROPERTIES_LOCATION_PATTERN.matcher(name).matches()) {
264+
this.reachabilityMetadataProperties.put(name, details);
245265
}
246266
if (BootZipCopyAction.this.layerResolver != null) {
247267
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(details);
@@ -271,6 +291,7 @@ void finish() throws IOException {
271291
writeLoaderEntriesIfNecessary(null);
272292
writeJarToolsIfNecessary();
273293
writeClassPathIndexIfNecessary();
294+
writeNativeImageArgFileIfNecessary();
274295
// We must write the layer index last
275296
writeLayersIndexIfNecessary();
276297
}
@@ -321,9 +342,45 @@ private void writeClassPathIndexIfNecessary() throws IOException {
321342
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
322343
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
323344
if (classPathIndex != null) {
324-
List<String> lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"").toList();
325-
writeEntry(classPathIndex, ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines),
326-
true);
345+
Set<String> libraryNames = this.writtenLibraries.keySet();
346+
List<String> lines = libraryNames.stream().map((line) -> "- \"" + line + "\"").toList();
347+
ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines);
348+
writeEntry(classPathIndex, writer, true);
349+
}
350+
}
351+
352+
private void writeNativeImageArgFileIfNecessary() throws IOException {
353+
Set<String> excludes = new LinkedHashSet<>();
354+
for (Map.Entry<String, FileCopyDetails> entry : this.writtenLibraries.entrySet()) {
355+
DependencyDescriptor descriptor = BootZipCopyAction.this.resolvedDependencies
356+
.find(entry.getValue().getFile());
357+
LibraryCoordinates coordinates = (descriptor != null) ? descriptor.getCoordinates() : null;
358+
FileCopyDetails propertiesFile = (coordinates != null)
359+
? this.reachabilityMetadataProperties.get(REACHABILITY_METADATA_PROPERTIES_LOCATION
360+
.formatted(coordinates.getGroupId(), coordinates.getArtifactId()))
361+
: null;
362+
if (propertiesFile != null) {
363+
try (InputStream inputStream = propertiesFile.open()) {
364+
Properties properties = new Properties();
365+
properties.load(inputStream);
366+
if (Boolean.parseBoolean(properties.getProperty("override"))) {
367+
excludes.add(entry.getKey());
368+
}
369+
}
370+
}
371+
}
372+
// https://docs.oracle.com/en/java/javase/18/docs/specs/man/java.html#java-command-line-argument-files
373+
if (excludes != null) {
374+
List<String> args = new ArrayList<>();
375+
for (String exclude : excludes) {
376+
int lastSlash = exclude.lastIndexOf('/');
377+
String jar = (lastSlash != -1) ? exclude.substring(lastSlash + 1) : exclude;
378+
args.add("--exclude-config");
379+
args.add("\"" + Pattern.quote(jar).replace("\\", "\\\\") + "\"");
380+
args.add("\"^/META-INF/native-image/.*\"");
381+
}
382+
ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, args);
383+
writeEntry("META-INF/native-image/argfile", writer, true);
327384
}
328385
}
329386

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java

+39-6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
7070
import org.springframework.boot.loader.tools.DefaultLaunchScript;
7171
import org.springframework.boot.loader.tools.JarModeLibrary;
72+
import org.springframework.util.FileCopyUtils;
7273

7374
import static org.assertj.core.api.Assertions.assertThat;
7475
import static org.mockito.ArgumentMatchers.any;
@@ -482,6 +483,7 @@ void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException {
482483
expected.add("- \"dependencies\":");
483484
expected.add(" - \"" + this.libPath + "first-library.jar\"");
484485
expected.add(" - \"" + this.libPath + "first-project-library.jar\"");
486+
expected.add(" - \"" + this.libPath + "fourth-library.jar\"");
485487
expected.add(" - \"" + this.libPath + "second-library.jar\"");
486488
if (!layerToolsJar.contains("SNAPSHOT")) {
487489
expected.add(" - \"" + layerToolsJar + "\"");
@@ -536,6 +538,7 @@ void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() th
536538
expected.add("- \"my-internal-deps\":");
537539
expected.add(" - \"" + this.libPath + "first-library.jar\"");
538540
expected.add(" - \"" + this.libPath + "first-project-library.jar\"");
541+
expected.add(" - \"" + this.libPath + "fourth-library.jar\"");
539542
expected.add(" - \"" + this.libPath + "second-library.jar\"");
540543
expected.add("- \"my-snapshot-deps\":");
541544
expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\"");
@@ -616,27 +619,43 @@ protected File newFile(String name) throws IOException {
616619
}
617620

618621
File createLayeredJar() throws IOException {
619-
return createLayeredJar((spec) -> {
622+
return createLayeredJar(false);
623+
}
624+
625+
File createLayeredJar(boolean addReachabilityProperties) throws IOException {
626+
return createLayeredJar(addReachabilityProperties, (spec) -> {
620627
});
621628
}
622629

623630
File createLayeredJar(Action<LayeredSpec> action) throws IOException {
631+
return createLayeredJar(false, action);
632+
}
633+
634+
File createLayeredJar(boolean addReachabilityProperties, Action<LayeredSpec> action) throws IOException {
624635
applyLayered(action);
625-
addContent();
636+
addContent(addReachabilityProperties);
626637
executeTask();
627638
return getTask().getArchiveFile().get().getAsFile();
628639
}
629640

630641
File createPopulatedJar() throws IOException {
631-
addContent();
642+
return createPopulatedJar(false);
643+
}
644+
645+
File createPopulatedJar(boolean addReachabilityProperties) throws IOException {
646+
addContent(addReachabilityProperties);
632647
executeTask();
633648
return getTask().getArchiveFile().get().getAsFile();
634649
}
635650

636651
abstract void applyLayered(Action<LayeredSpec> action);
637652

638-
@SuppressWarnings("unchecked")
639653
void addContent() throws IOException {
654+
addContent(false);
655+
}
656+
657+
@SuppressWarnings("unchecked")
658+
void addContent(boolean addReachabilityProperties) throws IOException {
640659
this.task.getMainClass().set("com.example.Main");
641660
File classesJavaMain = new File(this.temp, "classes/java/main");
642661
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
@@ -650,14 +669,20 @@ void addContent() throws IOException {
650669
staticResources.mkdir();
651670
File css = new File(staticResources, "test.css");
652671
css.createNewFile();
672+
if (addReachabilityProperties) {
673+
createReachabilityProperties(resourcesMain, "com.example", "first-library", "true");
674+
createReachabilityProperties(resourcesMain, "com.example", "second-library", "true");
675+
createReachabilityProperties(resourcesMain, "com.example", "fourth-library", "false");
676+
}
653677
this.task.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
654-
jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"),
655-
jarFile("second-project-library-SNAPSHOT.jar"));
678+
jarFile("third-library-SNAPSHOT.jar"), jarFile("fourth-library.jar"),
679+
jarFile("first-project-library.jar"), jarFile("second-project-library-SNAPSHOT.jar"));
656680
Set<ResolvedArtifact> artifacts = new LinkedHashSet<>();
657681
artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0"));
658682
artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0"));
659683
artifacts.add(
660684
mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
685+
artifacts.add(mockLibraryArtifact("fourth-library.jar", "com.example", "fourth-library", "1.0.0"));
661686
artifacts
662687
.add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0"));
663688
artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example",
@@ -682,6 +707,14 @@ void addContent() throws IOException {
682707
populateResolvedDependencies(configuration);
683708
}
684709

710+
protected void createReachabilityProperties(File directory, String groupId, String artifactId, String override)
711+
throws IOException {
712+
File targetDirectory = new File(directory, "META-INF/native-image/%s/%s".formatted(groupId, artifactId));
713+
File target = new File(targetDirectory, "reachability-metadata.properties");
714+
targetDirectory.mkdirs();
715+
FileCopyUtils.copy("override=%s\n".formatted(override).getBytes(StandardCharsets.ISO_8859_1), target);
716+
}
717+
685718
abstract void populateResolvedDependencies(Configuration configuration);
686719

687720
private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) {

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ void whenJarIsLayeredClasspathIndexPointsToLayeredLibs() throws IOException {
8686
try (JarFile jarFile = new JarFile(createLayeredJar())) {
8787
assertThat(entryLines(jarFile, "BOOT-INF/classpath.idx")).containsExactly(
8888
"- \"BOOT-INF/lib/first-library.jar\"", "- \"BOOT-INF/lib/second-library.jar\"",
89-
"- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/first-project-library.jar\"",
89+
"- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/fourth-library.jar\"",
90+
"- \"BOOT-INF/lib/first-project-library.jar\"",
9091
"- \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\"");
9192
}
9293
}
@@ -98,7 +99,8 @@ void classpathIndexPointsToBootInfLibs() throws IOException {
9899
.isEqualTo("BOOT-INF/classpath.idx");
99100
assertThat(entryLines(jarFile, "BOOT-INF/classpath.idx")).containsExactly(
100101
"- \"BOOT-INF/lib/first-library.jar\"", "- \"BOOT-INF/lib/second-library.jar\"",
101-
"- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/first-project-library.jar\"",
102+
"- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/fourth-library.jar\"",
103+
"- \"BOOT-INF/lib/first-project-library.jar\"",
102104
"- \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\"");
103105
}
104106
}
@@ -181,7 +183,15 @@ void metaInfServicesEntryIsPackagedBeneathClassesDirectory() throws IOException
181183
assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/services/com.example.Service")).isNotNull();
182184
assertThat(jarFile.getEntry("META-INF/services/com.example.Service")).isNull();
183185
}
186+
}
184187

188+
@Test
189+
void nativeImageArgFileWithExcludesIsWritten() throws IOException {
190+
try (JarFile jarFile = new JarFile(createLayeredJar(true))) {
191+
assertThat(entryLines(jarFile, "META-INF/native-image/argfile")).containsExactly("--exclude-config",
192+
"\"\\\\Qfirst-library.jar\\\\E\"", "\"^/META-INF/native-image/.*\"", "--exclude-config",
193+
"\"\\\\Qsecond-library.jar\\\\E\"", "\"^/META-INF/native-image/.*\"");
194+
}
185195
}
186196

187197
@Override

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@
2424
import org.gradle.api.artifacts.Configuration;
2525
import org.junit.jupiter.api.Test;
2626

27+
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
28+
2729
import static org.assertj.core.api.Assertions.assertThat;
2830

2931
/**
@@ -32,6 +34,7 @@
3234
* @author Andy Wilkinson
3335
* @author Scott Frederick
3436
*/
37+
@ClassPathExclusions("kotlin-daemon-client-*")
3538
class BootWarTests extends AbstractBootArchiveTests<BootWar> {
3639

3740
BootWarTests() {
@@ -115,7 +118,8 @@ void whenWarIsLayeredClasspathIndexPointsToLayeredLibs() throws IOException {
115118
try (JarFile jarFile = new JarFile(createLayeredJar())) {
116119
assertThat(entryLines(jarFile, "WEB-INF/classpath.idx")).containsExactly(
117120
"- \"WEB-INF/lib/first-library.jar\"", "- \"WEB-INF/lib/second-library.jar\"",
118-
"- \"WEB-INF/lib/third-library-SNAPSHOT.jar\"", "- \"WEB-INF/lib/first-project-library.jar\"",
121+
"- \"WEB-INF/lib/third-library-SNAPSHOT.jar\"", "- \"WEB-INF/lib/fourth-library.jar\"",
122+
"- \"WEB-INF/lib/first-project-library.jar\"",
119123
"- \"WEB-INF/lib/second-project-library-SNAPSHOT.jar\"");
120124
}
121125
}
@@ -127,7 +131,8 @@ void classpathIndexPointsToWebInfLibs() throws IOException {
127131
.isEqualTo("WEB-INF/classpath.idx");
128132
assertThat(entryLines(jarFile, "WEB-INF/classpath.idx")).containsExactly(
129133
"- \"WEB-INF/lib/first-library.jar\"", "- \"WEB-INF/lib/second-library.jar\"",
130-
"- \"WEB-INF/lib/third-library-SNAPSHOT.jar\"", "- \"WEB-INF/lib/first-project-library.jar\"",
134+
"- \"WEB-INF/lib/third-library-SNAPSHOT.jar\"", "- \"WEB-INF/lib/fourth-library.jar\"",
135+
"- \"WEB-INF/lib/first-project-library.jar\"",
131136
"- \"WEB-INF/lib/second-project-library-SNAPSHOT.jar\"");
132137
}
133138
}

0 commit comments

Comments
 (0)