Repository: GradleUp/shadow
Pull request #1337 (2025-03-12 03:40:44)

Set `Main-Class` attr for KMP 2.1.0 or above
- Follow up #1280.
- Refs #1333.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.


Goooler: /gemini review

Patch:
diff --git a/docs/changes/README.md b/docs/changes/README.md
index fe410590..702487ab 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -7,6 +7,7 @@
 
 - Add Kotlin DSL examples in docs. ([#1306](https://github.com/GradleUp/shadow/pull/1306))
 - Support using type-safe dependency accessors in `ShadowJar.dependencies`. ([#1322](https://github.com/GradleUp/shadow/pull/1322))
+- Set `Main-Class` attr for KMP 2.1.0 or above. ([#1337](https://github.com/GradleUp/shadow/pull/1337))
 
 **Changed**
 
diff --git a/docs/kmp-plugin/README.md b/docs/kmp-plugin/README.md
index 8c7c682e..3bcdbc10 100644
--- a/docs/kmp-plugin/README.md
+++ b/docs/kmp-plugin/README.md
@@ -15,7 +15,10 @@ configure additional tasks for bundling the shadowed JAR for its `jvm` target.
     val ktorVersion = "3.1.0"
 
     kotlin {
-      jvm()
+      jvm().mainRun {
+        // Optionally, set the main class for `runJvm`, it's available from Kotlin 2.1.0
+        mainClass = "myapp.MainKt"
+      }
       sourceSets {
         val commonMain by getting {
           dependencies {
@@ -49,7 +52,10 @@ configure additional tasks for bundling the shadowed JAR for its `jvm` target.
     def ktorVersion = "3.1.0"
 
     kotlin {
-      jvm()
+      jvm().mainRun {
+        // Optionally, set the main class for `runJvm`, it's available from Kotlin 2.1.0
+        it.mainClass.set('myapp.MainKt')
+      }
       sourceSets {
         commonMain {
           dependencies {
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt
index c0c44304..b2204a1f 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt
@@ -1,15 +1,21 @@
 package com.github.jengelman.gradle.plugins.shadow
 
 import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.registerShadowJarCommon
+import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import kotlin.collections.contains
 import org.gradle.api.Plugin
 import org.gradle.api.Project
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion as KgpVersion
 
 public abstract class ShadowKmpPlugin : Plugin<Project> {
+  private lateinit var kmpExtension: KotlinMultiplatformExtension
 
   override fun apply(project: Project) {
     with(project) {
-      val kmpExtension = extensions.getByType(KotlinMultiplatformExtension::class.java)
+      kmpExtension = extensions.getByType(KotlinMultiplatformExtension::class.java)
       val kotlinJvmMain = kmpExtension.jvm().compilations.named("main")
       registerShadowJarCommon { task ->
         task.from(kotlinJvmMain.map { it.output.allOutputs })
@@ -18,6 +24,24 @@ public abstract class ShadowKmpPlugin : Plugin<Project> {
             listOf(configurations.getByName(kotlinJvmMain.get().runtimeDependencyConfigurationName))
           },
         )
+        configureMainClass(task)
+      }
+    }
+  }
+
+  private fun Project.configureMainClass(task: ShadowJar) {
+    if (KgpVersion.DEFAULT < KgpVersion.KOTLIN_2_1) return
+
+    @OptIn(ExperimentalKotlinGradlePluginApi::class)
+    kmpExtension.jvm().mainRun {
+      // Fix cannot serialize object of type 'org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmRun'.
+      val mainClassName = provider { mainClass }
+      task.inputs.property("mainClassName", mainClassName)
+      task.doFirst {
+        val realClass = mainClassName.get().orNull
+        if (!task.manifest.attributes.contains(mainClassAttributeKey) && !realClass.isNullOrEmpty()) {
+          task.manifest.attributes[mainClassAttributeKey] = realClass
+        }
       }
     }
   }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KmpPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KmpPluginTest.kt
index 4b014c37..f0092425 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KmpPluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KmpPluginTest.kt
@@ -1,11 +1,16 @@
 package com.github.jengelman.gradle.plugins.shadow
 
 import assertk.assertThat
+import assertk.assertions.isEqualTo
+import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.util.containsEntries
+import com.github.jengelman.gradle.plugins.shadow.util.getMainAttr
 import kotlin.io.path.appendText
 import kotlin.io.path.writeText
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
 class KmpPluginTest : BasePluginTest() {
   @BeforeEach
@@ -51,4 +56,42 @@ class KmpPluginTest : BasePluginTest() {
       )
     }
   }
+
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun canSetMainClassAttribute(useShadowAttr: Boolean) {
+    val mainClassEntry = writeClass(sourceSet = "jvmMain", isJava = false)
+    val main2ClassEntry = writeClass(sourceSet = "jvmMain", isJava = false, className = "Main2")
+    val mainClassName = "my.Main"
+    val main2ClassName = "my.Main2"
+    val mainAttr = if (useShadowAttr) "attributes '$mainClassAttributeKey': '$main2ClassName'" else ""
+    projectScriptPath.appendText(
+      """
+        kotlin {
+          jvm().mainRun {
+            it.mainClass.set('$mainClassName')
+          }
+        }
+        $shadowJar {
+          manifest {
+            $mainAttr
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      containsEntries(
+        mainClassEntry,
+        main2ClassEntry,
+      )
+      if (useShadowAttr) {
+        getMainAttr(mainClassAttributeKey).isEqualTo(main2ClassName)
+      } else {
+        getMainAttr("Main-Class").isEqualTo(mainClassName)
+      }
+    }
+  }
 }