How do I create an aggregator tasks such that it c...
# community-support
v
How do I create an aggregator tasks such that it collects all
detektMain
tasks, but from within a current project's (
app-a
) dependency graph (not globally; I have multiple apps in the total build)
Copy code
tasks.register("detektAllDebug") {
        group = "verification"
        def projects = collectDependencyProjects(project(":foo:app"))
        def tasks = collectTasks(projects, "detektMain")
        tasks += "detektMockFakeFaceRecoFakeEsimStableDebug"
        tasks += "detektMockRealFaceRecoRealEsimStableDebug"

        dependsOn tasks
    }
I have something like this
Copy code
static Set<Project> collectDependencyProjects(project) {
    def collectedProjects = [].toSet()
    doCollectDependencyProjects(collectedProjects, project)
    collectedProjects.remove(project) // Remove root
    return collectedProjects
}

static void doCollectDependencyProjects(collectedProjects, project) {
    if (collectedProjects.contains(project)) return

    collectedProjects.add(project)

    def subProjects = project.getConfigurations()
            .inject([]) { acc, config -> // Accumulate all
                def configName = config.getName()
                // Ignore test projects
                if (configName != "testImplementation" && configName != "testApi") {
                    def dependencies = config.dependencies.withType(ProjectDependency)
                    if (dependencies.size() > 0) {
                        acc.addAll(dependencies)
                    }
                }
                acc
            }
            .collect { it.getDependencyProject() } // Map

    for (subProject in subProjects) {
        doCollectDependencyProjects(collectedProjects, subProject)
    }
}
Copy code
static def collectTasks(subprojects, name) {
    def allTasks = []
    for (project in subprojects) {
        def task = project.tasks.findByName(name)
        if (task != null) {
            allTasks += task
        }
    }
    return allTasks
}
where I recurse into the graph and collect Works, but I have feeling there is more gradle-y way
v
Does it work? Maybe sometimes and in specific situations maybe even reliable. Is it a good idea? Definitely not. Reading from the mutable model of other projects (getting configurations / tasks) is almost as bad as doing cross-project configuration.
As you only shared parts of your code, it is a bit hard to give concrete advice. If you for example want to depend on
detektMain
in all projects, it is often easiest to ensure that every project has a task with that name even if it does nothing and use
subprojects.forEach { dependsOn("${it.path}:detektMain") }
to depend on all of them. If it publishes its result as variant like test suites plugin and jacoco plugin do to support report aggregation, you can also use an artifact view to request these variants which will then also trigger the producing tasks. If not, you could just create such an outgoing configuration that has the necessary task dependency yourself and then depend on it. The artifact can even be an empty file tree afair.
v
Yea sorry I missed one line,
def projects = collectDependencyProjects(project(":foo:app"))
, I added is into the register block Now it's identical as what I do & accomplishes the goal correctly -- but yea idk if it's kosher, or rather how to make it kosher
> If you for example want to depend on detektMain in all projects, but I want not all, but only the subgraph of
:foo:app
(I have
:bar:app
,
:quax:app
) etc ... sort of like
:foo:assemble
assembles foo & it's subgraph -- not everything in the total build Is that what your suggestion would do? (hard for me to decypher, I'm not so versed in gradle)
v
The
forEach
would not do that. The artifact view way would do what you intend. You could have a look at the test report aggregation and jacoco report aggregation plugins, they do exactly what you want. The individual projects have outgoing configurations that provide the jacoco report / test report / ..., those outgoing configurations have the respective task dependencies. The aggregator project then uses an artifact view to get those configurations by requesting the respective attributes and thus get the reports to aggregate, having automatically the necessary task dependencies and all without bad-practice cross-project access.
For example something like
Copy code
val providerOfFileCollectionWithTheNecessaryTaskDependencies = configurations.compileClasspath.map {
    it.incoming.artifactView {
        withVariantReselection()
        attributes {
            //...
        }
        componentFilter {
            it is ProjectComponentIdentifier
        }
    }.files
}
to get the variant from all project dependencies in the
compileClasspath
that provide such an outgoing variant.
If the
detekt
plugin already registers such a variant, you just need to request it, if not, you would need to create such outgoing configurations in the respective projects for example by using a convention plugin in those projects.
For example like in
foo
subproject
Copy code
configurations.consumable("detektMain") {
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
        attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE, objects.named("detekt-main"));
    }
    outgoing.artifact(File("")) { builtBy("detektMain") }
}
and in the root project
Copy code
dependencies {
    implementation(project(":foo"))
}
val bar by tasks.registering {
    dependsOn(
        configurations.compileClasspath.map {
            it.incoming.artifactView {
                withVariantReselection()
                attributes {
                    attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
                    attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE, objects.named("detekt-main"));
                }
                componentFilter {
                    it is ProjectComponentIdentifier
                }
            }.files
        }
    )
}
Now invoking the
bar
task will trigger
:foo:detektMain
.
v
oh boy 😄 okay I'll try to sort it out thank you!
btw why is my way bad? it breaks what exactly .. configuration avoidance?
v
You do access other projects mutable model by requesting their configurations and tasks. This is almost as bad as doing cross-project configuration. It only works if the projects you access happen to be configured before your code runs. And even then it still introduces project coupling. This is highly discouraged and disturbs more sophisticated Gradle features and optimizations. For example it will not be compatible with the upcoming isolated projects which will allow the configuration of projects to be done in parallel which will speed up configuration and IDE sync. It would afair for example also not work if you would use
--configure-on-demand
. ... The variant-aware solution should be clean and compatible with all that.
v
I see, I try, thank you!
👌 1
v
Don't fall into the trap to set the artifact view to
lenient
, it does not do what one might think. 🙂 An artifact view is always lenient in regard to only getting the variants from the dependencies that provide it. Setting it to
lenient
would additionally ignore almost any exception that can happen, that is most often not what you want.