Alex Beggs
05/08/2025, 8:56 PMbase
⢠IncludeBuild second
⦠project a
⦠project b
:second:a
has a task for executing verifyPaparazzi
however I can't call it on Project base
verifyPaparazzi
and subsequently execute Ba's verifyPaparrazi
it complains that base doesn't have the task.
I can call :second:a:verifyPaparrazi
directly. Which means I can setup a task that is dependent on that task in the base
project as suggested here
My question is, how can I do this dynamically without violating the isolated project best practices.
second/settings.gradle.kts
gradle.lifecycle.afterProject {
if (this.name == "second") {
return@afterProject
}
if (this.rootProject.tasks.findByName("verifyPaparazzi") == null) {
this.rootProject.tasks.register("verifyPaparazzi")
}
val task = this.rootProject.tasks.findByName("verifyPaparazzi")
this.tasks.findByName("verifyPaparazzi")?.let {
task!!.dependsOn(it)
}
}
This then puts a verifyPaparazzi
call at the :second:verifyPaparrazi
level instead of :second:a:verifyPaparrazi
and additional projects that might have it. Collectively putting them into one call at the root of the included build second
The next step would be to add a task in the base
project
tasks.register("verifyPaparazzi") {
dependsOn(gradle.includedBuild("second").task(":verifyPaparazzi"))
}
am I thinking about this all wrong, or is there currently no way to add task dependencies dynamically without violating the Isolated Projects?Vampire
05/09/2025, 9:01 AMsecond
and that adds a verifyPaparrazi
task to all of them, then have in the rootproject of second
something like
val verifyPaparazzi by tasks.registering {
allprojects.map { "${it.path}:verifyPaparazzi" }.forEach {
dependsOn(it)
}
}
ephemient
05/09/2025, 12:46 PMtasks.addRule("if exists") { name ->
if (name.endsWith("IfExists")) {
val task = tasks.findByName(name.removeSuffix("IfExists"))
tasks.register(name) {
if (task != null) dependsOn(task)
}
}
}
to all subprojects, and then the root project can
for (name in arrayOf("verifyPaparazzi")) {
tasks.register(name) {
dependsOn(subprojects.map { "${it.path}:${name}IfExists" })
}
}
which would be extensible to other tasks without further changesVampire
05/09/2025, 12:51 PM...IfExists
task is realized before the ...
task was registered, couldn't it?Alex Beggs
05/09/2025, 3:50 PMVampire
05/09/2025, 3:56 PMI didn't mention this before, but this task doesn't always exist in the projects. In this case, I don't think the above would work in this situation.Only if you only read half of my message š
Vampire
05/09/2025, 3:56 PMAlex Beggs
05/09/2025, 3:57 PMAlex Beggs
05/09/2025, 3:57 PMVampire
05/09/2025, 3:58 PMdoVerifyPaparazzi
and have it as lifecycle task and in the projects where you actually have the verifyPaparazzi
task add a dependency from doVerifiyPaparazzi
to verifyPaparazzi
or similarVampire
05/09/2025, 3:59 PMdependsOn(String)
and on that task in the including buildAlex Beggs
05/09/2025, 3:59 PMVampire
05/09/2025, 4:01 PMdependsOn
is idiomatic.Alex Beggs
05/09/2025, 4:02 PMAlex Beggs
05/12/2025, 6:53 PMafterEvaluate {
val buildTypesList: List<String> =
(androidExtension?.buildTypes?.map { it.name.capitalizeFirstLetter() }?.toList()
?: emptyList<String>()) + listOf("")
buildTypesList.forEach { buildTypeName ->
tasks.register("checkSnapshots${buildTypeName}") {
println("Configuration for ${this.name} task")
group = "verification of snapshots for Paparazzi"
description = "Encapsulates the verifyPaparazzi task in a lifecycle task"
doLast {
println("Check Snapshots!!!")
}
val taskName = "verifyPaparazzi${buildTypeName}"
if (tasks.findByName(taskName) != null) {
this.dependsOn(tasks.named(taskName))
}
}
}
}
Alex Beggs
05/12/2025, 9:31 PMorg.gradle.api.internal.initialization.DefaultClassLoaderScope@2edad3f4 must be locked before it can be used to compute a classpath!
Vampire
05/12/2025, 10:06 PMverifyPaparazzi<buildType> should only get configured when we call the checkSnapshots<BuildType> or if called directly.If you do not break task-configuration avoidance, a task is only going to be realized and configured if it will actually be part of the task execution graph, so only if it is going to be executed. There is no need to make any extra effort besides not breaking task-confguration avoidance.
Is there any downside to this approach that you see andJust reading the first line of the snippet, the answer already is "Yes". Any use of
afterEvaluate
is a downside.
The main earnings for using it are timing problems, ordering problems, and race conditions.
It is almost never appropriate to use it, unless you have to deal with plugins that also follow that bad practice,
or in rare occasions where you need to bridge legacy primitive properties with proper lazy `Property`s.
is there a better approachNo idea, because it is Android, and Android is always "special". But
buildTypes
is a NamedDomainObjectContainer
, so probably something like
androidExtension?.buildTypes?.configureEach {
val buildTypeName = name.uppercaseFirstChar()
tasks.register("checkSnapshots${buildTypeName}") {
//...
}
}
for the check part.
Or maybe alternatively a task rule instead of statically registering the tasks, something like
tasks.addRule("Pattern: checkSnapshots<BuildTypeName>") {
if (startsWith("checkSnapshots") && (androidExtension?.buildTypes?.names?.contains(substring(14).replaceFirstChar { it.lowercaseChar() }) == true)) {
tasks.register(this) {
//...
}
}
}
The tasks.findByName
has multiple problems, for example it breaks task-configuration avoidance, requiring the task to be realized.
If really needed, better check tasks.names
for containing the task name and then use tasks.named("...")
.
But either way, this requires that the task is already registered at the time you call this, so like always with afterEvaluate
you have a race condition where the task could not yet be registered but gets registered later.
You could use tasks.named { ... }
or tasks.matching { ... }
instead, which also works fine without afterEvaluate
, but you should then first restrict the set of tasks to the correct type using withType
, because if you use named
or matching
, and iterate over that task set (dependsOn
also needs to iterate) then you break task-configuration avoidance for all those tasks, as all of them are going to be realized. Those two are most often and optimally only used with a following configureEach
, as that works lazily only on the elements that are going to be realized anyway. But for depending this does not really help.
So it might be better to ensure, that for all build types there is an according verifyPaparazzi...
task, even if it does not do anything and then depend on it, knowing that it is there instead of checking whether it is.
org.gradle.api.internal.initialization.DefaultClassLoaderScope@2edad3f4 must be locked before it can be used to compute a classpath!Never seen that, can you share a build
--scan
URL?Alex Beggs
05/13/2025, 1:58 PMorg.gradle.api.internal.initialization.DefaultClassLoaderScope@2edad3f4 must be locked before it can be used to compute a classpath!
> Never seen that, can you share a build --scan
URL?
I think what was occurring, I was not filtering out the root project, thus attempting to modify in an afterEvaluate block (hadn't removed it yet) and depend on it alsoAlex Beggs
05/13/2025, 2:01 PMverifyPaparazzi<buildType> should only get configured when we call the checkSnapshots<BuildType> or if called directly.
> If you do not break task-configuration avoidance, a task is only going to be realized and configured if it will actually be part of the task execution graph, so only if it is going to be executed.
> There is no need to make any extra effort besides not breaking task-confguration avoidance.
So this shouldn't be an issue calling the findByName since it will only happen during the checkSnapshotsBuildType call.
It does sound like tasks.names
is the way to go since it is lazy.
I didn't follow:
> because if you use named
or matching
, and iterate over that task set (dependsOn
also needs to iterate) then you break task-configuration avoidance for all those tasks, as all of them are going to be realized.
Why would they be realized if they are just stringsAlex Beggs
05/13/2025, 2:09 PMconfigureEach
is that a preferred way to detect if something exists? But then it wouldn't get configured in configureondemand
if I understand correctly unless setup.Vampire
05/13/2025, 2:27 PMSo this shouldn't be an issue calling the findByName since it will only happen during the checkSnapshots<BuildType> call.Well, it can be an issue. For example if for some reason you break task-configuration avoidance for the
checkSnapshots...
task, you then also break it along for the verifyPaparazzi...
task.
It does sound likeDepends on how you define lazy. It does not break task-configuration avoidance. But still it requires that the task is already registered at the time you check the names.is the way to go since it is lazy.tasks.names
Why would they be realized if they are just stringsNo I don't follow, don't know what you mean. If you do
tasks.matching { it.name == "foo" }
and then iterate this, for example by giving it to dependsOn
, all tasks in tasks
are realized to check the it.name ==
condition.
If you do tasks.named { it == "foo" }
the original intention was, that iterating it would not realize all tasks, but unfortunately it currently does just like matching
does.
If you do tasks.(matching|named) { ... }.configureEach { ... }
this is task-configuration avoidance safe as it only runs the configure action for those tasks anyway realized,
but that does not help for depending on the tasks.
You could maybe do something like
if (tasks.names.contains("foo")) { dependsOn(tasks.named("foo")) }
it should be task-configuration avoidance safe,
but still requires that the task is already registered the moment you call that construct.Alex Beggs
05/13/2025, 2:29 PMif (tasks.names.contains("foo")) { dependsOn(tasks.named("foo")) }
This is what I ended up doingVampire
05/13/2025, 2:30 PMwithout using something like afterEvaluate, how would you ensure that you have previously registered a task that you depend on,If you develop plugin X and plugin Y adds task A, then you typically either apply plugin Y from plugin X and thus know task A will already be registered, making it safe to use
tasks.named("A")
without further check,
or if you do not depend on plugin Y / task A being present, but want to react to them being used by the consumer, you would use pluginManager.withPlugin("Y") { ... }
which would be executed iff plugin Y is applied and only as soon as that happened.Vampire
05/13/2025, 2:30 PMThis is what I ended up doingYeah, well, you have to live with the consequences. š
Alex Beggs
05/13/2025, 2:31 PMtasks.matching { it.name == "foo" }
in combination with dependsOn
would cause the task to be configured? But tasks.matching { it.name == "foo" }
itself won't, the reason for my confusion is that you followed it by configureEach
and said it would be fineAlex Beggs
05/13/2025, 2:31 PMVampire
05/13/2025, 2:36 PMtasks.matching { it.name == "foo" }
per-se is fine, as it does not yet do anything.
But as soon as you iterate that, for example using .forEach
or .all
or whatever else, you realize each and every element in tasks
so that you can check its name.
If really needing something like that (but better avoid it) you should at least prefix with withType
so that only the tasks with that type are realized, unless the type is DefaultTask
because that would again be all tasks.
.configureEach
is the exception, as it will only be executed on those elements that are realized anyway due to some other reason.
So it can be used to configure the element in question if it actually is going to be realized and used.
But for depending on tasks, this does not help.