gradle: how to remove task dependency - gradle

Gradle makes it easy to add task dependencies at runtime.
Is it also possible to remove a task dependency after it has been added?
As a use case, let's say we want to run checkstyle in a separate pipeline step, not as part of the main build. So, we apply the checkstyle plugin, but that adds some tasks that depend on check. I would like to break that dependency, so that checkstyle only runs when explicitly run, not as part of build (which depends on check, which depends on checkstyle*).
To accomplish the above, I could run the main pipeline step as build -x checkstyleMain -x checkstyleTest.
Another workaround would be to set the enabled property of the checkstyle tasks to false.
But for this question, I am interested in a generic way to remove a task dependency so that the task graph is missing the edge between 2 tasks, like that dependency was never added.
The problem is not an easy one because:
removing a dependency before it was added won't have any effect
trying to remove a dependency on gradle.taskGraph.whenReady will probably fail (since it's no longer allowed to change the task graph.
A good answer would also be that this is for sure not possible, in which case I could raise a feature request with the Gradle team.
Update 1
I tried to convert #Chriki's groovy to kotlin, like this:
project.afterEvaluate {
tasks.check {
dependsOn -= tasks.find {
it.name.startsWith("checkstyle")
}
}
}
But this doesn't work. I get an error: "Removing a task dependency from a task instance is not supported."

Executing ./gradlew bar on the following self-contained build, will not run the foo task, even though it was declared as a dependency of bar (tested with Gradle 7.1):
task foo {
doFirst {
println 'foo'
}
}
task bar {
dependsOn foo
doFirst {
println 'bar'
}
}
project.afterEvaluate {
bar.dependsOn -= foo
}
In your case, you could hence add something like the following to your build.gradle to unconditionally remove the undesired Checkstyle task dependencies from the check task:
project.afterEvaluate {
check.dependsOn -= checkstyleMain
check.dependsOn -= checkstyleTest
}
Using the Kotlin DSL, the following works for my build script above:
project.afterEvaluate {
val bar = tasks.findByPath("bar")
if (bar != null) {
bar.setDependsOn(bar.dependsOn - listOf(tasks.findByPath("foo"), "foo"))
}
}
It’s important not to use the -= operator.
Please excuse the verbosity. I’m sure this can be written more concisely but I’m not fluent enough in Kotlin.

Thanks to great #Chriki answer, I've got mine as well. Leaving it here for others to use it.
I'm using quarkus and annotation processing. When upgraded to newer version, I had circular dependencies:
Circular dependency between the following tasks:
:kspKotlin
+--- :quarkusGenerateCode
| \--- :processResources
| \--- :kspKotlin (*)
\--- :quarkusGenerateCodeDev
\--- :processResources (*)
In order to break that, I used the following code in my build.gradle.kts:
project.afterEvaluate {
tasks.findByPath("application:quarkusGenerateCode")?.let {
it.setDependsOn(it.dependsOn.map { it as Provider<*> }.filter { it.get() !is ProcessResources })
}
tasks.findByPath("application:quarkusGenerateCodeDev")?.let {
it.setDependsOn(it.dependsOn.map { it as Provider<*> }.filter { it.get() !is ProcessResources })
}
}
If you have more than one module that has circular dependency, you may want to use this snippet, as it searches through all modules:
project.afterEvaluate {
getTasksByName("quarkusGenerateCode", true).forEach { task ->
task.setDependsOn(task.dependsOn.map { it as Provider<Task> }.filter { it.get().name != "processResources" })
}
getTasksByName("quarkusGenerateCodeDev", true).forEach { task ->
task.setDependsOn(task.dependsOn.map { it as Provider<Task> }.filter { it.get().name != "processResources" })
}
}
It assumes that provider is a type Task and searches the breaking dependency by name.

Related

Print sorted list of dependencies for all projects in Gradle build

I want to create a complete list of dependencies for all projects in my Gradle build to track changes over time.
For this purpose, both the list of projects and the list of all dependencies (direct and transitive) must be sorted. Without both, the diff between releases is useless.
My first attempt was
fun listDependencies(project: Project, configName: String) {
val config = project.configurations.findByName(configName) ?: return
println()
println("$configName of module ${project.name}:")
config.allDependencies.map {
" ${it.group}:${it.name}:${it.version}"
}.sorted().forEach {
println(it)
}
}
fun listDependencies(project: Project) {
listDependencies(project, "compileClasspath")
listDependencies(project, "testCompileClasspath")
}
/** Create sorted list of dependencies per module and only for the compileClasspath and testCompileClasspath */
task("listDependencies") {
doLast {
println("Dependency list per project")
allprojects.sortedBy { it.name }
.forEach {
listDependencies(it)
}
}
}
In the output, most versions are null and all transitive dependencies are missing.
I had to force dependency resolution by changing the config.allDependencies above to
config.resolvedConfiguration.resolvedArtifacts.map {
" ${it.moduleVersion}"
}.sorted().forEach {
println(it)
}
This works but gives me a warning that I shouldn't call resolvedConfiguration (Resolving unsafe configuration resolution errors).
What do I have to change to run this task once after the configuration phase only when I ask for it by running Gradle with ./gradlew listDependencies?
We don't need this task in every build.

How to set gradle subproject artifact as task input?

My gradle build has a subproject with a task that produces a file
$ ./gradlew :strings:tokenizeStrings # creates strings/string_tokens.csv
then in my root project I have a task which consumes that file
tasks.generateLocalizationFiles {
inputTokensCsvFile.set(layout.projectDirectory.file("strings/string_tokens.csv"))
}
this works, but since gradle doesn't know about the dependency, it only works if I run the two tasks manually in the right order
$ ./gradlew :strings:tokenizeStrings
$ ./gradlew :generateLocalizationFiles
I want to add the proper dependency to gradle so that I can run just :generateLocalizationFiles and it will go into the subproject and do whatever it needs to. But I can't figure out the right way to do it.
What I've tried:
Following Simple sharing of artifacts between projects, I tried adding a consumable configuration to the suproject build script
val localizationData by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
tasks.tokenizeStrings {
artifacts {
add("localizationData", outputTokensCsvFile) {
builtBy(this)
}
}
}
and then a resolvable configuration plus the dependency to the root project build script
val localizedStringData by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
// hook up our resolvable configuration to the strings' consumable configuration
dependencies {
localizedStringData(project(mapOf(
"path" to ":strings",
"configuration" to "localizationData")
))
}
tasks.generateLocalizationFiles {
dependsOn(localizedStringData)
inputTokensCsvFile.set(localizedStringData.singleFile)
}
but that fails, seemingly because the consumable configuration is not populated?
Caused by: java.lang.IllegalStateException: Expected configuration ':localizedStringData' to contain exactly one file, however, it contains no files.
You need to add the outgoing artifact directly in the subproject build script, not inside the task configuration (which is only run lazily). You also don't need builtBy if you're using a RegularFileProperty for the artifact.
val localizationData by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
artifacts {
add("localizationData", tasks.tokenizeStrings.flatMap { it.outputTokensCsvFile })
}
The trick is to use flatMap to lazily access the task. You should similarly use map when passing it to the task resolving the data. That allows for lazy task creation and implicitly tells gradle about the dependency between the two:
tasks.generateLocalizationFiles {
inputTokensCsvFile.set(localizedStringData.elements.map { it.first().asFile })
}
This still feels somewhat hacky, since it would be very clumsy if you wanted to repeat this for many artifacts, but it does seem to be the idiomatic way of doing it in gradle since it doesn't require any explicit dependency creation via builtBy/dependsOn.

Execute gradle task on sub projects

I have a MultiModule gradle project that I am trying to configure.
Root
projA
projB
other
projC
projD
projE
...
What I want to be able to do is have a task in the root build.gradle which will execute the buildJar task in each of the projects in the other directory.
I know I can do
configure(subprojects.findAll {it.name != 'tropicalFish'}) {
task hello << { task -> println "$task.project.name"}
}
But this will also get projA and projB, I want to only run the task on c,d,e...
Please let me know the best way to achieve this.
Not entirely sure which of these you're after, but they should cover your bases.
1. Calling the tasks directly
You should just be able to call
gradle :other/projC:hello :other/projD:hello
I tested this with:
# Root/build.gradle
allprojects {
task hello << { task -> println "$task.project.name" }
}
and
# Root/settings.gradle
include 'projA'
include 'projB'
include 'other/projC'
include 'other/projD'
2. Only creating tasks in the sub projects
Or is it that you only want the task created on the other/* projects?
If the latter, then the following works:
# Root/build.gradle
allprojects {
if (project.name.startsWith("other/")) {
task hello << { task -> println "$task.project.name" }
}
}
and it can then be called with:
$ gradle hello
:other/projC:hello
other/projC
:other/projD:hello
other/projD
3. Creating a task that runs tasks in the subprojects only
This version matches my reading of your question meaning there's already a task on the subprojects (buildJar), and creating a task in root that will only call the subprojects other/*:buildJar
allprojects {
task buildJar << { task -> println "$task.project.name" }
if (project.name.startsWith("other/")) {
task runBuildJar(dependsOn: buildJar) {}
}
}
This creates a task "buildJar" on every project, and "runBuildJar" on the other/* projects only, so you can call:
$ gradle runBuildJar
:other/projC:buildJar
other/projC
:other/projC:runBuildJar
:other/projD:buildJar
other/projD
:other/projD:runBuildJar
Your question can be read many ways, hope this covers them all :)
All of the ways mentioned by Mark can be used but all of them have some cons. So I am adding one more option:
4. Switching the current project
gradle -p other hello
This switches the "current project" and then runs all tasks named hello under the current project.
Example 5. Defining common behavior of all projects and subprojects,
allprojects {
task hello {
doLast { task ->
println "I'm $task.project.name"
}
}
}
subprojects {
hello {
doLast {
println "- I depend on water"
}
}
}
From the Gradle documentation,
https://docs.gradle.org/current/userguide/multi_project_builds.html

Gradle: replace module dependency with project dependency

Using Gradle 1.12, is it possible to create a resolution strategy rule that replaces a module dependency with a project one under certain circumstances?
The reason for this is that we have many projects in the company (dozens), and I don't want to pollute the build scripts with things like:
dependencies {
elastic "company:somelib:1.0.+", "SomeLib"
}
Instead i'd like to achieve something along the lines of:
dependencies {
compile "company:somelib:1.0.+"
}
...
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (/* ... Check if project is in build ... */) {
details.useTarget project(':SomeLib')
}
}
}
So far I have not been able to replace a module dependency with a project one in a resolution strategy rule. Is there a way to achieve this?
EDIT: These are things I tried (all resulted in an error):
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (/* ... Check if project is in build ... */) {
details.useTarget project(':SomeLib')
details.useTarget ':SomeLib'
// Since I noticed this is how actual project dependencies look like
details.useTarget 'Project:SomeLib:version'
details.useTarget new DefaultProjectDependency(...)
}
}
}
For future reference, this is the code that I've used in the end. This example implements our very specific flavor of this desired behavior, but others could take it as a starting point and tweak as needed.
gradle.projectsEvaluated {
def prjMap = [:]
allprojects.each { prj ->
prjMap[prj.archivesBaseName] = prj
}
allprojects.each { prj ->
def replace = []
prj.configurations.each { conf ->
conf.dependencies.each { dep ->
if (dep.group == 'company' && prjMap[dep.name] != null) {
replace += [conf: conf.name, dep: dep]
}
}
}
replace.each { rep ->
println "Replacing: $prj.name\t$rep.conf\t$rep.dep.name ==>> ${prjMap[rep.dep.name].name}"
prj.configurations.all*.exclude(group: 'company', module: rep.dep.name)
rep.dep.properties.excludeRules.each { ex ->
prj.configurations.all*.exclude(group: ex.group, module: ex.module)
}
prj.dependencies.add(rep.conf, prjMap[rep.dep.name])
}
}
}
Note that while replacing, I used aggressive exclude statements. This is because we have a hellish nightmare of cyclic dependencies and lib projects declaring whole apps as a transitive dep because they need some value class. In a more sane environment, one could simply eliminate the previous dependency entry like so:
replace.each { rep ->
println "Replacing: $prj.name\t$rep.conf\t$rep.dep.name ==>> ${prjMap[rep.dep.name].name}"
prj.dependencies.remove(rep.dep)
prj.dependencies.add(rep.conf, prjMap[rep.dep.name])
}
No, a resolution strategy cannot do this. It might be possible to implement a solution that, at the end of the configuration phase, iterates over the configurations' dependencies and replaces certain external dependencies with project dependencies. Not sure if it can be done without using Gradle internals, though.

Why my dependency is printed twice?

I have the following fairly simple build.gradle build script:
repositories {
mavenCentral()
}
configurations { libs }
dependencies {
libs 'org.hibernate:hibernate-core:4.3.5.Final'
}
configurations.libs.files { println it }
When I run it with gradlew build (I'm using Gradle 1.12, latest at the moment) I get the following:
DefaultExternalModuleDependency{group='org.hibernate', name='hibernate-core', version='4.3.5.Final', configuration='default'}
DefaultExternalModuleDependency{group='org.hibernate', name='hibernate-core', version='4.3.5.Final', configuration='default'}
These seem to be the same dependencies, but I don't get it why there are 2 of them when I have added just a single one.
Does anyone know why? Did I do something wrong? Or is there something I don't understand?
The Configuration#files method expects a predicate. (For API details, see Configuration in the Gradle Build Language Reference.) If you instead pass it a closure containing a println statement, (seemingly) strange things will happen. You probably want something like the following instead:
// configurations should only be resolved in
// the execution phase, so let's declare a task
task printLibs {
doLast {
configurations.libs.each { println it }
}
}

Resources