Use a more compact layers.idx format

Update the `layers.idx` format so that it is more compact and can be
parsed by third-parties as YAML.

Closes gh-20860
This commit is contained in:
Phillip Webb
2020-04-06 15:40:54 -07:00
parent 36fd2ed249
commit 65672a1150
18 changed files with 475 additions and 168 deletions

View File

@@ -31,8 +31,9 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
@@ -103,30 +104,25 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
List<String> layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies",
"application");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
List<String> expectedDependencies = new ArrayList<>();
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar");
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
Set<String> expectedSnapshotDependencies = new TreeSet<>();
expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar");
(layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar);
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("spring-boot-loader"))
.allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("application")).containsExactly("META-INF/MANIFEST.MF",
"BOOT-INF/classes/example/Main.class", "BOOT-INF/classes/static/file.txt", "BOOT-INF/classpath.idx",
"BOOT-INF/layers.idx");
assertThat(indexedLayers.get("application")).containsExactly("BOOT-INF/classes/", "BOOT-INF/classpath.idx",
"BOOT-INF/layers.idx", "META-INF/");
BuildResult listLayers = this.gradleBuild.build("listLayers");
assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
String listLayersOutput = listLayers.getOutput();
assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames);
BuildResult extractLayers = this.gradleBuild.build("extractLayers");
assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach(
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
assertExtractedLayers(layerNames, indexedLayers);
}
@TestTemplate
@@ -151,7 +147,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
List<String> layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies",
"static", "app");
assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames);
List<String> expectedDependencies = new ArrayList<>();
Set<String> expectedDependencies = new TreeSet<>();
expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar");
expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar");
List<String> expectedSnapshotDependencies = new ArrayList<>();
@@ -160,23 +156,48 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies);
assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar");
assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies);
assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/file.txt");
assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/");
List<String> appLayer = new ArrayList<>(indexedLayers.get("app"));
List<String> nonLoaderEntries = Arrays.asList("META-INF/MANIFEST.MF", "BOOT-INF/classes/example/Main.class",
"BOOT-INF/classpath.idx", "BOOT-INF/layers.idx");
Set<String> nonLoaderEntries = new TreeSet<>();
nonLoaderEntries.add("BOOT-INF/classes/example/");
nonLoaderEntries.add("BOOT-INF/classpath.idx");
nonLoaderEntries.add("BOOT-INF/layers.idx");
nonLoaderEntries.add("META-INF/");
assertThat(appLayer).containsSubsequence(nonLoaderEntries);
appLayer.removeAll(nonLoaderEntries);
assertThat(appLayer).allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate());
assertThat(appLayer).containsExactly("org/");
BuildResult listLayers = this.gradleBuild.build("listLayers");
assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
String listLayersOutput = listLayers.getOutput();
assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames);
BuildResult extractLayers = this.gradleBuild.build("extractLayers");
assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertExtractedLayers(layerNames, indexedLayers);
}
private void assertExtractedLayers(List<String> layerNames, Map<String, List<String>> indexedLayers)
throws IOException {
Map<String, List<String>> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames);
assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet());
extractedLayers.forEach(
(name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name)));
extractedLayers.forEach((name, contents) -> {
List<String> index = indexedLayers.get(name);
List<String> unexpected = new ArrayList<>();
for (String file : contents) {
if (!isInIndex(index, file)) {
unexpected.add(name);
}
}
assertThat(unexpected).isEmpty();
});
}
private boolean isInIndex(List<String> index, String file) {
for (String candidate : index) {
if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) {
return true;
}
}
return false;
}
private void writeMainClass() {
@@ -213,11 +234,21 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
}
private Map<String, List<String>> readLayerIndex(JarFile jarFile) throws IOException {
Map<String, List<String>> index = new LinkedHashMap<>();
ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) {
return reader.lines().map((line) -> line.split(" "))
.collect(Collectors.groupingBy((layerAndPath) -> layerAndPath[0], LinkedHashMap::new,
Collectors.mapping((layerAndPath) -> layerAndPath[1], Collectors.toList())));
String line = reader.readLine();
String layer = null;
while (line != null) {
if (line.startsWith("- ")) {
layer = line.substring(3, line.length() - 2);
}
else if (line.startsWith(" - ")) {
index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1));
}
line = reader.readLine();
}
return index;
}
}

View File

@@ -20,6 +20,7 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
@@ -103,12 +104,27 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application");
assertThat(index).contains("dependencies BOOT-INF/lib/first-library.jar",
"dependencies BOOT-INF/lib/second-library.jar",
"snapshot-dependencies BOOT-INF/lib/third-library-SNAPSHOT.jar",
"application BOOT-INF/classes/com/example/Application.class",
"application BOOT-INF/classes/application.properties",
"application BOOT-INF/classes/static/test.css");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\"");
expected.add(" - \"BOOT-INF/lib/second-library.jar\"");
if (!layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add("- \"spring-boot-loader\":");
expected.add(" - \"org/\"");
expected.add("- \"snapshot-dependencies\":");
if (layerToolsJar.contains("SNAPSHOT")) {
expected.add(" - \"" + layerToolsJar + "\"");
}
expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"");
expected.add("- \"application\":");
expected.add(" - \"BOOT-INF/classes/\"");
expected.add(" - \"BOOT-INF/classpath.idx\"");
expected.add(" - \"BOOT-INF/layers.idx\"");
expected.add(" - \"META-INF/\"");
assertThat(index).containsExactlyElementsOf(expected);
}
}
@@ -134,12 +150,25 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
"resources", "application");
assertThat(index).contains("my-internal-deps BOOT-INF/lib/first-library.jar",
"my-internal-deps BOOT-INF/lib/second-library.jar",
"my-snapshot-deps BOOT-INF/lib/third-library-SNAPSHOT.jar",
"application BOOT-INF/classes/com/example/Application.class",
"application BOOT-INF/classes/application.properties",
"resources BOOT-INF/classes/static/test.css");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"my-deps\":");
expected.add(" - \"" + layerToolsJar + "\"");
expected.add("- \"my-internal-deps\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\"");
expected.add(" - \"BOOT-INF/lib/second-library.jar\"");
expected.add("- \"my-snapshot-deps\":");
expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"");
expected.add("- \"resources\":");
expected.add(" - \"BOOT-INF/classes/static/\"");
expected.add("- \"application\":");
expected.add(" - \"BOOT-INF/classes/application.properties\"");
expected.add(" - \"BOOT-INF/classes/com/\"");
expected.add(" - \"BOOT-INF/classpath.idx\"");
expected.add(" - \"BOOT-INF/layers.idx\"");
expected.add(" - \"META-INF/\"");
expected.add(" - \"org/\"");
assertThat(index).containsExactlyElementsOf(expected);
}
}
@@ -261,11 +290,13 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
}
private Set<String> getLayerNames(List<String> index) {
return index.stream().map(this::getLayerName).collect(Collectors.toCollection(LinkedHashSet::new));
}
private String getLayerName(String indexLine) {
return indexLine.substring(0, indexLine.indexOf(" "));
Set<String> layerNames = new LinkedHashSet<>();
for (String line : index) {
if (line.startsWith("- ")) {
layerNames.add(line.substring(3, line.length() - 2));
}
}
return layerNames;
}
@Override