Is always getting same instance of the task when do project.tasks.getByName(taskName)
?
I would like to create a singleton task in the root project that will receive messages from other tasks and after receiving all messages perform some validation.
To start with: do not use getByName
, that breaks task-configuration avoidance.
At least if done in configuration phase.
If done in execution phase it is not better though, as accessing project
at execution time is deprecated.
But to answer the actual question: maybe.
Theoretically a task could be replaced, even though it is very bad practice and highly discouraged.
But either way, maybe also here elaborate on your use-case, you probably have an XY problem here and there is most probably a better way to achieve whatever you try, but for that it must be clear what you try to achieve exactly.
I would like to collect all snapshots for every project and throw an exception if they are presented.
I collect SNAPSHOT with configuration.afterEvaluate
in plugin into errors
set, create a task within current project passing errors
into constructor. Some code omitted…
class BuildChecksPlugin : Plugin<Project> {
override fun apply(project: Project) {
val errors = mutableSetOf<String>()
project.afterEvaluate {
<collect snapshots async into errors with project.configurations.filter { canBeResolved(it) }.forEach { config -> config.incoming.afterResolve { ... } >
val finalizerTask = createOrGetFinalizerTaskIfNotCreated(project.rootProject)
finalizerTask.addProject(project)
val collectorTask = project.tasks.register(
SnapshotPresenceCollectorTask.NAME,
SnapshotPresenceCollectorTask::class.java,
finalizerTask,
errors
).get()
val compileTask = getTaskSilently(project, "compileKotlin")?.finalizedBy(collectorTask)
val compileTestKotlin = getTaskSilently(project, "compileTestKotlin")
?.finalizedBy(collectorTask)
// some projects don't have compileTestKotlin and compileKotlin tasks, but should be removed
// from finalizerTask
if (compileTask == null && compileTestKotlin == null) {
project.gradle.taskGraph.whenReady {
it.allTasks[it.allTasks.size - 1].doLast {
finalizerTask.addErrorsFromProject(project, errors)
}
}
}
}
private fun createOrGetFinalizerTaskIfNotCreated(project: Project) =
getTaskSilently(project, SnapshotPresenceFinalizerTask.NAME) as SnapshotPresenceFinalizerTask?
?: project.tasks.register(
SnapshotPresenceFinalizerTask.NAME,
SnapshotPresenceFinalizerTask::class.java
).get()
}
//-------------------------------------------------------------------------
open class SnapshotPresenceFinalizerTask : DefaultTask() {
private val errors = mutableSetOf<String>()
private val projects = mutableSetOf<Project>()
@TaskAction
fun run() {
<throw an exception, the method should not be invoked>
}
fun addProject(project: Project) {
projects.add(project)
}
fun addErrorsFromProject(project: Project, prjErrors: MutableSet<String>) {
this.errors += prjErrors
this.projects.remove(project)
if (this.projects.isEmpty()) {
if (this.errors.isNotEmpty()) {
<throw an exception>
}
}
}
}
//-------------------------------------------------------------------------
open class SnapshotPresenceCollectorTask
@Inject constructor(
private val finalizerTask: SnapshotPresenceFinalizerTask,
private val errors: MutableSet<String>
) : DefaultTask() {
@TaskAction
fun run() {
finalizerTask.addErrorsFromProject(project, errors)
}
}
That’s actually an extremely bad idea.
afterEvaluate
is highly discouraged. The main gain you get from using it are timing problems, ordering problems, and race conditions. It is like using SwingUtilities.invokeLater
or Platform.runLater
to “fix” a GUI problem. It just does symptom treatment, delaying the problem to a later, harder to reproduce, harder to debug, and harder to fix point in time.
Instead of the cursed afterEvaluate
use proper lazy API, so for example project.configurations.matching { ... }.configureEach { ...afterResolve... }
, then you don’t need to manually delay something. Actually can probably even leave out the filter
/ matching
and just attach the afterResolve
action to all configurations using project.configurations.configureEach { ... }
, because I think if the collection is not resolvable it should be a no-op.
Doing cross-project configuration like registering a task in another project (like the root project), or reconfiguring a task in another project, or even just getting information from another project’s model are highly problematic and discouraged. You instantly introduce project coupling which works against or disables some of the more sophisticated features or optimizations.
Registering a task for not executing it, just for coordination / collection is an extreme abuse and pretty-much non-sense. For that you have shared build services. They are scoped to the current build execution, to that you can access from all projects safely and use it to collect and share the information you need.
Also adding a doLast { ... }
to all tasks just to trigger one “after all finished” action is pretty bad.
Let alone the way you configure “all” tasks by using taskGraph .whenReady
instead of just using the safe tasks.configureEach { ... }
, besides that you should not do it anyway.
Also it makes little sense to use the task-configuration avoidance safe tasks.register
if you immediately break task-configuration avoidance again by calling get()
on it. If you would really immediately need the task instance, use tasks.create
, but in practically all cases use neither.
Also, even if it would work somehow, your logic is flaky, as you output the errors as soon as the first task of the last project has finished executing, but you might miss resolutions happening in later tasks of that or other projects.
Ok, let’s stop the ranting, here is what you should do:
- create a shared build service
- make the shared build service implement
Autocloseable
- make the shared build service implement
OperationCompletionListener
- register the build service as
OperationCompletionListener
- in the
close()
method throw your exception with your findings - in the the
afterResolve
add the findings to a collector field in the shared build service - do not use any
afterEvaluate
orfilter
orforEach
or custom tasks or modify any tasks
By having the shared build service being an OperationCompletionListener
you make sure it is closed only between the last task was executed and the build being finished, the implementation of the OperationCompletionListener
method can be empty and the constructor also does not need to do anything.