In trying to use the lazy APIs for task creation, ...
# plugin-development
f
In trying to use the lazy APIs for task creation, I keep running into issues. In some plugins that are developed separately, they want to react to a creation of a task, and create subsequent tasks from it. In some cases, we don’t know what tasks are being created up front from the point of view of the plugin. I went from:
Copy code
tasks.withType(TaskType.class).all(task -> methodThatRegistersAnotherTask(task)
to:
Copy code
tasks.withType(TaskType.class).configureEach(task -> methodThatRegistersAnotherTask(task))
and I’m met with:
Copy code
Caused by: org.gradle.api.internal.AbstractMutationGuard$IllegalMutationException: DefaultTaskContainer#register(String, Class, Action) on task set cannot be executed in the current context.
From digging around, the
DefaultTaskContainer
seems to pass in the mutation guard for the
CrossProjectConfigurator
and that then means I can’t create a task while configuring another one. For the most part, I’m not even trying to configure the task, I’m just trying to use the listening capabilities of
TaskContainer
and
NamedDomainObjectContainer
etc. so I can create more tasks. This is also happening within the same project. It feels like it’s particularly strict, and that I’m forced to either use
.all
,
afterEvaluate
or know upfront what tasks are present for me so I can create new tasks. I had previously tried to add it to a separate
NamedDomainObjectSet
but of course, once the listeners are set up, the configuration actions are called within the overall task configuration action resulting in the above error message. Am I stuck using
.all
?
a
yeah, using
all {}
to eagerly initialise is fine for this purpose, although it's best avoided. Possibly you could rearchitect to avoid it? E.g. create a
TaskTypeSpec
that configures all aspects of
TaskType
. Create a NamedDomainObjectContainer for the `TaskTypeSpec`s. In the upstream plugin, do
taskTypeSpec.all {}
to register all the tasks proactively. And in the downstream plugin you can lazily configure the specs, or eagerly react if necessary (e.g. to register the other tasks).
t
You'll want to register the task when a given task is registered, not when it's configured (which means it's being used, which happens after the task graph has been computed) https://github.com/gradle/gradle/issues/25262 contains a workaround using an internal API, but IIRC there's another workaround using only public APIs that was once shared in this slack 🤔
v
I don't remember any public API way to do it. Depending on exact use-case there might be alternatives though, like adding a
doLast
action instead of registering another task, or reacting to the original cause for the task, like handling source sets instead of compile tasks directly and so on.
👍 1
f
Thanks for all the replies!
yeah, using
all {}
to eagerly initialise is fine for this purpose, although it’s best avoided.
It works, but doesn’t really feel fine. We’re trying to reduce our footprint, but the
.all
kinda blows up in our face. I have separate timing issues elsewhere, but this certainly doesn’t help
Possibly you could rearchitect to avoid it? E.g. create a
TaskTypeSpec
that configures all aspects of
TaskType
. Create a NamedDomainObjectContainer for the `TaskTypeSpec`s. In the upstream plugin, do
taskTypeSpec.all {}
to register all the tasks proactively. And in the downstream plugin you can lazily configure the specs, or eagerly react if necessary (e.g. to register the other tasks).
That is a thought but feels like I’d be passing some interface that looks like
Task
around everywhere, and still feels a bit meh imo.
You’ll want to register the task when a given task is registered, not when it’s configured (which means it’s being used, which happens after the task graph has been computed)
https://github.com/gradle/gradle/issues/25262 contains a workaround using an internal API, but IIRC there’s another workaround using only public APIs that was once shared in this slack 🤔
This explanation makes a lot of sense and has made things click for me. What you’re mentioning + the attached issue is exactly what I want.
I don’t remember any public API way to do it.
Depending on exact use-case there might be alternatives though, like adding a
doLast
action instead of registering another task, or reacting to the original cause for the task, like handling source sets instead of compile tasks directly and so on.
I think we prefer to avoid the
doLast
method because it messes with attribution in build scans. We don’t necessarily maintain all the plugins that react to tasks that we do maintain. I think your approach re reacting to the original cause similar to the
TypeSpec
piece above does make sense, but that feels like something typically planned for. Lots of other plugins that we don’t actively write but our devs do will do this style of task reactivity. Doing what you’re saying re introducing some intermediate type would end up proliferating to a lot of places. I’m somewhat curious about downsides to adding a method to the `TaskContainer`:
tasks.whenTaskRegistered(String|Spec<String>|Class|String & Class, (name&type OR TaskProvider) -> action)
etc. Then you know it’s safe to call
.named
and whatever you want to do within
action
. Basically exposing the method linked and/or going above and making it nicer to use. I’m going to have a harder time convincing devs who write plugins to add all the aforementioned scaffolding to avoid an
all
. I also think it’d make it a lot more straightforward to flag via the deprecation/problems api that
.all
is not ideal on a
TaskContainer
and there is a relatively simple alternative.