Connecting tasks across projects while maintaining configuration cache compatibility

I have a scenario in which 1 project has to be aware of the output of n other projects, fundamentally it aggregates certain information from each of those projects into a single publication.

The mechanism for doing this today is configuration cache unfriendly as it trips the disallowed types error from Configuration Cache Requirements for your Build Logic

The implementation is essentially as per Best Practices for Tasks or Configuring Tasks Lazily but getting access to the task means accessing another project which then trips the above problem

is How to Share Artifacts Between Projects with Gradle the only alternative mechanism that will work? or is there some other approach/ trick that can allow me to connect tasks across projects?

Even without configuration cache, cross-project model access (even if only reading and not configuring the other model) is highly problematic and strongly discouraged bad practice. So CC or not, you should definitely change that.

Latest with isolated projects this will even be worse a thing to do / not possible anymore.

If you know the path to a task of another project and just want to depend on it in terms of having it run, you can use the string-y way like dependsOn(":foo:bar"), while that is of course only indicated if on the left-hand side is a lifecycle task, otherwise it is also already a code-smell and most probably a sign of doing something wrong.

If you want to share task output from one project to another, then yes, you last link is the way to go. This is also what the test-report-aggregation and jacoco-report-aggregation plugins use under-the hood, as well as the mechanism behind jvm-test-fixtures plugin.

yes it’s a one way flow of information (one project aggregates information produced by the others)

there must be something different about one particular use case though that makes it config cache unfriendly as another similar case does not fail in the same way

the compatible example is just consuming the output of a particular task

def aggregatingTask = project.tasks.register('aggregator', SomeTask)
project.allprojects { Project p ->
    aggregatingTask.configure {
        it.someFiles.from p.tasks.withType(TaskProvidesSomeData)
    }
}

the incompatible example is more complex as it requires reading some other configuration object like

def aggregatingTask = project.tasks.register('aggregator', SomeTask)
project.allprojects { Project p ->
    def someConfigs = p.extensions.getByName(MY_EXTENSION_NAME) as NamedDomainObjectContainer<MyConfig>
    someConfigs.configureEach { config ->
        // do anything with a task from the aggregating project that references config
    }
}

I suspect this is something to do with how that MyConfig class is instantiated but it’s unclear exactly where that link comes from (the trace provided in the configuration cache report is basically a long tour through gradle internals)

As I said, both are highly discouraged bad-practice and should be replaced.
Just like the usage of allprojects { ... } and any other means of doing cross-project model access.

Yes I understand that but it’s not in itself the cause of the configuration cache incompatibility

No, it is not.
As I said, this is highly problematic even without CC.
Why CC fails on the one construct is hard to say without seeing the CC problem report and the code.

I can’t share internal code but I’m fairly sure it leaks through from the container, the section of the report that shows the break is. I think configureClosure mentioned here is the someConfigs.configureEach in my sample code so I think the solution is likely to stop linking to a project owned DomainObjectContainer and instead have that project publish the info in some serialised format as a dependency that can be consumed

- field `configureClosure` of `org.gradle.util.internal.ConfigureUtil$WrappedConfigureAction` 
    - bean of type `org.gradle.util.internal.ConfigureUtil$WrappedConfigureAction` 
        - field `val$action` of `org.gradle.internal.code.DefaultUserCodeApplicationContext$CurrentApplication$1` 
            - bean of type `org.gradle.internal.code.DefaultUserCodeApplicationContext$CurrentApplication$1`
                - field `delegate` of `org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction`
                    - bean of type `org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction`
                        - field `val$action` of `org.gradle.api.internal.DefaultMutationGuard$1`
                            - bean of type `org.gradle.api.internal.DefaultMutationGuard$1`
                                - field `val$action` of `org.gradle.api.internal.DefaultMutationGuard$1`
                                    - bean of type `org.gradle.api.internal.DefaultMutationGuard$1`
                                        - field `actions` of `org.gradle.internal.ImmutableActionSet$SetWithFewActions`
                                            - bean of type `org.gradle.internal.ImmutableActionSet$SetWithFewActions`
                                                - field `addActions` of `org.gradle.api.internal.collections.DefaultCollectionEventRegister`
                                                    - bean of type `org.gradle.api.internal.collections.DefaultCollectionEventRegister`
                                                        - field `eventRegister` of `org.gradle.api.internal.FactoryNamedDomainObjectContainer_Decorated`
                                                            - bean of type `org.gradle.api.internal.FactoryNamedDomainObjectContainer_Decorated`