I am working with a project that has composite bui...
# plugin-development
a
I am working with a project that has composite builds. I wanted to write my code with regard to the best practices for Isolated Projects. I know this is still in transition and development, but I was wondering if someone had a suggestion. I have Project
base
• 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
Copy code
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
Copy code
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?
v
I guess the easiest and cleanest is to have a convention plugin that you apply to all projects in
second
and that adds a
verifyPaparrazi
task to all of them, then have in the rootproject of
second
something like
Copy code
val verifyPaparazzi by tasks.registering {
    allprojects.map { "${it.path}:verifyPaparazzi" }.forEach {
        dependsOn(it)
    }
}
e
or have a convention plugin that adds a task rule like
Copy code
tasks.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
Copy code
for (name in arrayOf("verifyPaparazzi")) {
    tasks.register(name) {
        dependsOn(subprojects.map { "${it.path}:${name}IfExists" })
    }
}
which would be extensible to other tasks without further changes
v
But that could fail (or rather miss the dependency) if such an
...IfExists
task is realized before the
...
task was registered, couldn't it?
a
@Vampire > val verifyPaparazzi by tasks.registering { > allprojects.map { "${it.path}:verifyPaparazzi" }.forEach { > dependsOn(it) > } > } I 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. @ephemient approach doesn't seem to impact the isolated project best practices, since it just assumes it either exists or not and is handled by each project individually, but each project has the task regardless. However, you did point out a potential race condition which I don't know enough about.
v
I 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 šŸ˜‰
First half was about registering it in every project using a convention plugin that is applied everwhere
a
ah right, I did read it but lost the context. šŸ™‚
good point, that would work, and seems reasonable
v
You can also make that
doVerifyPaparazzi
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 similar
The point just was, make sure you have a task that exists in every project, then depend on that task in all projects from the root project using
dependsOn(String)
and on that task in the including build
a
that makes sense, so regardless you just call the task whether or not it is executing something
v
Exactly, that's how lifecycle tasks work. They never have actions, but their sole purpose is to depend on other tasks ... or not. Lifecycle tasks on the left-hand side is also practically the only case where an explicit
dependsOn
is idiomatic.
a
thanks for the explanation
šŸ‘Œ 1
I have a question about lazy configuration. verifyPaparazzibuildType should only get configured when we call the checkSnapshotsBuildType or if called directly. Is there any downside to this approach that you see and is there a better approach to finding if the task exists to add it as a dependency on each project?
Copy code
afterEvaluate {

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))
                }
            }
        }
}
also any thoughts why I might be getting the following error
Copy code
org.gradle.api.internal.initialization.DefaultClassLoaderScope@2edad3f4 must be locked before it can be used to compute a classpath!
v
verifyPaparazzi<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 and
Just 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 approach
No idea, because it is Android, and Android is always "special". But
buildTypes
is a
NamedDomainObjectContainer
, so probably something like
Copy code
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
Copy code
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?
a
>
Copy code
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? 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 also
>
Copy code
verifyPaparazzi<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 strings
without using something like afterEvaluate, how would you ensure that you have previously registered a task that you depend on, it sounds like the order matters when you register things, which gives this mix of imperative and declarative functionality... What is the approach around this? You mentioned
configureEach
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.
v
So 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 like
tasks.names
is the way to go since it is lazy.
Depends 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.
Why would they be realized if they are just strings
No 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
Copy code
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.
a
Copy code
if (tasks.names.contains("foo")) { dependsOn(tasks.named("foo")) }
This is what I ended up doing
v
without 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.
This is what I ended up doing
Yeah, well, you have to live with the consequences. šŸ˜„
šŸ˜… 1
a
so for clarification this
tasks.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 fine
btw appreciate the feedback
v
Exactly,
tasks.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.
šŸ‘ 1