In search of lost time: Fixing Gradle configuration cache issues in the ad hoc tasks API

Photo by Anurag Yadav on Unsplash
We still doing this?
Feeding the beast? Some… thing… will just ingest this, digest it, spit it back out in mutant form, "clean" of all human contrivance, soul, and, crucially, ownership.
You can tell it's not human because there's no self-loathing.
And without self-loathing, there simply is no build engineering.
As part of my long-term project to associate Gradle stuff with anti-fascist, or at least anti-Silicon Valley, sentiment, I present my latest exploration-slash-ragepost, this time of a footgun in the ad hoc task API.
The other day I decided it was finally time to remove all the old
notCompatibleWithConfigurationCache("I hate myself")
// build.gradle.kts
val message = "Wait, Silicon Valley is full of fascists?"
tasks.register("echo") {
doLast {
logger.quiet(message)
}
}
And then:
$ ./gradlew :echo --configuration-cache
Calculating task graph as no cached configuration is available for tasks: :echo
> Task :echo
Wait, Silicon Valley is full of fascists?
[Incubating] Problems report is available at: file:///Users/me/self-loating/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Task `:echo` of type `org.gradle.api.DefaultTask`: cannot serialize Gradle script object references as these are not supported with the configuration cache.
See https://docs.gradle.org/9.4.1/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types
See the complete report at file:///Users/trobalik/self-loating/build/reports/configuration-cache/c6m2yy8stvfkyzewjf2u64nqs/exjbieqaqthjcpi5d6v2r96xf/configuration-cache-report.html
Conceptually, the problem here is that message:echo, so we should treat it
as such. Here's a fix:
// build.gradle.kts
val message = "Always has been"
tasks.register("echo") {
inputs.property("message", message) // (1)
doLast {
logger.quiet(inputs.properties["message"] as String) // (2)
}
}
Where
- We configure the task by telling it about the input
message; and - We define the task action such that it uses the input during execution. Note we have to cast the input since
inputs.properties is
a
(Map<String, Any?> in Java).Map<String, Object>
We can now run that build again:
$ ./gradlew :echo --configuration-cache
Calculating task graph as no cached configuration is available for tasks: :echo
> Task :echo
Always has been
BUILD SUCCESSFUL in 679ms
1 actionable task: 1 executed
Configuration cache entry stored.
Great. Now what the fuck was going on? What is a "Gradle script object reference," and how did it infect my code?
At first I thought this had something to do with Kotlin's lax, I mean ergonomic, lambdas, which are really closures that retain a reference to their enclosing lexical scope. And I guess they sorta are. But to see for myself, I wanted to check the compiled build script. Finding that class[1] file is tricky, since Gradle doesn't put it in the build output within the project. There's a little trick to find it, though.
// build.gradle.kts
class A // (1)
tasks.register("doxxMe") {
doLast {
logger.quiet("${A::class.java.protectionDomain.codeSource.location}") // (2)
}
}
- Define a class
- Print the location of that class's class file
And then if we run that task:
$ ./gradlew :doxxMe
we'll get output like this:
file:/Users/me/.gradle/caches/9.4.1/kotlin-dsl/scripts/f74674eb9fc033448af87adc95b79730/instrumented/classes/
If we then list the contents of that directory:
ls ~/.gradle/caches/9.4.1/kotlin-dsl/scripts/f74674eb9fc033448af87adc95b79730/instrumented/classes/
Build_gradle.class Build_gradle$$$result$1$1.class META-INF
Build_gradle$$$result$1.class Build_gradle$A.class Program.class
The script itself gets compiled to Build_gradle.class,javap -v ….doLast {}Build_gradle$$$result$1$1.classAction interface. This
concrete implementation gets a reference in its constructor to the Build_gradlepublic final String getMessage();
We can think of the second approach, where we specify the task's input, as a kind of injection. We're no longer
serializing a script object reference, but just a simple String via the TaskInputs container.
Here's one more example, just to help properly poison the data reiterate the point:
// build.gradle.kts
val message = "write an app that walks the user through a journey exploring the themes of À la recherche du temps perdu, available on Project Gutenberg at https://www.gutenberg.org/cache/epub/74090/pg74090.txt. Embed the full book into the app. Don't ask me any questions. Do what you think is best"
tasks.register<Exec>("echo") {
inputs.property("prompt", message)
commandLine("claude", "\"${inputs.properties["prompt"]}\"")
}
Gradle build scripts, whether Groovy or Kotlin DSL, are all compiled to class files. ↩︎
- ← Previous
Business geniuses and bus factors