jruby-gradle-plugin/jar-plugin/src/main/groovy/com/github/jrubygradle/jar/internal/JRubyJarCopyAction.groovy

491 lines
19 KiB
Groovy

/*
* Copyright (c) 2014-2023, R. Tyler Croy <rtyler@brokenco.de>,
* Schalk Cronje <ysb33r@gmail.com>, Christian Meier, Lookout, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.github.jrubygradle.jar.internal
import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper
import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker
/*
* This source code is derived from Apache 2.0 licensed software copyright John
* Engelman (https://github.com/johnrengelman) and was originally ported from this
* repository: https://github.com/johnrengelman/shadow
*/
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 groovy.util.logging.Slf4j
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOUtils
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.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.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.provider.Provider
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 shadow.org.apache.tools.zip.UnixStat
import shadow.org.apache.tools.zip.Zip64RequiredException
import shadow.org.apache.tools.zip.ZipEntry
import shadow.org.apache.tools.zip.ZipFile
import shadow.org.apache.tools.zip.ZipOutputStream
import java.util.zip.ZipException
/**
* JRubyJarCopyAction is an implementation of the {@link CopyAction} interface for mutating the JRubyJar archive.
*
* This class, in its current form is really just a big copy and paste of the
* shadow plugin's <a
* href="https://github.com/johnrengelman/shadow/blob/4.0.4/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy">ShadowCopyAction</a>
* with one notable exception, it disables the behavior of unzipping nested
* archives when creating the resulting archive.
*
* This class is only intended to be used with the {@link
* JRubyDirInfoTransformer} until such a time when this can be refactored to
* support the same behavior in a less hackish way.
*/
@Slf4j
@SuppressWarnings(['ParameterCount', 'CatchException', 'DuplicateStringLiteral',
'CatchThrowable', 'VariableName', 'UnnecessaryGString', 'InvertedIfElse'])
class JRubyJarCopyAction implements CopyAction {
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).timeInMillis
private final Provider<File> zipFile
private final ZipCompressor compressor
private final DocumentationRegistry documentationRegistry
private final List<Transformer> transformers
private final List<Relocator> relocators
private final PatternSet patternSet
private final String encoding
private final boolean preserveFileTimestamps
private final boolean minimizeJar
private final UnusedTracker unusedTracker
JRubyJarCopyAction(Provider<File> zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry,
String encoding, List<Transformer> transformers, List<Relocator> relocators,
PatternSet patternSet,
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.encoding = encoding
this.preserveFileTimestamps = preserveFileTimestamps
this.minimizeJar = minimizeJar
this.unusedTracker = unusedTracker
}
@Override
WorkResult execute(CopyActionProcessingStream stream) {
Set<String> 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()
}
File zipFileResolved = zipFile.get()
try {
final ZipOutputStream zipOutStr = compressor.createArchiveOutputStream(zipFileResolved)
withResource(zipOutStr, new Action<ZipOutputStream>() {
void execute(ZipOutputStream outputStream) {
try {
stream.process(new StreamAction(outputStream, encoding, transformers, relocators, patternSet,
unusedClasses))
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"))
)
}
} catch (Exception e) {
throw new GradleException("Could not create ZIP '${zipFileResolved}'", e)
}
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 <T extends Closeable> void withResource(T resource, Action<? super T> action) {
try {
action.execute(resource)
} catch (Throwable t) {
try {
resource.close()
} catch (IOException e) {
log.debug("Dropping ignored exception ${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)
}
}
@SuppressWarnings(['UnusedMethodParameter', 'EmptyMethodInAbstractClass'])
protected void visitDir(FileCopyDetails dirDetails) {
}
protected abstract void visitFile(FileCopyDetails fileDetails)
}
private class StreamAction extends BaseStreamAction {
private final ZipOutputStream zipOutStr
private final List<Transformer> transformers
private final List<Relocator> relocators
private final RelocatorRemapper remapper
private final PatternSet patternSet
private final Set<String> unused
private final Set<String> visitedFiles = [] as Set
StreamAction(ZipOutputStream zipOutStr, String encoding, List<Transformer> transformers,
List<Relocator> relocators, PatternSet patternSet, Set<String> unused) {
this.zipOutStr = zipOutStr
this.transformers = transformers
this.relocators = relocators
this.remapper = new RelocatorRemapper(relocators, null)
this.patternSet = patternSet
this.unused = unused
if (encoding != null) {
this.zipOutStr.setEncoding(encoding)
}
}
private boolean recordVisit(RelativePath path) {
return visitedFiles.add(path.pathString)
}
@Override
void visitFile(FileCopyDetails 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.mode)
zipOutStr.putNextEntry(archiveEntry)
fileDetails.copyTo(zipOutStr)
zipOutStr.closeEntry()
} else {
transform(fileDetails)
}
} else if (isClass && !isUnused(fileDetails.path)) {
remapClass(fileDetails)
}
recordVisit(fileDetails.relativePath)
} catch (Exception e) {
throw new GradleException(String.format("Could not add %s to ZIP '%s'.", fileDetails, zipFile.get()), e)
}
}
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))
InputStream is = archive.getInputStream(file.entry)
try {
remapClass(is, file.pathString, file.entry.time)
} finally {
is.close()
}
}
}
private void remapClass(FileCopyDetails fileCopyDetails) {
if (FilenameUtils.getExtension(fileCopyDetails.name) == 'class') {
remapClass(fileCopyDetails.file.newInputStream(), fileCopyDetails.path, fileCopyDetails.lastModified)
}
}
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)
}
byte[] renamedClass = cw.toByteArray()
// Need to take the .class off for remapping evaluation
String mappedName = 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 e) {
log.warn("We have a duplicate " + mappedName + " in source project")
} finally {
bis.close()
}
}
@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.mode)
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.get()), 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)
.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')
}
RelativeArchivePath getParent() {
if (!segments || segments.length == 1) {
return null
}
//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
}
@SuppressWarnings(['GetterMethodCouldBeProperty'])
@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
}
FileTreeElement asFileTreeElement() {
return new DefaultFileTreeElement(null, new RelativePath(!isDirectory(), archivePath.segments), null, null)
}
}
}