Issue in a custom plugin with plugins DSL and android plugin dependency


(Thomas) #1

Hello
I’m currently working on a plugin using kotlin-dsl and testkit.
It depends on the android gradle plugin, using the following directive:

compileOnly(“com.android.tools.build:gradle:3.1.4”)

Following the Test Kit guide, I’m using .withPluginClasspath() to test my plugin. This require to use a plugins {} block in the tested build.gradle.kts file.
My plugin uses the AppExtension from Android Plugin classpath to depends some task on the android prebuild.

The issue is when in my test, I use the plugins {} block:

plugins {
 id("com.android.application") version("3.3.0-alpha13")
 id("greet") // my plugin
}

I get the following error :

Caused by: java.lang.ClassNotFoundException: com.android.build.gradle.AppExtension

.
If I remove the plugins DSL block, and use the legacy buildscript, targetting to a jar, it works, meaning (for me) that the issue probably comes from the Plugins DSL and some classpath loading problem.

Here’s a github sample that easily reproduces it: https://github.com/NitroG42/testplugin
Here there is 3 branches:

  • main is in kotlin, use Plugins DSL and doesn’t work
  • workingbuildscript is in kotlin, use legacy Buildscript and works
  • groovy is in groovy, use Plugins DSL and doesn’t work (it was to be sure the issue didn’t come from Kotlin-DSL)

You just need to run ./gradlew cleanTest test --info

@st_oehme, sorry to bother you but looking for an answer I saw you respond to a few similar issue.

Thanks for reading !


(Stefan Oehme) #2

Classpath injection puts your plugin in a higher classloader so it can’t see other plugins that are only referenced in a test build script. Make the Android plugin a testRuntime dependency in your main build and apply it without a version in the test for this to work.


(Thomas) #3

OK now I understand the classpath issue, thanks !
I’m trying to use the testRuntime dependency but I can’t make it work.
In my main build.gradle.kts I have:

dependencies {
compileOnly(“com.android.tools.build:gradle:3.1.4”)
testRuntime(“com.android.tools.build:gradle:3.1.4”)
testImplementation(“junit:junit:4.12”)
}

And then I use in my test:

givenBuildScript("""
        plugins {
            id "com.android.application"
            id "greet"
        }

        greeting {
            message = "$message"
        }
    """)

without a settings.gradle.kts because it should take the local dependency if I understand what’s happening ? Unfortunatelly it doesn’t seem to locate the plugin (./gradlew cleanTest test --info) :

* Where:
Build file '/private/var/folders/lv/7bz6yszn6hzg2hd1vlzysswc0000gn/T/junit7375830087897982030/build.gradle' line: 3

* What went wrong:
Plugin [id: 'com.android.application'] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Gradle TestKit (classpath: /Users/nitrog42/Workspace/IdeaProjects/testplugingithub/build/classes/java/main:/Users/nitrog42/Workspace/IdeaProjects/testplugingithub/build/classes/kotlin/main:/Users/nitrog42/Workspace/IdeaProjects/testplugingithub/build/resources/main)
- Plugin Repositories (plugin dependency must include a version number for this source)

I tried using a settings.gradle.kts, but I don’t really know what I should put in it.
I also tried using the same version that is declared in the testRuntime but same issue.
Also, I’m using Gradle 4.10.2

Thank you!


(Stefan Oehme) #4

Sorry I spoke too soon, we only take the runtime classpath, not the test runtime classpath (to minimize the difference between testing and production usage of your plugin). You can change that using

gradlePlugin {
  pluginSourceSet = sourceSets.test
}

(Thomas) #5

Ok now when my main build.gradle.kts is:

plugins {
    `java-gradle-plugin`
    `kotlin-dsl`
}

gradlePlugin {
    plugins {
        register("greet") {
            id = "greet"
            implementationClass = "samples.GreetPlugin"
            version = "1.0"
        }

    }
    pluginSourceSet(sourceSets.get("test"))
}

dependencies {
    compileOnly("com.android.tools.build:gradle:3.1.4")
    testRuntime("com.android.tools.build:gradle:3.1.4")
    testImplementation("junit:junit:4.12")
}

repositories {
    google()
    jcenter()
}

I get:

Selected primary task ‘test’ from project :

FAILURE: Build failed with an exception.

  • What went wrong:
    Circular dependency between the following tasks:
    :pluginUnderTestMetadata
    — :pluginUnderTestMetadata (*)

(*) - details omitted (listed previously)

I tried in groovy to be sure but I get the same issue.
Sorry if I’m doing something wrong here, I’m not 100% sure of what I’m doing !


(Stefan Oehme) #6

Ah that’s because the metadate file itself is part of the test sourceSets classpath, so we’re creating a cyclic dependency here.

I guess well have to do something a little lower level instead:


configurations {
  optionalPlugins
  compileOnly.extendsFrom optionalPlugins
}

dependencies {
  optionalPlugins("com.android.tools.build:gradle:3.1.4")
}

pluginUnderTestMetadata.pluginClasspath.from(configurations.optionalPlugins)

Note that I’m typing this on my phone, I haven’t tested it.


(Thomas) #7

Awesome! It works perfectly in groovy, unfortunatelly it won’t with Kotlin-DSL, it seems to break the gradlePlugins {} block

EDIT ; Was wrong, it works with kotlin-dsl too an old reliquate of code on my side.

Thank you very much for your help!
I have one last question; as I feel very weak on those things, do you have any reading recommandation about those subject ?


(Stefan Oehme) #8

The source code of the JavaGradlePluginPlugin is the thing I looked at. Apart from that the solution above requires some knowledge on the use of Configurations to create custom dependency “scopes”.