How `buildscript` relates to `pluginManagement`

hi I’m looking for help in understanding how buildscript relates to pluginManagement in terms of declaring plugins and their versions. In my reproduction project there are two plugins:

When I:

  • Put both plugins in buildscript everything works fine (commit with highlighted buildscript)
  • Put both plugins in pluginManagement block, everything works fine (commit)
  • Put sqldelight in buildscript and kotlin-gradle in pluginManagement I’ve got failed build (commit)

The reason for failed build is lack of pod method with specific signature in cocoapod extension. This method (with new signature) is not present in kotlin-gradle version 1.4.0 to which the project is getting downgraded. So the questions are:

  • why adding sqldelight in buildscript downgrades kotlin-gradle? Why to version 1.4.0?
  • why it’s not happening when I add sqldelight via pluginManagement?

I’ve taken a quick peek inside the POM for com.squareup.sqldelight:gradle-plugin:1.4.4 and found this:

<dependencies>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jdk8</artifactId>
      <version>1.4.0</version>
      <scope>compile</scope>
    </dependency>
    ...
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-gradle-plugin</artifactId>
      <version>1.4.0</version>
      <scope>runtime</scope>
    </dependency>
</dependencies>

I think this at least explains where the Kotlin 1.4.0 dependency is coming from. (It is not using Kotlin 1.4.20 as you are claiming.)

My guess is that the buildscript and pluginManagement classpaths are being resolved independently from each other,

1 Like

Thank you! You’re right. com.squareup.sqldelight:gradle-plugin:1.4.4 uses Kotlin 1.4.0. This makes the issue less weird.
My mistake was that I checked their develop branch, not the 1.4.4 release.

My guess is that the buildscript and pluginManagement classpaths are being resolved independently from each other,

After your explanation on Kotlin’s version, I think they are resolved independently but later they start to depend on each other (is that even possible?) and now I’m starting to believe that buildscript is superior to pluginManagement.

I did another experiment by setting Kotlin modules with version in module’s plugin{} (and removing plugins from pluginManagement, leaving sqldelight in buildscript).

The result is broken build with message:

Plugin request for plugin already on the classpath must not include a version (source)

So does this scenario makes sense?

  1. buildscript is resolved (with Kotlin 1.4.0)
  2. pluginManagement is resolved (with Kotlin 1.4.31)
  3. Module starts building with both of the classpaths.
  4. buildscript is selected as primary classpath (again: is it possible?) so the Kotlin version will be 1.4.0

Firstly, I must emphasise that I don’t claim to understand how this part of Gradle is implemented. Having said that, are you aware that Gradle already contains its own version of the Kotlin libraries?

$ ./gradlew -version

------------------------------------------------------------
Gradle 6.8.2
------------------------------------------------------------

Build time:   2021-02-05 12:53:00 UTC
Revision:     b9bd4a5c6026ac52f690eaf2829ee26563cad426

Kotlin:       1.4.20
Groovy:       2.5.12
Ant:          Apache Ant(TM) version 1.10.9 compiled on September 27 2020
JVM:          1.8.0_202 (Oracle Corporation 25.202-b08)
OS:           Linux 5.10.22-200.fc33.x86_64 amd64

When I write Gradle plugins in Kotlin, I assume that the plugin will use Gradle’s built-in Kotlin libraries rather than trying to download its own.

But anyway, the last time I checked, I dimly remember that Gradle loaded all plugins into a single classloader whose parent classloader contained the buildscript dependencies. Splitting different versions of Kotlin across these two classloaders therefore sounds like a Bad Idea :tm: to me.

I should also point out that this sqldelight Gradle plugin has an explicit runtime dependency on the Kotlin Gradle plugin:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-gradle-plugin</artifactId>
    <version>1.4.0</version>
    <scope>runtime</scope>
</dependency>

which suggests that sqldelight could contain a line of code like this:

project.pluginManager.apply("org.jetbrains.kotlin.jvm")

If this is indeed the case then you should probably respect its choice of Kotlin version, and not try to override it.

Cheers,
Chris

1 Like

are you aware that Gradle already contains its own version of the Kotlin libraries?

I’ve heard about it but as I think about this now, I think it does include Kotlin but not Kotlin Gradle Plugin (so only standard library + runtime. Is that correct?).

Splitting different versions of Kotlin across these two classloaders therefore sounds like a Bad Idea :tm: to me.

I agree. I ended up in using only pluginManagement but I was just curious what’s happening when there are both pluginManagement and buildscript.

I should also point out that this sqldelight Gradle plugin has an explicit runtime dependency on the Kotlin Gradle plugin:

Thanks for that, I haven’t seen this. I think I’ve found a line you refer to here as project “returns this project” (docs).


Thank you very much for spending time on that, I appreciate it.

The plugins in pluginManagement { plugins { ... } } are not resolved at all.
This block does not apply any plugin anywhere.
It just defines the default versions for plugins when you apply them to the normal build scripts without specifying a version, but you can override it by specifying an explicit version.

A plugins { ... } block outside pluginManagement basically adds the plugin jar to the build script class path and applies it (there are more details to that though) and is always preferable over using a buildscript { ... } block. The buildscript { ... } block imho should only be used for dependencies of which you want to use classes directly in the build script and if you have that case, then you should better create a plugin for that instead of doing it in the build script. The buildscript { ... } block adds the plugin jar to the build script class path already, so if you then apply the plugin using the plugins { ... } block you must not specify any more version, but as I said, this is rather uncommon and should be avoided.

1 Like

Thanks for input! I understand that using buildscript { ... } is not a preferable way to apply plugins. My further question is more of curiosity nature.

The buildscript { ... } block adds the plugin jar to the build script class path already, so if you then apply the plugin using the plugins { ... } block you must not specify any more version

Okay I get it. But what about buildscript { ... } and pluginsManagement { plugins { ... } }? Is it safe to say that Gradle will favour buildscript { ... } version over one defined in pluginManagement { plugins { ... } } or it’s about lifecycle of the build that pluginManagement { plugins { ... } } is evaluated (I mean only “it takes version”, not in a meaning resolved) and then buildscript { ... } overrides it?

I don’t know how it is implemented exactly and it shouldn’t be relevant anyway.
The point is, that pluginManagement { plugins { ... } } defines default versions that are used if a plugin is actually applied and has no other version specified.
By adding the plugin to the buildscript class path you are specifying a different version, so it takes precedence.

1 Like

Right, this is an approach I’ll use, and completely remove buildscript { ... } block. Thanks!

I’ll also link to a thread on Gradle Slack where this issue has been discussed, but rather from the perspective of a library developer, not a consumer.

https://app.slack.com/client/TA7ULVA9K/CA745PZHN/thread/CA745PZHN-1616102285.005900