Repository: GradleUp/shadow
Pull request #1267 (2025-02-18 11:52:17)

Minor stuffs
Refs #1233.


Patch:
diff --git a/lint-baseline.xml b/lint-baseline.xml
index d8e6256d..58a4aaee 100644
--- a/lint-baseline.xml
+++ b/lint-baseline.xml
@@ -37,66 +37,66 @@
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.file.CopyActionProcessingStreamAction"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.DocumentationRegistry"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt"
-            line="19"
+            file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
+            line="26"
             column="1"/>
     </issue>
 
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.file.copy.FileCopyDetailsInternal"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.file.CopyActionProcessingStreamAction"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt"
-            line="20"
+            file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
+            line="27"
             column="1"/>
     </issue>
 
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.DocumentationRegistry"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.file.DefaultFilePermissions"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
-            line="22"
+            line="28"
             column="1"/>
     </issue>
 
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.file.DefaultFilePermissions"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.file.copy.CopyAction"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
-            line="23"
+            line="29"
             column="1"/>
     </issue>
 
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.file.copy.CopyAction"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.file.copy.CopyActionProcessingStream"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
-            line="24"
+            line="30"
             column="1"/>
     </issue>
 
     <issue
         id="InternalGradleApiUsage"
         message="Avoid using internal Gradle APIs"
-        errorLine1="import org.gradle.api.internal.file.copy.CopyActionProcessingStream"
-        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        errorLine1="import org.gradle.api.internal.file.copy.FileCopyDetailsInternal"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt"
-            line="25"
+            line="31"
             column="1"/>
     </issue>
 
@@ -107,7 +107,7 @@
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt"
-            line="35"
+            line="34"
             column="1"/>
     </issue>
 
@@ -118,7 +118,7 @@
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt"
-            line="36"
+            line="35"
             column="1"/>
     </issue>
 
@@ -129,7 +129,7 @@
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt"
-            line="37"
+            line="36"
             column="1"/>
     </issue>
 
@@ -140,7 +140,7 @@
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt"
-            line="38"
+            line="37"
             column="1"/>
     </issue>
 
