Repository: GradleUp/shadow (kotlin)

Issue:
Value of a manifest attribute must not be null
I'm getting the following when running shadowJar.

> gradle shadowJar
> :compileJava UP-TO-DATE
> :processResources UP-TO-DATE
> :classes UP-TO-DATE
> :shadowJar FAILED
> 
> FAILURE: Build failed with an exception.
> - What went wrong:
>   Execution failed for task ':shadowJar'.
>   
>   > The value of a manifest attribute must not be null (Key=Main-Class).
> - Try:
>   Run with --stacktrace option to get the stack trace. Run with --info or --debug
>   option to get more log output.
> 
> BUILD FAILED
> 
> Total time: 6.362 secs

Gradle file has a manifest with a Main-Class key. http://pastebin.com/WVVdsbeu

Other gradle operations run as expected. Any ideas?

Pull request:
- Closes #130.
- Closes #515.
- Closes #613.

---

- [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 a27858a31..616e9e252 100644
--- a/src/docs/changes/README.md
+++ b/src/docs/changes/README.md
@@ -3,6 +3,10 @@
 
 ## [Unreleased]
 
+**Fixed**
+
+- Support overriding `mainClass` provided by `JavaApplication`. ([#1182](https://github.com/GradleUp/shadow/pull/1182))
+
 
 ## [v9.0.0-beta6] (2025-01-23)
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
index 0e0fec3c7..998abe74d 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
@@ -36,7 +36,10 @@ public abstract class ShadowApplicationPlugin : Plugin<Project> {
     shadowJar.configure { jar ->
       jar.inputs.property("mainClassName", classNameProvider)
       jar.doFirst {
-        jar.manifest.attributes["Main-Class"] = classNameProvider.get()
+        // Inject the Main-Class attribute if it is not already present.
+        if (!jar.manifest.attributes.contains("Main-Class")) {
+          jar.manifest.attributes["Main-Class"] = classNameProvider.get()
+        }
       }
     }
   }


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 d2e8d437b..009d74479 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
@@ -8,6 +8,7 @@ import assertk.assertions.exists
 import assertk.assertions.isEqualTo
 import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Companion.SHADOW_INSTALL_TASK_NAME
 import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Companion.SHADOW_RUN_TASK_NAME
+import com.github.jengelman.gradle.plugins.shadow.util.Issue
 import com.github.jengelman.gradle.plugins.shadow.util.JarPath
 import com.github.jengelman.gradle.plugins.shadow.util.containsEntries
 import com.github.jengelman.gradle.plugins.shadow.util.getContent
@@ -45,7 +46,7 @@ class ApplicationPluginTest : BasePluginTest() {
 
     assertThat(result.output).contains(
       "Running application with JDK 17",
-      "Hello, World! (foo)",
+      "Hello, World! (foo) from Main",
     )
 
     commonAssertions(jarPath("build/install/myapp-shadow/lib/myapp-1.0-all.jar"))
@@ -108,6 +109,33 @@ class ApplicationPluginTest : BasePluginTest() {
     commonAssertions(jarPath("build/install/myapp-shadow/lib/myapp-1.0-all.jar"))
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/613",
+  )
+  @Test
+  fun canOverrideMainClassAttrInManifestBlock() {
+    writeMainClass(className = "Main2")
+    prepare(
+      projectBlock = """
+        shadowJar {
+          manifest {
+            attributes 'Main-Class': 'shadow.Main2'
+          }
+        }
+      """.trimIndent(),
+    )
+
+    val result = run(SHADOW_RUN_TASK_NAME)
+
+    assertThat(result.output).contains(
+      "Hello, World! (foo) from Main2",
+    )
+    commonAssertions(
+      jarPath("build/install/myapp-shadow/lib/myapp-1.0-all.jar"),
+      mainClassAttr = "shadow.Main2",
+    )
+  }
+
   private fun prepare(
     projectBlock: String = "",
     settingsBlock: String = "",
@@ -142,10 +170,11 @@ class ApplicationPluginTest : BasePluginTest() {
   private fun commonAssertions(
     jarPath: JarPath,
     entriesContained: Array<String> = arrayOf("a.properties", "a2.properties", "shadow/Main.class"),
+    mainClassAttr: String = "shadow.Main",
   ) {
     assertThat(jarPath).useAll {
       containsEntries(*entriesContained)
-      getMainAttr("Main-Class").isEqualTo("shadow.Main")
+      getMainAttr("Main-Class").isEqualTo(mainClassAttr)
     }
   }
 
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
index 54d85eaf2..7a9bc607c 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
@@ -155,16 +155,17 @@ abstract class BasePluginTest {
 
   fun writeMainClass(
     withImports: Boolean = false,
+    className: String = "Main",
   ) {
     val imports = if (withImports) "import junit.framework.Test;" else ""
 
-    path("src/main/java/shadow/Main.java").writeText(
+    path("src/main/java/shadow/$className.java").writeText(
       """
         package shadow;
         $imports
-        public class Main {
+        public class $className {
           public static void main(String[] args) {
-            String content = String.format("Hello, World! (%s)", args);
+            String content = String.format("Hello, World! (%s) from $className", args);
             System.out.println(content);
           }
         }