From e46bd204917bc004c1a92c7d1ffa36250b192904 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 10 Dec 2020 14:29:26 +0100 Subject: [PATCH] Introduce CleanProperties PropertiesFileTransformer uses `java.utils.Properties` internally as a storage. `java.utils.Properties` `store0()` contains `bw.write("#" + new Date().toString());` that prepends current timestamp before any content (after comments). This effectively breaks reproducible builds that use PropertiesFileTransformer because every new build has different timestamp in transformed files. CleanProperties implementation is introduced in order to remove prepended timestamp when creating output stream. --- .../shadow/internal/CleanProperties.groovy | 32 ++++++++ .../PropertiesFileTransformer.groovy | 11 ++- .../PropertiesFileTransformerTest.groovy | 76 +++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerTest.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy new file mode 100644 index 000000000..f81b64a2b --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy @@ -0,0 +1,32 @@ +/* + * Source https://stackoverflow.com/a/39043903/519333 + */ +package com.github.jengelman.gradle.plugins.shadow.internal + +class CleanProperties extends Properties { + private static class StripFirstLineStream extends FilterOutputStream { + + private boolean firstLineSeen = false + + StripFirstLineStream(final OutputStream out) { + super(out) + } + + @Override + void write(final int b) throws IOException { + if (firstLineSeen) { + super.write(b); + } else if (b == '\n') { + super.write(b); + + firstLineSeen = true; + } + } + + } + + @Override + void store(final OutputStream out, final String comments) throws IOException { + super.store(new StripFirstLineStream(out), null) + } +} diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy index 31e1a5452..2d113ac2f 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy @@ -19,6 +19,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers +import com.github.jengelman.gradle.plugins.shadow.internal.CleanProperties import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.codehaus.plexus.util.IOUtil @@ -117,7 +118,7 @@ import static groovy.lang.Closure.IDENTITY class PropertiesFileTransformer implements Transformer { private static final String PROPERTIES_SUFFIX = '.properties' - private Map propertiesEntries = [:] + private Map propertiesEntries = [:] @Input List paths = [] @@ -179,15 +180,17 @@ class PropertiesFileTransformer implements Transformer { } private Properties loadAndTransformKeys(InputStream is) { - Properties props = new Properties() - props.load(is) + Properties props = new CleanProperties() + if (is != null) { + props.load(is) + } return transformKeys(props) } private Properties transformKeys(Properties properties) { if (keyTransformer == IDENTITY) return properties - def result = new Properties() + def result = new CleanProperties() properties.each { key, value -> result.put(keyTransformer.call(key), value) } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerTest.groovy new file mode 100644 index 000000000..ff23aed96 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerTest.groovy @@ -0,0 +1,76 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import com.github.jengelman.gradle.plugins.shadow.ShadowStats +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import org.apache.tools.zip.ZipOutputStream +import org.junit.Before +import org.junit.Test + +import java.util.zip.ZipFile + +import static java.util.Arrays.asList +import static org.junit.Assert.* + +/** + * Test for {@link PropertiesFileTransformer}. + */ +final class PropertiesFileTransformerTest extends TransformerTestSupport { + static final String MANIFEST_NAME = "META-INF/MANIFEST.MF" + + private PropertiesFileTransformer transformer + + @Before + void setUp() { + transformer = new PropertiesFileTransformer() + } + + @Test + void testHasTransformedResource() { + transformer.transform(new TransformerContext(MANIFEST_NAME)) + + assertTrue(transformer.hasTransformedResource()) + } + + @Test + void testHasNotTransformedResource() { + assertFalse(transformer.hasTransformedResource()) + } + + @Test + void testTransformation() { + transformer.transform(new TransformerContext(MANIFEST_NAME, getResourceStream(MANIFEST_NAME), Collections.emptyList(), new ShadowStats())) + + def testableZipFile = File.createTempFile("testable-zip-file-", ".jar") + def fileOutputStream = new FileOutputStream(testableZipFile) + def bufferedOutputStream = new BufferedOutputStream(fileOutputStream) + def zipOutputStream = new ZipOutputStream(bufferedOutputStream) + + try { + transformer.modifyOutputStream(zipOutputStream, false) + } finally { + zipOutputStream.close() + } + def targetLines = readFrom(testableZipFile, MANIFEST_NAME) + + assertFalse(targetLines.isEmpty()) + + assertTrue(targetLines.contains("Manifest-Version=1.0")) + } + + static List readFrom(File jarFile, String resourceName) { + def zip = new ZipFile(jarFile) + try { + def entry = zip.getEntry(resourceName) + if (!entry) { + return Collections.emptyList() + } + return zip.getInputStream(entry).readLines() + } finally { + zip.close() + } + } + + InputStream getResourceStream(String resource) { + this.class.classLoader.getResourceAsStream(resource) + } +}