Repository: GradleUp/shadow (kotlin)

Issue:
Allow disabling Multi-Release
### What feature do you want to see added?

Currently, if any dependency is `Multi-Release`, the MANIFEST.MF is forced to have `Multi-Release: true`. Currently this causes issues in my use-case, as `Multi-Release` is getting set to `true` with no `versions` folder being generated because my dependency has only a `module-info.class` in this directory.

### Upstream changes

_No response_

### Are you interested in contributing this feature?

_No response_

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

Patch:
diff --git a/api/shadow.api b/api/shadow.api
index 1e8955af6..cf28c3d2c 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -191,6 +191,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar
 	protected fun copy ()V
 	protected fun createCopyAction ()Lorg/gradle/api/internal/file/copy/CopyAction;
 	public fun dependencies (Lorg/gradle/api/Action;)V
+	public fun getAddMultiReleaseAttribute ()Lorg/gradle/api/provider/Property;
 	public fun getApiJars ()Lorg/gradle/api/file/ConfigurableFileCollection;
 	protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations;
 	public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty;
diff --git a/docs/changes/README.md b/docs/changes/README.md
index ae8fad21f..801e647b1 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -7,6 +7,7 @@
 
 - Allow opting out of `shadowRuntimeElements` variant. ([#1662](https://github.com/GradleUp/shadow/pull/1662))
 - Allow opting out of `TARGET_JVM_VERSION_ATTRIBUTE`. ([#1674](https://github.com/GradleUp/shadow/pull/1674))
+- Allow opting out of `Multi-Release` attribute. ([#1675](https://github.com/GradleUp/shadow/pull/1675))
 
 ## Changed
 
diff --git a/docs/configuration/README.md b/docs/configuration/README.md
index 8086d4cd9..7a38b0051 100644
--- a/docs/configuration/README.md
+++ b/docs/configuration/README.md
@@ -153,6 +153,36 @@ methods on the `shadowJar.manifest` object can be used to configure the upstream
     }
     ```
 
+## Adding Multi-Release Manifest Attribute
+
+The [`ShadowJar`][ShadowJar] task can automatically add the `Multi-Release` attribute to the JAR manifest if any of
+the included dependencies contain this attribute. This is controlled by the `addMultiReleaseAttribute` property.
+
+By default, `addMultiReleaseAttribute` is set to `true`. When enabled, Shadow will scan all dependencies being merged
+into the shadow JAR. If any dependency JAR has the `Multi-Release` manifest attribute set to `true`, Shadow will add
+`Multi-Release: true` to the manifest of the resulting shadow JAR.
+
+You can disable this behavior by setting `addMultiReleaseAttribute` to `false`:
+
+=== "Kotlin"
+
+    ```kotlin
+    tasks.shadowJar {
+      addMultiReleaseAttribute = false
+    }
+    ```
+
+=== "Groovy"
+
+    ```groovy
+    tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+      addMultiReleaseAttribute = false
+    }
+    ```
+
+This is useful if you want to control the presence of the `Multi-Release` attribute manually or avoid inheriting it
+from dependencies.
+
 ## Adding Extra Files
 
 The [`ShadowJar`][ShadowJar] task is a subclass of the [`Jar`][Jar] task, which means that the[`Jar.from`][Jar.from]
diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md
index 5635f73bd..9d8101bcd 100644
--- a/docs/getting-started/README.md
+++ b/docs/getting-started/README.md
@@ -148,14 +148,16 @@ build script. Passing property values on the command line is particularly helpfu
 Here are the options that can be passed to the `shadowJar`:
 
 ```
---enable-auto-relocation          Enables auto relocation of packages in the dependencies.
---no-enable-auto-relocation       Disables option --enable-auto-relocation.
---fail-on-duplicate-entries       Fails build if the ZIP entries in the shadowed JAR are duplicate.
---no-fail-on-duplicate-entries    Disables option --fail-on-duplicate-entries.
---minimize-jar                    Minimizes the jar by removing unused classes.
---no-minimize-jar                 Disables option --minimize-jar.
---relocation-prefix               Prefix used for auto relocation of packages in the dependencies.
---rerun                           Causes the task to be re-run even if up-to-date.
+--add-multi-release-attribute       Adds the multi-release attribute to the manifest if any dependencies contain it.
+--no-add-multi-release-attribute    Disables option --add-multi-release-attribute.
+--enable-auto-relocation            Enables auto relocation of packages in the dependencies.
+--no-enable-auto-relocation         Disables option --enable-auto-relocation.
+--fail-on-duplicate-entries         Fails build if the ZIP entries in the shadowed JAR are duplicate.
+--no-fail-on-duplicate-entries      Disables option --fail-on-duplicate-entries.
+--minimize-jar                      Minimizes the jar by removing unused classes.
+--no-minimize-jar                   Disables option --minimize-jar.
+--relocation-prefix                 Prefix used for auto relocation of packages in the dependencies.
+--rerun                             Causes the task to be re-run even if up-to-date.
 ```
 
 Also, you can view more information about the [`ShadowJar`][ShadowJar] task by running the following command:
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 61e2a57e5..b754aa879 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
@@ -184,6 +184,16 @@ public abstract class ShadowJar : Jar() {
   @get:Option(option = "fail-on-duplicate-entries", description = "Fails build if the ZIP entries in the shadowed JAR are duplicate.")
   public open val failOnDuplicateEntries: Property<Boolean> = objectFactory.property(false)
 
+  /**
+   * Adds the [java.util.jar.Attributes.Name.MULTI_RELEASE] attribute to the manifest of the shadow JAR if any
+   * dependencies contain the attribute.
+   *
+   * Defaults to `true`.
+   */
+  @get:Input
+  @get:Option(option = "add-multi-release-attribute", description = "Adds the multi-release attribute to the manifest if any dependencies contain it.")
+  public open val addMultiReleaseAttribute: Property<Boolean> = objectFactory.property(true)
+
   @Internal
   override fun getManifest(): InheritManifest = super.getManifest() as InheritManifest
 
@@ -443,6 +453,10 @@ public abstract class ShadowJar : Jar() {
     }
 
   private fun injectMultiReleaseAttrIfPresent() {
+    if (!addMultiReleaseAttribute.get()) {
+      logger.info("Skipping adding $multiReleaseAttributeKey attribute to the manifest as it is disabled.")
+      return
+    }
     val includeMultiReleaseAttr = includedDependencies.files.any {
       try {
         JarFile(it).use { jarFile ->


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt
index cf2412d09..df938894d 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt
@@ -106,6 +106,7 @@ class JavaPluginsTest : BasePluginTest() {
         containsOnly(project.runtimeConfiguration)
       }
       assertThat(failOnDuplicateEntries.get()).isFalse()
+      assertThat(addMultiReleaseAttribute.get()).isTrue()
     }
 
     assertThat(shadowConfig.artifacts.files).contains(shadowTask.archiveFile.get().asFile)
@@ -116,6 +117,8 @@ class JavaPluginsTest : BasePluginTest() {
     val result = run("help", "--task", shadowJarPath)
 
     assertThat(result.output).contains(
+      "--add-multi-release-attribute     Adds the multi-release attribute to the manifest if any dependencies contain it.",
+      "--no-add-multi-release-attribute     Disables option --add-multi-release-attribute.",
       "--enable-auto-relocation     Enables auto relocation of packages in the dependencies.",
       "--no-enable-auto-relocation     Disables option --enable-auto-relocation.",
       "--fail-on-duplicate-entries     Fails build if the ZIP entries in the shadowed JAR are duplicate.",
@@ -249,8 +252,9 @@ class JavaPluginsTest : BasePluginTest() {
   @Issue(
     "https://github.com/GradleUp/shadow/issues/449",
   )
-  @Test
-  fun containsMultiReleaseAttrIfAnyDependencyContainsIt() {
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun containsMultiReleaseAttrIfAnyDependencyContainsIt(addAttribute: Boolean) {
     writeClientAndServerModules()
     path("client/build.gradle").appendText(
       """
@@ -261,13 +265,55 @@ class JavaPluginsTest : BasePluginTest() {
         }
       """.trimIndent() + lineSeparator,
     )
+    path("server/build.gradle").appendText(
+      """
+        $shadowJarTask {
+          addMultiReleaseAttribute = $addAttribute
+        }
+      """.trimIndent(),
+    )
 
-    run(serverShadowJarPath)
+    val result = run(serverShadowJarPath, infoArgument)
 
-    assertThat(outputServerShadowedJar).useAll {
-      transform { it.mainAttrSize }.isGreaterThan(1)
-      getMainAttr(multiReleaseAttributeKey).isEqualTo("true")
+    val info = "Skipping adding Multi-Release attribute to the manifest as it is disabled."
+    if (addAttribute) {
+      assertThat(result.output).doesNotContain(info)
+    } else {
+      assertThat(result.output).contains(info)
     }
+
+    val expected = if (addAttribute) "true" else null
+    assertThat(outputServerShadowedJar.use { it.getMainAttr(multiReleaseAttributeKey) })
+      .isEqualTo(expected)
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun containsMultiReleaseAttrByCliOption(enable: Boolean) {
+    writeClientAndServerModules()
+    path("client/build.gradle").appendText(
+      """
+        $jarTask {
+          manifest {
+            attributes '$multiReleaseAttributeKey': 'true'
+          }
+        }
+      """.trimIndent() + lineSeparator,
+    )
+
+    val flag = if (enable) "--add-multi-release-attribute" else "--no-add-multi-release-attribute"
+    val result = run(serverShadowJarPath, infoArgument, flag)
+
+    val info = "Skipping adding Multi-Release attribute to the manifest as it is disabled."
+    if (enable) {
+      assertThat(result.output).doesNotContain(info)
+    } else {
+      assertThat(result.output).contains(info)
+    }
+
+    val expected = if (enable) "true" else null
+    assertThat(outputServerShadowedJar.use { it.getMainAttr(multiReleaseAttributeKey) })
+      .isEqualTo(expected)
   }
 
   @Issue(