Repository: GradleUp/shadow (kotlin)

Issue:
Provide automatic inclusion of Multi-Release in MANIFEST.MF
### Shadow Version
4.0.3
### Expected Behavior
If any dependencies included have the `Multi-Release` flag set to `true` in `MANIFEST.MF`, the shadow jar should include this entry.
### Actual Behavior
Currently a manual process. Requires knowledge of all dependencies and manually adding required entry to `MANIFEST.MF`.
### Background
Current versions of Java allow for multi-release jars to be created. However, in order for it to work, `Multi-Release: true` needs to be present in the manifest. One popular example of a multi-release jar is the Log4J API (`org.apache.logging.log4j:log4j-api`). See this [stack overflow question](https://stackoverflow.com/questions/52953483/logmanager-getlogger-is-unable-to-determine-class-name-on-java-11) for more details. It would be nice if the shadow plugin would automate this process.

Pull request description:
Closes #449.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/src/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.

Patch:
diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md
index a16a0334b..db9751af0 100644
--- a/src/docs/changes/README.md
+++ b/src/docs/changes/README.md
@@ -6,6 +6,7 @@
 **Added**
 
 - Sync `ShadowApplicationPlugin` with `ApplicationPlugin`. ([#1224](https://github.com/GradleUp/shadow/pull/1224))
+- Inject `Multi-Release` manifest attribute if any dependency contains it. ([#1239](https://github.com/GradleUp/shadow/pull/1239))
 
 **Changed**
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
index ee62f471b..f8265f3fe 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
@@ -27,6 +27,11 @@ internal val mainClassAttributeKey = JarAttributeName.MAIN_CLASS.toString()
  */
 internal val classPathAttributeKey = JarAttributeName.CLASS_PATH.toString()
 
+/**
+ * Known as `Multi-Release` in the manifest file.
+ */
+internal val multiReleaseAttributeKey = JarAttributeName.MULTI_RELEASE.toString()
+
 /**
  * This is used for creating a [DefaultFileTreeElement] with default values.
  * [file], [chmod], and [stat] should be non-null, so they are set to dummy values here.
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 773a84ca6..2beaf3efe 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
@@ -9,6 +9,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.MinimizeDependencyFil
 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.internal.fileCollection
+import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.internal.property
 import com.github.jengelman.gradle.plugins.shadow.internal.setProperty
 import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets
@@ -22,6 +23,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransf
 import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
 import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer.Companion.create
 import java.io.File
+import java.io.IOException
 import java.util.jar.JarFile
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.Action
@@ -263,6 +265,7 @@ public abstract class ShadowJar :
   @TaskAction
   override fun copy() {
     from(includedDependencies)
+    injectMultiReleaseAttrIfPresent()
     super.copy()
     logger.info(stats.toString())
   }
@@ -324,4 +327,23 @@ 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
+        }
+      }
+    if (includeMultiReleaseAttr) {
+      manifest.attributes[multiReleaseAttributeKey] = true
+    }
+  }
 }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
index f4242c7f8..69bef1588 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
@@ -12,6 +12,7 @@ import assertk.assertions.single
 import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_JAR_TASK_NAME
 import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey
+import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.legacy.LegacyShadowPlugin
 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import com.github.jengelman.gradle.plugins.shadow.util.BooleanParameterizedTest
@@ -189,6 +190,30 @@ class JavaPluginTest : BasePluginTest() {
     }
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/449",
+  )
+  @Test
+  fun containsMultiReleaseAttrIfAnyDependencyContainsIt() {
+    writeClientAndServerModules()
+    path("client/build.gradle").appendText(
+      """
+        jar {
+          manifest {
+            attributes '$multiReleaseAttributeKey': 'true'
+          }
+        }
+      """.trimIndent() + System.lineSeparator(),
+    )
+
+    run(serverShadowJarTask)
+
+    assertThat(outputServerShadowJar).useAll {
+      transform { it.manifest.mainAttributes }.isNotEmpty()
+      getMainAttr(multiReleaseAttributeKey).isEqualTo("true")
+    }
+  }
+
   @Issue(
     "https://github.com/GradleUp/shadow/issues/352",
     "https://github.com/GradleUp/shadow/issues/729",