Repository: GradleUp/shadow (kotlin)

Issue:
Plugin is incompatible with isolated projects feature
Please check the [User Guide](http://imperceptiblethoughts.com/shadow) before submitting "how do I do 'x'?" questions!

### Shadow Version

8.1.1

### Gradle Version

8.5

### Expected Behavior

Plugin does not violate [project isolation](https://gradle.github.io/configuration-cache/) constraints, no errors are raised when a project to which the plugin is applied which also enables project isolation is being imported into IntelliJ.

### Actual Behavior

An error is thrown when a project to which the plugin is applied which also enables project isolation is being imported into IntelliJ. The following stacktrace is present in the configuration cache report generated upon the error:
```
org.gradle.api.InvalidUserCodeException: Cannot access project ':' from project ':services'
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callGetProperty(AbstractCallSite.java:329)
	at com.github.jengelman.gradle.plugins.shadow.ShadowPlugin$_apply_closure1.doCall$original(ShadowPlugin.groovy:24)
	at com.github.jengelman.gradle.plugins.shadow.ShadowPlugin$_apply_closure1.doCall(ShadowPlugin.groovy)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:139)
	at com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.apply(ShadowPlugin.groovy:15)
	at com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.apply(ShadowPlugin.groovy)
	at org.gradle.api.internal.plugins.DefaultPluginContainer.apply(DefaultPluginContainer.java:70)
	....
```

That's because [the plugin calls `getPlugins()` on the `rootProject` instance from sub projects](https://github.com/johnrengelman/shadow/blob/2f94a2558023b208ffb840471281703cb2c9b222/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy#L24) which is not compatible with isolated projects.

One fix for this problem would be to check if `com.gradle.enterprise` plugin is applied to the `Settings` object [(because that's what you should be applying the plugin in newer versions to according to the docs](https://docs.gradle.com/enterprise/gradle-plugin/#gradle_6_x_and_later)) rather than the root project object but sadly you cannot get to an instance of `Settings` from an instance of `Project`. Another fix would be for the `com.gradle.enterprise` plugin to register itself as a plugin on `Gradle` instance (to which you can get from an instance of `Project`) together with an instance of `GradleEnterpriseExtension` but for that we would probably need core Gradle team to change the implementation as I don't believe this is happening now (hard to tell given `com.gradle.enterprise` is closed source).

As a workaround, I will reverse engineer `com.github.jengelman.gradle.plugins.shadow.ShadowPlugin` as I have a convention plugin applying it in my project and simply boil it down to:
```
project.with {
    plugins.apply(ShadowBasePlugin)
    plugins.withType(JavaPlugin) {
        plugins.apply(ShadowJavaPlugin)
    }
}
```
Nevertheless, it would be good if this plugin was compatible with project isolation out of the box.

Pull request description:
Restore the Develocity Build Scan integration, which adds custom values to Build Scans about the execution of Shadow tasks. It had been first disabled (#993) to resolve #907 for consumers enabling [Isolated Projects][1] (IP), then removed altogether (#1014). 

- Code is added similar to what was removed #1014, but with changes to migrate it to Kotlin (it was Groovy)
- The code is made IP-compatible by using the `buildScan` extension that's available in subprojects (cd9c60071fa5af35a3bd82cd695c121c7ae34322), instead of the root project's extension. That was added in [Develocity Gradle plugin 4.0][2].
- A `compileOnly` dependency is added for `ShadowPlugin` to reference the `buildScan` extension class (that wasn't necessary in Groovy)
- Support for `com.gradle.enterprise` which was present in the original code is omitted. There's no version of that plugin supporting the `buildScan` extension in subprojects, thus adding custom values would have to take place in the root project (not IP-compatible). That plugin is replaced by `com.gradle.develocity` and no longer maintained.

Currently, the custom values added by this PR are an example, since `ShadowStats` was also removed recently (#1264).

### Tests

A functional test around IP compatibility is added to demonstrate that the fix works and to prevent future regressions.

- [Build Scan showing the test failing before the fix][3], reproducing the same error reported in the original issue (#907)
- [Build Scan showing the test passing after the fix][4]

[1]: https://docs.gradle.org/current/userguide/isolated_projects.html
[2]: https://docs.gradle.com/develocity/gradle-plugin/current/#4_0
[3]: https://scans.gradle.com/s/r3yoxewbrtx5g/tests/task/:functionalTest/details/com.github.jengelman.gradle.plugins.shadow.IsolatedProjectsCompatibilityTest/applyToProjectWithIsolatedProjectsEnabled()?top-execution=1
[4]: https://scans.gradle.com/s/y2dxeizmtcobe/tests/task/:functionalTest/details/com.github.jengelman.gradle.plugins.shadow.IsolatedProjectsCompatibilityTest/applyToProjectWithIsolatedProjectsEnabled()?top-execution=1

---

- [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 bea4872d9..8973dc15a 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -66,11 +66,16 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin
 }
 
 public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowPlugin : org/gradle/api/Plugin {
+	public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/ShadowPlugin$Companion;
+	public static final field ENABLE_DEVELOCITY_INTEGRATION_PROPERTY Ljava/lang/String;
 	public fun <init> ()V
 	public synthetic fun apply (Ljava/lang/Object;)V
 	public fun apply (Lorg/gradle/api/Project;)V
 }
 
+public final class com/github/jengelman/gradle/plugins/shadow/ShadowPlugin$Companion {
+}
+
 public abstract class com/github/jengelman/gradle/plugins/shadow/legacy/LegacyShadowPlugin : org/gradle/api/Plugin {
 	public fun <init> ()V
 	public synthetic fun apply (Ljava/lang/Object;)V
diff --git a/build.gradle.kts b/build.gradle.kts
index b2a6fc6c3..97f5f1c35 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -107,6 +107,7 @@ develocity {
 }
 
 dependencies {
+  compileOnly(libs.develocity)
   compileOnly(libs.kotlin.kmp)
   api(libs.apache.ant) // Types from Ant are exposed in the public API.
   implementation(libs.apache.commonsIo)
@@ -119,6 +120,7 @@ dependencies {
 
   testPluginClasspath(libs.agp)
   testPluginClasspath(libs.foojayResolver)
+  testPluginClasspath(libs.develocity)
   testPluginClasspath(libs.kotlin.kmp)
   testPluginClasspath(libs.pluginPublish)
 
diff --git a/docs/changes/README.md b/docs/changes/README.md
index b9ea958ca..828fc3048 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -11,6 +11,10 @@
 
 - Honor `options.release` for target JVM attribute. ([#1502](https://github.com/GradleUp/shadow/pull/1502))
 
+**Changed**
+- Restore Develocity Build Scan integration. ([#1505](https://github.com/GradleUp/shadow/pull/1505))  
+  It is still disabled by default, you can enable it by setting `com.gradleup.shadow.enableDevelocityIntegration = true`.
+
 ## [9.0.0-rc1](https://github.com/GradleUp/shadow/releases/tag/9.0.0-rc1) - 2025-07-02
 
 > This release is a major update from the 8.3.x series. The plugin has been fully rewritten in Kotlin, bringing
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ba42ffbe9..1ef1e3b62 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,6 +19,7 @@ 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:1.0.0"
+develocity = "com.gradle:develocity-gradle-plugin:4.0.2"
 kotlin-kmp = { module = "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin", version.ref = "kotlin" }
 pluginPublish = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "pluginPublish" }
 # AGP version should match the min Gradle version used in tests.
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 1f0b52a8f..7d176d2f1 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
@@ -1,6 +1,8 @@
 package com.github.jengelman.gradle.plugins.shadow
 
 import com.github.jengelman.gradle.plugins.shadow.internal.KOTLIN_MULTIPLATFORM_PLUGIN_ID
+import com.github.jengelman.gradle.plugins.shadow.internal.addBuildScanCustomValues
+import com.github.jengelman.gradle.plugins.shadow.internal.findOptionalProperty
 import com.github.jengelman.gradle.plugins.shadow.legacy.LegacyShadowPlugin
 import org.gradle.api.Plugin
 import org.gradle.api.Project
@@ -28,6 +30,7 @@ public abstract class ShadowPlugin : Plugin<Project> {
           "See https://developer.android.com/build/publish-library/fused-library",
       )
     }
+    project.configureBuildScan()
 
     // Apply the legacy plugin last.
     // Because we apply the ShadowJavaPlugin/ShadowApplication plugin in a withType callback for the
@@ -36,4 +39,19 @@ public abstract class ShadowPlugin : Plugin<Project> {
     // the behavior with the old plugin when applying in that order.
     apply(LegacyShadowPlugin::class.java)
   }
+
+  private fun Project.configureBuildScan() {
+    val enableDevelocityIntegration = findOptionalProperty(ENABLE_DEVELOCITY_INTEGRATION_PROPERTY)?.toBoolean() ?: false
+    if (enableDevelocityIntegration) {
+      logger.info("Enabling Develocity integration for Shadow plugin.")
+    } else {
+      logger.info("Skipping Develocity integration for Shadow plugin.")
+      return
+    }
+    addBuildScanCustomValues()
+  }
+
+  public companion object {
+    public const val ENABLE_DEVELOCITY_INTEGRATION_PROPERTY: String = "com.gradleup.shadow.enableDevelocityIntegration"
+  }
 }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt
index 170668b21..b3f7ca15b 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt
@@ -1,5 +1,7 @@
 package com.github.jengelman.gradle.plugins.shadow.internal
 
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import com.gradle.develocity.agent.gradle.DevelocityConfiguration
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.distribution.DistributionContainer
@@ -42,6 +44,20 @@ internal inline val Project.javaPluginExtension: JavaPluginExtension
 internal inline val Project.javaToolchainService: JavaToolchainService
   get() = extensions.getByType(JavaToolchainService::class.java)
 
+@Suppress("GradleProjectIsolation") // TODO: we can't call 'providers.gradleProperty' instead due to https://github.com/gradle/gradle/issues/23572.
+internal fun Project.findOptionalProperty(propertyName: String): String? = findProperty(propertyName)?.toString()
+
+internal fun Project.addBuildScanCustomValues() {
+  val develocity = extensions.findByType(DevelocityConfiguration::class.java) ?: return
+  val buildScan = develocity.buildScan
+  tasks.withType(ShadowJar::class.java).configureEach { task ->
+    buildScan.buildFinished {
+      buildScan.value("shadow.${task.path}.executed", "true")
+      buildScan.value("shadow.${task.path}.didWork", task.didWork.toString())
+    }
+  }
+}
+
 internal inline val TaskContainer.jar: TaskProvider<Jar>
   get() = named("jar", Jar::class.java)
 


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 f5908ae2e..658fa4978 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
@@ -427,6 +427,10 @@ abstract class BasePluginTest {
       "-Dorg.gradle.configuration-cache.parallel=true",
     )
 
+    // TODO: enable this flag for all tests once we have fixed all issues with isolated projects.
+    //  See https://github.com/GradleUp/shadow/pull/1139.
+    const val IP_ARGUMENT = "-Dorg.gradle.unsafe.isolated-projects=true"
+
     fun String.toProperties(): Properties = Properties().apply { load(byteInputStream()) }
 
     fun implementationFiles(vararg paths: Path): String {
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
index b205fa180..9aa61b2dd 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginTest.kt
@@ -16,6 +16,7 @@ import assertk.assertions.isNull
 import assertk.assertions.isTrue
 import assertk.assertions.single
 import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_JAR_TASK_NAME
+import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.Companion.ENABLE_DEVELOCITY_INTEGRATION_PROPERTY
 import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey
 import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey
@@ -34,6 +35,7 @@ import kotlin.io.path.appendText
 import kotlin.io.path.invariantSeparatorsPathString
 import kotlin.io.path.name
 import kotlin.io.path.outputStream
+import kotlin.io.path.readText
 import kotlin.io.path.writeText
 import org.gradle.api.plugins.JavaPlugin
 import org.gradle.api.plugins.JavaPlugin.API_CONFIGURATION_NAME
@@ -795,6 +797,35 @@ class JavaPluginTest : BasePluginTest() {
     }
   }
 
+  @Test
+  fun integrateWithDevelocityBuildScan() {
+    writeClientAndServerModules()
+    settingsScriptPath.writeText(
+      """
+        plugins {
+          id 'com.gradle.develocity'
+        }
+        ${settingsScriptPath.readText()}
+      """.trimIndent(),
+    )
+
+    val result = run(
+      serverShadowJarTask,
+      IP_ARGUMENT,
+      "-P${ENABLE_DEVELOCITY_INTEGRATION_PROPERTY}=true",
+      "-Dscan.dump", // Using scan.dump avoids actually publishing a Build Scan, writing it to a file instead.
+      "--info",
+    )
+
+    assertThat(result.output).all {
+      contains(
+        "Enabling Develocity integration for Shadow plugin.",
+        "Build scan written",
+      )
+      doesNotContain("Configuration cache problems")
+    }
+  }
+
   private fun dependencies(configuration: String, vararg flags: String): String {
     return run("dependencies", "--configuration", configuration, *flags).output
   }