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 ()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/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, + ) + } + } +} 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 get() = named(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + + @JvmStatic + public fun Project.registerShadowJarCommon( + action: Action, + ): TaskProvider { + 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 { + + 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 { 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.