Repository: GradleUp/shadow (kotlin)

Issue:
Unable to publish only unshadowed artifacts
I've used 5.2.0 before and then decided to migrate to 6.1.0
What I found is that `project.components.java` is updated with shadowJar configurations here:
https://github.com/johnrengelman/shadow/blame/b3677a26c706aec9aa7b035b9d36a091523aff97/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy#L56

So, I can't use maven-publish to publish only raw, unshadowed JARs: publication JAR conflict happens.
Please, make adding shadowJar things to java software component optional: I'd enable this option in project config.
Thank you!

Pull request description:
Closes #651.

---

- [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 e2b278bb2..a8a91dad2 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -30,10 +30,12 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugi
 }
 
 public final class com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin$Companion {
+	public final synthetic fun getShadow (Lorg/gradle/api/Project;)Lcom/github/jengelman/gradle/plugins/shadow/ShadowExtension;
 	public final synthetic fun getShadow (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider;
 }
 
 public abstract interface class com/github/jengelman/gradle/plugins/shadow/ShadowExtension {
+	public abstract fun getAddShadowVariantIntoJavaComponent ()Lorg/gradle/api/provider/Property;
 }
 
 public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin : org/gradle/api/Plugin {
diff --git a/docs/changes/README.md b/docs/changes/README.md
index b1970711f..a7f50a13b 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -5,6 +5,10 @@
 
 ## Changed
 
+- Allow opting out of `shadowRuntimeElements` variant. ([#1662](https://github.com/GradleUp/shadow/pull/1662))
+
+## Changed
+
 - Don't inject `TargetJvmVersion` attribute when automatic JVM targeting is disabled. ([#1666](https://github.com/GradleUp/shadow/pull/1666))
 
 
diff --git a/docs/publishing/README.md b/docs/publishing/README.md
index 1c07c40d3..a8feeccc7 100644
--- a/docs/publishing/README.md
+++ b/docs/publishing/README.md
@@ -49,6 +49,64 @@ artifact and dependencies in the POM file.
     }
     ```
 
+## Shadow Variant in Default Java Component
+
+The Shadow plugin adds an optional variant to the `java` component when publishing. This variant contains the shadowed
+JAR. This allows consumers of the published library to choose between the standard JAR and the shadowed JAR.
+
+This feature is enabled by default. It can be disabled by setting the `addShadowVariantIntoJavaComponent` property in
+the `shadow` extension to `false`. If you want to publish the standard JAR only, disable this feature like:
+
+=== "Kotlin"
+
+    ```kotlin
+    plugins {
+      java
+      `maven-publish`
+      id("com.gradleup.shadow")
+    }
+
+    shadow {
+      addShadowVariantIntoJavaComponent = false
+    }
+
+    publishing {
+      publications {
+        create<MavenPublication>("shadow") {
+          from(components["shadow"])
+        }
+      }
+      repositories {
+        maven("https://repo.myorg.com")
+      }
+    }
+    ```
+
+=== "Groovy"
+
+    ```groovy
+    plugins {
+      id 'java'
+      id 'maven-publish'
+      id 'com.gradleup.shadow'
+    }
+
+    shadow {
+      addShadowVariantIntoJavaComponent = false
+    }
+
+    publishing {
+      publications {
+        shadow(MavenPublication) {
+          from components.shadow
+        }
+      }
+      repositories {
+        maven { url = 'https://repo.myorg.com' }
+      }
+    }
+    ```
+
 ## Shadow Configuration and Publishing
 
 The Shadow plugin provides a custom configuration (`configurations.shadow`) to specify
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
index 7292e981b..b4d7b0052 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt
@@ -1,5 +1,7 @@
 package com.github.jengelman.gradle.plugins.shadow
 
+import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.CONFIGURATION_NAME
+import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.EXTENSION_NAME
 import org.gradle.api.GradleException
 import org.gradle.api.NamedDomainObjectProvider
 import org.gradle.api.Plugin
@@ -17,7 +19,9 @@ public abstract class ShadowBasePlugin : Plugin<Project> {
     if (GradleVersion.current() < GradleVersion.version("8.11")) {
       throw GradleException("This version of Shadow supports Gradle 8.11+ only. Please upgrade.")
     }
-    extensions.create(EXTENSION_NAME, ShadowExtension::class.java)
+    with(extensions.create(EXTENSION_NAME, ShadowExtension::class.java)) {
+      addShadowVariantIntoJavaComponent.convention(true)
+    }
     @Suppress("EagerGradleConfiguration") // this should be created eagerly.
     configurations.create(CONFIGURATION_NAME)
   }
@@ -45,5 +49,9 @@ public abstract class ShadowBasePlugin : Plugin<Project> {
     @get:JvmSynthetic
     public inline val ConfigurationContainer.shadow: NamedDomainObjectProvider<Configuration>
       get() = named(CONFIGURATION_NAME)
+
+    @get:JvmSynthetic
+    public inline val Project.shadow: ShadowExtension
+      get() = extensions.getByType(ShadowExtension::class.java)
   }
 }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt
index c20749974..6fe3df63b 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt
@@ -1,6 +1,13 @@
 package com.github.jengelman.gradle.plugins.shadow
 
-/**
- * TODO: haven't removed this as I want to address issues like https://github.com/GradleUp/shadow/issues/651 in it.
- */
-public interface ShadowExtension
+import org.gradle.api.provider.Property
+
+public interface ShadowExtension {
+  /**
+   * If `true`, publishes the [ShadowJavaPlugin.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME] as an optional variant of
+   * the `java` component. This affects how consumers resolve the published artifact.
+   *
+   * Defaults to `true`.
+   */
+  public val addShadowVariantIntoJavaComponent: Property<Boolean>
+}
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 a3982d2c5..78ed3bf26 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
@@ -86,16 +86,19 @@ public abstract class ShadowJavaPlugin @Inject constructor(
 
   protected open fun Project.configureComponents() {
     val shadowRuntimeElements = configurations.shadowRuntimeElements.get()
-    components.named("java", AdhocComponentWithVariants::class.java) {
-      it.addVariantsFromConfiguration(shadowRuntimeElements) { variant ->
-        variant.mapToOptional()
-      }
-    }
     val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME)
     components.add(shadowComponent)
     shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { variant ->
       variant.mapToMavenScope("runtime")
     }
+    afterEvaluate {
+      if (!shadow.addShadowVariantIntoJavaComponent.get()) return@afterEvaluate
+      components.named("java", AdhocComponentWithVariants::class.java) {
+        it.addVariantsFromConfiguration(shadowRuntimeElements) { variant ->
+          variant.mapToOptional()
+        }
+      }
+    }
   }
 
   protected open fun Project.configureJavaGradlePlugin() {


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 98a25cb73..dfc1da6c9 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
@@ -66,6 +66,10 @@ class JavaPluginsTest : BasePluginTest() {
     assertThat(project.plugins.hasPlugin(LegacyShadowPlugin::class.java)).isTrue()
     assertThat(project.tasks.findByName(SHADOW_JAR_TASK_NAME)).isNull()
 
+    with(project.extensions.getByType(ShadowExtension::class.java)) {
+      assertThat(addShadowVariantIntoJavaComponent.get()).isTrue()
+    }
+
     project.plugins.apply(JavaPlugin::class.java)
     val shadowTask = project.tasks.getByName(SHADOW_JAR_TASK_NAME) as ShadowJar
     val shadowConfig = project.configurations.getByName(ShadowBasePlugin.CONFIGURATION_NAME)
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt
index 3b650516e..24dc887bc 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt
@@ -46,6 +46,8 @@ import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.condition.DisabledOnOs
 import org.junit.jupiter.api.condition.OS
 import org.junit.jupiter.api.io.TempDir
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
 class PublishingTest : BasePluginTest() {
   @TempDir
@@ -435,6 +437,108 @@ class PublishingTest : BasePluginTest() {
     }
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/651",
+  )
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun publishShadowVariantJar(addShadowVariant: Boolean) {
+    projectScript.appendText(
+      publishingBlock(
+        projectBlock = """
+          dependencies {
+            implementation 'my:a:1.0'
+            shadow 'my:b:1.0'
+          }
+          shadow {
+            addShadowVariantIntoJavaComponent = $addShadowVariant
+          }
+        """.trimIndent(),
+        publicationsBlock = """
+          shadow(MavenPublication) {
+            from components.java
+          }
+        """.trimIndent(),
+      ),
+    )
+
+    publish()
+
+    val assertVariantsCommon = { gmm: GradleModuleMetadata ->
+      assertThat(gmm.apiElementsVariant).all {
+        transform { it.attributes }.containsOnly(
+          *commonVariantAttrs,
+          Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL,
+          Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_API,
+        )
+        transform { it.coordinates }.isEmpty()
+      }
+      assertThat(gmm.runtimeElementsVariant).all {
+        transform { it.attributes }.containsOnly(
+          *commonVariantAttrs,
+          Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL,
+          Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME,
+        )
+        transform { it.coordinates }.containsOnly(
+          "my:a:1.0",
+        )
+      }
+    }
+    val entriesCommon = arrayOf(
+      "maven-1.0.jar",
+      "maven-1.0.jar.md5",
+      "maven-1.0.jar.sha1",
+      "maven-1.0.jar.sha256",
+      "maven-1.0.jar.sha512",
+      "maven-1.0.module",
+      "maven-1.0.module.md5",
+      "maven-1.0.module.sha1",
+      "maven-1.0.module.sha256",
+      "maven-1.0.module.sha512",
+      "maven-1.0.pom",
+      "maven-1.0.pom.md5",
+      "maven-1.0.pom.sha1",
+      "maven-1.0.pom.sha256",
+      "maven-1.0.pom.sha512",
+    )
+    val artifactEntries = repoPath("my/maven/1.0/").entries
+    val gmm = gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module"))
+    val pomDependencies = pomReader.read(repoPath("my/maven/1.0/maven-1.0.pom"))
+      .dependencies.map { it.coordinate to it.scope }
+
+    if (addShadowVariant) {
+      assertThat(artifactEntries).containsOnly(
+        "maven-1.0-all.jar",
+        "maven-1.0-all.jar.md5",
+        "maven-1.0-all.jar.sha1",
+        "maven-1.0-all.jar.sha256",
+        "maven-1.0-all.jar.sha512",
+        *entriesCommon,
+      )
+      assertThat(gmm.variantNames).containsOnly(
+        API_ELEMENTS_CONFIGURATION_NAME,
+        RUNTIME_ELEMENTS_CONFIGURATION_NAME,
+        SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME,
+      )
+      assertVariantsCommon(gmm)
+      assertShadowVariantCommon(gmm)
+      assertThat(pomDependencies).containsOnly(
+        "my:a:1.0" to "runtime",
+        "my:b:1.0" to "compile",
+      )
+    } else {
+      assertThat(artifactEntries).containsOnly(*entriesCommon)
+      assertThat(gmm.variantNames).containsOnly(
+        API_ELEMENTS_CONFIGURATION_NAME,
+        RUNTIME_ELEMENTS_CONFIGURATION_NAME,
+      )
+      assertVariantsCommon(gmm)
+      assertThat(pomDependencies).containsOnly(
+        "my:a:1.0" to "runtime",
+      )
+    }
+  }
+
   private fun repoPath(relative: String): Path {
     return remoteRepoPath.resolve(relative).also {
       check(it.exists()) { "Path not found: $it" }