Repository: GradleUp/shadow (kotlin)

Issue:
Shadow + Kotlin Multiplatform
Please check the [User Guide](http://imperceptiblethoughts.com/shadow) before submitting "how do I do 'x'?" questions!

### Shadow Version
4.0.2

### Gradle Version
5.0

### Kotlin Version
1,3.20

### Expected Behavior
```
implementation project(":kotlin-mpp-project-with-jvm-preset")
```
Supposed to shade a jar from the project.

```
shadow project(":kotlin-mpp-project-with-jvm-preset")
```
Supposed to add `kotlin-mpp-project-with-jvm-preset-jvm` dependency to the shadow pom.

### Actual Behavior

Neither of those things happen.

Pull request description:
- Closes #451
- Closes #484.
- Closes #585.

---

- [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/api/shadow.api b/api/shadow.api
index 1feac44ae..5570a339b 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -51,11 +51,19 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugi
 	protected fun configureConfigurations (Lorg/gradle/api/Project;)V
 	protected fun configureJavaGradlePlugin (Lorg/gradle/api/Project;)V
 	protected fun configureShadowJar (Lorg/gradle/api/Project;)V
+	public static final fun registerShadowJarCommon (Lorg/gradle/api/Project;Lorg/gradle/api/Action;)Lorg/gradle/api/tasks/TaskProvider;
 }
 
 public final class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin$Companion {
 	public final fun getShadowJar (Lorg/gradle/api/tasks/TaskContainer;)Lorg/gradle/api/tasks/TaskProvider;
 	public final fun getShadowRuntimeElements (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider;
+	public final fun registerShadowJarCommon (Lorg/gradle/api/Project;Lorg/gradle/api/Action;)Lorg/gradle/api/tasks/TaskProvider;
+}
+
+public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin : org/gradle/api/Plugin {
+	public fun <init> ()V
+	public synthetic fun apply (Ljava/lang/Object;)V
+	public fun apply (Lorg/gradle/api/Project;)V
 }
 
 public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowPlugin : org/gradle/api/Plugin {
diff --git a/build.gradle.kts b/build.gradle.kts
index 81ba15916..24b04fc1c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -51,6 +51,7 @@ val testPluginClasspath by configurations.registering {
 }
 
 dependencies {
+  compileOnly(libs.kotlin.kmp)
   implementation(libs.apache.ant)
   implementation(libs.apache.commonsIo)
   implementation(libs.apache.log4j)
@@ -62,6 +63,7 @@ dependencies {
 
   testPluginClasspath(libs.foojayResolver)
   testPluginClasspath(libs.pluginPublish)
+  testPluginClasspath(libs.kotlin.kmp)
 
   lintChecks(libs.androidx.gradlePluginLints)
 }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7f847f636..07266742a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,4 +1,5 @@
 [versions]
+kotlin = "2.1.10"
 moshi = "1.15.2"
 
 [libraries]
@@ -15,13 +16,14 @@ plexus-xml = "org.codehaus.plexus:plexus-xml:4.0.4"
 xmlunit = "org.xmlunit:xmlunit-legacy:2.10.0"
 moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
 moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
-foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin:0.9.0"
 
 pluginPublish = "com.gradle.publish:plugin-publish-plugin:1.3.1"
 mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.30.0"
 gitPublish = "org.ajoberstar.git-publish:gradle-git-publish:5.1.0"
 jetbrains-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0"
 node = "com.github.node-gradle:gradle-node-plugin:7.1.0"
+foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin:0.9.0"
+kotlin-kmp = { module = "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin", version.ref = "kotlin" }
 
 androidx-gradlePluginLints = "androidx.lint:lint-gradle:1.0.0-alpha03"
 # Dummy to get renovate updates, the version is used in rootProject build.gradle with spotless.
@@ -31,7 +33,7 @@ junit-bom = "org.junit:junit-bom:5.12.0"
 assertk = "com.willowtreeapps.assertk:assertk:0.28.1"
 
 [plugins]
-kotlin-jvm = "org.jetbrains.kotlin.jvm:2.1.10"
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 android-lint = "com.android.lint:8.8.1"
 jetbrains-bcv = "org.jetbrains.kotlinx.binary-compatibility-validator:0.17.0"
 spotless = "com.diffplug.spotless:7.0.2"
diff --git a/src/docs/.vuepress/config.js b/src/docs/.vuepress/config.js
index 5e6b4b157..0d1794345 100644
--- a/src/docs/.vuepress/config.js
+++ b/src/docs/.vuepress/config.js
@@ -26,6 +26,7 @@ module.exports = {
       '/configuration/reproducible-builds/',
       '/custom-tasks/',
       '/application-plugin/',
+      '/kmp-plugin/',
       '/publishing/',
       '/multi-project/',
       '/plugins/',
diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md
index a29f644aa..6b9c854f4 100644
--- a/src/docs/changes/README.md
+++ b/src/docs/changes/README.md
@@ -3,6 +3,11 @@
 
 ## [Unreleased]
 
+**Added**
+
+- Compat Kotlin Multiplatform plugin. ([#1280](https://github.com/GradleUp/shadow/pull/1280))  
+  You still need to manually configure `manifest.attributes` (e.g. `Main-Class` attr) in the `shadowJar` task if necessary.
+
 **Fixed**
 
 - Fix the last modified time of shadowed directories. ([#1277](https://github.com/GradleUp/shadow/pull/1277))
diff --git a/src/docs/kmp-plugin/README.md b/src/docs/kmp-plugin/README.md
new file mode 100644
index 000000000..207c91e29
--- /dev/null
+++ b/src/docs/kmp-plugin/README.md
@@ -0,0 +1,37 @@
+# Integrating with Kotlin Multiplatform Plugin
+
+Shadow honors Kotlin's
+[`org.jetbrains.kotlin.multiplatform`](https://kotlinlang.org/docs/multiplatform-intro.html) plugin and will automatically
+configure additional tasks for bundling the shadowed JAR for its `jvm` target.
+```groovy
+// Using Shadow with KMP Plugin
+plugins {
+  id 'org.jetbrains.kotlin.multiplatform'
+  id 'com.gradleup.shadow'
+}
+
+def ktorVersion = "3.1.0"
+
+kotlin {
+  jvm()
+  sourceSets {
+    commonMain {
+      dependencies {
+        implementation "io.ktor:ktor-client-core$ktorVersion"
+      }
+    }
+    jvmMain {
+      dependencies {
+        implementation "io.ktor:ktor-client-okhttp$ktorVersion"
+      }
+    }
+  }
+}
+
+tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+  manifest {
+    // Optionally, set the main class for the shadowed JAR.
+    attributes 'Main-Class': 'com.example.MainKt'
+  }
+}
+```
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt
index 43a8325c0..b93e64ee7 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt
@@ -8,6 +8,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration
 import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets
 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import javax.inject.Inject
+import org.gradle.api.Action
 import org.gradle.api.NamedDomainObjectProvider
 import org.gradle.api.Plugin
 import org.gradle.api.Project
@@ -38,10 +39,7 @@ public abstract class ShadowJavaPlugin @Inject constructor(
 
   protected open fun Project.configureShadowJar() {
     val jarTask = tasks.jar
-    val taskProvider = tasks.register(SHADOW_JAR_TASK_NAME, ShadowJar::class.java) { task ->
-      task.group = ShadowBasePlugin.GROUP_NAME
-      task.description = "Create a combined JAR of project and runtime dependencies"
-      task.archiveClassifier.set("all")
+    val taskProvider = registerShadowJarCommon { task ->
       @Suppress("EagerGradleConfiguration")
       task.manifest.inheritFrom(jarTask.get().manifest)
       val attrProvider = jarTask.map { it.manifest.attributes[classPathAttributeKey]?.toString().orEmpty() }
@@ -54,15 +52,6 @@ public abstract class ShadowJavaPlugin @Inject constructor(
       }
       task.from(sourceSets.named("main").map { it.output })
       task.configurations.convention(provider { listOf(runtimeConfiguration) })
-      task.exclude(
-        "META-INF/INDEX.LIST",
-        "META-INF/*.SF",
-        "META-INF/*.DSA",
-        "META-INF/*.RSA",
-        // module-info.class in Multi-Release folders.
-        "META-INF/versions/**/module-info.class",
-        "module-info.class",
-      )
     }
     artifacts.add(configurations.shadow.name, taskProvider)
   }
@@ -132,5 +121,26 @@ public abstract class ShadowJavaPlugin @Inject constructor(
 
     public inline val ConfigurationContainer.shadowRuntimeElements: NamedDomainObjectProvider<Configuration>
       get() = named(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME)
+
+    @JvmStatic
+    public fun Project.registerShadowJarCommon(
+      action: Action<ShadowJar>,
+    ): TaskProvider<ShadowJar> {
+      return tasks.register(SHADOW_JAR_TASK_NAME, ShadowJar::class.java) { task ->
+        task.group = ShadowBasePlugin.GROUP_NAME
+        task.description = "Create a combined JAR of project and runtime dependencies"
+        task.archiveClassifier.set("all")
+        task.exclude(
+          "META-INF/INDEX.LIST",
+          "META-INF/*.SF",
+          "META-INF/*.DSA",
+          "META-INF/*.RSA",
+          // module-info.class in Multi-Release folders.
+          "META-INF/versions/**/module-info.class",
+          "module-info.class",
+        )
+        action.execute(task)
+      }
+    }
   }
 }
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
new file mode 100644
index 000000000..c0c443041
--- /dev/null
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt
@@ -0,0 +1,24 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.registerShadowJarCommon
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+
+public abstract class ShadowKmpPlugin : Plugin<Project> {
+
+  override fun apply(project: Project) {
+    with(project) {
+      val kmpExtension = extensions.getByType(KotlinMultiplatformExtension::class.java)
+      val kotlinJvmMain = kmpExtension.jvm().compilations.named("main")
+      registerShadowJarCommon { task ->
+        task.from(kotlinJvmMain.map { it.output.allOutputs })
+        task.configurations.convention(
+          provider {
+            listOf(configurations.getByName(kotlinJvmMain.get().runtimeDependencyConfigurationName))
+          },
+        )
+      }
+    }
+  }
+}
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.kt
index c7c7f908a..7a57fbd42 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.kt
@@ -19,6 +19,10 @@ public abstract class ShadowPlugin : Plugin<Project> {
       withType(ApplicationPlugin::class.java) {
         apply(ShadowApplicationPlugin::class.java)
       }
+      withId("org.jetbrains.kotlin.multiplatform") {
+        apply(ShadowKmpPlugin::class.java)
+      }
+
       // Apply the legacy plugin last.
       // Because we apply the ShadowJavaPlugin/ShadowApplication plugin in a withType callback for the
       // respective JavaPlugin/ApplicationPlugin, it may still apply before the shadowJar task is created etc.


Test patch:
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 a2cadc5a4..d814149fe 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
@@ -103,7 +103,7 @@ abstract class BasePluginTest {
   val outputServerShadowJar: JarPath get() = jarPath("server/build/libs/server-1.0-all.jar")
 
   fun getDefaultProjectBuildScript(
-    javaPlugin: String = "java",
+    plugin: String = "java",
     withGroup: Boolean = false,
     withVersion: Boolean = false,
   ): String {
@@ -111,7 +111,7 @@ abstract class BasePluginTest {
     val versionInfo = if (withVersion) "version = '1.0'" else ""
     return """
       plugins {
-        id('$javaPlugin')
+        id('$plugin')
         id('com.gradleup.shadow')
       }
       $groupInfo
@@ -194,26 +194,43 @@ abstract class BasePluginTest {
     packageName: String = "my",
     withImports: Boolean = false,
     className: String = "Main",
+    isJava: Boolean = true,
   ): String {
-    val imports = if (withImports) "import junit.framework.Test;" else ""
-    val classRef = if (withImports) "\"Refs: \" + Test.class.getName()" else "\"Refs: null\""
-    path("src/$sourceSet/java/$packageName/$className.java").writeText(
-      """
-        package $packageName;
-        $imports
-        public class $className {
-          public static void main(String[] args) {
-            if (args.length == 0) {
-              throw new IllegalArgumentException("No arguments provided.");
+    if (isJava) {
+      val imports = if (withImports) "import junit.framework.Test;" else ""
+      val classRef = if (withImports) "\"Refs: \" + Test.class.getName()" else "\"Refs: null\""
+      path("src/$sourceSet/java/$packageName/$className.java").writeText(
+        """
+          package $packageName;
+          $imports
+          public class $className {
+            public static void main(String[] args) {
+              if (args.length == 0) throw new IllegalArgumentException("No arguments provided.");
+              String content = String.format("Hello, World! (%s) from $className", (Object[]) args);
+              System.out.println(content);
+              System.out.println($classRef);
             }
-            String content = String.format("Hello, World! (%s) from $className", (Object[]) args);
-            System.out.println(content);
-            System.out.println($classRef);
           }
-        }
-      """.trimIndent(),
-    )
-    return packageName.replace('.', '/') + "/$className.class"
+        """.trimIndent(),
+      )
+    } else {
+      val imports = if (withImports) "import junit.framework.Test;" else ""
+      val classRef = if (withImports) "\"Refs: \" + Test.class.getName()" else "\"Refs: null\""
+      path("src/$sourceSet/kotlin/$packageName/$className.kt").writeText(
+        """
+          package $packageName
+          $imports
+          fun main(vararg args: String) {
+            if (args.isEmpty()) throw IllegalArgumentException("No arguments provided.")
+            val content ="Hello, World! (%s) from $className".format(*args)
+            println(content)
+            println($classRef)
+          }
+        """.trimIndent(),
+      )
+    }
+    val baseClassPath = packageName.replace('.', '/') + "/$className"
+    return if (isJava) "$baseClassPath.class" else "${baseClassPath}Kt.class"
   }
 
   fun writeClientAndServerModules(
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
new file mode 100644
index 000000000..0cd58c3ce
--- /dev/null
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KmpPluginTest.kt
@@ -0,0 +1,54 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import assertk.assertThat
+import com.github.jengelman.gradle.plugins.shadow.util.containsEntries
+import kotlin.io.path.appendText
+import kotlin.io.path.writeText
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class KmpPluginTest : BasePluginTest() {
+  @BeforeEach
+  override fun setup() {
+    super.setup()
+    val projectBuildScript = getDefaultProjectBuildScript(
+      plugin = "org.jetbrains.kotlin.multiplatform",
+      withGroup = true,
+      withVersion = true,
+    )
+    projectScriptPath.writeText(projectBuildScript)
+  }
+
+  @Test
+  fun compatKmpJvmTarget() {
+    val mainClass = writeMainClass(sourceSet = "jvmMain", isJava = false)
+    projectScriptPath.appendText(
+      """
+        kotlin {
+          jvm()
+          sourceSets {
+            commonMain {
+              dependencies {
+                implementation 'my:b:1.0'
+              }
+            }
+            jvmMain {
+              dependencies {
+                implementation 'my:a:1.0'
+              }
+            }
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      containsEntries(
+        mainClass,
+        *entriesInAB,
+      )
+    }
+  }
+}