diff --git a/src/docs/application-plugin/README.md b/src/docs/application-plugin/README.md
index 3d2765d1..a04f90c0 100644
--- a/src/docs/application-plugin/README.md
+++ b/src/docs/application-plugin/README.md
@@ -79,8 +79,8 @@ application {
 // `shadow` is the name of the distribution created by Shadow plugin
 distributions.named('shadow') {
   // Optionally, you can add more files into extra directory in the distribution like this:
-  contents.into('extra') {
-    from project.file('extra/echo.sh')
+  contents.from('extra/echo.sh') {
+    into 'extra'
   }
 }
 ```
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
index 38622b7f..241b491d 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
@@ -14,6 +14,7 @@ public abstract class ShadowBasePlugin : Plugin<Project> {
     if (GradleVersion.current() < GradleVersion.version("8.3")) {
       throw GradleException("This version of Shadow supports Gradle 8.3+ only. Please upgrade.")
     }
+    @Suppress("DEPRECATION")
     project.extensions.create(EXTENSION_NAME, ShadowExtension::class.java, project)
     project.configurations.create(CONFIGURATION_NAME)
   }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt
deleted file mode 100644
index c7fb90fe..00000000
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt
+++ /dev/null
@@ -1,256 +0,0 @@
-package com.github.jengelman.gradle.plugins.shadow.internal
-
-import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.ArchiveFileTreeElement
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.RelativeArchivePath
-import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
-import java.io.File
-import java.io.InputStream
-import java.util.zip.ZipException
-import org.apache.tools.zip.UnixStat
-import org.apache.tools.zip.ZipFile
-import org.apache.tools.zip.ZipOutputStream
-import org.gradle.api.GradleException
-import org.gradle.api.file.FileCopyDetails
-import org.gradle.api.file.FileTreeElement
-import org.gradle.api.file.RelativePath
-import org.gradle.api.internal.file.CopyActionProcessingStreamAction
-import org.gradle.api.internal.file.copy.FileCopyDetailsInternal
-import org.gradle.api.logging.Logger
-import org.gradle.api.tasks.util.PatternSet
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.commons.ClassRemapper
-
-/**
- * Modified from [org.gradle.api.internal.file.archive.ZipCopyAction.StreamAction](https://github.com/gradle/gradle/blob/b893c2b085046677cf858fb3d5ce00e68e556c3a/platforms/core-configuration/file-operations/src/main/java/org/gradle/api/internal/file/archive/ZipCopyAction.java#L89).
- */
-internal class RealStreamAction(
-  private val zipOutStr: ZipOutputStream,
-  encoding: String?,
-  private val transformers: Set<Transformer>,
-  private val relocators: Set<Relocator>,
-  private val patternSet: PatternSet,
-  private val unusedClasses: Set<String>,
-  private val zipFile: File,
-  private val preserveFileTimestamps: Boolean,
-  private val logger: Logger,
-) : CopyActionProcessingStreamAction {
-  private val remapper = RelocatorRemapper(relocators)
-  private val visitedFiles = mutableSetOf<String>()
-
-  init {
-    if (encoding != null) {
-      this.zipOutStr.setEncoding(encoding)
-    }
-  }
-
-  override fun processFile(details: FileCopyDetailsInternal) {
-    if (details.isDirectory) visitDir(details) else visitFile(details)
-  }
-
-  private fun visitFile(fileDetails: FileCopyDetails) {
-    if (fileDetails.isJar) {
-      processArchive(fileDetails)
-    } else {
-      try {
-        val isClass = fileDetails.isClass
-        if (!remapper.hasRelocators() || !isClass) {
-          if (isTransformable(fileDetails)) {
-            transform(fileDetails)
-          } else {
-            val mappedPath = remapper.map(fileDetails.relativePath.pathString)
-            val entry = zipEntry(mappedPath, preserveFileTimestamps, fileDetails.lastModified) {
-              unixMode = UnixStat.FILE_FLAG or fileDetails.permissions.toUnixNumeric()
-            }
-            zipOutStr.putNextEntry(entry)
-            fileDetails.copyTo(zipOutStr)
-            zipOutStr.closeEntry()
-          }
-        } else if (isClass && !isUnused(fileDetails.path)) {
-          remapClass(fileDetails)
-        }
-        recordVisit(fileDetails.relativePath)
-      } catch (e: Exception) {
-        throw GradleException("Could not add $fileDetails to ZIP '$zipFile'.", e)
-      }
-    }
-  }
-
-  private fun visitDir(dirDetails: FileCopyDetails) {
-    try {
-      // Trailing slash in name indicates that entry is a directory.
-      val path = dirDetails.relativePath.pathString + "/"
-      val entry = zipEntry(path, preserveFileTimestamps, dirDetails.lastModified) {
-        unixMode = UnixStat.DIR_FLAG or dirDetails.permissions.toUnixNumeric()
-      }
-      zipOutStr.putNextEntry(entry)
-      zipOutStr.closeEntry()
-      recordVisit(dirDetails.relativePath)
-    } catch (e: Exception) {
-      throw GradleException("Could not add $dirDetails to ZIP '$zipFile'.", e)
-    }
-  }
-
-  private fun recordVisit(path: RelativePath): Boolean {
-    return visitedFiles.add(path.pathString)
-  }
-
-  private fun processArchive(fileDetails: FileCopyDetails) {
-    ZipFile(fileDetails.file).use { archive ->
-      archive.entries.asSequence()
-        .map {
-          ArchiveFileTreeElement(RelativeArchivePath(it, preserveFileTimestamps))
-        }
-        .filter {
-          patternSet.asSpec.isSatisfiedBy(it.asFileTreeElement())
-        }.forEach { archiveElement ->
-          if (archiveElement.relativePath.isFile) {
-            visitArchiveFile(archiveElement, archive)
-          }
-        }
-    }
-  }
-
-  private fun visitArchiveDirectory(archiveDir: RelativeArchivePath) {
-    if (recordVisit(archiveDir)) {
-      zipOutStr.putNextEntry(archiveDir.entry)
-      zipOutStr.closeEntry()
-    }
-  }
-
-  private fun visitArchiveFile(archiveFile: ArchiveFileTreeElement, archive: ZipFile) {
-    val archiveFilePath = archiveFile.relativePath
-    if (archiveFile.isClass || !isTransformable(archiveFile)) {
-      if (recordVisit(archiveFilePath) && !isUnused(archiveFilePath.entry.name)) {
-        if (!remapper.hasRelocators() || !archiveFile.isClass) {
-          copyArchiveEntry(archiveFilePath, archive)
-        } else {
-          remapClass(archiveFilePath, archive)
-        }
-      }
-    } else {
-      transform(archiveFile, archive)
-    }
-  }
-
-  private fun addParentDirectories(file: RelativeArchivePath?) {
-    file?.let {
-      addParentDirectories(it.parent)
-      if (!it.isFile) {
-        visitArchiveDirectory(it)
-      }
-    }
-  }
-
-  private fun isUnused(classPath: String): Boolean {
-    val classPathWithoutExtension = classPath.substringBeforeLast(".")
-    val className = classPathWithoutExtension.replace('/', '.')
-    val result = unusedClasses.contains(className)
-    if (result) {
-      logger.debug("Dropping unused class: $className")
-    }
-    return result
-  }
-
-  private fun remapClass(file: RelativeArchivePath, archive: ZipFile) {
-    if (file.isClass) {
-      val entry = zipEntry(remapper.mapPath(file) + CLASS_SUFFIX, preserveFileTimestamps)
-      addParentDirectories(RelativeArchivePath(entry, preserveFileTimestamps))
-      remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time)
-    }
-  }
-
-  private fun remapClass(fileCopyDetails: FileCopyDetails) {
-    if (fileCopyDetails.isClass) {
-      fileCopyDetails.file.inputStream().use {
-        remapClass(it, fileCopyDetails.path, fileCopyDetails.lastModified)
-      }
-    }
-  }
-
-  /**
-   * 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 fun remapClass(classInputStream: InputStream, path: String, lastModified: Long) {
-    // 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's maven-bundle-plugin
-    // that use the constant pool to determine the dependencies of a class.
-    val cw = ClassWriter(0)
-    val cr = ClassReader(classInputStream)
-    val cv = ClassRemapper(cw, remapper)
-
-    try {
-      cr.accept(cv, ClassReader.EXPAND_FRAMES)
-    } catch (t: Throwable) {
-      throw GradleException("Error in ASM processing class $path", t)
-    }
-
-    val renamedClass = cw.toByteArray()
-    // Temporarily remove the multi-release prefix.
-    val multiReleasePrefix = "^META-INF/versions/\\d+/".toRegex().find(path)?.value.orEmpty()
-    val newPath = path.replace(multiReleasePrefix, "")
-    val mappedName = multiReleasePrefix + remapper.mapPath(newPath)
-    try {
-      // Now we put it back on so the class file is written out with the right extension.
-      zipOutStr.putNextEntry(zipEntry("$mappedName.class", preserveFileTimestamps, lastModified))
-      renamedClass.inputStream().use {
-        it.copyTo(zipOutStr)
-      }
-      zipOutStr.closeEntry()
-    } catch (_: ZipException) {
-      logger.warn("We have a duplicate $mappedName in source project")
-    }
-  }
-
-  private fun copyArchiveEntry(archiveFile: RelativeArchivePath, archive: ZipFile) {
-    val mappedPath = remapper.map(archiveFile.entry.name)
-    val entry = zipEntry(mappedPath, preserveFileTimestamps, archiveFile.entry.time)
-    val mappedFile = RelativeArchivePath(entry, preserveFileTimestamps)
-    addParentDirectories(mappedFile)
-    zipOutStr.putNextEntry(mappedFile.entry)
-    archive.getInputStream(archiveFile.entry).use {
-      it.copyTo(zipOutStr)
-    }
-    zipOutStr.closeEntry()
-  }
-
-  private fun transform(element: ArchiveFileTreeElement, archive: ZipFile) {
-    transformAndClose(element, archive.getInputStream(element.relativePath.entry))
-  }
-
-  private fun transform(details: FileCopyDetails) {
-    transformAndClose(details, details.file.inputStream())
-  }
-
-  private fun transformAndClose(element: FileTreeElement, inputStream: InputStream) {
-    inputStream.use { steam ->
-      val mappedPath = remapper.map(element.relativePath.pathString)
-      transformers.find {
-        it.canTransformResource(element)
-      }?.transform(
-        TransformerContext(
-          path = mappedPath,
-          inputStream = steam,
-          relocators = relocators,
-        ),
-      )
-    }
-  }
-
-  private fun isTransformable(element: FileTreeElement): Boolean {
-    return transformers.any { it.canTransformResource(element) }
-  }
-
-  companion object {
-    const val CLASS_SUFFIX = ".class"
-
-    private val FileCopyDetails.isClass: Boolean get() = relativePath.pathString.endsWith(CLASS_SUFFIX)
-    private val FileCopyDetails.isJar: Boolean get() = relativePath.pathString.endsWith(".jar")
-  }
-}
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
index e8fb652b..c076b53c 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
@@ -1,32 +1,43 @@
 package com.github.jengelman.gradle.plugins.shadow.tasks
 
-import com.github.jengelman.gradle.plugins.shadow.internal.RealStreamAction
-import com.github.jengelman.gradle.plugins.shadow.internal.RealStreamAction.Companion.CLASS_SUFFIX
+import com.github.jengelman.gradle.plugins.shadow.internal.RelocatorRemapper
 import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor
 import com.github.jengelman.gradle.plugins.shadow.internal.createDefaultFileTreeElement
 import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 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 java.io.File
 import java.io.InputStream
 import java.io.OutputStream
 import java.util.GregorianCalendar
+import java.util.zip.ZipException
+import kotlin.sequences.forEach
+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.GradleException
+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.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.logging.Logging
 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.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.commons.ClassRemapper
 
 /**
  * Modified from [org.gradle.api.internal.file.archive.ZipCopyAction.java](https://github.com/gradle/gradle/blob/b893c2b085046677cf858fb3d5ce00e68e556c3a/platforms/core-configuration/file-operations/src/main/java/org/gradle/api/internal/file/archive/ZipCopyAction.java).
@@ -53,7 +64,7 @@ public open class ShadowCopyAction(
     try {
       zipOutStream.use { zos ->
         stream.process(
-          RealStreamAction(
+          StreamAction(
             zos,
             encoding,
             transformers,
@@ -146,7 +157,235 @@ public open class ShadowCopyAction(
     override fun copyTo(file: File): Boolean = throw UnsupportedOperationException()
   }
 
+  private class StreamAction(
+    private val zipOutStr: ZipOutputStream,
+    encoding: String?,
+    private val transformers: Set<Transformer>,
+    private val relocators: Set<Relocator>,
+    private val patternSet: PatternSet,
+    private val unusedClasses: Set<String>,
+    private val zipFile: File,
+    private val preserveFileTimestamps: Boolean,
+    private val logger: Logger,
+  ) : CopyActionProcessingStreamAction {
+    private val remapper = RelocatorRemapper(relocators)
+    private val visitedFiles = mutableSetOf<String>()
+
+    init {
+      if (encoding != null) {
+        this.zipOutStr.setEncoding(encoding)
+      }
+    }
+
+    override fun processFile(details: FileCopyDetailsInternal) {
+      if (details.isDirectory) visitDir(details) else visitFile(details)
+    }
+
+    private fun visitFile(fileDetails: FileCopyDetails) {
+      if (fileDetails.isJar) {
+        processArchive(fileDetails)
+      } else {
+        try {
+          val isClass = fileDetails.isClass
+          if (!remapper.hasRelocators() || !isClass) {
+            if (isTransformable(fileDetails)) {
+              transform(fileDetails)
+            } else {
+              val mappedPath = remapper.map(fileDetails.relativePath.pathString)
+              val entry = zipEntry(mappedPath, preserveFileTimestamps, fileDetails.lastModified) {
+                unixMode = UnixStat.FILE_FLAG or fileDetails.permissions.toUnixNumeric()
+              }
+              zipOutStr.putNextEntry(entry)
+              fileDetails.copyTo(zipOutStr)
+              zipOutStr.closeEntry()
+            }
+          } else if (isClass && !isUnused(fileDetails.path)) {
+            remapClass(fileDetails)
+          }
+          recordVisit(fileDetails.relativePath)
+        } catch (e: Exception) {
+          throw GradleException("Could not add $fileDetails to ZIP '$zipFile'.", e)
+        }
+      }
+    }
+
+    private fun visitDir(dirDetails: FileCopyDetails) {
+      try {
+        // Trailing slash in name indicates that entry is a directory.
+        val path = dirDetails.relativePath.pathString + "/"
+        val entry = zipEntry(path, preserveFileTimestamps, dirDetails.lastModified) {
+          unixMode = UnixStat.DIR_FLAG or dirDetails.permissions.toUnixNumeric()
+        }
+        zipOutStr.putNextEntry(entry)
+        zipOutStr.closeEntry()
+        recordVisit(dirDetails.relativePath)
+      } catch (e: Exception) {
+        throw GradleException("Could not add $dirDetails to ZIP '$zipFile'.", e)
+      }
+    }
+
+    private fun recordVisit(path: RelativePath): Boolean {
+      return visitedFiles.add(path.pathString)
+    }
+
+    private fun processArchive(fileDetails: FileCopyDetails) {
+      ZipFile(fileDetails.file).use { archive ->
+        archive.entries.asSequence()
+          .map {
+            ArchiveFileTreeElement(RelativeArchivePath(it, preserveFileTimestamps))
+          }
+          .filter {
+            patternSet.asSpec.isSatisfiedBy(it.asFileTreeElement())
+          }.forEach { archiveElement ->
+            if (archiveElement.relativePath.isFile) {
+              visitArchiveFile(archiveElement, archive)
+            }
+          }
+      }
+    }
+
+    private fun visitArchiveDirectory(archiveDir: RelativeArchivePath) {
+      if (recordVisit(archiveDir)) {
+        zipOutStr.putNextEntry(archiveDir.entry)
+        zipOutStr.closeEntry()
+      }
+    }
+
+    private fun visitArchiveFile(archiveFile: ArchiveFileTreeElement, archive: ZipFile) {
+      val archiveFilePath = archiveFile.relativePath
+      if (archiveFile.isClass || !isTransformable(archiveFile)) {
+        if (recordVisit(archiveFilePath) && !isUnused(archiveFilePath.entry.name)) {
+          if (!remapper.hasRelocators() || !archiveFile.isClass) {
+            copyArchiveEntry(archiveFilePath, archive)
+          } else {
+            remapClass(archiveFilePath, archive)
+          }
+        }
+      } else {
+        transform(archiveFile, archive)
+      }
+    }
+
+    private fun addParentDirectories(file: RelativeArchivePath?) {
+      file?.let {
+        addParentDirectories(it.parent)
+        if (!it.isFile) {
+          visitArchiveDirectory(it)
+        }
+      }
+    }
+
+    private fun isUnused(classPath: String): Boolean {
+      val classPathWithoutExtension = classPath.substringBeforeLast(".")
+      val className = classPathWithoutExtension.replace('/', '.')
+      val result = unusedClasses.contains(className)
+      if (result) {
+        logger.debug("Dropping unused class: $className")
+      }
+      return result
+    }
+
+    private fun remapClass(file: RelativeArchivePath, archive: ZipFile) {
+      if (file.isClass) {
+        val entry = zipEntry(remapper.mapPath(file) + CLASS_SUFFIX, preserveFileTimestamps)
+        addParentDirectories(RelativeArchivePath(entry, preserveFileTimestamps))
+        remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time)
+      }
+    }
+
+    private fun remapClass(fileCopyDetails: FileCopyDetails) {
+      if (fileCopyDetails.isClass) {
+        fileCopyDetails.file.inputStream().use {
+          remapClass(it, fileCopyDetails.path, fileCopyDetails.lastModified)
+        }
+      }
+    }
+
+    /**
+     * 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 fun remapClass(classInputStream: InputStream, path: String, lastModified: Long) {
+      // 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's maven-bundle-plugin
+      // that use the constant pool to determine the dependencies of a class.
+      val cw = ClassWriter(0)
+      val cr = ClassReader(classInputStream)
+      val cv = ClassRemapper(cw, remapper)
+
+      try {
+        cr.accept(cv, ClassReader.EXPAND_FRAMES)
+      } catch (t: Throwable) {
+        throw GradleException("Error in ASM processing class $path", t)
+      }
+
+      val renamedClass = cw.toByteArray()
+      // Temporarily remove the multi-release prefix.
+      val multiReleasePrefix = "^META-INF/versions/\\d+/".toRegex().find(path)?.value.orEmpty()
+      val newPath = path.replace(multiReleasePrefix, "")
+      val mappedName = multiReleasePrefix + remapper.mapPath(newPath)
+      try {
+        // Now we put it back on so the class file is written out with the right extension.
+        zipOutStr.putNextEntry(zipEntry("$mappedName.class", preserveFileTimestamps, lastModified))
+        renamedClass.inputStream().use {
+          it.copyTo(zipOutStr)
+        }
+        zipOutStr.closeEntry()
+      } catch (_: ZipException) {
+        logger.warn("We have a duplicate $mappedName in source project")
+      }
+    }
+
+    private fun copyArchiveEntry(archiveFile: RelativeArchivePath, archive: ZipFile) {
+      val mappedPath = remapper.map(archiveFile.entry.name)
+      val entry = zipEntry(mappedPath, preserveFileTimestamps, archiveFile.entry.time)
+      val mappedFile = RelativeArchivePath(entry, preserveFileTimestamps)
+      addParentDirectories(mappedFile)
+      zipOutStr.putNextEntry(mappedFile.entry)
+      archive.getInputStream(archiveFile.entry).use {
+        it.copyTo(zipOutStr)
+      }
+      zipOutStr.closeEntry()
+    }
+
+    private fun transform(element: ArchiveFileTreeElement, archive: ZipFile) {
+      transformAndClose(element, archive.getInputStream(element.relativePath.entry))
+    }
+
+    private fun transform(details: FileCopyDetails) {
+      transformAndClose(details, details.file.inputStream())
+    }
+
+    private fun transformAndClose(element: FileTreeElement, inputStream: InputStream) {
+      inputStream.use { steam ->
+        val mappedPath = remapper.map(element.relativePath.pathString)
+        transformers.find {
+          it.canTransformResource(element)
+        }?.transform(
+          TransformerContext(
+            path = mappedPath,
+            inputStream = steam,
+            relocators = relocators,
+          ),
+        )
+      }
+    }
+
+    private fun isTransformable(element: FileTreeElement): Boolean {
+      return transformers.any { it.canTransformResource(element) }
+    }
+
+    companion object {
+      private val FileCopyDetails.isClass: Boolean get() = relativePath.pathString.endsWith(CLASS_SUFFIX)
+      private val FileCopyDetails.isJar: Boolean get() = relativePath.pathString.endsWith(".jar")
+    }
+  }
+
   public companion object {
+    private const val CLASS_SUFFIX = ".class"
     private val logger = Logging.getLogger(ShadowCopyAction::class.java)
     public val CONSTANT_TIME_FOR_ZIP_ENTRIES: Long = GregorianCalendar(1980, 1, 1, 0, 0, 0).timeInMillis
   }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
index 4172b3ca..857cb4d9 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
@@ -320,18 +320,17 @@ public abstract class ShadowJar :
 
   private fun injectMultiReleaseAttrIfPresent() {
     // TODO: https://github.com/GradleUp/shadow/pull/1239#discussion_r1946064032.
-    val includeMultiReleaseAttr = includedDependencies.files.filter { it.extension == "jar" }
-      .any {
-        try {
-          JarFile(it).use { jarFile ->
-            // Manifest might be null or the attribute name is invalid, or any other case.
-            runCatching { jarFile.manifest.mainAttributes.getValue(multiReleaseAttributeKey) }.getOrNull()
-          } == "true"
-        } catch (_: IOException) {
-          // If the jar file is not valid, ignore it.
-          false
-        }
+    val includeMultiReleaseAttr = includedDependencies.files.any {
+      try {
+        JarFile(it).use { jarFile ->
+          // Manifest might be null or the attribute name is invalid, or any other case.
+          runCatching { jarFile.manifest.mainAttributes.getValue(multiReleaseAttributeKey) }.getOrNull()
+        } == "true"
+      } catch (_: IOException) {
+        // If the jar file is not valid, ignore it.
+        false
       }
+    }
     if (includeMultiReleaseAttr) {
       manifest.attributes[multiReleaseAttributeKey] = true
     }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
index e2f79e7f..fb9b9021 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
@@ -156,8 +156,8 @@ class ApplicationPluginTest : BasePluginTest() {
     prepare(
       projectBlock = """
         distributions.named('$DISTRIBUTION_NAME') {
-          contents.into('extra') {
-            from project.file('extra/echo.sh')
+          contents.from('extra/echo.sh') {
+            into 'extra'
           }
         }
       """.trimIndent(),