getByType or withType failing in composite build

I have a set of custom plugins which add a number of different extensions/tasks & whose behaviour can vary when run in a composite build, essentially there is some work which must only be performed in the one true root project of the build.

This is somewhat inconvenient in gradle due to the way included builds are isolated from each other however can be made to work. However one problem I’m consistently encountering is where attempts by some included projects to access extensions or tasks provided by the root project fail unexpectedly.

For example, in a composite build with 2 identically configured (with respect to plugins) included builds make this call

def rootProject = project.gradle.parent ? project.gradle.parent.rootProject : project.rootProject
rootProject.extensions.getByType(ChgServiceProvider).getService(envName)

in one included project, this succeeds
in another included project, the following exception is thrown

2022-02-03T16:43:55.780+0000 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Caused by: org.gradle.api.UnknownDomainObjectException: Extension of type 'ChgServiceProvider' does not exist. Currently registered extension types: [ExtraPropertiesExtension, ChgServiceProvider, BasePluginExtension, DefaultArtifactPublicationSet]

notice that the required extension type is reported to be available

similarly rootProject.tasks.withType(SomeCustomTask).configureEach is not called in one project but is executed in another

from debugging into gradle, it can be seen that such calls fail inside an action generated by org.gradle.api.internal.collections.CollectionFilter.filtered(action)

the registered action itself fails because the !type.isInstance(object) check fails, the actual object is reported as type SomeCustomTask_Decorated so has been decorated by gradle as usual. The fact the check fails in one project but not another might suggest some classloader related problem.

I cannot reproduce this behaviour in an extremely simple cutdown project.
I can reliably reproduce this behaviour in a more complex project.

Does anyone have any information on how this part of gradle (class decoration, classloaders etc) works with respect to composite builds? This might help me extend my simple cutdown project in order to make a reproducible defect.

Have you tried using projectsEvaluated { } on the Gradle object?

https://docs.gradle.org/current/javadoc/org/gradle/api/invocation/Gradle.html#projectsEvaluated-org.gradle.api.Action-

Adds an action to be called when all projects for the build have been evaluated. The project objects are fully configured and are ready to use to populate the task graph.

When modifying or reading the state of subprojects in the root project, in the past, I often ran into issues with subprojects not being set up yet. I don’t know with certainty how this impacts composite builds, but, the solution to my problem was to take action only once all the subprojects had been evaluated.

From the root project it would be like this:

gradle.projectsEvaluated {
    ...now do setup that requires all plugins to be applied...
}

With that said, it is strongly discouraged now for projects to reach into each other’s states. If possible, it would be suggested to find an alternative.

Thanks but it’s not a question of timing as the action is registered and evaluated as normal

The problem is that the withType filter filters out the action because the object (the task in question) fails to pass the Class.isInstance(object) test.

I can see the Class instances are different so it suggests gradle has generated 2 separate classes for 2 separate included builds in this particular build (but not in a simpler cutdown version)

My network blocks uploading a picture of the debugger but I verified there are 2 separate classloaders involved, one for each included build. The format of the toString is like

VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:included.build.A]:buildSrc[:included.build.A]:root-project[:included.build.A](export)})

VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:included.build.B]:buildSrc[:included.build.B]:root-project[:included.build.B](export)})

the type of the task being evaluated is of the latter type so the action registered against the former does not fire

I can now reproduce this issue, use of withType (or other similar methods) from an included build will only work if the respective projects have the same buildscript classpath. In this case, the VisitableURLClassLoader has the same spec (particularly the same classpath and hence implementationHash) and so the same classloader will be reused. If one of the projects applies some additional plugins that bring more jars onto the classpath, a separate classloader will be used and a withType call will now silently fail.

It seems like a bug to me, will raise it on github

logged as https://github.com/gradle/gradle/issues/19831