Lazy task will be configured even if it should not

I have a subproject with the following build-script:

project.tasks.register("ThisBooms") {
    println("This booms.. Why?!")
    throw IllegalArgumentException("WHAT?")
}

My root-build script looks like this:

allprojects { project ->
    project.tasks.register("strange") {strangeTask ->
        def otherTasks = project.tasks.matching { task ->
            task.name == 'help'
        }
        strangeTask.dependsOn(otherTasks)
    }
}

Now, when I execute strange (./gradlew strange) Gradle will crash with the following error message:

This booms.. Why?!

FAILURE: Build failed with an exception.

* Where:
Build file '/private/tmp/gradleError/sub/build.gradle' line: 3

* What went wrong:
Could not determine the dependencies of task ':sub:strange'.
> Could not create task ':sub:ThisBooms'.
   > Could not find method IllegalArgumentException() for arguments [WHAT?] on task ':sub:
ThisBooms' of type org.gradle.api.DefaultTask.

For me this doesn’t make sense at all.
Why does the strange task execution will configure the ThisBooms task (and therefore crash)?
strange has no dependency to ThisBooms at all (and not vince versa!).

Do I something wrong?
Did I missunderstand something with the task configuration avoidance?

Here is a reproducer for the lazy people out there :grinning:
gradleError.zip (67.9 KB)

project.tasks.matching calls TaskContainer.matching(Closure) which passes every task to the predicate. This requires each task to be instantiated and configured.

Try this instead

allprojects { project ->
    tasks.register('strange') {
        dependsOn tasks.named('help')
    }
}

Or more simply

dependsOn 'help'
1 Like

All tasks in the TaskCollection need to be realized for this.
Not immediately when matching is called, but when the task collection is queried, because the Action can access the Task so it has to be realized.

If you know the task is already added (e. g. because you applied the plugin that adds it, or because it is a standard task like help, then bertter use tasks.named("help").

matching you usually use if the task might already be there but might also be added later.
In that case you have two options left.
If your actual use-case really is dependsOn, then you can also supply a plain String and Gradle will resolve it as late as possible, like dependsOn("help").

If you really need matching, then if possible restrict by type first, so that only the tasks of that type need to be realized for checking the matching predicate, like tasks.withType<FooType>().matching { it.name == "foo" } .

1 Like

Thank you for the help and especially the explanation!
It makes sense that matching{} requires all tasks to be configured.

I now ended up with the solution @Lance posted above:

allprojects { project ->
    tasks.register('strange') {
        dependsOn tasks.named('help')
    }
}

I mean, this was just a short reproducer code, my original code differs quite a bit, but at the end it is a simple tasks.named(taskName) call :wink:

1 Like