Maybe I’m misreading the documentation here (it’s very confusing), but my understanding is that providers.exec isn’t designed to run during the execution phase (though you can use it (abuse it?) to do that); it’s designed to run during the configuration phase, allowing you to invalidate the cached configuration model (the task graph). https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.provider/-provider-factory/exec.html
Allows lazy access to the output of the external process.
When the process output is read at configuration time it is considered as an input to the configuration model. Consequent builds will re-execute the process to obtain the output and check if the cached model is still up-to-date.
The process input and output streams cannot be configured.
I don’t want to invalidate the configuration cache. I just want to run a process at execution time.
Another issue with providers.exec is that stdout and stderr aren’t auto-piped to the console and “cannot be configured.”
tasks.register("myTask") {
doLast {
exec {
commandLine("sh", "-c", "echo OK myTask")
}
}
}
tasks.register("myTask2") {
doLast {
providers.exec {
commandLine("sh", "-c", "echo OK myTask2")
}.result.get()
}
}
myTask logs OK on the Gradle console; myTask2 logs nothing. To get the stdout and stderr off of providers.exec, I had to write it like this:
tasks.register("myTask2ManualOutput") {
doLast {
val execOutput = providers.exec {
commandLine("sh", "-c", "echo OK myTask2; echo ERR myTask2 >&2; exit 1")
isIgnoreExitValue = true
}
val exitCode = execOutput.result.get().exitValue
val stdout = execOutput.standardOutput.asText.get()
val stderr = execOutput.standardError.asText.get()
println("STDOUT: $stdout")
println("STDERR: $stderr")
println("Exit Code: $exitCode")
if (exitCode != 0) {
throw GradleException("Command failed with exit code $exitCode")
}
}
}
The docs said with some embarrassment that using ExecOperations.exec requires “some ceremony”, but this is way more ceremonious. It’s not even close to a drop-in replacement for project.exec. (But then, it was never really supposed to be.)
BTW, you know who generated this sample? An LLM! I didn’t know the difference between an ExecResult and an ExecOutput. (And why would I? None of this is documented in any user guide or tutorial.) The LLM generated it in one shot.
Before this week, I’d never heard of a Gradle “provider” or a Gradle “service injection” in Gradle scripts. Gradle tutorials don’t cover this material, because ordinary users of Gradle (even developers of basic custom tasks) didn’t normally need to learn such things.
https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.provider/-provider-factory/exec.html doesn’t even say how to use it. I only learned that I could call providers.exec when an LLM told me about it.
That’s why I filed https://github.com/gradle/gradle/issues/34483. I had to read the entire Gradle book to even understand how to use ExecOperations.exec and providers.exec. I’m still not 100% sure I understand them well enough to use them correctly.
And I’m not the only one struggling!
I strongly agree with @johanE’s take on the situation. Gradle never required me to learn this much about the internals of Gradle (providers, service injections, ProcessOutputValueSource, ExecOutput vs ExecResult).
I’m sure it all made sense at the time, but executing a process in Gradle now requires an AbstractSingletonProxyFactoryBean, the sort of architecture for architecture’s sake that people have been teasing us JVM folks about for decades.
Having said all that, the configuration cache seems like a good, important thing to have, and I do see why having build scripts monkeying around with the Project at execution time would make optimizations impossible.
What I’m asking for in my filed issue is a one-liner transformation from project.exec to another thing. In #34483 I suggest execOps.exec instead, and that execOps would be automatically available in scope at execution time, or, barring that, having a one-liner at the top of my build script that will give me an execOps instance that I can use at execution time.
The closest thing we have right now is to add this at the top of my build.gradle.kts file:
interface InjectedExecOps {
@get:Inject val execOps: ExecOperations
}
val execOps = project.objects.newInstance<InjectedExecOps>().execOps
And then I can replace project.exec (or just plain-old exec) with execOps.exec and get the exact behavior I want.