# Replacing deprecated \`Project#exec\` in \`doFirst\`/\`doLast\`

**URL:** <https://discuss.gradle.org/t/replacing-deprecated-project-exec-in-dofirst-dolast/51381>\
**Category:** Help/Discuss\
**Created:** [July 30, 2025, 3:09am UTC](https://discuss.gradle.org/t/replacing-deprecated-project-exec-in-dofirst-dolast/51381 "2025-07-30T03:09:35Z")\
**Posts on this page:** 1\
**Showing post:** 5

<div class="post-metadata">

**Author:** ![dfabulich](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/dfabulich/32/11692_2.png) [@dfabulich](https://discuss.gradle.org/u/dfabulich)\
**Post date:** [July 31, 2025, 1:08am UTC](https://discuss.gradle.org/t/replacing-deprecated-project-exec-in-dofirst-dolast/51381/5 "2025-07-31T01:08:32Z")

</div>

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](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.”

```gradle
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:

```gradle
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](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](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!

> [@Gradle 8.11.1 change project.exec to?](https://discuss.gradle.org/t/gradle-8-11-1-change-project-exec-to/49991):
>
> I get this information that I should move project.exec to Use ExecOperations.exec(Action) or ProviderFactory.exec(Action) instead. I can’t figure out how either of them. This is the code currently used: ext.getCurrentBranchName = { new ByteArrayOutputStream().withStream { os -\> exec { executable = 'git' args = ['symbolic-ref', '--short', 'HEAD'] standardOutput = os } return os.toString() } } ext.branch = getCurrentBranchNa…

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`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.html), 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:

```gradle
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.

---

_[View the full topic](https://discuss.gradle.org/t/replacing-deprecated-project-exec-in-dofirst-dolast/51381)._
