Commit 8f5ef951 authored by Scott Frederick's avatar Scott Frederick

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
parent b3ccefdb
...@@ -183,15 +183,15 @@ class BootZipCopyAction implements CopyAction { ...@@ -183,15 +183,15 @@ class BootZipCopyAction implements CopyAction {
*/ */
private class Processor { private class Processor {
private ZipArchiveOutputStream out; private final ZipArchiveOutputStream out;
private final LayersIndex layerIndex; private final LayersIndex layerIndex;
private LoaderZipEntries.WrittenEntries writtenLoaderEntries; 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) { Processor(ZipArchiveOutputStream out) {
this.out = out; this.out = out;
...@@ -224,11 +224,8 @@ class BootZipCopyAction implements CopyAction { ...@@ -224,11 +224,8 @@ class BootZipCopyAction implements CopyAction {
private void processDirectory(FileCopyDetails details) throws IOException { private void processDirectory(FileCopyDetails details) throws IOException {
String name = details.getRelativePath().getPathString(); String name = details.getRelativePath().getPathString();
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name + '/'); ZipArchiveEntry entry = new ZipArchiveEntry(name + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | details.getMode()); prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode());
entry.setTime(time);
this.out.putArchiveEntry(entry); this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry(); this.out.closeArchiveEntry();
this.writtenDirectories.add(name); this.writtenDirectories.add(name);
...@@ -236,11 +233,8 @@ class BootZipCopyAction implements CopyAction { ...@@ -236,11 +233,8 @@ class BootZipCopyAction implements CopyAction {
private void processFile(FileCopyDetails details) throws IOException { private void processFile(FileCopyDetails details) throws IOException {
String name = details.getRelativePath().getPathString(); String name = details.getRelativePath().getPathString();
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name); ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | details.getMode()); prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode());
entry.setTime(time);
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) { if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, entry); prepareStoredEntry(details, entry);
...@@ -257,13 +251,11 @@ class BootZipCopyAction implements CopyAction { ...@@ -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); String parentDirectory = getParentDirectory(name);
if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) { if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) {
writeParentDirectoriesIfNecessary(parentDirectory, time);
ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/'); ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); prepareEntry(entry, parentDirectory, time, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
entry.setTime(time);
this.out.putArchiveEntry(entry); this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry(); this.out.closeArchiveEntry();
} }
...@@ -293,8 +285,7 @@ class BootZipCopyAction implements CopyAction { ...@@ -293,8 +285,7 @@ class BootZipCopyAction implements CopyAction {
// Don't write loader entries until after META-INF folder (see gh-16698) // Don't write loader entries until after META-INF folder (see gh-16698)
return; return;
} }
LoaderZipEntries loaderEntries = new LoaderZipEntries( LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime());
BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES);
this.writtenLoaderEntries = loaderEntries.writeTo(this.out); this.writtenLoaderEntries = loaderEntries.writeTo(this.out);
if (BootZipCopyAction.this.layerResolver != null) { if (BootZipCopyAction.this.layerResolver != null) {
for (String name : this.writtenLoaderEntries.getFiles()) { for (String name : this.writtenLoaderEntries.getFiles()) {
...@@ -320,7 +311,7 @@ class BootZipCopyAction implements CopyAction { ...@@ -320,7 +311,7 @@ class BootZipCopyAction implements CopyAction {
private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException { private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException {
String name = location + library.getName(); String name = location + library.getName();
writeEntry(name, ZipEntryWriter.fromInputStream(library.openStream()), false, writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false,
(entry) -> prepareStoredEntry(library.openStream(), entry)); (entry) -> prepareStoredEntry(library.openStream(), entry));
if (BootZipCopyAction.this.layerResolver != null) { if (BootZipCopyAction.this.layerResolver != null) {
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library); Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library);
...@@ -328,29 +319,14 @@ class BootZipCopyAction implements CopyAction { ...@@ -328,29 +319,14 @@ class BootZipCopyAction implements CopyAction {
} }
} }
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
prepareStoredEntry(details.open(), archiveEntry);
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
}
}
private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
Crc32OutputStream crcStream = new Crc32OutputStream();
int size = FileCopyUtils.copy(input, crcStream);
archiveEntry.setSize(size);
archiveEntry.setCompressedSize(size);
archiveEntry.setCrc(crcStream.getCrc());
}
private void writeClassPathIndexIfNecessary() throws IOException { private void writeClassPathIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index"); String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
if (classPathIndex != null) { if (classPathIndex != null) {
List<String> lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"") List<String> lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"")
.collect(Collectors.toList()); .collect(Collectors.toList());
writeEntry(classPathIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, lines), true); writeEntry(classPathIndex, ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines),
true);
} }
} }
...@@ -361,23 +337,22 @@ class BootZipCopyAction implements CopyAction { ...@@ -361,23 +337,22 @@ class BootZipCopyAction implements CopyAction {
Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute"); Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute");
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
this.layerIndex.add(layer, name); this.layerIndex.add(layer, name);
writeEntry(name, (entry, out) -> this.layerIndex.writeTo(out), false); writeEntry(name, this.layerIndex::writeTo, false);
} }
} }
private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex) throws IOException { private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex)
throws IOException {
writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE); writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE);
} }
private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex, private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex,
ZipEntryCustomizer entryCustomizer) throws IOException { ZipEntryCustomizer entryCustomizer) throws IOException {
writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES);
ZipArchiveEntry entry = new ZipArchiveEntry(name); ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); prepareEntry(entry, name, getTime(), UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
entryCustomizer.customize(entry); entryCustomizer.customize(entry);
this.out.putArchiveEntry(entry); this.out.putArchiveEntry(entry);
entryWriter.writeTo(entry, this.out); entryWriter.writeTo(this.out);
this.out.closeArchiveEntry(); this.out.closeArchiveEntry();
if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) { if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) {
Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name);
...@@ -385,9 +360,42 @@ class BootZipCopyAction implements CopyAction { ...@@ -385,9 +360,42 @@ class BootZipCopyAction implements CopyAction {
} }
} }
private long getTime(FileCopyDetails details) { private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mode) throws IOException {
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified() writeParentDirectoriesIfNecessary(name, time);
: CONSTANT_TIME_FOR_ZIP_ENTRIES; 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)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
}
}
private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
Crc32OutputStream crcStream = new Crc32OutputStream();
int size = FileCopyUtils.copy(input, crcStream);
archiveEntry.setSize(size);
archiveEntry.setCompressedSize(size);
archiveEntry.setCrc(crcStream.getCrc());
}
private Long getTime() {
return getTime(null);
}
private Long getTime(FileCopyDetails details) {
if (!BootZipCopyAction.this.preserveFileTimestamps) {
return CONSTANT_TIME_FOR_ZIP_ENTRIES;
}
if (details != null) {
return details.getLastModified();
}
return null;
} }
} }
...@@ -414,41 +422,40 @@ class BootZipCopyAction implements CopyAction { ...@@ -414,41 +422,40 @@ class BootZipCopyAction implements CopyAction {
* Callback used to write a zip entry data. * Callback used to write a zip entry data.
*/ */
@FunctionalInterface @FunctionalInterface
private interface ZipEntryWriter { private interface ZipEntryContentWriter {
/** /**
* Write the entry data. * Write the entry data.
* @param entry the entry being written
* @param out the output stream used to write the data * @param out the output stream used to write the data
* @throws IOException on IO error * @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 * Create a new {@link ZipEntryContentWriter} that will copy content from the
* {@link InputStream}. * given {@link InputStream}.
* @param in the source input stream * @param in the source input stream
* @return a new {@link ZipEntryWriter} instance * @return a new {@link ZipEntryContentWriter} instance
*/ */
static ZipEntryWriter fromInputStream(InputStream in) { static ZipEntryContentWriter fromInputStream(InputStream in) {
return (entry, out) -> { return (out) -> {
StreamUtils.copy(in, out); StreamUtils.copy(in, out);
in.close(); in.close();
}; };
} }
/** /**
* Create a new {@link ZipEntryWriter} that will copy content from the given * Create a new {@link ZipEntryContentWriter} that will copy content from the
* lines. * given lines.
* @param encoding the required character encoding * @param encoding the required character encoding
* @param lines the lines to write * @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) { static ZipEntryContentWriter fromLines(String encoding, Collection<String> lines) {
return (entry, out) -> { return (out) -> {
OutputStreamWriter writer = new OutputStreamWriter(out, encoding); OutputStreamWriter writer = new OutputStreamWriter(out, encoding);
for (String line : lines) { for (String line : lines) {
writer.append(line + "\n"); writer.append(line).append("\n");
} }
writer.flush(); writer.flush();
}; };
......
...@@ -29,11 +29,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; ...@@ -29,11 +29,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.FileTreeElement;
import org.springframework.util.StreamUtils;
/** /**
* Internal utility used to copy entries from the {@code spring-boot-loader.jar}. * Internal utility used to copy entries from the {@code spring-boot-loader.jar}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class LoaderZipEntries { class LoaderZipEntries {
...@@ -84,11 +87,7 @@ class LoaderZipEntries { ...@@ -84,11 +87,7 @@ class LoaderZipEntries {
} }
private void copy(InputStream in, OutputStream out) throws IOException { private void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096]; StreamUtils.copy(in, out);
int bytesRead = -1;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} }
/** /**
......
...@@ -40,6 +40,7 @@ import java.util.zip.ZipInputStream; ...@@ -40,6 +40,7 @@ import java.util.zip.ZipInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.api.Project; 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.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Jar;
import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testfixtures.ProjectBuilder;
...@@ -56,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -56,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* *
* @param <T> the type of the concrete BootArchive implementation * @param <T> the type of the concrete BootArchive implementation
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Scott Frederick
*/ */
abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
...@@ -330,6 +332,12 @@ 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 @Test
void reproducibleOrderingCanBeEnabled() throws IOException { void reproducibleOrderingCanBeEnabled() throws IOException {
this.task.setMainClassName("com.example.Main"); this.task.setMainClassName("com.example.Main");
......
...@@ -101,14 +101,10 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> { ...@@ -101,14 +101,10 @@ class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", 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/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class",
"BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css"); "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"); List<String> index = entryLines(jarFile, "BOOT-INF/layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application"); "snapshot-dependencies", "application");
String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); 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<>(); List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":"); expected.add("- \"dependencies\":");
expected.add(" - \"BOOT-INF/lib/first-library.jar\""); expected.add(" - \"BOOT-INF/lib/first-library.jar\"");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment