Use current timestamp for index files with Gradle

This commit removes changes the timestamp used when writing the
classpath and layers index files in the Gradle plugin to be the
current timestamp unless `preserveFileTimestamps=true`. It also
polishes some duplication in the handling of entry attributes
when creating the fat archive and adds a test to verify that
the Gradle plugin uses the same fixed timestamp constant as
Gradle uses internally.

See gh-21005
This commit is contained in:
Scott Frederick 2020-04-22 19:01:21 -05:00
parent b3ccefdb57
commit 8f5ef951de
4 changed files with 93 additions and 83 deletions

View File

@ -183,15 +183,15 @@ class BootZipCopyAction implements CopyAction {
*/
private class Processor {
private ZipArchiveOutputStream out;
private final ZipArchiveOutputStream out;
private final LayersIndex layerIndex;
private LoaderZipEntries.WrittenEntries writtenLoaderEntries;
private Set<String> writtenDirectories = new LinkedHashSet<>();
private final Set<String> writtenDirectories = new LinkedHashSet<>();
private Set<String> writtenLibraries = new LinkedHashSet<>();
private final Set<String> writtenLibraries = new LinkedHashSet<>();
Processor(ZipArchiveOutputStream out) {
this.out = out;
@ -224,11 +224,8 @@ class BootZipCopyAction implements CopyAction {
private void processDirectory(FileCopyDetails details) throws IOException {
String name = details.getRelativePath().getPathString();
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
entry.setTime(time);
prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode());
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
this.writtenDirectories.add(name);
@ -236,11 +233,8 @@ class BootZipCopyAction implements CopyAction {
private void processFile(FileCopyDetails details) throws IOException {
String name = details.getRelativePath().getPathString();
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
entry.setTime(time);
prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode());
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, entry);
@ -257,13 +251,11 @@ class BootZipCopyAction implements CopyAction {
}
}
private void writeParentDirectoriesIfNecessary(String name, long time) throws IOException {
private void writeParentDirectoriesIfNecessary(String name, Long time) throws IOException {
String parentDirectory = getParentDirectory(name);
if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) {
writeParentDirectoriesIfNecessary(parentDirectory, time);
ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
entry.setTime(time);
prepareEntry(entry, parentDirectory, time, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
}
@ -293,8 +285,7 @@ class BootZipCopyAction implements CopyAction {
// Don't write loader entries until after META-INF folder (see gh-16698)
return;
}
LoaderZipEntries loaderEntries = new LoaderZipEntries(
BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES);
LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime());
this.writtenLoaderEntries = loaderEntries.writeTo(this.out);
if (BootZipCopyAction.this.layerResolver != null) {
for (String name : this.writtenLoaderEntries.getFiles()) {
@ -320,7 +311,7 @@ class BootZipCopyAction implements CopyAction {
private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException {
String name = location + library.getName();
writeEntry(name, ZipEntryWriter.fromInputStream(library.openStream()), false,
writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false,
(entry) -> prepareStoredEntry(library.openStream(), entry));
if (BootZipCopyAction.this.layerResolver != null) {
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library);
@ -328,6 +319,55 @@ class BootZipCopyAction implements CopyAction {
}
}
private void writeClassPathIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
if (classPathIndex != null) {
List<String> lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"")
.collect(Collectors.toList());
writeEntry(classPathIndex, ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines),
true);
}
}
private void writeLayersIndexIfNecessary() throws IOException {
if (BootZipCopyAction.this.layerResolver != null) {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String name = (String) manifestAttributes.get("Spring-Boot-Layers-Index");
Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute");
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
this.layerIndex.add(layer, name);
writeEntry(name, this.layerIndex::writeTo, false);
}
}
private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex)
throws IOException {
writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE);
}
private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex,
ZipEntryCustomizer entryCustomizer) throws IOException {
ZipArchiveEntry entry = new ZipArchiveEntry(name);
prepareEntry(entry, name, getTime(), UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
entryCustomizer.customize(entry);
this.out.putArchiveEntry(entry);
entryWriter.writeTo(this.out);
this.out.closeArchiveEntry();
if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) {
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
this.layerIndex.add(layer, name);
}
}
private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mode) throws IOException {
writeParentDirectoriesIfNecessary(name, time);
entry.setUnixMode(mode);
if (time != null) {
entry.setTime(time);
}
}
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
prepareStoredEntry(details.open(), archiveEntry);
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
@ -344,50 +384,18 @@ class BootZipCopyAction implements CopyAction {
archiveEntry.setCrc(crcStream.getCrc());
}
private void writeClassPathIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
if (classPathIndex != null) {
List<String> lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"")
.collect(Collectors.toList());
writeEntry(classPathIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, lines), true);
private Long getTime() {
return getTime(null);
}
private Long getTime(FileCopyDetails details) {
if (!BootZipCopyAction.this.preserveFileTimestamps) {
return CONSTANT_TIME_FOR_ZIP_ENTRIES;
}
}
private void writeLayersIndexIfNecessary() throws IOException {
if (BootZipCopyAction.this.layerResolver != null) {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String name = (String) manifestAttributes.get("Spring-Boot-Layers-Index");
Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute");
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
this.layerIndex.add(layer, name);
writeEntry(name, (entry, out) -> this.layerIndex.writeTo(out), false);
if (details != null) {
return details.getLastModified();
}
}
private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex) throws IOException {
writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE);
}
private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex,
ZipEntryCustomizer entryCustomizer) throws IOException {
writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
entryCustomizer.customize(entry);
this.out.putArchiveEntry(entry);
entryWriter.writeTo(entry, this.out);
this.out.closeArchiveEntry();
if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) {
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
this.layerIndex.add(layer, name);
}
}
private long getTime(FileCopyDetails details) {
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified()
: CONSTANT_TIME_FOR_ZIP_ENTRIES;
return null;
}
}
@ -414,41 +422,40 @@ class BootZipCopyAction implements CopyAction {
* Callback used to write a zip entry data.
*/
@FunctionalInterface
private interface ZipEntryWriter {
private interface ZipEntryContentWriter {
/**
* Write the entry data.
* @param entry the entry being written
* @param out the output stream used to write the data
* @throws IOException on IO error
*/
void writeTo(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException;
void writeTo(ZipArchiveOutputStream out) throws IOException;
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* {@link InputStream}.
* Create a new {@link ZipEntryContentWriter} that will copy content from the
* given {@link InputStream}.
* @param in the source input stream
* @return a new {@link ZipEntryWriter} instance
* @return a new {@link ZipEntryContentWriter} instance
*/
static ZipEntryWriter fromInputStream(InputStream in) {
return (entry, out) -> {
static ZipEntryContentWriter fromInputStream(InputStream in) {
return (out) -> {
StreamUtils.copy(in, out);
in.close();
};
}
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* lines.
* Create a new {@link ZipEntryContentWriter} that will copy content from the
* given lines.
* @param encoding the required character encoding
* @param lines the lines to write
* @return a new {@link ZipEntryWriter} instance
* @return a new {@link ZipEntryContentWriter} instance
*/
static ZipEntryWriter fromLines(String encoding, Collection<String> lines) {
return (entry, out) -> {
static ZipEntryContentWriter fromLines(String encoding, Collection<String> lines) {
return (out) -> {
OutputStreamWriter writer = new OutputStreamWriter(out, encoding);
for (String line : lines) {
writer.append(line + "\n");
writer.append(line).append("\n");
}
writer.flush();
};

View File

@ -29,11 +29,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.gradle.api.file.FileTreeElement;
import org.springframework.util.StreamUtils;
/**
* Internal utility used to copy entries from the {@code spring-boot-loader.jar}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
class LoaderZipEntries {
@ -84,11 +87,7 @@ class LoaderZipEntries {
}
private void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
int bytesRead = -1;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
StreamUtils.copy(in, out);
}
/**

View File

@ -40,6 +40,7 @@ import java.util.zip.ZipInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.api.Project;
import org.gradle.api.internal.file.archive.ZipCopyAction;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.testfixtures.ProjectBuilder;
@ -56,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @param <T> the type of the concrete BootArchive implementation
* @author Andy Wilkinson
* @author Scott Frederick
*/
abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
@ -330,6 +332,12 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
}
@Test
void constantTimestampMatchesGradleInternalTimestamp() {
assertThat(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES)
.isEqualTo(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
@Test
void reproducibleOrderingCanBeEnabled() throws IOException {
this.task.setMainClassName("com.example.Main");

View File

@ -101,14 +101,10 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar",
"BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class",
"BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css");
ZipEntry layersIndexEntry = jarFile.getEntry("BOOT-INF/layers.idx");
assertThat(layersIndexEntry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName();
ZipEntry layerToolsEntry = jarFile.getEntry(layerToolsJar);
assertThat(layerToolsEntry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\"");