`compileOnly` dependency in buildSrc

Hey there,

I’m building a buildSrc plugin for our internal purposes, and I don’t quite understand how Gradle resolves classpath for the build. Let’s say in buildSrc/build.gradle I have:

dependencies {
  implementation "com.android.tools.build:gradle:3.0.0"

and in /build.gradle:

buildscripts {
  dependencies {
     classpath "com.android.tools.build:gradle:3.1.2"

Now no matter what I do in buildSrc/build.gradle, the build seems to always be using plugin 3.0.0, and never lets me use the version from root project’s build gradle. I understand the buildSrc’s classpath has precedence, but then when I put plugina as compileOnly, then my build fails with NoClassDefFound when I access classes from Android Gradle Plugin in my buildSrc plugin. Shouldn’t it use classpath provided by the root project’s build.gradle?

This actually isn’t very bad, since we can maintain versions in buildSrc, but I’m still curious as to why I can’t get it to work.

Also do I understand correctly, that when I publish my buildSrc plugins as regular jar plugins to pull inside /build.gradle normally, all the dependencies will be resolved just like with regular dependencies? As in there should be no NoClassDefFound when I use compileOnly, and even if I provide AGP in implementation or compile configuration, it would be resolved to more recent version if there are two classpath depedendencies defined?

Can you create a reproducible example project? When I create a project with what you’ve provided, 3.1.2 is used. Your expectation matches what I’m seeing, so the example project would help to determine what you’re doing differently that is changing the behavior.

@jjustinic I have created a sample repo here https://github.com/lwasyl/gradle_classpath_problem.

The buildSrc imports AGP version 3.0.0, the build script 3.1.2. Now the thing is, in AGP 3.1.2 androidTestApi configuration is deprecated and produces a warning – this is what I read to determine which AGP version is on the classpath.

As the repo is now, when you configure the project there’s no warning. If you play with values (I left the commented lines with other versions) you’ll see that version from ./buildSrc/build.gradle overrides the one from ./build.gradle`

One thing that I don’t understand – why with compileOnly the build can’t find AGP classes (in runtime), even though they’re explicitly defined in build.gradle's buildscript.classpath?

Another thing is that ./gradlew buildEnvironment doesn’t take into account classpath provided by the buildSrc plugin. From what I’ve seen this is a known limitation, but it’s extremely misleading, I think – unless I’m wrong at some point. Perhaps you were using it to determine which version of the plugin is used in your repro? Anyway that’s why I’m relying on the warning to determine plugin version that’s actually used by Gradle.

Thanks for interest!

@st_oehme I will ping you, since you inspired me to write this stuff with your blog post :wink: Is there anything I’m heavily misunderstanding? Or is there any source for how Gradle’s classpath resolution works? Or maybe even where in code should I start digging to figure this out?

The buildSrc classloader that holds your custom plugin is the parent to all others. Therefor it has precedence and your custom plugin can’t see things defined lower (e.g. in the root project). For consistency I recommend defining all your plugin dependencies in buildSrc if you are writing custom plugins. That avoids any surprises.

Alternatively, if you want behavior that is closer to an independently developed plugin, you can put your custom logic into a different folder and use includeBuild 'folderContainingMyPluginBuild' in your settings.gradle file. Then your plugin will need a group/name/version and can be referred as such in any buildscript {} block in your project. It will then take part in dependency resolution normally. The cost is a bit more ceremony. I may write another blog post about that approach.