From 429fb3a58a1caa5ec6854a0945919edcd03e83fc Mon Sep 17 00:00:00 2001 From: Jonathan Chapman and Jim Cifarelli Date: Mon, 16 Mar 2015 11:23:43 -0400 Subject: [PATCH 01/15] Displaying `warn` level messages when adding a file attempts to overwrite an existing resource --- .../shadow/tasks/ShadowCopyAction.groovy | 570 ++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy new file mode 100644 index 000000000..e37f00cf6 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -0,0 +1,570 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks + +import com.github.jengelman.gradle.plugins.shadow.ShadowStats +import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper +import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker +import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import groovy.util.logging.Slf4j +import org.apache.commons.io.FilenameUtils +import org.apache.commons.io.IOUtils +import org.apache.tools.zip.UnixStat +import org.apache.tools.zip.Zip64RequiredException +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipFile +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.UncheckedIOException +import org.gradle.api.file.FileCopyDetails +import org.gradle.api.file.FilePermissions +import org.gradle.api.file.FileTreeElement +import org.gradle.api.file.RelativePath +import org.gradle.api.internal.DocumentationRegistry +import org.gradle.api.internal.file.CopyActionProcessingStreamAction +import org.gradle.api.internal.file.DefaultFilePermissions +import org.gradle.api.internal.file.DefaultFileTreeElement +import org.gradle.api.internal.file.copy.CopyAction +import org.gradle.api.internal.file.copy.CopyActionProcessingStream +import org.gradle.api.internal.file.copy.FileCopyDetailsInternal +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.WorkResult +import org.gradle.api.tasks.WorkResults +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.util.PatternSet +import org.gradle.internal.UncheckedException +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.commons.ClassRemapper + +import java.util.zip.ZipException + +@Slf4j +class ShadowCopyAction implements CopyAction { + static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).getTimeInMillis() + + private final File zipFile + private final ZipCompressor compressor + private final DocumentationRegistry documentationRegistry + private final List transformers + private final List relocators + private final PatternSet patternSet + private final ShadowStats stats + private final String encoding + private final boolean preserveFileTimestamps + private final boolean minimizeJar + private final UnusedTracker unusedTracker + + ShadowCopyAction(File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry, + String encoding, List transformers, List relocators, + PatternSet patternSet, ShadowStats stats, + boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker) { + + this.zipFile = zipFile + this.compressor = compressor + this.documentationRegistry = documentationRegistry + this.transformers = transformers + this.relocators = relocators + this.patternSet = patternSet + this.stats = stats + this.encoding = encoding + this.preserveFileTimestamps = preserveFileTimestamps + this.minimizeJar = minimizeJar + this.unusedTracker = unusedTracker + } + + @Override + WorkResult execute(CopyActionProcessingStream stream) { + Set unusedClasses + if (minimizeJar) { + stream.process(new BaseStreamAction() { + @Override + void visitFile(FileCopyDetails fileDetails) { + // All project sources are already present, we just need + // to deal with JAR dependencies. + if (isArchive(fileDetails)) { + unusedTracker.addDependency(fileDetails.file) + } + } + }) + unusedClasses = unusedTracker.findUnused() + } else { + unusedClasses = Collections.emptySet() + } + + ZipOutputStream zipOutStr + + try { + zipOutStr = compressor.createArchiveOutputStream(zipFile) + } catch (Exception e) { + throw new GradleException("Could not create ZIP '${zipFile.toString()}'", e) + } + + try { + withResource(zipOutStr, new Action() { + void execute(ZipOutputStream outputStream) { + try { + stream.process(new StreamAction(outputStream, encoding, transformers, relocators, patternSet, + unusedClasses, stats)) + processTransformers(outputStream) + } catch (Exception e) { + log.error('ex', e) + //TODO this should not be rethrown + throw e + } + } + }) + } catch (UncheckedIOException e) { + if (e.cause instanceof Zip64RequiredException) { + throw new Zip64RequiredException( + String.format("%s\n\nTo build this archive, please enable the zip64 extension.\nSee: %s", + e.cause.message, documentationRegistry.getDslRefForProperty(Zip, "zip64")) + ) + } + } + return WorkResults.didWork(true) + } + + private void processTransformers(ZipOutputStream stream) { + transformers.each { Transformer transformer -> + if (transformer.hasTransformedResource()) { + transformer.modifyOutputStream(stream, preserveFileTimestamps) + } + } + } + + private long getArchiveTimeFor(long timestamp) { + return preserveFileTimestamps ? timestamp : CONSTANT_TIME_FOR_ZIP_ENTRIES + } + + private ZipEntry setArchiveTimes(ZipEntry zipEntry) { + if (!preserveFileTimestamps) { + zipEntry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES) + } + return zipEntry + } + + private static void withResource(T resource, Action action) { + try { + action.execute(resource) + } catch(Throwable t) { + try { + resource.close() + } catch (IOException e) { + log.warn("Could not close resource $resource", e) + } + throw UncheckedException.throwAsUncheckedException(t) + } + + try { + resource.close() + } catch (IOException e) { + throw new UncheckedIOException(e) + } + } + + abstract class BaseStreamAction implements CopyActionProcessingStreamAction { + protected boolean isArchive(FileCopyDetails fileDetails) { + return fileDetails.relativePath.pathString.endsWith('.jar') + } + + protected boolean isClass(FileCopyDetails fileDetails) { + return FilenameUtils.getExtension(fileDetails.path) == 'class' + } + + @Override + void processFile(FileCopyDetailsInternal details) { + if (details.directory) { + visitDir(details) + } else { + visitFile(details) + } + } + + protected void visitDir(FileCopyDetails dirDetails) {} + + protected abstract void visitFile(FileCopyDetails fileDetails) + } + + private class StreamAction extends BaseStreamAction { + + private final ZipOutputStream zipOutStr + private final List transformers + private final List relocators + private final RelocatorRemapper remapper + private final PatternSet patternSet + private final Set unused + private final ShadowStats stats + + private Map visitedFiles = new HashMap() + + StreamAction(ZipOutputStream zipOutStr, String encoding, List transformers, + List relocators, PatternSet patternSet, Set unused, + ShadowStats stats) { + this.zipOutStr = zipOutStr + this.transformers = transformers + this.relocators = relocators + this.remapper = new RelocatorRemapper(relocators, stats) + this.patternSet = patternSet + this.unused = unused + this.stats = stats + if(encoding != null) { + this.zipOutStr.setEncoding(encoding) + } + } + + private boolean recordVisit(path, size, originJar) { + if (visitedFiles.containsKey(path.toString())) { + return false + } + + if (originJar == null) { + originJar = "" + } + + visitedFiles.put(path.toString(), [size: size, originJar: originJar]) + return true + } + + private boolean recordVisit(path) { + return recordVisit(path.toString(), 0, null) + } + + private boolean recordVisit(FileCopyDetails fileCopyDetails) { + return recordVisit(fileCopyDetails.relativePath, fileCopyDetails.size, null) + } + + @Override + void visitFile(FileCopyDetails fileDetails) { + if (!isArchive(fileDetails)) { + try { + boolean isClass = isClass(fileDetails) + if (!remapper.hasRelocators() || !isClass) { + if (!isTransformable(fileDetails)) { + String mappedPath = remapper.map(fileDetails.relativePath.pathString) + ZipEntry archiveEntry = new ZipEntry(mappedPath) + archiveEntry.setTime(getArchiveTimeFor(fileDetails.lastModified)) + archiveEntry.unixMode = (UnixStat.FILE_FLAG | fileDetails.permissions.toUnixNumeric()) + zipOutStr.putNextEntry(archiveEntry) + fileDetails.copyTo(zipOutStr) + zipOutStr.closeEntry() + } else { + transform(fileDetails) + } + } else if (isClass && !isUnused(fileDetails.path)) { + remapClass(fileDetails) + } + recordVisit(fileDetails) + } catch (Exception e) { + throw new GradleException(String.format("Could not add %s to ZIP '%s'.", fileDetails, zipFile), e) + } + } else { + processArchive(fileDetails) + } + } + + private void processArchive(FileCopyDetails fileDetails) { + stats.startJar() + ZipFile archive = new ZipFile(fileDetails.file) + try { + List archiveElements = archive.entries.collect { + new ArchiveFileTreeElement(new RelativeArchivePath(it, fileDetails)) + } + Spec patternSpec = patternSet.getAsSpec() + List filteredArchiveElements = archiveElements.findAll { ArchiveFileTreeElement archiveElement -> + patternSpec.isSatisfiedBy(archiveElement) + } + filteredArchiveElements.each { ArchiveFileTreeElement archiveElement -> + if (archiveElement.relativePath.file) { + visitArchiveFile(archiveElement, archive, fileDetails) + } + } + } finally { + archive.close() + } + stats.finishJar() + } + + private void visitArchiveDirectory(RelativeArchivePath archiveDir) { + if (recordVisit(archiveDir.toString())) { + zipOutStr.putNextEntry(archiveDir.entry) + zipOutStr.closeEntry() + } + } + + private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive, FileCopyDetails fileDetails) { + def archiveFilePath = archiveFile.relativePath + def archiveFileSize = archiveFile.size + + if (archiveFile.classFile || !isTransformable(archiveFile)) { + if (recordVisit(archiveFilePath.toString(), archiveFileSize, fileDetails.relativePath) && !isUnused(archiveFilePath.entry.name)) { + if (!remapper.hasRelocators() || !archiveFile.classFile) { + copyArchiveEntry(archiveFilePath, archive) + } else { + remapClass(archiveFilePath, archive) + } + } else { + def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) + if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size) && !archiveFilePath.toString().startsWith('META-INF/')) { + log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") + log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + } + } + } else { + transform(archiveFile, archive) + } + } + + private void addParentDirectories(RelativeArchivePath file) { + if (file) { + addParentDirectories(file.parent) + if (!file.file) { + visitArchiveDirectory(file) + } + } + } + + private boolean isUnused(String classPath) { + final String className = FilenameUtils.removeExtension(classPath) + .replace('/' as char, '.' as char) + final boolean result = unused.contains(className) + if (result) { + log.debug("Dropping unused class: $className") + } + return result + } + + private void remapClass(RelativeArchivePath file, ZipFile archive) { + if (file.classFile) { + ZipEntry zipEntry = setArchiveTimes(new ZipEntry(remapper.mapPath(file) + '.class')) + addParentDirectories(new RelativeArchivePath(zipEntry)) + remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time) + } + } + + private void remapClass(FileCopyDetails fileCopyDetails) { + if (FilenameUtils.getExtension(fileCopyDetails.name) == 'class') { + InputStream is = fileCopyDetails.file.newInputStream() + try { + remapClass(is, fileCopyDetails.path, fileCopyDetails.lastModified) + } finally { + is.close() + } + } + } + + /** + * Applies remapping to the given class with the specified relocation path. The remapped class is then written + * to the zip file. classInputStream is closed automatically to prevent future file leaks. + * See #364 and #408. + */ + private void remapClass(InputStream classInputStream, String path, long lastModified) { + InputStream is = classInputStream + ClassReader cr = new ClassReader(is) + + // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool. + // Copying the original constant pool should be avoided because it would keep references + // to the original class names. This is not a problem at runtime (because these entries in the + // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin + // that use the constant pool to determine the dependencies of a class. + ClassWriter cw = new ClassWriter(0) + + ClassVisitor cv = new ClassRemapper(cw, remapper) + + try { + cr.accept(cv, ClassReader.EXPAND_FRAMES) + } catch (Throwable ise) { + throw new GradleException("Error in ASM processing class " + path, ise) + } finally { + is.close() + } + + byte[] renamedClass = cw.toByteArray() + + // Temporarily remove the multi-release prefix. + String multiReleasePrefix = path.find("^META-INF/versions/\\d+/") ?: "" + path = path.replace(multiReleasePrefix, "") + String mappedName = multiReleasePrefix + remapper.mapPath(path) + + InputStream bis = new ByteArrayInputStream(renamedClass) + try { + // Now we put it back on so the class file is written out with the right extension. + ZipEntry archiveEntry = new ZipEntry(mappedName + ".class") + archiveEntry.setTime(getArchiveTimeFor(lastModified)) + zipOutStr.putNextEntry(archiveEntry) + IOUtils.copyLarge(bis, zipOutStr) + zipOutStr.closeEntry() + } catch (ZipException ignored) { + log.warn("We have a duplicate " + mappedName + " in source project") + } finally { + bis.close() + } + } + + private void copyArchiveEntry(RelativeArchivePath archiveFile, ZipFile archive) { + String mappedPath = remapper.map(archiveFile.entry.name) + ZipEntry entry = new ZipEntry(mappedPath) + entry.setTime(getArchiveTimeFor(archiveFile.entry.time)) + RelativeArchivePath mappedFile = new RelativeArchivePath(entry) + addParentDirectories(mappedFile) + zipOutStr.putNextEntry(mappedFile.entry) + InputStream is = archive.getInputStream(archiveFile.entry) + try { + IOUtils.copyLarge(is, zipOutStr) + } finally { + is.close() + } + zipOutStr.closeEntry() + } + + @Override + protected void visitDir(FileCopyDetails dirDetails) { + try { + // Trailing slash in name indicates that entry is a directory + String path = dirDetails.relativePath.pathString + '/' + ZipEntry archiveEntry = new ZipEntry(path) + archiveEntry.setTime(getArchiveTimeFor(dirDetails.lastModified)) + archiveEntry.unixMode = (UnixStat.DIR_FLAG | dirDetails.permissions.toUnixNumeric()) + zipOutStr.putNextEntry(archiveEntry) + zipOutStr.closeEntry() + recordVisit(dirDetails.relativePath) + } catch (Exception e) { + throw new GradleException(String.format("Could not add %s to ZIP '%s'.", dirDetails, zipFile), e) + } + } + + private void transform(ArchiveFileTreeElement element, ZipFile archive) { + transformAndClose(element, archive.getInputStream(element.relativePath.entry)) + } + + private void transform(FileCopyDetails details) { + transformAndClose(details, details.file.newInputStream()) + } + + private void transformAndClose(FileTreeElement element, InputStream is) { + try { + String mappedPath = remapper.map(element.relativePath.pathString) + transformers.find { it.canTransformResource(element) }.transform( + TransformerContext.builder() + .path(mappedPath) + .is(is) + .relocators(relocators) + .stats(stats) + .build() + ) + } finally { + is.close() + } + } + + private boolean isTransformable(FileTreeElement element) { + return transformers.any { it.canTransformResource(element) } + } + + } + + class RelativeArchivePath extends RelativePath { + + ZipEntry entry + + RelativeArchivePath(ZipEntry entry) { + super(!entry.directory, entry.name.split('/')) + this.entry = entry + } + + boolean isClassFile() { + return lastName.endsWith('.class') + } + + @Override + RelativeArchivePath getParent() { + if (!segments || segments.length == 1) { + return null + } else { + //Parent is always a directory so add / to the end of the path + String path = segments[0..-2].join('/') + '/' + return new RelativeArchivePath(setArchiveTimes(new ZipEntry(path))) + } + } + } + + class ArchiveFileTreeElement implements FileTreeElement { + + private final RelativeArchivePath archivePath + + ArchiveFileTreeElement(RelativeArchivePath archivePath) { + this.archivePath = archivePath + } + + boolean isClassFile() { + return archivePath.classFile + } + + @Override + File getFile() { + return null + } + + @Override + boolean isDirectory() { + return archivePath.entry.directory + } + + @Override + long getLastModified() { + return archivePath.entry.lastModifiedDate.time + } + + @Override + long getSize() { + return archivePath.entry.size + } + + @Override + InputStream open() { + return null + } + + @Override + void copyTo(OutputStream outputStream) { + + } + + @Override + boolean copyTo(File file) { + return false + } + + @Override + String getName() { + return archivePath.pathString + } + + @Override + String getPath() { + return archivePath.lastName + } + + @Override + RelativeArchivePath getRelativePath() { + return archivePath + } + + @Override + int getMode() { + return archivePath.entry.unixMode + } + + @Override + FilePermissions getPermissions() { + return new DefaultFilePermissions(getMode()) + } + + FileTreeElement asFileTreeElement() { + return new DefaultFileTreeElement(null, new RelativePath(!isDirectory(), archivePath.segments), null, null) + } + } +} From 366ba521e4b74a9bc7b32aae466fec098503fa52 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman and Jim Cifarelli Date: Thu, 19 Mar 2015 09:56:29 -0400 Subject: [PATCH 02/15] Adding tests, not filtering META-INF/ by default --- .../shadow/tasks/ShadowCopyAction.groovy | 8 +- .../plugins/shadow/ShadowPluginSpec.groovy | 1278 +++++++++++++++++ 2 files changed, 1284 insertions(+), 2 deletions(-) create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index e37f00cf6..c41caadb7 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -308,9 +308,13 @@ class ShadowCopyAction implements CopyAction { } } else { def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) - if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size) && !archiveFilePath.toString().startsWith('META-INF/')) { + if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") - log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + if (archiveFileInVisitedFiles.originJar) { + log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + } else { + log.warn(" --> file originated from project sourcecode") + } } } } else { diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy new file mode 100644 index 000000000..9f74f22d8 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -0,0 +1,1278 @@ +package com.github.jengelman.gradle.plugins.shadow + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.plugins.JavaPlugin +import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testkit.runner.BuildResult +import spock.lang.Ignore +import spock.lang.IgnoreIf +import spock.lang.Issue +import spock.lang.Unroll + +import java.util.jar.Attributes +import java.util.jar.JarFile + +class ShadowPluginSpec extends PluginSpecification { + + def 'apply plugin'() { + given: + String projectName = 'myshadow' + String version = '1.0.0' + + Project project = ProjectBuilder.builder().withName(projectName).build() + project.version = version + + when: + project.plugins.apply(ShadowPlugin) + + then: + project.plugins.hasPlugin(ShadowPlugin) + project.plugins.hasPlugin(com.github.jengelman.gradle.plugins.shadow.legacy.LegacyShadowPlugin) + + and: + assert !project.tasks.findByName('shadowJar') + + when: + project.plugins.apply(JavaPlugin) + + then: + ShadowJar shadow = project.tasks.findByName('shadowJar') + assert shadow + assert shadow.archiveBaseName.get() == projectName + assert shadow.destinationDirectory.get().asFile == new File(project.layout.buildDirectory.asFile.get(), 'libs') + assert shadow.archiveVersion.get() == version + assert shadow.archiveClassifier.get() == 'all' + assert shadow.archiveExtension.get() == 'jar' + + and: + Configuration shadowConfig = project.configurations.findByName('shadow') + assert shadowConfig + shadowConfig.artifacts.file.contains(shadow.archiveFile.get().asFile) + + } + + @IgnoreIf({ + // Gradle 8.3 doesn't support Java 21. + JavaVersion.current().majorVersion.toInteger() >= 21 + }) + @Unroll + def 'Compatible with Gradle #version'() { + given: + File one = buildJar('one.jar').insertFile('META-INF/services/shadow.Shadow', + 'one # NOTE: No newline terminates this line/file').write() + + repo.module('shadow', 'two', '1.0').insertFile('META-INF/services/shadow.Shadow', + 'two # NOTE: No newline terminates this line/file').publish() + + buildFile << """ + dependencies { + implementation 'junit:junit:3.8.2' + implementation files('${escapedPath(one)}') + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + mergeServiceFiles() + } + """.stripIndent() + + when: + run(['shadowJar']) { + it.withGradleVersion(version) + it.withDebug(true) + it.withTestKitDir(getTestKitDir()) + } + + then: + assert output.exists() + + where: + version << ['8.3'] + } + + def 'Error in Gradle versions < 8.3'() { + given: + buildFile << """ + dependencies { + implementation 'junit:junit:3.8.2' + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + mergeServiceFiles() + } + """.stripIndent() + + expect: + runWithFailure(['shadowJar']) { + it.withGradleVersion('7.0') + it.withDebug(true) + it.withTestKitDir(getTestKitDir()) + } + } + + def 'shadow copy'() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + from('${artifact.path}') + from('${project.path}') + } + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + } + + def 'warns when a file is masked by a previously shadowed resource'() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + |task shadow(type: ${ShadowJar.name}) { + | destinationDir = buildDir + | baseName = 'shadow' + | from('${artifact.path}') + | from('${project.path}') + |} + """.stripMargin() + + when: + runner.arguments << 'shadow' + ExecutionResult result = runner.run() + + then: + success(result) + assert result.standardOutput =~ /IGNORING META-INF\/MANIFEST\.MF from test-artifact-1\.0-SNAPSHOT\.jar, size is different \(3115 vs 25\)\s --> file originated from project sourcecode/ + assert result.standardOutput =~ /IGNORING META-INF\/MANIFEST\.MF from test-project-1\.0-SNAPSHOT\.jar, size is different \(3906 vs 25\)\s --> file originated from project sourcecode/ + } + + def 'include project sources'() { + given: + file('src/main/java/shadow/Passed.java') << ''' + package shadow; + public class Passed {} + '''.stripIndent() + + buildFile << """ + dependencies { implementation 'junit:junit:3.8.2' } + + // tag::rename[] + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveBaseName = 'shadow' + archiveClassifier = null + archiveVersion = null + archiveVersion.convention(null) + } + // end::rename[] + """.stripIndent() + + when: + run('shadowJar') + + then: + contains(output("shadow.jar"), ['shadow/Passed.class', 'junit/framework/Test.class']) + + and: + doesNotContain(output("shadow.jar"), ['/']) + } + + def 'include project dependencies'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + + import client.Client; + + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + + """.stripIndent() + + File serverOutput = getFile('server/build/libs/server-all.jar') + + when: + run(':server:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'client/Client.class', + 'server/Server.class', + 'junit/framework/Test.class' + ]) + } + + /** + * 'Server' depends on 'Client'. 'junit' is independent. + * The minimize shall remove 'junit'. + */ + def 'minimize by keeping only transitive dependencies'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + + import client.Client; + + public class Server { + private final String client = Client.class.getName(); + } + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize() + } + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + """.stripIndent() + + File serverOutput = getFile('server/build/libs/server-all.jar') + + when: + runWithDebug(':server:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'client/Client.class', + 'server/Server.class' + ]) + doesNotContain(serverOutput, ['junit/framework/Test.class']) + } + + /** + * 'Client', 'Server' and 'junit' are independent. + * 'junit' is excluded from the minimize. + * The minimize shall remove 'Client' but not 'junit'. + */ + def 'exclude a dependency from minimize'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + exclude(dependency('junit:junit:.*')) + } + } + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + """.stripIndent() + + File serverOutput = getFile('server/build/libs/server-all.jar') + + when: + runWithDebug(':server:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'server/Server.class', + 'junit/framework/Test.class' + ]) + doesNotContain(serverOutput, ['client/Client.class']) + } + + /** + * 'Client', 'Server' and 'junit' are independent. + * Unused classes of 'client' and theirs dependencies shouldn't be removed. + */ + def 'exclude a project from minimize '() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + exclude(project(':client')) + } + } + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + """.stripIndent() + + File serverOutput = file('server/build/libs/server-all.jar') + + when: + runWithDebug(':server:shadowJar') + + then: + contains(serverOutput, [ + 'client/Client.class', + 'server/Server.class' + ]) + } + + /** + * 'Client', 'Server' and 'junit' are independent. + * Unused classes of 'client' and theirs dependencies shouldn't be removed. + */ + def 'exclude a project from minimize - shall not exclude transitive dependencies that are used in subproject'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + import junit.framework.TestCase; + public class Client extends TestCase { + public static void main(String[] args) {} + } + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + exclude(project(':client')) + } + } + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + """.stripIndent() + + File serverOutput = file('server/build/libs/server-all.jar') + + when: + runWithDebug(':server:shadowJar') + + then: + contains(serverOutput, [ + 'client/Client.class', + 'server/Server.class', + 'junit/framework/TestCase.class' + ]) + } + + /** + * 'Client', 'Server' and 'junit' are independent. + * Unused classes of 'client' and theirs dependencies shouldn't be removed. + */ + def 'exclude a project from minimize - shall not exclude transitive dependencies from subproject that are not used'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client { } + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + exclude(project(':client')) + } + } + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(':client') } + """.stripIndent() + + File serverOutput = file('server/build/libs/server-all.jar') + + when: + runWithDebug(':server:shadowJar') + + then: + contains(serverOutput, [ + 'client/Client.class', + 'server/Server.class', + 'junit/framework/TestCase.class' + ]) + } + + + /** + * 'api' used as api for 'impl', and depended on 'lib'. 'junit' is independent. + * The minimize shall remove 'junit', but not 'api'. + * Unused classes of 'api' and theirs dependencies also shouldn't be removed. + */ + def 'use minimize with dependencies with api scope'() { + given: + file('settings.gradle') << """ + include 'api', 'lib', 'impl' + """.stripIndent() + + file('lib/src/main/java/lib/LibEntity.java') << """ + package lib; + public interface LibEntity {} + """.stripIndent() + + file('lib/src/main/java/lib/UnusedLibEntity.java') << """ + package lib; + public class UnusedLibEntity implements LibEntity {} + """.stripIndent() + + file('lib/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('api/src/main/java/api/Entity.java') << """ + package api; + public interface Entity {} + """.stripIndent() + + file('api/src/main/java/api/UnusedEntity.java') << """ + package api; + import lib.LibEntity; + public class UnusedEntity implements LibEntity {} + """.stripIndent() + + file('api/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { + implementation 'junit:junit:3.8.2' + implementation project(':lib') + } + """.stripIndent() + + file('impl/src/main/java/impl/SimpleEntity.java') << """ + package impl; + import api.Entity; + public class SimpleEntity implements Entity {} + """.stripIndent() + + file('impl/build.gradle') << """ + apply plugin: 'java-library' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize() + } + + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':api') } + """.stripIndent() + + File serverOutput = getFile('impl/build/libs/impl-all.jar') + + when: + runWithDebug(':impl:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'impl/SimpleEntity.class', + 'api/Entity.class', + 'api/UnusedEntity.class', + 'lib/LibEntity.class', + ]) + doesNotContain(serverOutput, ['junit/framework/Test.class', 'lib/UnusedLibEntity.class']) + } + + /** + * 'api' used as api for 'impl', and 'lib' used as api for 'api'. + * Unused classes of 'api' and 'lib' shouldn't be removed. + */ + def 'use minimize with transitive dependencies with api scope'() { + given: + file('settings.gradle') << """ + include 'api', 'lib', 'impl' + """.stripIndent() + + file('lib/src/main/java/lib/LibEntity.java') << """ + package lib; + public interface LibEntity {} + """.stripIndent() + + file('lib/src/main/java/lib/UnusedLibEntity.java') << """ + package lib; + public class UnusedLibEntity implements LibEntity {} + """.stripIndent() + + file('lib/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('api/src/main/java/api/Entity.java') << """ + package api; + public interface Entity {} + """.stripIndent() + + file('api/src/main/java/api/UnusedEntity.java') << """ + package api; + import lib.LibEntity; + public class UnusedEntity implements LibEntity {} + """.stripIndent() + + file('api/build.gradle') << """ + apply plugin: 'java-library' + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':lib') } + """.stripIndent() + + file('impl/src/main/java/impl/SimpleEntity.java') << """ + package impl; + import api.Entity; + public class SimpleEntity implements Entity {} + """.stripIndent() + + file('impl/build.gradle') << """ + apply plugin: 'java-library' + apply plugin: 'com.gradleup.shadow' + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize() + } + + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':api') } + """.stripIndent() + + File serverOutput = getFile('impl/build/libs/impl-all.jar') + + when: + runWithDebug(':impl:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'impl/SimpleEntity.class', + 'api/Entity.class', + 'api/UnusedEntity.class', + 'lib/LibEntity.class', + 'lib/UnusedLibEntity.class' + ]) + } + + def 'depend on project shadow jar'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + relocate 'junit.framework', 'client.junit.framework' + } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + + import client.Client; + import client.junit.framework.Test; + + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(path: ':client', configuration: 'shadow') } + """.stripIndent() + + File serverOutput = getFile('server/build/libs/server.jar') + + when: + run(':server:jar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'server/Server.class' + ]) + + and: + doesNotContain(serverOutput, [ + 'client/Client.class', + 'junit/framework/Test.class', + 'client/junit/framework/Test.class' + ]) + } + + def 'shadow a project shadow jar'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + repositories { maven { url "${repo.uri}" } } + dependencies { implementation 'junit:junit:3.8.2' } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + relocate 'junit.framework', 'client.junit.framework' + } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + + import client.Client; + import client.junit.framework.Test; + + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.gradleup.shadow' + + repositories { maven { url "${repo.uri}" } } + dependencies { implementation project(path: ':client', configuration: 'shadow') } + """.stripIndent() + + File serverOutput = getFile('server/build/libs/server-all.jar') + + when: + run(':server:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'client/Client.class', + 'client/junit/framework/Test.class', + 'server/Server.class', + ]) + + and: + doesNotContain(serverOutput, [ + 'junit/framework/Test.class' + ]) + } + + def "exclude INDEX.LIST, *.SF, *.DSA, and *.RSA by default"() { + given: + repo.module('shadow', 'a', '1.0') + .insertFile('a.properties', 'a') + .insertFile('META-INF/INDEX.LIST', 'JarIndex-Version: 1.0') + .insertFile('META-INF/a.SF', 'Signature File') + .insertFile('META-INF/a.DSA', 'DSA Signature Block') + .insertFile('META-INF/a.RSA', 'RSA Signature Block') + .insertFile('META-INF/a.properties', 'key=value') + .publish() + + file('src/main/java/shadow/Passed.java') << ''' + package shadow; + public class Passed {} + '''.stripIndent() + + buildFile << """ + dependencies { implementation 'shadow:a:1.0' } + """.stripIndent() + + when: + run('shadowJar') + + then: + contains(output, ['a.properties', 'META-INF/a.properties']) + + and: + doesNotContain(output, ['META-INF/INDEX.LIST', 'META-INF/a.SF', 'META-INF/a.DSA', 'META-INF/a.RSA']) + } + + def "include runtime configuration by default"() { + given: + repo.module('shadow', 'a', '1.0') + .insertFile('a.properties', 'a') + .publish() + + repo.module('shadow', 'b', '1.0') + .insertFile('b.properties', 'b') + .publish() + + buildFile << """ + dependencies { + runtimeOnly 'shadow:a:1.0' + shadow 'shadow:b:1.0' + } + """.stripIndent() + + when: + run('shadowJar') + + then: + contains(output, ['a.properties']) + + and: + doesNotContain(output, ['b.properties']) + } + + def "include java-library configurations by default"() { + given: + repo.module('shadow', 'api', '1.0') + .insertFile('api.properties', 'api') + .publish() + + repo.module('shadow', 'implementation-dep', '1.0') + .insertFile('implementation-dep.properties', 'implementation-dep') + .publish() + + repo.module('shadow', 'implementation', '1.0') + .insertFile('implementation.properties', 'implementation') + .dependsOn('implementation-dep') + .publish() + + repo.module('shadow', 'runtimeOnly', '1.0') + .insertFile('runtimeOnly.properties', 'runtimeOnly') + .publish() + + buildFile.text = getDefaultBuildScript('java-library') + buildFile << """ + dependencies { + api 'shadow:api:1.0' + implementation 'shadow:implementation:1.0' + runtimeOnly 'shadow:runtimeOnly:1.0' + } + """.stripIndent() + + when: + runWithDebug('shadowJar') + + then: + contains(output, ['api.properties', 'implementation.properties', + 'runtimeOnly.properties', 'implementation-dep.properties']) + } + + def "doesn't include compileOnly configuration by default"() { + given: + repo.module('shadow', 'a', '1.0') + .insertFile('a.properties', 'a') + .publish() + + repo.module('shadow', 'b', '1.0') + .insertFile('b.properties', 'b') + .publish() + + buildFile << """ + dependencies { + runtimeOnly 'shadow:a:1.0' + compileOnly 'shadow:b:1.0' + } + """.stripIndent() + + when: + run('shadowJar') + + then: + contains(output, ['a.properties']) + + and: + doesNotContain(output, ['b.properties']) + } + + def "default copying strategy"() { + given: + repo.module('shadow', 'a', '1.0') + .insertFile('META-INF/MANIFEST.MF', 'MANIFEST A') + .publish() + + repo.module('shadow', 'b', '1.0') + .insertFile('META-INF/MANIFEST.MF', 'MANIFEST B') + .publish() + + buildFile << """ + dependencies { + runtimeOnly 'shadow:a:1.0' + runtimeOnly 'shadow:b:1.0' + } + """.stripIndent() + + when: + run('shadowJar') + + then: + JarFile jar = new JarFile(output) + assert jar.entries().collect().size() == 2 + } + + def "Class-Path in Manifest not added if empty"() { + given: + + buildFile << """ + dependencies { implementation 'junit:junit:3.8.2' } + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + + and: + JarFile jar = new JarFile(output) + Attributes attributes = jar.manifest.getMainAttributes() + assert attributes.getValue('Class-Path') == null + } + + @Issue('SHADOW-65') + def "add shadow configuration to Class-Path in Manifest"() { + given: + + buildFile << """ + // tag::shadowConfig[] + dependencies { + shadow 'junit:junit:3.8.2' + } + // end::shadowConfig[] + + // tag::jarManifest[] + jar { + manifest { + attributes 'Class-Path': '/libs/a.jar' + } + } + // end::jarManifest[] + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + + and: 'SHADOW-65 - combine w/ existing Class-Path' + JarFile jar = new JarFile(output) + Attributes attributes = jar.manifest.getMainAttributes() + String classpath = attributes.getValue('Class-Path') + assert classpath == '/libs/a.jar junit-3.8.2.jar' + + } + + @Issue('SHADOW-92') + def "do not include null value in Class-Path when jar file does not contain Class-Path"() { + given: + + buildFile << """ + dependencies { shadow 'junit:junit:3.8.2' } + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + + and: + JarFile jar = new JarFile(output) + Attributes attributes = jar.manifest.getMainAttributes() + String classpath = attributes.getValue('Class-Path') + assert classpath == 'junit-3.8.2.jar' + + } + + @Issue('SHADOW-203') + def "support ZipCompression.STORED"() { + given: + + buildFile << """ + dependencies { shadow 'junit:junit:3.8.2' } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + zip64 true + entryCompression = org.gradle.api.tasks.bundling.ZipEntryCompression.STORED + } + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + + } + + def 'api project dependency with version'() { + given: + file('settings.gradle') << """ + include 'api', 'lib', 'impl' + """.stripIndent() + + file('lib/build.gradle') << """ + apply plugin: 'java' + version = '1.0' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('api/src/main/java/api/UnusedEntity.java') << """ + package api; + public class UnusedEntity {} + """.stripIndent() + + file('api/build.gradle') << """ + apply plugin: 'java' + version = '1.0' + repositories { maven { url "${repo.uri}" } } + dependencies { + implementation 'junit:junit:3.8.2' + implementation project(':lib') + } + """.stripIndent() + + file('impl/build.gradle') << """ + apply plugin: 'java-library' + apply plugin: 'com.gradleup.shadow' + + version = '1.0' + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':api') } + + shadowJar.minimize() + """.stripIndent() + + File serverOutput = getFile('impl/build/libs/impl-1.0-all.jar') + + when: + runWithDebug(':impl:shadowJar') + + then: + serverOutput.exists() + contains(serverOutput, [ + 'api/UnusedEntity.class', + ]) + } + + @Issue('SHADOW-143') + @Ignore("This spec requires > 15 minutes and > 8GB of disk space to run") + def "check large zip files with zip64 enabled"() { + given: + repo.module('shadow', 'a', '1.0') + .insertFile('a.properties', 'a') + .insertFile('a2.properties', 'a2') + .publish() + + file('src/main/java/myapp/Main.java') << """ + package myapp; + public class Main { + public static void main(String[] args) { + System.out.println("TestApp: Hello World! (" + args[0] + ")"); + } + } + """.stripIndent() + + buildFile << """ + apply plugin: 'application' + + application { + mainClass = 'myapp.Main' + } + + dependencies { + implementation 'shadow:a:1.0' + } + + def generatedResourcesDir = new File(project.layout.buildDirectory.asFile.get(), "generated-resources") + + task generateResources { + doLast { + def rnd = new Random() + def buf = new byte[128 * 1024] + for (x in 0..255) { + def dir = new File(generatedResourcesDir, x.toString()) + dir.mkdirs() + for (y in 0..255) { + def file = new File(dir, y.toString()) + rnd.nextBytes(buf) + file.bytes = buf + } + } + } + } + + sourceSets { + main { + output.dir(generatedResourcesDir, builtBy: generateResources) + } + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + zip64 = true + } + + runShadow { + args 'foo' + } + """.stripIndent() + + settingsFile << "rootProject.name = 'myapp'" + + when: + BuildResult result = run('runShadow') + + then: 'tests that runShadow executed and exited' + assert result.output.contains('TestApp: Hello World! (foo)') + + + } + + @Issue("SHADOW-303") + @Ignore("Plugin has been deprecated") + def "doesn't error when adding aspectj plugin"() { + given: + buildFile.text = """ + buildscript { + repositories { + maven { + url "https://maven.eveoh.nl/content/repositories/releases" + } + } + + dependencies { + classpath "nl.eveoh:gradle-aspectj:2.0" + } + } + """.stripIndent() + + buildFile << defaultBuildScript + + buildFile << """ + project.ext { + aspectjVersion = '1.8.12' + } + + apply plugin: 'aspectj' + apply plugin: 'application' + + application { + mainClass = 'myapp.Main' + } + + repositories { + mavenCentral() + } + + runShadow { + args 'foo' + } + + """ + + file('src/main/java/myapp/Main.java') << """ + package myapp; + public class Main { + public static void main(String[] args) { + System.out.println("TestApp: Hello World! (" + args[0] + ")"); + } + } + """.stripIndent() + + when: + BuildResult result = run('runShadow') + + then: 'tests that runShadow executed and exited' + assert result.output.contains('TestApp: Hello World! (foo)') + } + + @Issue("https://github.com/GradleUp/shadow/issues/609") + def "doesn't error when using application mainClass property"() { + given: + buildFile.text = defaultBuildScript + + buildFile << """ + project.ext { + aspectjVersion = '1.8.12' + } + + apply plugin: 'application' + + application { + mainClass.set('myapp.Main') + } + + repositories { + mavenCentral() + } + + runShadow { + args 'foo' + } + + """ + + file('src/main/java/myapp/Main.java') << """ + package myapp; + public class Main { + public static void main(String[] args) { + System.out.println("TestApp: Hello World! (" + args[0] + ")"); + } + } + """.stripIndent() + + when: + BuildResult result = run('runShadow') + + then: 'tests that runShadow executed and exited' + assert result.output.contains('TestApp: Hello World! (foo)') + } + + @Issue("https://github.com/GradleUp/shadow/pull/459") + def 'exclude gradleApi() by default'() { + given: + buildFile.text = getDefaultBuildScript('java-gradle-plugin') + + file('src/main/java/my/plugin/MyPlugin.java') << """ + package my.plugin; + import org.gradle.api.Plugin; + import org.gradle.api.Project; + public class MyPlugin implements Plugin { + public void apply(Project project) { + System.out.println("MyPlugin: Hello World!"); + } + } + """.stripIndent() + file('src/main/resources/META-INF/gradle-plugins/my.plugin.properties') << """ + implementation-class=my.plugin.MyPlugin + """.stripIndent() + + when: + run('shadowJar') + + then: + assert output.exists() + + and: + JarFile jar = new JarFile(output) + assert jar.entries().collect().findAll { it.name.endsWith('.class') }.size() == 1 + } + + private String escapedPath(File file) { + file.path.replaceAll('\\\\', '\\\\\\\\') + } +} From bd44ee7d98a7406ee8af2861f4dc2ba229231938 Mon Sep 17 00:00:00 2001 From: Another User Date: Fri, 10 Jun 2022 12:37:09 +0200 Subject: [PATCH 03/15] Cleaned up "Collision logging" and added StandardFilesMergeTransformer to merge standard files --- .../shadow/tasks/ShadowCopyAction.groovy | 61 ++- .../plugins/shadow/tasks/ShadowJar.java | 513 ++++++++++++++++++ .../StandardFilesMergeTransformer.groovy | 127 +++++ .../transformers/TransformerContext.groovy | 27 + .../StandardFilesMergeTransformerTest.groovy | 61 +++ 5 files changed, 768 insertions(+), 21 deletions(-) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformerTest.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index c41caadb7..5a2585a1c 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -5,6 +5,7 @@ import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext import groovy.util.logging.Slf4j @@ -19,12 +20,10 @@ import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.UncheckedIOException import org.gradle.api.file.FileCopyDetails -import org.gradle.api.file.FilePermissions import org.gradle.api.file.FileTreeElement import org.gradle.api.file.RelativePath import org.gradle.api.internal.DocumentationRegistry import org.gradle.api.internal.file.CopyActionProcessingStreamAction -import org.gradle.api.internal.file.DefaultFilePermissions import org.gradle.api.internal.file.DefaultFileTreeElement import org.gradle.api.internal.file.copy.CopyAction import org.gradle.api.internal.file.copy.CopyActionProcessingStream @@ -40,6 +39,7 @@ import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper +import javax.annotation.Nullable import java.util.zip.ZipException @Slf4j @@ -59,9 +59,9 @@ class ShadowCopyAction implements CopyAction { private final UnusedTracker unusedTracker ShadowCopyAction(File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry, - String encoding, List transformers, List relocators, - PatternSet patternSet, ShadowStats stats, - boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker) { + String encoding, List transformers, List relocators, + PatternSet patternSet, ShadowStats stats, + boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker) { this.zipFile = zipFile this.compressor = compressor @@ -150,7 +150,7 @@ class ShadowCopyAction implements CopyAction { private static void withResource(T resource, Action action) { try { action.execute(resource) - } catch(Throwable t) { + } catch (Throwable t) { try { resource.close() } catch (IOException e) { @@ -199,11 +199,11 @@ class ShadowCopyAction implements CopyAction { private final Set unused private final ShadowStats stats - private Map visitedFiles = new HashMap() + private Map visitedFiles = new HashMap<>() StreamAction(ZipOutputStream zipOutStr, String encoding, List transformers, - List relocators, PatternSet patternSet, Set unused, - ShadowStats stats) { + List relocators, PatternSet patternSet, Set unused, + ShadowStats stats) { this.zipOutStr = zipOutStr this.transformers = transformers this.relocators = relocators @@ -211,13 +211,21 @@ class ShadowCopyAction implements CopyAction { this.patternSet = patternSet this.unused = unused this.stats = stats - if(encoding != null) { + if (encoding != null) { this.zipOutStr.setEncoding(encoding) } } - private boolean recordVisit(path, size, originJar) { - if (visitedFiles.containsKey(path.toString())) { + /** + * Record visit and return true if visited for the first time. + * + * @param path Visited path. + * @param size Size. + * @param originJar JAR it originated from. + * @return True if wasn't visited already. + */ + private boolean recordVisit(String path, long size, @Nullable RelativePath originJar) { + if (visitedFiles.containsKey(path)) { return false } @@ -296,24 +304,28 @@ class ShadowCopyAction implements CopyAction { } private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive, FileCopyDetails fileDetails) { - def archiveFilePath = archiveFile.relativePath - def archiveFileSize = archiveFile.size + RelativeArchivePath archiveFilePath = archiveFile.relativePath + long archiveFileSize = archiveFile.size if (archiveFile.classFile || !isTransformable(archiveFile)) { - if (recordVisit(archiveFilePath.toString(), archiveFileSize, fileDetails.relativePath) && !isUnused(archiveFilePath.entry.name)) { + def path = archiveFilePath.toString() + if (recordVisit(path, archiveFileSize, archiveFilePath) && !isUnused(archiveFilePath.entry.name)) { if (!remapper.hasRelocators() || !archiveFile.classFile) { copyArchiveEntry(archiveFilePath, archive) } else { remapClass(archiveFilePath, archive) } } else { - def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) + def archiveFileInVisitedFiles = visitedFiles.get(path) if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") if (archiveFileInVisitedFiles.originJar) { - log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + log.warn("\t--> origin JAR was ${archiveFileInVisitedFiles.originJar}") } else { - log.warn(" --> file originated from project sourcecode") + log.warn("\t--> file originated from project sourcecode") + } + if (new StandardFilesMergeTransformer().canTransformResource(archiveFile)) { + log.warn("\t--> Recommended transformer is " + StandardFilesMergeTransformer.class.name) } } } @@ -408,6 +420,12 @@ class ShadowCopyAction implements CopyAction { } } + /** + * Copy archive entry. + * + * @param archiveFile Source archive entry. + * @param archive Source archive. + */ private void copyArchiveEntry(RelativeArchivePath archiveFile, ZipFile archive) { String mappedPath = remapper.map(archiveFile.entry.name) ZipEntry entry = new ZipEntry(mappedPath) @@ -441,19 +459,20 @@ class ShadowCopyAction implements CopyAction { } private void transform(ArchiveFileTreeElement element, ZipFile archive) { - transformAndClose(element, archive.getInputStream(element.relativePath.entry)) + transformAndClose(element, archive, archive.getInputStream(element.relativePath.entry)) } private void transform(FileCopyDetails details) { - transformAndClose(details, details.file.newInputStream()) + transformAndClose(details, null, details.file.newInputStream()) } - private void transformAndClose(FileTreeElement element, InputStream is) { + private void transformAndClose(FileTreeElement element, @Nullable ZipFile archive, InputStream is) { try { String mappedPath = remapper.map(element.relativePath.pathString) transformers.find { it.canTransformResource(element) }.transform( TransformerContext.builder() .path(mappedPath) + .origin(archive) .is(is) .relocators(relocators) .stats(stats) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java new file mode 100644 index 000000000..00ace7e24 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -0,0 +1,513 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import com.github.jengelman.gradle.plugins.shadow.ShadowStats; +import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter; +import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter; +import com.github.jengelman.gradle.plugins.shadow.internal.GradleVersionUtil; +import com.github.jengelman.gradle.plugins.shadow.internal.MinimizeDependencyFilter; +import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker; +import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor; +import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator; +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator; +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator; +import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; +import org.gradle.api.Action; +import org.gradle.api.Task; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.file.FileCollection; +import org.gradle.api.internal.DocumentationRegistry; +import org.gradle.api.internal.file.FileResolver; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.*; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.util.PatternSet; +import org.jetbrains.annotations.NotNull; + +@CacheableTask +public class ShadowJar extends Jar implements ShadowSpec { + + private List transformers; + private List relocators; + private List configurations; + private transient DependencyFilter dependencyFilter; + private boolean enableRelocation; + private String relocationPrefix = "shadow"; + private boolean minimizeJar; + private final transient DependencyFilter dependencyFilterForMinimize; + private FileCollection toMinimize; + private FileCollection apiJars; + private FileCollection sourceSetsClassesDirs; + + private final ShadowStats shadowStats = new ShadowStats(); + + private final ConfigurableFileCollection includedDependencies = getProject().files(new Callable() { + + @Override + public FileCollection call() { + return dependencyFilter.resolve(configurations); + } + }); + + public ShadowJar() { + super(); + setDuplicatesStrategy( + DuplicatesStrategy.INCLUDE); //shadow filters out files later. This was the default behavior in Gradle < 6.x + versionUtil = new GradleVersionUtil(getProject().getGradle().getGradleVersion()); + dependencyFilter = new DefaultDependencyFilter(getProject()); + dependencyFilterForMinimize = new MinimizeDependencyFilter(getProject()); + setManifest(new DefaultInheritManifest(getServices().get(FileResolver.class))); + /* + Add as default the StandardFilesMergeTransformer, remove it with "removeDefaultTransformers()". + This is added by default, because otherwise: + a) In projects with many dependencies the user gets flooded with information about duplicated entries + like "META-INF/notice.txt", "META-INF/license.txt"... + b) Important licensing information written in META-INF/license.txt and other files may be lost. + c) Helpful information written in readme files may be lost. + d) The merging of plain text files is safe, there is no important logic to follow. Not like MANIFEST.MF, + property files, xml files, etc. Merged HTML may not look that good, but it works. + */ + transformers = new ArrayList<>(Collections.singletonList(new StandardFilesMergeTransformer())); + relocators = new ArrayList<>(); + configurations = new ArrayList<>(); + + this.getInputs().property("minimize", new Callable() { + @Override + public Boolean call() throws Exception { + return minimizeJar; + } + }); + this.getOutputs().doNotCacheIf("Has one or more transforms or relocators that are not cacheable", + new Spec() { + @Override + public boolean isSatisfiedBy(Task task) { + for (Transformer transformer : transformers) { + if (!isCacheableTransform(transformer.getClass())) { + return true; + } + } + for (Relocator relocator : relocators) { + if (!isCacheableRelocator(relocator.getClass())) { + return true; + } + } + return false; + } + }); + } + + @Override + public ShadowJar minimize() { + minimizeJar = true; + return this; + } + + @Override + public ShadowJar minimize(Action c) { + minimize(); + if (c != null) { + c.execute(dependencyFilterForMinimize); + } + return this; + } + + @Override + @Internal + public ShadowStats getStats() { + return shadowStats; + } + + @Override + public InheritManifest getManifest() { + return (InheritManifest) super.getManifest(); + } + + @Override + @NotNull + protected CopyAction createCopyAction() { + DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class); + final UnusedTracker unusedTracker = minimizeJar ? UnusedTracker.forProject(getApiJars(), + getSourceSetsClassesDirs().getFiles(), getToMinimize()) : null; + return new ShadowCopyAction(getArchiveFile().get().getAsFile(), getInternalCompressor(), documentationRegistry, + this.getMetadataCharset(), transformers, relocators, getRootPatternSet(), shadowStats, + isPreserveFileTimestamps(), minimizeJar, unusedTracker); + } + + @Classpath + FileCollection getToMinimize() { + if (toMinimize == null) { + toMinimize = minimizeJar + ? dependencyFilterForMinimize.resolve(configurations).minus(getApiJars()) + : getProject().getObjects().fileCollection(); + } + return toMinimize; + } + + @Classpath + FileCollection getApiJars() { + if (apiJars == null) { + apiJars = minimizeJar + ? UnusedTracker.getApiJarsFromProject(getProject()) + : getProject().getObjects().fileCollection(); + } + return apiJars; + } + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + FileCollection getSourceSetsClassesDirs() { + if (sourceSetsClassesDirs == null) { + ConfigurableFileCollection allClassesDirs = getProject().getObjects().fileCollection(); + if (minimizeJar) { + for (SourceSet sourceSet : getProject().getExtensions().getByType(SourceSetContainer.class)) { + FileCollection classesDirs = sourceSet.getOutput().getClassesDirs(); + allClassesDirs.from(classesDirs); + } + } + sourceSetsClassesDirs = allClassesDirs.filter(File::isDirectory); + } + return sourceSetsClassesDirs; + } + + @Internal + protected ZipCompressor getInternalCompressor() { + return GradleVersionUtil.getInternalCompressor(getEntryCompression(), this); + } + + @TaskAction + @Override + protected void copy() { + if (enableRelocation) { + RelocationUtil.configureRelocation(this, relocationPrefix); + } + from(getIncludedDependencies()); + super.copy(); + getLogger().info(shadowStats.toString()); + } + + @Classpath + public FileCollection getIncludedDependencies() { + return includedDependencies; + } + + /** + * Utility method for assisting between changes in Gradle 1.12 and 2.x. + * + * @return this + */ + @Internal + protected PatternSet getRootPatternSet() { + return GradleVersionUtil.getRootPatternSet(getMainSpec()); + } + + /** + * Configure inclusion/exclusion of module and project dependencies into uber jar. + * + * @param c the configuration of the filter + * @return this + */ + @Override + public ShadowJar dependencies(Action c) { + if (c != null) { + c.execute(dependencyFilter); + } + return this; + } + + /** + * Add a Transformer instance for modifying JAR resources and configure. + * + * @param clazz the transformer to add. Must have a no-arg constructor + * @return this + */ + @Override + public ShadowJar transform(Class clazz) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + return transform(clazz, null); + } + + /** + * Add a Transformer instance for modifying JAR resources and configure. + * + * @param clazz the transformer class to add. Must have no-arg constructor + * @param c the configuration for the transformer + * @return this + */ + @Override + public ShadowJar transform(Class clazz, Action c) + throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + T transformer = clazz.getDeclaredConstructor().newInstance(); + addTransform(transformer, c); + return this; + } + + /** + * Removes all default transformers. + *
Right now only {@link StandardFilesMergeTransformer} is added as default transformer, this method removes + * it. + * + * @return this + */ + public ShadowJar removeDefaultTransformers() { + final java.util.Optional standardFilesMergeTransformer = transformers.stream() // + .filter(StandardFilesMergeTransformer.class::isInstance) // + .findAny(); + standardFilesMergeTransformer.ifPresent(transformer -> transformers.remove(transformer)); + return this; + } + + private boolean isCacheableTransform(Class clazz) { + return clazz.isAnnotationPresent(CacheableTransformer.class); + } + + /** + * Add a preconfigured transformer instance. + * + * @param transformer the transformer instance to add + * @return this + */ + @Override + public ShadowJar transform(Transformer transformer) { + addTransform(transformer, null); + return this; + } + + private void addTransform(T transformer, Action c) { + if (c != null) { + c.execute(transformer); + } + + transformers.add(transformer); + } + + /** + * Syntactic sugar for merging service files in JARs. + * + * @return this + */ + @Override + public ShadowJar mergeServiceFiles() { + try { + transform(ServiceFileTransformer.class); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | + InstantiationException ignored) { + } + return this; + } + + /** + * Syntactic sugar for merging service files in JARs. + * + * @return this + */ + @Override + public ShadowJar mergeServiceFiles(final String rootPath) { + try { + transform(ServiceFileTransformer.class, serviceFileTransformer -> serviceFileTransformer.setPath(rootPath)); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | + InstantiationException ignored) { + } + return this; + } + + /** + * Syntactic sugar for merging service files in JARs. + * + * @return this + */ + @Override + public ShadowJar mergeServiceFiles(Action configureClosure) { + try { + transform(ServiceFileTransformer.class, configureClosure); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | + InstantiationException ignored) { + } + return this; + } + + /** + * Syntactic sugar for merging Groovy extension module descriptor files in JARs + * + * @return this + */ + @Override + public ShadowJar mergeGroovyExtensionModules() { + try { + transform(GroovyExtensionModuleTransformer.class); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | + InstantiationException ignored) { + } + return this; + } + + /** + * Syntax sugar for merging service files in JARs + * + * @return this + */ + @Override + public ShadowJar append(final String resourcePath) { + try { + transform(AppendingTransformer.class, transformer -> transformer.setResource(resourcePath)); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | + InstantiationException ignored) { + } + return this; + } + + /** + * Add a class relocator that maps each class in the pattern to the provided destination. + * + * @param pattern the source pattern to relocate + * @param destination the destination package + * @return this + */ + @Override + public ShadowJar relocate(String pattern, String destination) { + return relocate(pattern, destination, null); + } + + /** + * Add a class relocator that maps each class in the pattern to the provided destination. + * + * @param pattern the source pattern to relocate + * @param destination the destination package + * @param configure the configuration of the relocator + * @return this + */ + @Override + public ShadowJar relocate(String pattern, String destination, Action configure) { + SimpleRelocator relocator = new SimpleRelocator(pattern, destination, new ArrayList(), + new ArrayList()); + addRelocator(relocator, configure); + return this; + } + + /** + * Add a relocator instance. + * + * @param relocator the relocator instance to add + * @return this + */ + @Override + public ShadowJar relocate(Relocator relocator) { + addRelocator(relocator, null); + return this; + } + + /** + * Add a relocator of the provided class. + * + * @param relocatorClass the relocator class to add. Must have a no-arg constructor. + * @return this + */ + @Override + public ShadowJar relocate(Class relocatorClass) + throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + return relocate(relocatorClass, null); + } + + private void addRelocator(R relocator, Action configure) { + if (configure != null) { + configure.execute(relocator); + } + + relocators.add(relocator); + } + + /** + * Add a relocator of the provided class and configure. + * + * @param relocatorClass the relocator class to add. Must have a no-arg constructor + * @param configure the configuration for the relocator + * @return this + */ + @Override + public ShadowJar relocate(Class relocatorClass, Action configure) + throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + R relocator = relocatorClass.getDeclaredConstructor().newInstance(); + addRelocator(relocator, configure); + return this; + } + + private boolean isCacheableRelocator(Class relocatorClass) { + return relocatorClass.isAnnotationPresent(CacheableRelocator.class); + } + + @Nested + public List getTransformers() { + return this.transformers; + } + + public void setTransformers(List transformers) { + this.transformers = transformers; + } + + @Nested + public List getRelocators() { + return this.relocators; + } + + public void setRelocators(List relocators) { + this.relocators = relocators; + } + + @Classpath @Optional + public List getConfigurations() { + return this.configurations; + } + + public void setConfigurations(List configurations) { + this.configurations = configurations; + } + + @Internal + public DependencyFilter getDependencyFilter() { + return this.dependencyFilter; + } + + public void setDependencyFilter(DependencyFilter filter) { + this.dependencyFilter = filter; + } + + @Input + public boolean isEnableRelocation() { + return enableRelocation; + } + + public void setEnableRelocation(boolean enableRelocation) { + this.enableRelocation = enableRelocation; + } + + @Input + public String getRelocationPrefix() { + return relocationPrefix; + } + + public void setRelocationPrefix(String relocationPrefix) { + this.relocationPrefix = relocationPrefix; + } +} diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy new file mode 100644 index 000000000..c814dead8 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.github.jengelman.gradle.plugins.shadow.transformers + + +import org.apache.commons.io.FilenameUtils +import org.apache.commons.io.IOUtils +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipOutputStream +import org.codehaus.plexus.util.IOUtil +import org.gradle.api.file.FileTreeElement + +/** + * Merges standard files, like "META-INF/license.txt", "META-INF/notice.txt", "readme.txt" into one, + * writing as prefix where the content comes from, so no license information or important hints gets lost. + * + * @author Jan-Hendrik Diederich + */ +class StandardFilesMergeTransformer implements Transformer { + private class StandardFile { + List origins = new ArrayList<>(); + String content; + + StandardFile(String origin, String content) { + this.origins.add(origin) + this.content = content + } + } + + private final List mergedFiles = [ + "META-INF/license", // + "META-INF/notice", // + "META-INF/readme", // + "readme", // + ] + + private final List fileExtensions = [ + "txt", "md", "htm", "html" + ] + + // Can't use normal HashMap, ...notice.txt and ...NOTICE.txt would otherwise be different entries. + private Map> fileEntries = new TreeMap<>(String.CASE_INSENSITIVE_ORDER) + + @Override + boolean canTransformResource(FileTreeElement element) { + String path = element.relativePath.pathString + mergedFiles.stream() // + .anyMatch(mergeFile -> { + if (path.equalsIgnoreCase(mergeFile)) { + return true + } else { + for (extension in fileExtensions) { + if (path.equalsIgnoreCase(mergeFile + "." + extension)) { + return true + } + } + return false + } + }) + } + + @Override + void transform(TransformerContext context) { + List files = fileEntries.computeIfAbsent(context.path, key -> new ArrayList<>()) + + OutputStream outputStream = new ByteArrayOutputStream() + IOUtils.copyLarge(context.is, outputStream) + + def fileContent = outputStream.toString() + // Remove leading and trailing newlines. Don't trim whitespaces, so centered headers stay centered. + def trimmedFileContent = fileContent.replaceAll("^[\\r\\n]+|[\\r\\n]+\$", "") + + var standardFile = files.stream() // + .filter(entry -> trimmedFileContent.equalsIgnoreCase(entry.content)) // + .findAny() + String originName = context.origin != null + ? FilenameUtils.getName(context.origin.name) + : "Sourcecode" + if (standardFile.isPresent()) { + standardFile.get().origins.add(originName) + } else { + files.add(new StandardFile(originName, trimmedFileContent)) + } + } + + @Override + boolean hasTransformedResource() { + return fileEntries.size() > 0 + } + + @Override + void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { + fileEntries.each { String path, List files -> + ZipEntry entry = new ZipEntry(path) + entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) + os.putNextEntry(entry) + IOUtil.copy(toInputStream(files), os) + os.closeEntry() + } + } + + private static InputStream toInputStream(List entries) { + String joined = entries.stream() // + .map(entry -> "Origins: " + entry.origins.sort().join(", ") // + + "\n\n" + entry.content) // + .collect() // + .join("\n" + "=".repeat(80) + "\n") + new ByteArrayInputStream(joined.getBytes()) + } +} \ No newline at end of file diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy new file mode 100644 index 000000000..d5dfc956b --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy @@ -0,0 +1,27 @@ +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 com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction +import groovy.transform.Canonical +import groovy.transform.builder.Builder +import org.apache.tools.zip.ZipFile + +import javax.annotation.Nullable + +@Canonical +@Builder +class TransformerContext { + + String path + InputStream is + List relocators + ShadowStats stats + + @Nullable + ZipFile origin + + static long getEntryTimestamp(boolean preserveFileTimestamps, long entryTime) { + preserveFileTimestamps ? entryTime : ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES + } +} diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformerTest.groovy new file mode 100644 index 000000000..3dba6bf28 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformerTest.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.github.jengelman.gradle.plugins.shadow.transformers + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +/** + * Test for {@link StandardFilesMergeTransformer}. + * + * @author Benjamin Bentmann + * @version $Id: ApacheNoticeResourceTransformerTest.java 673906 2008-07-04 05:03:20Z brett $ + * + * Modified from org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformerTest.java + */ +class StandardFilesMergeTransformerTest extends TransformerTestSupport { + + private StandardFilesMergeTransformer transformer + + static { + /* + * NOTE: The Turkish locale has an usual case transformation for the letters "I" and "i", making it a prime + * choice to test for improper case-less string comparisons. + */ + Locale.setDefault(new Locale("tr")) + } + + @Before + void setUp() { + this.transformer = new StandardFilesMergeTransformer() + } + + @Test + void testCanTransformResource() { + assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/NOTICE"))) + assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/NOTICE.TXT"))) + assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/Notice.txt"))) + assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/Notice.hTml"))) + assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF"))) + } +} \ No newline at end of file From 95338d04ba401e32985d5499d2342169f5b6df0b Mon Sep 17 00:00:00 2001 From: Another User Date: Fri, 10 Jun 2022 14:51:50 +0200 Subject: [PATCH 04/15] Made StandardFilesMergeTransformer cacheable. --- .../shadow/transformers/StandardFilesMergeTransformer.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy index c814dead8..72c1639d2 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/StandardFilesMergeTransformer.groovy @@ -33,8 +33,9 @@ import org.gradle.api.file.FileTreeElement * * @author Jan-Hendrik Diederich */ +@CacheableTransformer class StandardFilesMergeTransformer implements Transformer { - private class StandardFile { + private class StandardFile implements Serializable { List origins = new ArrayList<>(); String content; From 355c600b65a57601e20fd2a680f4bffc94fe8000 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman and Jim Cifarelli Date: Mon, 16 Mar 2015 11:23:43 -0400 Subject: [PATCH 05/15] Displaying `warn` level messages when adding a file attempts to overwrite an existing resource --- .../shadow/tasks/ShadowCopyAction.groovy | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 5a2585a1c..a4af88032 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -199,7 +199,7 @@ class ShadowCopyAction implements CopyAction { private final Set unused private final ShadowStats stats - private Map visitedFiles = new HashMap<>() + private Map visitedFiles = new HashMap() StreamAction(ZipOutputStream zipOutStr, String encoding, List transformers, List relocators, PatternSet patternSet, Set unused, @@ -216,16 +216,8 @@ class ShadowCopyAction implements CopyAction { } } - /** - * Record visit and return true if visited for the first time. - * - * @param path Visited path. - * @param size Size. - * @param originJar JAR it originated from. - * @return True if wasn't visited already. - */ - private boolean recordVisit(String path, long size, @Nullable RelativePath originJar) { - if (visitedFiles.containsKey(path)) { + private boolean recordVisit(path, size, originJar) { + if (visitedFiles.containsKey(path.toString())) { return false } @@ -304,29 +296,21 @@ class ShadowCopyAction implements CopyAction { } private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive, FileCopyDetails fileDetails) { - RelativeArchivePath archiveFilePath = archiveFile.relativePath - long archiveFileSize = archiveFile.size + def archiveFilePath = archiveFile.relativePath + def archiveFileSize = archiveFile.size if (archiveFile.classFile || !isTransformable(archiveFile)) { - def path = archiveFilePath.toString() - if (recordVisit(path, archiveFileSize, archiveFilePath) && !isUnused(archiveFilePath.entry.name)) { + if (recordVisit(archiveFilePath.toString(), archiveFileSize, fileDetails.relativePath) && !isUnused(archiveFilePath.entry.name)) { if (!remapper.hasRelocators() || !archiveFile.classFile) { copyArchiveEntry(archiveFilePath, archive) } else { remapClass(archiveFilePath, archive) } } else { - def archiveFileInVisitedFiles = visitedFiles.get(path) - if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { + def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) + if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size) && !archiveFilePath.toString().startsWith('META-INF/')) { log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") - if (archiveFileInVisitedFiles.originJar) { - log.warn("\t--> origin JAR was ${archiveFileInVisitedFiles.originJar}") - } else { - log.warn("\t--> file originated from project sourcecode") - } - if (new StandardFilesMergeTransformer().canTransformResource(archiveFile)) { - log.warn("\t--> Recommended transformer is " + StandardFilesMergeTransformer.class.name) - } + log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") } } } else { From 7224b298d35513cc06bfd3f4cad7c4489114fc53 Mon Sep 17 00:00:00 2001 From: Jonathan Chapman and Jim Cifarelli Date: Thu, 19 Mar 2015 09:56:29 -0400 Subject: [PATCH 06/15] Adding tests, not filtering META-INF/ by default --- .../gradle/plugins/shadow/tasks/ShadowCopyAction.groovy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index a4af88032..5559387a1 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -308,9 +308,13 @@ class ShadowCopyAction implements CopyAction { } } else { def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) - if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size) && !archiveFilePath.toString().startsWith('META-INF/')) { + if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") - log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + if (archiveFileInVisitedFiles.originJar) { + log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + } else { + log.warn(" --> file originated from project sourcecode") + } } } } else { From 4449f35309389f33dd4c90d9203c99e9a019a9c3 Mon Sep 17 00:00:00 2001 From: Another User Date: Fri, 10 Jun 2022 12:37:09 +0200 Subject: [PATCH 07/15] Cleaned up "Collision logging" and added StandardFilesMergeTransformer to merge standard files --- .../shadow/tasks/ShadowCopyAction.groovy | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 5559387a1..cfd310d16 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -199,7 +199,7 @@ class ShadowCopyAction implements CopyAction { private final Set unused private final ShadowStats stats - private Map visitedFiles = new HashMap() + private Map visitedFiles = new HashMap<>() StreamAction(ZipOutputStream zipOutStr, String encoding, List transformers, List relocators, PatternSet patternSet, Set unused, @@ -216,13 +216,21 @@ class ShadowCopyAction implements CopyAction { } } - private boolean recordVisit(path, size, originJar) { - if (visitedFiles.containsKey(path.toString())) { + /** + * Record visit and return true if visited for the first time. + * + * @param path Visited path. + * @param size Size. + * @param originJar JAR it originated from. + * @return True if wasn't visited already. + */ + private boolean recordVisit(String path, long size, @Nullable RelativePath originJar) { + if (visitedFiles.containsKey(path)) { return false } if (originJar == null) { - originJar = "" + originJar = new RelativePath(false) } visitedFiles.put(path.toString(), [size: size, originJar: originJar]) @@ -296,24 +304,28 @@ class ShadowCopyAction implements CopyAction { } private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive, FileCopyDetails fileDetails) { - def archiveFilePath = archiveFile.relativePath - def archiveFileSize = archiveFile.size + RelativeArchivePath archiveFilePath = archiveFile.relativePath + long archiveFileSize = archiveFile.size if (archiveFile.classFile || !isTransformable(archiveFile)) { - if (recordVisit(archiveFilePath.toString(), archiveFileSize, fileDetails.relativePath) && !isUnused(archiveFilePath.entry.name)) { + def path = archiveFilePath.toString() + if (recordVisit(path, archiveFileSize, archiveFilePath) && !isUnused(archiveFilePath.entry.name)) { if (!remapper.hasRelocators() || !archiveFile.classFile) { copyArchiveEntry(archiveFilePath, archive) } else { remapClass(archiveFilePath, archive) } } else { - def archiveFileInVisitedFiles = visitedFiles.get(archiveFilePath.toString()) + def archiveFileInVisitedFiles = visitedFiles.get(path) if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") if (archiveFileInVisitedFiles.originJar) { - log.warn(" --> origin JAR was ${archiveFileInVisitedFiles.originJar}") + log.warn("\t--> origin JAR was ${archiveFileInVisitedFiles.originJar}") } else { - log.warn(" --> file originated from project sourcecode") + log.warn("\t--> file originated from project sourcecode") + } + if (new StandardFilesMergeTransformer().canTransformResource(archiveFile)) { + log.warn("\t--> Recommended transformer is " + StandardFilesMergeTransformer.class.name) } } } From 3f06ef3d81821530b5d8ed2ade8869726f9189e8 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Diederich Date: Fri, 3 Mar 2023 12:30:55 +0100 Subject: [PATCH 08/15] Fixed merge problems --- .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 00ace7e24..806a0f8dd 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -7,11 +7,14 @@ import java.util.List; import java.util.concurrent.Callable; +import javax.annotation.Nonnull; + import com.github.jengelman.gradle.plugins.shadow.ShadowStats; import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter; import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter; import com.github.jengelman.gradle.plugins.shadow.internal.GradleVersionUtil; import com.github.jengelman.gradle.plugins.shadow.internal.MinimizeDependencyFilter; +import com.github.jengelman.gradle.plugins.shadow.internal.RelocationUtil; import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker; import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor; import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator; @@ -24,16 +27,15 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer; import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; import org.gradle.api.Action; -import org.gradle.api.Task; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.DocumentationRegistry; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.internal.file.copy.CopyAction; -import org.gradle.api.specs.Spec; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; @@ -46,7 +48,6 @@ import org.gradle.api.tasks.*; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; -import org.jetbrains.annotations.NotNull; @CacheableTask public class ShadowJar extends Jar implements ShadowSpec { @@ -147,7 +148,7 @@ public InheritManifest getManifest() { } @Override - @NotNull + @Nonnull protected CopyAction createCopyAction() { DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class); final UnusedTracker unusedTracker = minimizeJar ? UnusedTracker.forProject(getApiJars(), From 5a5c75945f1081fd9d4db7c4371a3dd899b9643c Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Tue, 31 Oct 2023 20:06:52 +0100 Subject: [PATCH 09/15] Lower the log-level for duplicated "META-INF/MANIFEST.MF" from warn to debug So the user doesn't get flooded with superfluous logs --- .../shadow/tasks/ShadowCopyAction.groovy | 29 +++++++++++++++--- .../plugins/shadow/ShadowPluginSpec.groovy | 6 ++-- .../resources/test-artifact-1.0-SNAPSHOT.jar | Bin 0 -> 3324 bytes .../resources/test-project-1.0-SNAPSHOT.jar | Bin 0 -> 4123 bytes 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 src/test/resources/test-artifact-1.0-SNAPSHOT.jar create mode 100644 src/test/resources/test-project-1.0-SNAPSHOT.jar diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index cfd310d16..1a2bdbb94 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -8,6 +8,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import groovy.util.logging.Log import groovy.util.logging.Slf4j import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils @@ -16,6 +17,7 @@ import org.apache.tools.zip.Zip64RequiredException import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipFile import org.apache.tools.zip.ZipOutputStream +import org.codehaus.groovy.transform.LogASTTransformation import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.UncheckedIOException @@ -38,14 +40,18 @@ import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory import javax.annotation.Nullable import java.util.zip.ZipException -@Slf4j + class ShadowCopyAction implements CopyAction { static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).getTimeInMillis() + final static Logger log = LoggerFactory.getLogger(ShadowCopyAction.class); + private final File zipFile private final ZipCompressor compressor private final DocumentationRegistry documentationRegistry @@ -318,14 +324,27 @@ class ShadowCopyAction implements CopyAction { } else { def archiveFileInVisitedFiles = visitedFiles.get(path) if (archiveFileInVisitedFiles && (archiveFileInVisitedFiles.size != fileDetails.size)) { - log.warn("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}, size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") + // Give of only a debug-level warning for this file: + final String lowLevelWarningFile = "META-INF/MANIFEST.MF" + + final logDebug = (String msg) -> { log.debug(msg) } + final logWarn = (String msg) -> { log.warn(msg) } + + final Closure logger + if (archiveFilePath.toString() == lowLevelWarningFile) { + logger = logDebug + } else { + logger = logWarn + } + logger("IGNORING ${archiveFilePath} from ${fileDetails.relativePath}," + + " size is different (${fileDetails.size} vs ${archiveFileInVisitedFiles.size})") if (archiveFileInVisitedFiles.originJar) { - log.warn("\t--> origin JAR was ${archiveFileInVisitedFiles.originJar}") + logger("\t--> origin JAR was ${archiveFileInVisitedFiles.originJar}") } else { - log.warn("\t--> file originated from project sourcecode") + logger("\t--> file originated from project sourcecode") } if (new StandardFilesMergeTransformer().canTransformResource(archiveFile)) { - log.warn("\t--> Recommended transformer is " + StandardFilesMergeTransformer.class.name) + logger("\t--> Recommended transformer is " + StandardFilesMergeTransformer.class.name) } } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index 9f74f22d8..fa120bb04 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -151,10 +151,8 @@ class ShadowPluginSpec extends PluginSpecification { ExecutionResult result = runner.run() then: - success(result) - assert result.standardOutput =~ /IGNORING META-INF\/MANIFEST\.MF from test-artifact-1\.0-SNAPSHOT\.jar, size is different \(3115 vs 25\)\s --> file originated from project sourcecode/ - assert result.standardOutput =~ /IGNORING META-INF\/MANIFEST\.MF from test-project-1\.0-SNAPSHOT\.jar, size is different \(3906 vs 25\)\s --> file originated from project sourcecode/ - } + assert result.output =~ /\s*IGNORING Weird-File\.StrangeFormat from test-project-1\.0-SNAPSHOT\.jar, size is different \([0-9]{4} vs [0-9]{2}\)\s+--> origin JAR was Weird-File.StrangeFormat/ + } def 'include project sources'() { given: diff --git a/src/test/resources/test-artifact-1.0-SNAPSHOT.jar b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..639ac32a8e40d6b3be6ad80efb0ded1cb45a8eb6 GIT binary patch literal 3324 zcmWIWW@h1H0D-cG4;Eksl;8x?zOEsTx}JV+`TRsgZ*3~+9=KSU$gDb`lo)+nNojal9t?R_W{$xqm6fx}s zDiu5DbO#B02L+eR)mkee!!0z~I7qZc_;k=`pp2JsvDbk|e(U_!3DkA13->ZRQ6Ox) zxWd^uPA^W60p=`B*Ruefm0y$&cQ~dBZm5dHg2d#ER6Gg=Q5EJUmZj$5Q7(y1c|lHT zdS+fR9&PgYv=wJ0rXZZi0gkJyXQcXhnHd;zSs55K2y6DqOwvovNh~gI4T<&_b`<%y zY|6V$I!dB}0Uw>NHtpiRC%WefSC&p|fYTx2nR?Ezdv8U{{Dql>59w)$Ck8d@|yGBIh5Ocr<{l9!MeU`w!X*v z+SqNk*k3W!eJeTh;|T};cOKu9dHqdnQayg}QIy!U)c4=C+j8Ht%T)Pd_nVzvATEAt z>Qkr6nG4rT{0Kbrx1zOLcj4j#20=QaT2r?!z3G%$m%A~bCbq(G_qVLN!oFPg z-0zh)E_r%9oTI=nLjAyB)$8I7%`n@>uLc)$o|Lvb# zSaUBeXwRA(=T&kHW9*dv^V_dntJ&r~SNy5MJnpD}zurt!p0!c>hgQ*~mN|uc%sSmv zGg|v*6suZW_2!;blA1SDL+O0M!xgi57S}tUYUHZYGn=%)fX9jVyziN$Ge;!PoP4Tt z*mcqUB99$b`L1D?g`>A@O}f5$o7B9kIhFG|W%P_?-pkxSlF4&<%_fIM^Ot_veCt=F zmwh*zVWDWHuzt#O9X}OI_l$r3hg)AQEAV)6uUBEiDw{2<|1jHb;d7GM(yQQ8V*T-Z z-cC8T_mj-iFKEva4|MrtG{?p#uq(^@9{-OqGGz96+$fPcrto%(*K?7!6LzHs zt}XEPIw9Bir}T2C#j>w5k!Ejws?^IS-VEeiyd`Z)@Zrj+vnmYYD@%q4OX|ZdGY?JswS3V+Exw8R&ZWl<7w(CS3EnC!RpRuey(M#%Vtw-O7ml3HA%n^;tmnU7Sdy)83-@%!AozWDQ3oA|A)N8Mx67m3}q51IAmK-K%R z%1b-7o!R0R>`(LAnh~&dMT?91``3$V)0bvc&zkb(HLrg6(-T)ei{2=I(wH-S&&6`5 z*;k}8tkP9>C26&=$i;p5bh-{+D-m9^H%K_t~X8Sc_^*1UQS}KR?i>J(sbM(usN{ibV zSLl}HrKKO96zG{2l~lwTkm;FK*s0fZ{xmDxs|*a+rpAZQeY2EF5$Ht_mSB)!2v5x{ zO3`)8%t_S?E-6aPOHXynFUn0U2@T<7U~aK>Oa4U=+b)m2fbDife>s z9bf|rO*6s58|Gci!~!b55oTAAY&Lpf4zme+aSpRx8j2NMngHjl&#_7z{NcHJxNvl4CixP)f38 z-?C*JO9>}?(d1YvsZRL6Nkdt>&UOBGu5Z5Yn(w*q_j%s${hs?Ca|9#PPJlLgF8Qf{ zx%m3U2A)myEwvR5P2o!BU&B~|ZZJ$%aAD8|yd4N0eBXwdXqy_s^(`!6Ch)f=ZLP)# zMOYUUp}4=Jt+N1mpjUO|g_nX+N3#M#QHK>Wk#XYMZuuH1Vrk-4BrgJKK}gzvmrt0T zCsq~7e~z~m`4+rmr0Q){Gls%m@wd8uh%1%PKJS`a)W=%^Eb}ZcE9@%l0D5M?m(0Lq z)&>`NwN`+q8Q>?@+msX^@RZMp+lQEGLZc@I~*?%rSsVL|GVlxkD-Pu-H%&+%$H~C(A*~OE?rg6~Er!t`oSrcYqPOazm zR!HPgY7e^>o1R*Ec{HI%gu6QmsVi`!#)RXN_BB>HC5?1Rmb@GnF_Bb~hh%ej>XflP zv)(PzL7nhP?$3il7N3R4R#wctwHz6b>xysB@wnQ-1&-=0R773~d8$|IAZ^k7()pN6 zu$EJqHZk$Uqc^S#o|2duBZ(5p!wZBvUJl_Ask`~(2K=-lvgb^lxlwvD6N{+A-%MMw z8a^#i#}m#a*xR?qy>8IgJ7GQHR;1~WD{x2p-pkX)pQo4dPbR1EXayc%`;+gn)3Km7 zFNk5D?Z9-gNUm^MP<*CBV~pO3ZVrQedD?7)Q0DAJSNRmU8yhu6b=(GiwuH*2Ih+zO z^oV+iq!5-@YaG4Uk(Omj?l8~mN|U-Q7urRQ>G_q5!HS%zrXGAj2S=7c3wVZ_U@o8v ze2Ruk343EHm#9q(B^;{KQQa{I|1Qe$c_3A4@xtgVSJ>O1sqTCs@jlPdd2_Q(Oiewa zvsKF6H(*og{`XaT&>zbECEQW7`STjtoR*IG+l(@U_(;qdTZn$aRUu(Bb;r4(X^!-R zCSKMt7`}cUYD=u&q_fLR?qVJh7`v}0Ad9Wxb)X2}dPhN-)eSjA!alUm3m= zajKyHM*9l&o%-+@=qw+n(vV!Mk&mN(c|X57#%s0=`cPRz@9tZHP;@zVvU``yyN9=8 zJ*%fR=icmUu}MC2+ef?I7iY;9I~V7l*To$_tSe>xrdiJ`o<(x0V&@)8qu5Q3d@MWO zMC%YygA@=4NfMU9SdVF0<-U+_p9{>|MNv!%nVT68tVQG!jmHj$@2agdZZl2Rd6{m} zil$yw6DrRATosoYiZ;$&XhcD0?s50oMFr?+LUi zCh6jnhcoa*wI|VokbO?4_WjYB3PcC}COAbNPq*zHGpJ*22oac{SxV}UJXFsy-g6)F z*J%BN@*+Qn+Q{dx7RiL)C#U{g_Mh}nu1mMoyrVS!EO8btqYb5Aep0hs5P8QI_HyA< zek5_wlaai5JmuZZ8Qo;3*esIW9W6xQ^LpQXhaiY<%+29r!sf&5pHr$22`l6e4BcMt zqKeTw=W1g)<*SsFzyJUVI{-l92kl7P(-VeqLwkFZkb@orT3kn3+Z)dW>li3qWU)yA zkgquntmJMZ7Z{47U9@p2yPwsh74^Nz#2u9<8Qj*KVElC4s)>b7qU#gdd9tnTX2JN< z(&UUNszPV@!j%w7(b4=XGP{%0&|?%G-cAl-K^JAdApW|ctL!y>IHEe5_;Fe8dZ}Fi z2jn?EM0%w|q@l7@HEhSLcC(&T&3IlD_hSjQLR5|T&_xwN7oO4A8O?C+xZvXONn)+z zc`l<@PftQ{Pt_^;^E^$2EW}-C;iZw z$Xk2=f4cWSMA-a#*pa2)Oj3Vuwx6HU9|f6h)?<)TjToH$yGs#;;=1I?dt&xWAye`L z2c|B|IIt-6bM+qy6@C+TuIfCyZ_+LI2bcI1#u!j%t%7(S%blpL_j!vlV!3u+(xQDx z+`4^52VIdW*JaBX9p7t{)(Ev1wCZhEoz8l)P)&cZn>C&DTg7lk`(i!Tc#mQh8N`$ss!Q(Ls0jx;1k?DTEgbtCHQP{>`YT))s?*94ns^;=Y(+FG^tu9y)!7YBOTvoEv}{ zc-Yr72atK&bxY&I13Hk49Dj5u7+|y%5p2fVD&$el5pej1c|@W?gg2E9h6 z7i~ne0ts4I)MeH1snNJ`*PKfogP~BXy!X)nM>Aen?KzbM)lpePz9qqXZcHTy$Az!# zJ+ymdS@LoL^g8aDD!Kln%BUOivQOEHaG-P&iIKk7SiktkS0b{>0pRmF4<0)I$8!RA zfhX3B;EMHzK1S>eIlu)_NT$jo16)P2tgxL+uulw?S~z$(CCnI}T9Q{@LTuq@e85kb z`&e7=vB%O2T^%c-BrgA27bU=-XeKhzXo=H}NY;sUjV04Fbjeg+T#~29Pj0hCRkar0gN|}Xk%%I50M>UV44Cq?Uyf8JV6{S?ngt4WAGjX z(4~pHdVoK9uQG@`f1!ZwG)FM(;A7n0mV-HfgA4den|5Bafa|cWZ8|NOHZp?0d$qqY z?duGi4KmG6uMXP8wczF2#B1hyJ#E_G1bRH}OtaUkgNB|3FIUa>_6VBYUL7<9H~99` z5cKW?Hi+$^H2b{{)!YdEYR0$6)9m;<-fc@fz46gmwzlVVo{Zo$kY(e7eQ(y+E81F- z*SY>?-mn;7Dq78GO`q54Hg{#34y+E^+V(=eNsX=w^tv{KX`5#q%)q({yuC_nPfF8? zbyE00C*7bL^cl9+4f-4z!SVlchOa8JzPMXAzV(T;z}I-~xvwtncI)$9DYS{!+aPT| w(XSn0b30nyuiyp)3WJ5%fgdy{=GEr3p)r{wm|18UbAlgF@J+oAasa^p0I)c~fdBvi literal 0 HcmV?d00001 From 125bad073dda6aa7070274dc8e3ced0a107f8e89 Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Wed, 1 Nov 2023 18:00:01 +0100 Subject: [PATCH 10/15] Adding JsonTransformer and tests for it --- .../transformers/JsonTransformer.groovy | 158 ++++++++++++++++++ .../JsonAppendingTransformerTest.groovy | 158 ++++++++++++++++++ .../resources/test-artifact-1.0-SNAPSHOT.jar | Bin 3324 -> 3725 bytes .../resources/test-project-1.0-SNAPSHOT.jar | Bin 4123 -> 4541 bytes 4 files changed, 316 insertions(+) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonTransformer.groovy create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonAppendingTransformerTest.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonTransformer.groovy new file mode 100644 index 000000000..810a34988 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonTransformer.groovy @@ -0,0 +1,158 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import org.gradle.api.file.FileTreeElement +import org.gradle.api.logging.Logging +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional + +import com.google.gson.JsonArray +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.google.gson.JsonParser +import com.google.gson.Gson + +import org.apache.tools.zip.ZipOutputStream +import org.apache.tools.zip.ZipEntry + +/** + * Merge multiple occurrence of JSON files. + * + * @author Logic Fan, extended to process an array of files by Jan-Hendrik Diederich + */ +@CacheableTransformer +class JsonTransformer implements Transformer { + private static final GSON = new Gson() + private static final LOGGER = Logging.getLogger(JsonTransformer.class) + + @Optional + @Input + List paths + + private Map matchedPath = [:] + + @Override + boolean canTransformResource(FileTreeElement element) { + String path = element.relativePath.pathString + for (p in paths) { + if (path.equalsIgnoreCase(p)) { + matchedPath[path] = null + return true + } + } + return false + } + + @Override + void transform(TransformerContext context) { + String path = context.getPath() + final JsonElement j + try { + j = JsonParser.parseReader(new InputStreamReader(context.is, "UTF-8")) + } catch (Exception e) { + throw new RuntimeException("error on processing json", e) + } + + matchedPath[path] = (matchedPath[path] == null) ? j : mergeJson(matchedPath[path], j) + } + + @Override + boolean hasTransformedResource() { + return !matchedPath.isEmpty() + } + + @Override + void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { + if (paths == null) { + throw new IllegalArgumentException("\"paths\" is null and not set") + } + for (Map.Entry entrySet in matchedPath) { + ZipEntry entry = new ZipEntry(entrySet.key) + entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) + os.putNextEntry(entry) + os.write(GSON.toJson(entrySet.value).getBytes()) + } + matchedPath = [:] + } + + /** + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
{@code lhs} {@code rhs} {@code return}
Any {@code JsonNull} {@code lhs}
{@code JsonNull} Any {@code rhs}
{@code JsonArray} {@code JsonArray} concatenation
{@code JsonObject} {@code JsonObject} merge for each key
{@code JsonPrimitive} {@code JsonPrimitive}return lhs if {@code lhs.equals(rhs)}, error otherwise
Other error
+ * @param lhs a {@code JsonElement} + * @param rhs a {@code JsonElement} + * @param id used for logging purpose only + * @return the merged {@code JsonElement} + */ + private static JsonElement mergeJson(JsonElement lhs, JsonElement rhs, String id = "") { + if (rhs == null || rhs instanceof JsonNull) { + return lhs + } else if (lhs == null || lhs instanceof JsonNull) { + return rhs + } else if (lhs instanceof JsonArray && rhs instanceof JsonArray) { + return mergeJsonArray(lhs as JsonArray, rhs as JsonArray) + } else if (lhs instanceof JsonObject && rhs instanceof JsonObject) { + return mergeJsonObject(lhs as JsonObject, rhs as JsonObject, id) + } else if (lhs instanceof JsonPrimitive && rhs instanceof JsonPrimitive) { + return mergeJsonPrimitive(lhs as JsonPrimitive, rhs as JsonPrimitive, id) + } else { + LOGGER.warn("conflicts for property {} detected, {} & {}", + id, lhs.toString(), rhs.toString()) + return lhs + } + } + + private static JsonPrimitive mergeJsonPrimitive(JsonPrimitive lhs, JsonPrimitive rhs, String id) { + // In Groovy, {@code a == b} is equivalent to {@code a.equals(b)} + if (lhs != rhs) { + LOGGER.warn("conflicts for property {} detected, {} & {}", + id, lhs.toString(), rhs.toString()) + } + return lhs + } + + private static JsonObject mergeJsonObject(JsonObject lhs, JsonObject rhs, String id) { + JsonObject object = new JsonObject() + + Set properties = new HashSet<>() + properties.addAll(lhs.keySet()) + properties.addAll(rhs.keySet()) + for (String property : properties) { + object.add(property, + mergeJson(lhs.get(property), rhs.get(property), id + ":" + property)) + } + + return object + } + + private static JsonArray mergeJsonArray(JsonArray lhs, JsonArray rhs) { + JsonArray array = new JsonArray() + + array.addAll(lhs) + array.addAll(rhs) + + return array + } +} \ No newline at end of file diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonAppendingTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonAppendingTransformerTest.groovy new file mode 100644 index 000000000..f6d373af2 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/JsonAppendingTransformerTest.groovy @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +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 com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import org.apache.tools.zip.ZipOutputStream +import org.junit.Before +import org.junit.Test + +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +/** + * Test for {@link JsonTransformer}. + * + * @author Jan-Hendrik Diederich + * + * Modified from com.github.jengelman.gradle.plugins.shadow.transformers.XmlAppendingTransformerTest.java + */ +class JsonAppendingTransformerTest extends TransformerTestSupport { + + JsonTransformer transformer + + static final String TEST_ARTIFACT_JAR = 'test-artifact-1.0-SNAPSHOT.jar' + static final String TEST_PROJECT_JAR = 'test-project-1.0-SNAPSHOT.jar' + + static final String TEST_JSON = 'test.json' + static final String TEST2_JSON = 'test2.json' + + @Before + void setUp() { + transformer = new JsonTransformer() + } + + @Test + void testCanTransformResource() { + transformer.paths = ["test.json"] + + assertTrue(this.transformer.canTransformResource(getFileElement("test.json"))) + assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF"))) + } + + @Test + void transformResource() { + transformer.transform(new TransformerContext(TEST_JSON, readFromTestJar(TEST_ARTIFACT_JAR, TEST_JSON), + Collections. emptyList(), new ShadowStats())) + transformer.transform(new TransformerContext(TEST2_JSON, readFromTestJar(TEST_ARTIFACT_JAR, TEST2_JSON), + Collections. emptyList(), new ShadowStats())) + + transformer.transform(new TransformerContext(TEST_JSON, readFromTestJar(TEST_PROJECT_JAR, TEST_JSON), + Collections. emptyList(), new ShadowStats())) + transformer.transform(new TransformerContext(TEST2_JSON, readFromTestJar(TEST_PROJECT_JAR, TEST2_JSON), + Collections. emptyList(), new ShadowStats())) + + def zipFileName = "testable-zip-file-" + def zipFileSuffix = ".jar" + def testableZipFile = File.createTempFile(zipFileName, zipFileSuffix) + def fileOutputStream = new FileOutputStream(testableZipFile) + def bufferedOutputStream = new BufferedOutputStream(fileOutputStream) + def zipOutputStream = new ZipOutputStream(bufferedOutputStream) + + transformer.paths = [TEST_JSON, TEST2_JSON] + try { + transformer.modifyOutputStream(zipOutputStream, false) + } finally { + zipOutputStream.close() + bufferedOutputStream.close() + fileOutputStream.close() + } + // Read 1st file. + String targetJson = readFromZipFile(testableZipFile.absolutePath, TEST_JSON) + println("Target JSON: \"" + targetJson + "\"") + + assertFalse(targetJson.isEmpty()) + assertTrue(targetJson.contains("\"C: Only here\"")) + + JsonElement jsonElement = JsonParser.parseString(targetJson) + JsonObject jsonObject = jsonElement.getAsJsonObject() + + JsonElement subAA = jsonObject.get("a.a") + + JsonElement subAA1 = subAA.getAsJsonObject().get("a.sub1") + assertEquals("A Sub 1", subAA1.asString) + + JsonElement subAA2 = subAA.getAsJsonObject().get("a.sub2") + assertEquals("A Sub 2", subAA2.asString) + + // Read 2nd file. + String target2Json = readFromZipFile(testableZipFile.absolutePath, TEST2_JSON) + assertFalse(target2Json.isEmpty()) + JsonElement jsonElement2 = JsonParser.parseString(target2Json) + JsonObject jsonObject2 = jsonElement2.getAsJsonObject() + JsonArray jsonArray2 = jsonObject2.get("Array").asJsonArray + assertEquals(List.of("A", "B", "C", "C", "D", "E"), + (List) jsonArray2.collect({ it -> it.getAsString() })) + } + + static InputStream readFromTestJar(String resourceName, String fileName) { + try (ZipInputStream inputStream = new ZipInputStream(getResourceStream(resourceName))) { + while (true) { + ZipEntry entry = inputStream.nextEntry + if (entry == null) { + break + } else if (entry.name == fileName) { + // Read the content of the entry + byte[] buffer = new byte[entry.size] + inputStream.read(buffer) + return new ByteArrayInputStream(buffer) + } + } + } + throw new IllegalArgumentException("Missing entry " + fileName) + } + + static String readFromZipFile(String resourceName, String fileName) { + def zip = new ZipFile(resourceName) + try { + ZipEntry entry = zip.getEntry(fileName) + if (!entry) { + throw new IllegalArgumentException("Missing entry " + fileName + " in " + resourceName) + } + return new String(zip.getInputStream(entry).readAllBytes()) + } finally { + zip.close() + } + } + + private static InputStream getResourceStream(String resource) { + JsonAppendingTransformerTest.class.classLoader.getResourceAsStream(resource) + } +} diff --git a/src/test/resources/test-artifact-1.0-SNAPSHOT.jar b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar index 639ac32a8e40d6b3be6ad80efb0ded1cb45a8eb6..dbe60ba230a033e51668b8fa924b9f141bf62d1f 100644 GIT binary patch delta 449 zcmew(*(UaN>YnU^sC$r#Oh~30RdhiK^vGRwRu@3%#)iD&zsU*d332~TLNnz*TJQp zTy0(solzT3scbL^n6_GsA;24EDPLoKVmR}C87)hojm|*K1+~=(VeRWor&OjaC+yaq z*3r@n+!Pe>>AZL7CB{yc zZD(W>Vder_#=ub4uz4XyV2}W-WCW618v8jIkOPGk5-4Z^0=Y@uFGB6*W z=a||8#HAJ742&!C$r#Oh~30RdhiK^vGRwRu@3%#)iD&zsU*d333#TLNnz*TJQp zTy98+%)pS)^R#kCMIbneN8{8F2@@Px65u(B}#0Y4BHurM%8 Date: Fri, 3 Nov 2023 12:36:36 +0100 Subject: [PATCH 11/15] Logging module-info.class collisions --- .../shadow/tasks/ShadowCopyAction.groovy | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 1a2bdbb94..b8b475496 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -8,8 +8,6 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext -import groovy.util.logging.Log -import groovy.util.logging.Slf4j import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils import org.apache.tools.zip.UnixStat @@ -17,7 +15,6 @@ import org.apache.tools.zip.Zip64RequiredException import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipFile import org.apache.tools.zip.ZipOutputStream -import org.codehaus.groovy.transform.LogASTTransformation import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.UncheckedIOException @@ -314,7 +311,40 @@ class ShadowCopyAction implements CopyAction { long archiveFileSize = archiveFile.size if (archiveFile.classFile || !isTransformable(archiveFile)) { - def path = archiveFilePath.toString() + String path = archiveFilePath.toString() + + if (path.endsWith("module-info.class")) { + log.warn("module-info collision") + + def moduleFileName = "module-info" + def moduleFileSuffix = ".class" + File disassembleModFile = File.createTempFile(moduleFileName, moduleFileSuffix) + + try (InputStream is = archive.getInputStream(archiveFilePath.entry)) { + try (OutputStream os = new FileOutputStream(disassembleModFile)) { + IOUtils.copyLarge(is, os) + } + } + + ProcessBuilder processBuilder = new ProcessBuilder("javap", disassembleModFile.absolutePath) + processBuilder.redirectErrorStream(true) + Process process = processBuilder.start() + InputStream inputStream = process.getInputStream() + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)) + String line + + while ((line = reader.readLine()) != null) { + log.warn(line) + } + + int exitCode = process.waitFor() + if (exitCode != 0) { + log.warn("Process exited with code " + exitCode) + } + + log.warn("module-info collision end") + } + if (recordVisit(path, archiveFileSize, archiveFilePath) && !isUnused(archiveFilePath.entry.name)) { if (!remapper.hasRelocators() || !archiveFile.classFile) { copyArchiveEntry(archiveFilePath, archive) From 77241f038ced4d91cda8d54fe729f48fa2f597b1 Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Wed, 4 Sep 2024 06:47:37 +0200 Subject: [PATCH 12/15] Fix rebase errors --- .../shadow/tasks/ShadowCopyAction.groovy | 9 ++--- .../plugins/shadow/tasks/ShadowJar.java | 33 ++++++++----------- .../plugins/shadow/ShadowPluginSpec.groovy | 7 ++-- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index b8b475496..b564efbc1 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -19,10 +19,12 @@ import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.UncheckedIOException import org.gradle.api.file.FileCopyDetails +import org.gradle.api.file.FilePermissions import org.gradle.api.file.FileTreeElement import org.gradle.api.file.RelativePath import org.gradle.api.internal.DocumentationRegistry import org.gradle.api.internal.file.CopyActionProcessingStreamAction +import org.gradle.api.internal.file.DefaultFilePermissions import org.gradle.api.internal.file.DefaultFileTreeElement import org.gradle.api.internal.file.copy.CopyAction import org.gradle.api.internal.file.copy.CopyActionProcessingStream @@ -43,7 +45,6 @@ import org.slf4j.LoggerFactory import javax.annotation.Nullable import java.util.zip.ZipException - class ShadowCopyAction implements CopyAction { static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).getTimeInMillis() @@ -245,7 +246,7 @@ class ShadowCopyAction implements CopyAction { } private boolean recordVisit(FileCopyDetails fileCopyDetails) { - return recordVisit(fileCopyDetails.relativePath, fileCopyDetails.size, null) + return recordVisit(fileCopyDetails.relativePath.toString(), fileCopyDetails.size, null) } @Override @@ -282,11 +283,11 @@ class ShadowCopyAction implements CopyAction { ZipFile archive = new ZipFile(fileDetails.file) try { List archiveElements = archive.entries.collect { - new ArchiveFileTreeElement(new RelativeArchivePath(it, fileDetails)) + new ArchiveFileTreeElement(new RelativeArchivePath(it)) } Spec patternSpec = patternSet.getAsSpec() List filteredArchiveElements = archiveElements.findAll { ArchiveFileTreeElement archiveElement -> - patternSpec.isSatisfiedBy(archiveElement) + patternSpec.isSatisfiedBy(archiveElement.asFileTreeElement()) } filteredArchiveElements.each { ArchiveFileTreeElement archiveElement -> if (archiveElement.relativePath.file) { diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 806a0f8dd..6065845f0 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -27,12 +27,14 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer; import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; import org.gradle.api.Action; +import org.gradle.api.Task; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.DocumentationRegistry; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.specs.Spec; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; @@ -78,10 +80,9 @@ public ShadowJar() { super(); setDuplicatesStrategy( DuplicatesStrategy.INCLUDE); //shadow filters out files later. This was the default behavior in Gradle < 6.x - versionUtil = new GradleVersionUtil(getProject().getGradle().getGradleVersion()); dependencyFilter = new DefaultDependencyFilter(getProject()); dependencyFilterForMinimize = new MinimizeDependencyFilter(getProject()); - setManifest(new DefaultInheritManifest(getServices().get(FileResolver.class))); + setManifest(new DefaultInheritManifest(getProject(), getServices().get(FileResolver.class))); /* Add as default the StandardFilesMergeTransformer, remove it with "removeDefaultTransformers()". This is added by default, because otherwise: @@ -96,28 +97,20 @@ public ShadowJar() { relocators = new ArrayList<>(); configurations = new ArrayList<>(); - this.getInputs().property("minimize", new Callable() { - @Override - public Boolean call() throws Exception { - return minimizeJar; - } - }); + this.getInputs().property("minimize", (Callable) () -> minimizeJar); this.getOutputs().doNotCacheIf("Has one or more transforms or relocators that are not cacheable", - new Spec() { - @Override - public boolean isSatisfiedBy(Task task) { - for (Transformer transformer : transformers) { - if (!isCacheableTransform(transformer.getClass())) { - return true; - } + task -> { + for (Transformer transformer : transformers) { + if (!isCacheableTransform(transformer.getClass())) { + return true; } - for (Relocator relocator : relocators) { - if (!isCacheableRelocator(relocator.getClass())) { - return true; - } + } + for (Relocator relocator : relocators) { + if (!isCacheableRelocator(relocator.getClass())) { + return true; } - return false; } + return false; }); } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index fa120bb04..4c26a3d7d 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -139,16 +139,15 @@ class ShadowPluginSpec extends PluginSpecification { buildFile << """ |task shadow(type: ${ShadowJar.name}) { - | destinationDir = buildDir - | baseName = 'shadow' + | destinationDirectory = buildDir + | archiveBaseName = 'shadow' | from('${artifact.path}') | from('${project.path}') |} """.stripMargin() when: - runner.arguments << 'shadow' - ExecutionResult result = runner.run() + BuildResult result = run('shadow') then: assert result.output =~ /\s*IGNORING Weird-File\.StrangeFormat from test-project-1\.0-SNAPSHOT\.jar, size is different \([0-9]{4} vs [0-9]{2}\)\s+--> origin JAR was Weird-File.StrangeFormat/ From cd413192e693b6757b52a8dc4a0d7bf53a7f6559 Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Sat, 7 Sep 2024 20:07:02 +0200 Subject: [PATCH 13/15] Added tests for StandardFilesMergeTransformer --- .../shadow/tasks/ShadowCopyAction.groovy | 6 ++-- .../plugins/shadow/tasks/ShadowJar.java | 22 +++++------- .../plugins/shadow/ShadowPluginSpec.groovy | 34 ++++++++++++++++-- .../resources/test-artifact-1.0-SNAPSHOT.jar | Bin 3725 -> 3906 bytes .../resources/test-project-1.0-SNAPSHOT.jar | Bin 4541 -> 4731 bytes 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index b564efbc1..8a42e7ddf 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -8,6 +8,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import groovy.util.logging.Slf4j import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils import org.apache.tools.zip.UnixStat @@ -39,17 +40,14 @@ import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper -import org.slf4j.Logger -import org.slf4j.LoggerFactory import javax.annotation.Nullable import java.util.zip.ZipException +@Slf4j class ShadowCopyAction implements CopyAction { static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).getTimeInMillis() - final static Logger log = LoggerFactory.getLogger(ShadowCopyAction.class); - private final File zipFile private final ZipCompressor compressor private final DocumentationRegistry documentationRegistry diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 6065845f0..70fd30b20 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -1,14 +1,5 @@ package com.github.jengelman.gradle.plugins.shadow.tasks; -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; - -import javax.annotation.Nonnull; - import com.github.jengelman.gradle.plugins.shadow.ShadowStats; import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter; import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter; @@ -27,14 +18,12 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.StandardFilesMergeTransformer; import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; import org.gradle.api.Action; -import org.gradle.api.Task; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.DocumentationRegistry; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.internal.file.copy.CopyAction; -import org.gradle.api.specs.Spec; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; @@ -47,9 +36,16 @@ import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.*; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; @CacheableTask public class ShadowJar extends Jar implements ShadowSpec { @@ -141,7 +137,7 @@ public InheritManifest getManifest() { } @Override - @Nonnull + @NotNull protected CopyAction createCopyAction() { DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class); final UnusedTracker unusedTracker = minimizeJar ? UnusedTracker.forProject(getApiJars(), diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index 4c26a3d7d..d6ea4d2e0 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -150,7 +150,38 @@ class ShadowPluginSpec extends PluginSpecification { BuildResult result = run('shadow') then: - assert result.output =~ /\s*IGNORING Weird-File\.StrangeFormat from test-project-1\.0-SNAPSHOT\.jar, size is different \([0-9]{4} vs [0-9]{2}\)\s+--> origin JAR was Weird-File.StrangeFormat/ + assert result.output =~ /\s*IGNORING Weird-File\.StrangeFormat from test-project-1\.0-SNAPSHOT\.jar,/ + / size is different \([0-9]{4} vs [0-9]{2}\)\s+--> origin JAR was Weird-File.StrangeFormat/ + + /* Shouldn't appear, because the default StandardFileTransformer should've merged it, + instead of just dropping all following licenses. */ + assert !result.output.contains('license.txt') + } + + def 'Tests the removal of the default transformer'() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + |task shadow(type: ${ShadowJar.name}) { + | destinationDirectory = buildDir + | archiveBaseName = 'shadow' + | removeDefaultTransformers() + | from('${artifact.path}') + | from('${project.path}') + |} + """.stripMargin() + + when: + BuildResult result = run('shadow') + + then: + assert result.output =~ /\s*IGNORING Weird-File\.StrangeFormat from test-project-1\.0-SNAPSHOT\.jar,/ + / size is different \([0-9]{4} vs [0-9]{2}\)\s+--> origin JAR was Weird-File.StrangeFormat/ + /\s+IGNORING test\.json from test-project-1\.0-SNAPSHOT.jar, size is different/ + // Without the StandardFileTransformer there should be a warning about multiple license files with the same name. + assert result.output.contains('license.txt') } def 'include project sources'() { @@ -214,7 +245,6 @@ class ShadowPluginSpec extends PluginSpecification { repositories { maven { url "${repo.uri}" } } dependencies { implementation project(':client') } - """.stripIndent() File serverOutput = getFile('server/build/libs/server-all.jar') diff --git a/src/test/resources/test-artifact-1.0-SNAPSHOT.jar b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar index dbe60ba230a033e51668b8fa924b9f141bf62d1f..8bc765711892594930293a8054285afbe67d7009 100644 GIT binary patch delta 403 zcmeB`JtQ~bkhcf}0|N&`<6QN~{~CM~WPv;lAO?y0x`sIFdiuHP=VT_Q<`t*vl~j~` zKI!9kR@*Ps;0f2s6FOlUCp|&~nz}VL9cEU=Fien|EXjI!vNik5&9_+N87Hf7nIG7#Q-4()A}da>`8h;NoN2&oViJ%be*2 z%jAh%woKxzKuUP>JVuVmuer3C{9s&x$yVGNOx-XpAE!@dl3sF7VsY{08@yVK_a-y* Z`7s7h4&u}06J!u%h+$=5xWEbW1pq)yX|wk*DE{W&_ouo&D&YDS%CCpb;iS!&3HsMzhv@cocx0E_2i{IB9q-%qBlR{$Yz{; zg;PR|g#iWx7~VSW19I|<()A}_;FOu{&dbO2fMIetuQ?MZB)6`Y?ICTw3ybwxWXV4C!glxWBSiB`8Jm|lMd_Td)x+-J-D@)DnLxT$yz*GOqW1R zC+3RWoXMMjGKTCxZouUEJZelb9Fvdpm`#4b$1!;(pB!WV^1_lm>>2uX1yUhhe)quPRAO?y0x`sIFdiuHP=VT_Q<`t*vl~j~$ zKFYL1#NmLtv8CyHi_n7|+^eP5P3Sq3)HH=TJ+l*iVBw}(tzs%&He0)rjjFUO}t(n{y zC(CkZPcG!wVCrU^{FmQsvXy`q(_IkLYVraBzR6el)tI(30lDszrFj*Y;#nr!^2$v1 z;ZtDhWtp7Er!sk+fEv>?j>*>q%qENSN=(k?l({FL>>k z?r~1u#V0knk57w9gbT=2pUfku#`t!!v7jGg@#H2!T|OZOA%;>`28ItjAO`~gP1A3Y delta 285 zcmeyZvR8S+(aEWtVv{RbWF}{F?%X_^C75M$qoCMkBd-076IsM28#8X%e2{4w>*^ak2rI$Yeu7Ev6h0Q)+U#puptWECQ2%@pCaPVw}8^MFA+J#q^sMD62dGBCO2r}@q KGBC^l8v+1V#ZHF+ From 3a1769b09a747293e1946b4cb0f696fc2360ca82 Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Sat, 7 Sep 2024 21:55:50 +0200 Subject: [PATCH 14/15] Adding option allowModuleInfos() to make it optional to include module-info.class files easily --- .../plugins/shadow/ShadowJavaPlugin.groovy | 125 ++++++++++++++++++ .../shadow/internal/RelocationUtil.groovy | 26 ++++ .../shadow/tasks/ShadowCopyAction.groovy | 99 +++++++++----- .../plugins/shadow/tasks/ShadowJar.java | 22 ++- .../plugins/shadow/ShadowPluginSpec.groovy | 30 ++++- .../resources/test-artifact-1.0-SNAPSHOT.jar | Bin 3906 -> 4848 bytes .../resources/test-project-1.0-SNAPSHOT.jar | Bin 4731 -> 5711 bytes 7 files changed, 266 insertions(+), 36 deletions(-) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/RelocationUtil.groovy diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy new file mode 100644 index 000000000..244f7a87d --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy @@ -0,0 +1,125 @@ +package com.github.jengelman.gradle.plugins.shadow + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.api.component.AdhocComponentWithVariants +import org.gradle.api.component.SoftwareComponentFactory +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.jvm.tasks.Jar +import org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin + +import javax.inject.Inject + +class ShadowJavaPlugin implements Plugin { + + public static final String SHADOW_JAR_TASK_NAME = 'shadowJar' + public static final String SHADOW_GROUP = 'Shadow' + public static final String SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME = 'shadowRuntimeElements' + + public static final String MODULE_INFO_CLASS = 'module-info.class' + + private final ProjectConfigurationActionContainer configurationActionContainer + private final SoftwareComponentFactory softwareComponentFactory + + @Inject + ShadowJavaPlugin(SoftwareComponentFactory softwareComponentFactory) { + this.softwareComponentFactory = softwareComponentFactory + } + + @Override + void apply(Project project) { + def shadowConfiguration = project.configurations.getByName(ShadowBasePlugin.CONFIGURATION_NAME) + def shadowTaskProvider = configureShadowTask(project, shadowConfiguration) + + project.configurations.named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) { + it.extendsFrom(shadowConfiguration) + } + + def shadowRuntimeElements = project.configurations.create(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { Configuration it -> + it.extendsFrom(shadowConfiguration) + it.canBeConsumed = true + it.canBeResolved = false + it.attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage, Usage.JAVA_RUNTIME)) + it.attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category, Category.LIBRARY)) + it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements, LibraryElements.JAR)) + it.attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling, Bundling.SHADOWED)) + } + it.outgoing.artifact(shadowTaskProvider) + } + + project.components.named("java", AdhocComponentWithVariants) { + it.addVariantsFromConfiguration(shadowRuntimeElements) { + it.mapToOptional() + } + } + + AdhocComponentWithVariants shadowComponent = softwareComponentFactory.adhoc(ShadowBasePlugin.COMPONENT_NAME) + project.components.add(shadowComponent) + shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { + it.mapToMavenScope("runtime") + } + + project.plugins.withType(JavaGradlePluginPlugin).configureEach { + // Remove the gradleApi so it isn't merged into the jar file. + // This is required because 'java-gradle-plugin' adds gradleApi() to the 'api' configuration. + // See https://github.com/gradle/gradle/blob/972c3e5c6ef990dd2190769c1ce31998a9402a79/subprojects/plugin-development/src/main/java/org/gradle/plugin/devel/plugins/JavaGradlePluginPlugin.java#L161 + project.configurations.named(JavaPlugin.API_CONFIGURATION_NAME) { + it.dependencies.remove(project.dependencies.gradleApi()) + } + // Compile only gradleApi() to make sure the plugin can compile against Gradle API. + project.configurations.named(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME) { + it.dependencies.add(project.dependencies.gradleApi()) + } + } + } + + protected static TaskProvider configureShadowTask(Project project, Configuration shadowConfiguration) { + SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer) + def jarTask = project.tasks.named(JavaPlugin.JAR_TASK_NAME, Jar) + def taskProvider = project.tasks.register(SHADOW_JAR_TASK_NAME, ShadowJar) { shadow -> + shadow.group = SHADOW_GROUP + shadow.description = 'Create a combined JAR of project and runtime dependencies' + shadow.archiveClassifier.set("all") + shadow.manifest.inheritFrom(jarTask.get().manifest) + def attrProvider = jarTask.map { it.manifest.attributes.get('Class-Path') } + def files = project.objects.fileCollection().from(shadowConfiguration) + shadow.doFirst { + if (!files.empty) { + def attrs = [attrProvider.getOrElse('')] + files.collect { it.name } + shadow.manifest.attributes 'Class-Path': attrs.join(' ').trim() + } + } + shadow.from(sourceSets.main.output) + shadow.configurations = [ + project.configurations.findByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) ?: + project.configurations.runtime, + ] + shadow.exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'module-info.class') + shadow.configurations = [project.configurations.findByName('runtimeClasspath') ? + project.configurations.runtimeClasspath : project.configurations.runtime] + /* + Remove excludes like this: + shadowJar { + ... + allowModuleInfos() + } + */ + def excludes = ['META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA'] + if (!shadow.isAllowModuleInfos()) { + excludes.add(MODULE_INFO_CLASS) + } + shadow.exclude(excludes) + } + project.artifacts.add(shadowConfiguration.name, taskProvider) + return taskProvider + } +} diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/RelocationUtil.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/RelocationUtil.groovy new file mode 100644 index 000000000..6054ddfab --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/RelocationUtil.groovy @@ -0,0 +1,26 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import java.util.jar.JarFile + +class RelocationUtil { + + static void configureRelocation(ShadowJar target, String prefix) { + def packages = [] as Set + target.configurations.each { configuration -> + configuration.files.each { jar -> + JarFile jf = new JarFile(jar) + jf.entries().each { entry -> + if (entry.name.endsWith(".class") && entry.name != ShadowJavaPlugin.MODULE_INFO_CLASS) { + packages << entry.name[0..entry.name.lastIndexOf('/') - 1].replaceAll('/', '.') + } + } + jf.close() + } + } + packages.each { + target.relocate(it, "${prefix}.${it}") + } + } +} diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 8a42e7ddf..da200bb61 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -1,5 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.tasks +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin import com.github.jengelman.gradle.plugins.shadow.ShadowStats import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker @@ -41,6 +42,7 @@ import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper +import javax.annotation.Nonnull import javax.annotation.Nullable import java.util.zip.ZipException @@ -59,11 +61,13 @@ class ShadowCopyAction implements CopyAction { private final boolean preserveFileTimestamps private final boolean minimizeJar private final UnusedTracker unusedTracker + private final boolean allowModuleInfos ShadowCopyAction(File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry, String encoding, List transformers, List relocators, PatternSet patternSet, ShadowStats stats, - boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker) { + boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker, + boolean allowModuleInfos) { this.zipFile = zipFile this.compressor = compressor @@ -76,6 +80,7 @@ class ShadowCopyAction implements CopyAction { this.preserveFileTimestamps = preserveFileTimestamps this.minimizeJar = minimizeJar this.unusedTracker = unusedTracker + this.allowModuleInfos = allowModuleInfos } @Override @@ -201,7 +206,17 @@ class ShadowCopyAction implements CopyAction { private final Set unused private final ShadowStats stats - private Map visitedFiles = new HashMap<>() + private class VisitedFileInfo { + long size + RelativePath originJar + + VisitedFileInfo(long size, @Nonnull RelativePath originJar) { + this.size = size + this.originJar = originJar + } + } + + private Map visitedFiles = new HashMap<>() StreamAction(ZipOutputStream zipOutStr, String encoding, List transformers, List relocators, PatternSet patternSet, Set unused, @@ -235,7 +250,7 @@ class ShadowCopyAction implements CopyAction { originJar = new RelativePath(false) } - visitedFiles.put(path.toString(), [size: size, originJar: originJar]) + visitedFiles.put(path.toString(), new VisitedFileInfo(size, originJar)) return true } @@ -312,37 +327,7 @@ class ShadowCopyAction implements CopyAction { if (archiveFile.classFile || !isTransformable(archiveFile)) { String path = archiveFilePath.toString() - if (path.endsWith("module-info.class")) { - log.warn("module-info collision") - - def moduleFileName = "module-info" - def moduleFileSuffix = ".class" - File disassembleModFile = File.createTempFile(moduleFileName, moduleFileSuffix) - - try (InputStream is = archive.getInputStream(archiveFilePath.entry)) { - try (OutputStream os = new FileOutputStream(disassembleModFile)) { - IOUtils.copyLarge(is, os) - } - } - - ProcessBuilder processBuilder = new ProcessBuilder("javap", disassembleModFile.absolutePath) - processBuilder.redirectErrorStream(true) - Process process = processBuilder.start() - InputStream inputStream = process.getInputStream() - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)) - String line - - while ((line = reader.readLine()) != null) { - log.warn(line) - } - - int exitCode = process.waitFor() - if (exitCode != 0) { - log.warn("Process exited with code " + exitCode) - } - - log.warn("module-info collision end") - } + listModuleInfoOnDemand(path, archive, archiveFilePath) if (recordVisit(path, archiveFileSize, archiveFilePath) && !isUnused(archiveFilePath.entry.name)) { if (!remapper.hasRelocators() || !archiveFile.classFile) { @@ -382,6 +367,52 @@ class ShadowCopyAction implements CopyAction { } } + /** + Information about the 'module-info.class' if it isn't excluded. Including can be done with + allowModuleInfos(), like this: +

+         shadowJar {
+            ...
+            allowModuleInfos()
+         }
+         
+ Based on the discussion in issue 710: GitHub Issue #710. + */ + private void listModuleInfoOnDemand(String path, ZipFile archive, RelativeArchivePath archiveFilePath) { + if (path.endsWith(ShadowJavaPlugin.MODULE_INFO_CLASS) && allowModuleInfos) { + log.warn("======== Warning: {}/{} contains module-info - Listing content ========", + RelativePath.parse(true, archive.name).lastName, path) + + def moduleFileName = "module-info" + def moduleFileSuffix = ".class" + File disassembleModFile = File.createTempFile(moduleFileName, moduleFileSuffix) + + try (InputStream is = archive.getInputStream(archiveFilePath.entry)) { + try (OutputStream os = new FileOutputStream(disassembleModFile)) { + IOUtils.copyLarge(is, os) + } + } + + ProcessBuilder processBuilder = new ProcessBuilder("javap", disassembleModFile.absolutePath) + processBuilder.redirectErrorStream(true) + Process process = processBuilder.start() + InputStream inputStream = process.getInputStream() + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)) + + String line + while ((line = reader.readLine()) != null) { + log.warn(line) + } + + int exitCode = process.waitFor() + if (exitCode != 0) { + log.warn("Process exited with code " + exitCode) + } + + log.warn("======== module-info content listing end ========") + } + } + private void addParentDirectories(RelativeArchivePath file) { if (file) { addParentDirectories(file.parent) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 70fd30b20..31620d3d3 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -1,5 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.tasks; +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin; import com.github.jengelman.gradle.plugins.shadow.ShadowStats; import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter; import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter; @@ -72,6 +73,8 @@ public FileCollection call() { } }); + private boolean isAllowModuleInfos; + public ShadowJar() { super(); setDuplicatesStrategy( @@ -144,7 +147,7 @@ protected CopyAction createCopyAction() { getSourceSetsClassesDirs().getFiles(), getToMinimize()) : null; return new ShadowCopyAction(getArchiveFile().get().getAsFile(), getInternalCompressor(), documentationRegistry, this.getMetadataCharset(), transformers, relocators, getRootPatternSet(), shadowStats, - isPreserveFileTimestamps(), minimizeJar, unusedTracker); + isPreserveFileTimestamps(), minimizeJar, unusedTracker, isAllowModuleInfos); } @Classpath @@ -269,6 +272,23 @@ public ShadowJar removeDefaultTransformers() { return this; } + /** + * Allows module-info.class's to be included in the final jar, and informs about the contents + * of the module-info.class files it finds. + * + * @return this + */ + public ShadowJar allowModuleInfos() { + this.isAllowModuleInfos = true; + getExcludes().remove(ShadowJavaPlugin.MODULE_INFO_CLASS); + return this; + } + + @Internal + public boolean isAllowModuleInfos() { + return isAllowModuleInfos; + } + private boolean isCacheableTransform(Class clazz) { return clazz.isAnnotationPresent(CacheableTransformer.class); } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index d6ea4d2e0..017e2c40f 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -156,6 +156,8 @@ class ShadowPluginSpec extends PluginSpecification { /* Shouldn't appear, because the default StandardFileTransformer should've merged it, instead of just dropping all following licenses. */ assert !result.output.contains('license.txt') + // No module listing should appear. module-info.class is excluded by default. + assert !result.output.contains('open module org.example') } def 'Tests the removal of the default transformer'() { @@ -167,7 +169,7 @@ class ShadowPluginSpec extends PluginSpecification { |task shadow(type: ${ShadowJar.name}) { | destinationDirectory = buildDir | archiveBaseName = 'shadow' - | removeDefaultTransformers() + | removeDefaultTransformers() | from('${artifact.path}') | from('${project.path}') |} @@ -182,6 +184,32 @@ class ShadowPluginSpec extends PluginSpecification { /\s+IGNORING test\.json from test-project-1\.0-SNAPSHOT.jar, size is different/ // Without the StandardFileTransformer there should be a warning about multiple license files with the same name. assert result.output.contains('license.txt') + // No module listing should appear. module-info.class is excluded by default. + assert !result.output.contains('open module org.example') + } + + def 'Tests the info about the module-info.class, if removed from standard excludes'() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + |task shadow(type: ${ShadowJar.name}) { + | destinationDirectory = buildDir + | archiveBaseName = 'shadow' + | allowModuleInfos() + | from('${artifact.path}') + | from('${project.path}') + |} + """.stripMargin() + + when: + BuildResult result = run('shadow') + + then: + assert result.output.contains('module-info.class') + // Because allowModuleInfos() is used, the module listing should appear. + assert result.output.contains('open module org.example') } def 'include project sources'() { diff --git a/src/test/resources/test-artifact-1.0-SNAPSHOT.jar b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar index 8bc765711892594930293a8054285afbe67d7009..d12faa8977c31d8da5da81ec4d0d1242d1c67f9b 100644 GIT binary patch delta 1081 zcmX>k_d#{TLY8_F1_lNW2IeY_$Yxf7ukAqIb|4l6;@td{(wtP?%)GRGz2uz4;$rKs z`Pc2(*w~`k;>y_6+}PgT`7`CxoRGU`_H3zfV_Wy&&!t6ALbjio6XM3ksG_-d&5=zm zZfs`p8zvYqTL+}q1@4N=a!tx(^k|qou~B%5o4)2oi4~61?_P*G?{NNtM@u97q9FNI zn;IJ%Co;0lkeG2$g_~J>!nDMM*#1wa4X>pobw#egYY_{alxe?rQ+df4k6`Xe!SaGZQ(!mAq7IdU? ztBR+tF_`jviNu{w#!-#d)Ew za-k(>wbT6%aSlE!*Z#73QKWme)+_CiwWZmu|BJ*j6N4gh@0`2sZ*|=+^>lFx+w#R~ zyY$a3xtTQk81rK%SmOE2;I7I z=$lDR#wnifOslv2=sJ|&=$zqH`6SIN=S{-&2@9uLyuQEr9)~-g9voAYTg zIl{QYP@iF1%LB9)Ve1r5)Rc)~J6=y=OTSRlof$v@w-3#DL~i;;@td{(wtP?%)GRGz2uz4;$rKs z`Pc2(*w~`k;>y_6+}PgT`7`CxoRGU`_H3zfV_Wy&&!t6ALbjio6XM3ksG_-d&5=zm zZfs`p8zvYqTL+}q1@4N=a!tx(^k|qou~B%5o4)2oi50HX?_P*G?{NNtM@wV-#76cN zLGr6Et!Qj)oXE&FLt@536>etj3DXi2k^>SLP8KtS7BXh$u~{%M1b8#EfSk?%cKYUg z&Ne1C2z&BGPLcZ5ip1Q4oKz!ym^z3TC0EuZ%IW{3{>+Z@Y6Rt0ij%YJf-yZKa@q_2mV=1d5IF+YXN}p2Lv^bz(|K!73 z&4sEq{-*OnUVNQx)fI6$z$oCDm2+idx1;p2x#9sir+x?CG|3 zYI9qZ^N(rsTkhE`_4drT5}*nwa)CghVM}8oCzj;M1&%@n21W)62G+mn2R}0221YF^ z2=HcPf>;4b9LTaP3@QxD5CMUPzmC#eko16TY^!PJ!H<*o@XAmADOQj^Pp%yS^7ay>rld4bj=Y+ufUmKrgff!BA~lD7&& z1jKw$2wp-rA4_T{)=D|%$sf5T_$0ulgMxfZV3hD5PGKeyq1!l(_ HevlFXK(lHN delta 139 zcmX@F^IK&@D(B>67O~ByTvbeymxzjNZsn_IVG?1U?8qf1!@>Xq0t{~*6FC_e@{7{- z1H2g_3Z!@-(jduZ&dE=OEt%GFPF4`HW4gyVIYGpQQDkzwi1g$GBD|C5iK;QZ<(_;@ Z)R(E4XR@f64xbQ%5JM>|1H%WfApky6AVUBE From 8e62f66fe430bb65f9cab58b1d80c76f876eb67a Mon Sep 17 00:00:00 2001 From: Jan Diederich Date: Thu, 19 Sep 2024 00:21:16 +0200 Subject: [PATCH 15/15] Fixed rebase errors with module-info exclusion and moved dependencies --- .../jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy index 244f7a87d..4f7c4404b 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy @@ -26,7 +26,6 @@ class ShadowJavaPlugin implements Plugin { public static final String MODULE_INFO_CLASS = 'module-info.class' - private final ProjectConfigurationActionContainer configurationActionContainer private final SoftwareComponentFactory softwareComponentFactory @Inject @@ -103,9 +102,6 @@ class ShadowJavaPlugin implements Plugin { project.configurations.findByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) ?: project.configurations.runtime, ] - shadow.exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'module-info.class') - shadow.configurations = [project.configurations.findByName('runtimeClasspath') ? - project.configurations.runtimeClasspath : project.configurations.runtime] /* Remove excludes like this: shadowJar {