You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gradle-plugins/gradle-plugin-shadow/src/main/groovy/org/xbib/gradle/plugin/shadow/tasks/ShadowCopyAction.groovy

504 lines
19 KiB
Groovy

package org.xbib.gradle.plugin.shadow.tasks
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.copy.CopyAction
import org.gradle.api.internal.file.copy.CopyActionProcessingStream
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal
import org.gradle.api.logging.Logger
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 org.xbib.gradle.plugin.shadow.ShadowStats
import org.xbib.gradle.plugin.shadow.impl.RelocatorRemapper
import org.xbib.gradle.plugin.shadow.internal.UnusedTracker
import org.xbib.gradle.plugin.shadow.internal.Utils
import org.xbib.gradle.plugin.shadow.internal.ZipCompressor
import org.xbib.gradle.plugin.shadow.relocation.Relocator
import org.xbib.gradle.plugin.shadow.transformers.Transformer
import org.xbib.gradle.plugin.shadow.transformers.TransformerContext
import org.xbib.gradle.plugin.shadow.zip.UnixStat
import org.xbib.gradle.plugin.shadow.zip.Zip64RequiredException
import org.xbib.gradle.plugin.shadow.zip.ZipEntry
import org.xbib.gradle.plugin.shadow.zip.ZipFile
import org.xbib.gradle.plugin.shadow.zip.ZipOutputStream
import java.time.LocalDate
import java.time.OffsetDateTime
import java.util.zip.ZipException
class ShadowCopyAction implements CopyAction {
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = LocalDate.of(1980, 1, 1).atStartOfDay()
.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli()
private final Logger log
private final 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 ShadowStats stats
private final String encoding
private final boolean preserveFileTimestamps
private final boolean minimizeJar
private final UnusedTracker unusedTracker
ShadowCopyAction(Logger log, File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry,
String encoding, List<Transformer> transformers, List<Relocator> relocators,
PatternSet patternSet, ShadowStats stats,
boolean preserveFileTimestamps, boolean minimizeJar, UnusedTracker unusedTracker) {
this.log = log
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<String> unusedClasses
if (minimizeJar) {
stream.process(new BaseStreamAction() {
@Override
void visitFile(FileCopyDetails fileDetails) {
if (isArchive(fileDetails)) {
unusedTracker.addDependency(fileDetails.file)
}
}
})
unusedClasses = unusedTracker.findUnused()
} else {
unusedClasses = Collections.emptySet()
}
try {
ZipOutputStream zipOutStr = compressor.createArchiveOutputStream(zipFile)
withResource(zipOutStr, new Action<ZipOutputStream>() {
void execute(ZipOutputStream outputStream) {
try {
stream.process(new StreamAction(outputStream, encoding, transformers, relocators, patternSet,
unusedClasses, stats))
processTransformers(outputStream)
} catch (Exception e) {
log.error(e.getMessage() as String, e)
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 '${zipFile.toString()}'", 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) {
// Ignored
}
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 Utils.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<Transformer> transformers
private final List<Relocator> relocators
private final RelocatorRemapper remapper
private final PatternSet patternSet
private final Set<String> unused
private final ShadowStats stats
private Set<String> visitedFiles = new HashSet<String>()
StreamAction(ZipOutputStream zipOutStr, String encoding, List<Transformer> transformers,
List<Relocator> relocators, PatternSet patternSet, Set<String> 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(RelativePath path) {
return visitedFiles.add(path.pathString)
}
@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.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), e)
}
} else {
processArchive(fileDetails)
}
}
private void processArchive(FileCopyDetails fileDetails) {
stats.startJar()
ZipFile archive = new ZipFile(fileDetails.file)
List<ArchiveFileTreeElement> archiveElements = archive.entries.collect {
new ArchiveFileTreeElement(new RelativeArchivePath(it, fileDetails))
}
Spec<FileTreeElement> patternSpec = patternSet.getAsSpec()
List<ArchiveFileTreeElement> filteredArchiveElements = archiveElements.findAll { ArchiveFileTreeElement archiveElement ->
patternSpec.isSatisfiedBy(archiveElement)
}
filteredArchiveElements.each { ArchiveFileTreeElement archiveElement ->
if (archiveElement.relativePath.file) {
visitArchiveFile(archiveElement, archive)
}
}
archive.close()
stats.finishJar()
}
private void visitArchiveDirectory(RelativeArchivePath archiveDir) {
if (recordVisit(archiveDir)) {
zipOutStr.putNextEntry(archiveDir.entry)
zipOutStr.closeEntry()
}
}
private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive) {
def archiveFilePath = archiveFile.relativePath
if (archiveFile.classFile || !isTransformable(archiveFile)) {
if (recordVisit(archiveFilePath) && !isUnused(archiveFilePath.entry.name)) {
if (!remapper.hasRelocators() || !archiveFile.classFile) {
copyArchiveEntry(archiveFilePath, archive)
} else {
remapClass(archiveFilePath, archive)
}
}
} 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 = Utils.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, null))
InputStream is = archive.getInputStream(file.entry)
try {
remapClass(is, file.pathString, file.entry.time)
} finally {
is.close()
}
}
}
private void remapClass(FileCopyDetails fileCopyDetails) {
if (Utils.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)
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()
String mappedName = remapper.mapPath(path)
InputStream bis = new ByteArrayInputStream(renamedClass)
try {
ZipEntry archiveEntry = new ZipEntry(mappedName + ".class")
archiveEntry.setTime(getArchiveTimeFor(lastModified))
zipOutStr.putNextEntry(archiveEntry)
Utils.copyLarge(bis, zipOutStr)
zipOutStr.closeEntry()
} catch (ZipException e) {
log.warn("there is 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, archiveFile.details)
addParentDirectories(mappedFile)
zipOutStr.putNextEntry(mappedFile.entry)
InputStream is = archive.getInputStream(archiveFile.entry)
try {
Utils.copyLarge(is, zipOutStr)
} finally {
is.close()
}
zipOutStr.closeEntry()
}
@Override
protected void visitDir(FileCopyDetails dirDetails) {
try {
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), e)
}
}
private void transform(ArchiveFileTreeElement element, ZipFile archive) {
InputStream is = archive.getInputStream(element.relativePath.entry)
try {
transform(element, is)
} finally {
is.close()
}
}
private void transform(FileCopyDetails details) {
transform(details, details.file.newInputStream())
}
private void transform(FileTreeElement element, InputStream inputStream) {
String mappedPath = remapper.map(element.relativePath.pathString)
transformers.find { it.canTransformResource(element) }.transform(
TransformerContext.builder()
.path(mappedPath)
.inputStream(inputStream)
.relocators(relocators)
.stats(stats)
.build()
)
}
private boolean isTransformable(FileTreeElement element) {
return transformers.any { it.canTransformResource(element) }
}
}
class RelativeArchivePath extends RelativePath {
ZipEntry entry
FileCopyDetails details
RelativeArchivePath(ZipEntry entry, FileCopyDetails fileDetails) {
super(!entry.directory, entry.name.split('/'))
this.entry = entry
this.details = fileDetails
}
boolean isClassFile() {
return lastName.endsWith('.class')
}
RelativeArchivePath getParent() {
if (!segments || segments.length == 1) {
return null
} else {
String path = segments[0..-2].join('/') + '/'
return new RelativeArchivePath(setArchiveTimes(new ZipEntry(path)), null)
}
}
}
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
}
}
}