How to depend on a global task in an isolated-projects-compatible way?

In a multi-project, parallel-enabled build, I have a precompiled script plugin that registers a “global” Exec task on the root project, and then a per-subproject Exec task that depends on that global task having been already executed. Here’s a simplified example:

gradle.rootProject {
	if (!tasks.names.contains("globalTask")) {
		tasks.register("globalTask").configure {
			doLast {
				println("*** Root task")
			}
		}
	}
}

tasks.register("subprojectTask").configure {
	dependsOn(":globalTask")
	doLast {
		println("*** Subproject task")
	}
}

This works as I need it to. If I run gradlew subprojectTask, the globalTask executes once, and after that subprojectTask executes for each subproject the precompiled script plugin is applied to (possibly in parallel) .

I’m wondering how this (or something equivalent) could be done in a manner that’s compatible with the upcoming isolated projects feature?

Currently, if I run gradlew subprojectTask -Dorg.gradle.unsafe.isolated-projects=true, I get errors:

FAILURE: Build failed with an exception.

* What went wrong:
Configuration cache problems found in this build.

10 problems were found storing the configuration cache, 9 of which seem unique.
- Plugin 'com.example.my-convention-plugin': Project ':subprojects:app' cannot access 'Project.tasks' functionality on another project ':'
[...]

I did not really collect experience with IP yet, but even without IP that snippet is not the best idea.
Any cross-project configuration is a bad idea, be it using allprojects { ... }, subprojects { ... }, project(...) { ... }, gradle.rootProject { ... } or any other means.

Even “just” reaching into another project’s model to get some value is a bad idea.

All this adds project coupling which disables or disturbs some more sophisticated Gradle features or optimizations, or even breaks some like IP in this case.

How to properly solve this is hard to tell, because it is unclear to me what this “global task” is supposed to do. So the exact use-case would be important to give a proper advice.

Possibilities are for example getting rid of that “global task” and doing whatever it does in a more appropriate way, doing that part in a settings plugin, requiring the consumer to also apply a specific plugin to the root project manually, …

Thanks for the reply. Agreed about project coupling. I was thinking maybe a subproject-to-root-project coupling might be a bit of a special case (as opposed to a subproject-to-subproject coupling), presuming the root project is by definition always there. Isolated projects aside, I found another issue with the approach. Invoking just :globalTask fails. If I understand correctly, it’s because without invoking subprojectTask, the plugin application by any subprojects is skipped, and so globalTask is never registered on the root project.

My specific use case for a global exec in this instance is invoking docker commands that check for and as needed create a builder (docker buildx inspect my-builder, docker buildx create --name my-builder ...). The builder would be used for any container images built by different subprojects (docker buildx build --builder my-builder ...). There’s no need to do it at all unless building an image, and there’s no need to do it more than once if building multiple images.

I’ve found that invoking the global commands from a shared build service seems to be a workable approach:

I was thinking maybe a subproject-to-root-project coupling might be a bit of a special case (as opposed to a subproject-to-subproject coupling), presuming the root project is by definition always there.

Well, it is not. :slight_smile:
Any cross-configuring or even cross-reading-model-information is a really bad idea.
And latest with IP, well, I stands for “Isolated”. :smiley:

Invoking just :globalTask fails. If I understand correctly, it’s because without invoking subprojectTask , the plugin application by any subprojects is skipped, and so globalTask is never registered on the root project.

In a “normal” project this should work.
But I guess you use “configure on demand”.
Didn’t use that either, but that could cause it, because if you only call a root project task, the subprojects will not be configured and thus the task will be missing.
To quote Configuration On Demand

Projects are considered decoupled when they interact solely through declared dependencies and task dependencies. Any direct modification or reading of another project’s object creates coupling between the projects. Coupling during configuration can result in flawed build outcomes when using ‘configuration on demand’, while coupling during execution can affect parallel execution.

I’ve found that invoking the global commands from a shared build service seems to be a workable approach:

Yes, a shared build service is what your use-case sounds like and would have been what I would have suggested now. :slight_smile:

1 Like