Gradle の公式ドキュメントの javax.inject.Inject を使ったサンプルを動かす方法（Project.exec のマイグレーション）

この記事は [ビジネスエンジニアリング株式会社（B-EN-G）アドベントカレンダー2024](https://qiita.com/advent-calendar/2024/b-en-g) の記事です。

## TL;DR
Gradle Version 8.11.1 （本記事 執筆時点で最新バージョン）の公式ドキュメントで  `javax.inject.Inject` を使ったサンプルが複数掲載されている。しかし、掲載箇所のスクリプトだけでは動作しない。
例えば、`ExecOperations` を使ったサンプル（[リンク](https://docs.gradle.org/8.11/userguide/service_injection.html#execoperations
) ）を動かすには、以下のように記述を追加する必要がある。

```gradle:build.gradle
import javax.inject.Inject

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'javax.inject:javax.inject:1'
    }
}

plugins {
    id 'java'
}
// ここより↑が追記部分
// ここから↓はサンプルスクリプトそのまま
abstract class MyExecOperationsTask extends DefaultTask {
    private ExecOperations execOperations

    @Inject //@javax.inject.Inject
    MyExecOperationsTask(ExecOperations execOperations) {
        this.execOperations = execOperations
    }

    @TaskAction
    void doTaskAction() {
        execOperations.exec {
            commandLine 'ls', '-la'
        }
    }
}

tasks.register("myInjectedExecOperationsTask", MyExecOperationsTask) {}
```
実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235622/019b0b41-e2c8-d7cd-3179-da79c246e6c0.png)

動作確認バージョン
```
Gradle 8.11
openjdk 21.0.5 2024-10-15 LTS
OpenJDK Runtime Environment Temurin-21.0.5+11 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.5+11 (build 21.0.5+11-LTS, mixed mode, sharing)
```

## 詳細：ここに至る経緯と得た知見
### きっかけ
Gradle において従来の外部コマンドを実行するタスクは以下である。
```gradle:build.gradle
tasks.register('simpleCommandExec') {
    exec {
        commandLine 'ls', '-la'
    }
}
```
しかしながら、 `exec` は 非推奨 deprecated である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235622/f6768268-7645-e6db-def9-e9d7cdfb9aad.png)

非推奨ではない外部コマンドの実行方法が知りたかった。

### 公式ドキュメントの記載
公式ドキュメントには以下のように記載されている（ [出典へのリンク](https://docs.gradle.org/8.11/userguide/upgrading_version_8.html#deprecated_project_exec) ）

> Deprecated `Project#exec` and `Project#javaexec`
> `The Project#exec(Closure)`, `Project#exec(Action)`, `Project#javaexec(Closure)`, `Project#javaexec(Action)` methods have been deprecated and will be removed in Gradle 9.0.
> 
> These methods are scheduled for removal as part of the ongoing effort to make writing configuration-cache-compatible code easier. There is no way to use these methods without breaking configuration cache requirements so it is recommended to migrate to a compatible alternative. The appropriate replacement for your use case depends on the context in which the method was previously called.
> 
> At execution time, for example in `@TaskAction` or `doFirst`/`doLast` callbacks, the use of Project instance is not allowed when the configuration cache is enabled. To run external processes, tasks should use an [injected `ExecOperation`](https://docs.gradle.org/current/userguide/service_injection.html#execoperations) service, which has the same API and can act as a drop-in replacement. The standard Java/Groovy/Kotlin process APIs, like java.lang.ProcessBuilder can be used as well.
> 
> At configuration time, only special Provider-based APIs must be used to run external processes when the configuration cache is enabled. You can use `ProviderFactory.exec` and `ProviderFactory.javaexec` to obtain the output of the process. A custom `ValueSource` implementation can be used for more sophisticated scenarios. [The configuration cache guide](https://docs.gradle.org/8.11/userguide/configuration_cache.html#config_cache:requirements:external_processes) has a more elaborate example of using these APIs.

非推奨になった経緯の説明とともに、`Gradle 9.0` で削除される。
代替手段は以下の 5個であることが示されている。
- [インジェクションされた ExecOperation](https://docs.gradle.org/current/userguide/service_injection.html#execoperations) service
- 標準の Java/Groovy/Kotlin プロセス API（例：`java.lang.ProcessBuilder`）
- `ProviderFactory.exec`
- `ProviderFactory.javaexec`
- カスタム `ValueSource` 実装

代替手段の 1つ目で出てくる [インジェクションされた ExecOperation](https://docs.gradle.org/current/userguide/service_injection.html#execoperations) service
のリンク先に掲載されているサンプルコードは以下のとおりです。

```gradle:build.gradle
abstract class MyObjectFactoryTask extends DefaultTask {
    private ObjectFactory objectFactory

    @Inject //@javax.inject.Inject
    MyObjectFactoryTask(ObjectFactory objectFactory) {
        this.objectFactory = objectFactory
    }

    @TaskAction
    void doTaskAction() {
        var outputDirectory = objectFactory.directoryProperty()
        outputDirectory.convention(project.layout.projectDirectory)
        println(outputDirectory.get())
    }
}

tasks.register("myInjectedObjectFactoryTask",MyObjectFactoryTask) {}
```
しかし、上記のソースコードを `build.gradle` に張り付けるだけでは、`@Inject` の部分でコンパイルエラーが出て実行できません。解決するためには、[冒頭で示したソースコード](#TLDR) のようにする必要があります。

### 感想文
- `gradle` を雰囲気で使っている勢としては動くようにするまで時間がかかった
- `jakarta.inject.Inject` ではなく `javax.inject.Inject` なんだね
    - Issue は上がっている https://github.com/gradle/gradle/issues/20183

## おまけ：他の実装方法
### `ProviderFactory.exec`
```gradle:build.gradle
tasks.register("sampleProvidersExec") {
    def result = providers.exec {
        commandLine 'ls', '-la'
    }.standardOutput.asText.get()
    print result
}
```

```gradle:build.gradle
def gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.get()

tasks.register("printGitVersion") {
    doLast {
        println gitVersion
    }
}
```

### カスタム `ValueSource` 実装

```gradle:build.gradle
import org.gradle.api.provider.ValueSourceParameters

// >8 >8 snip >8 >8 -----

abstract class GitVersionValueSource implements ValueSource<String, ValueSourceParameters.None> {
    @Inject
    abstract ExecOperations getExecOperations()

    String obtain() {
        ByteArrayOutputStream output = new ByteArrayOutputStream()
        execOperations.exec {
            it.commandLine "git", "--version"
            it.standardOutput = output
        }
        return new String(output.toByteArray())
    }
}
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()

tasks.register("printGitVersion") {
    doLast {
        println gitVersion
    }
}
```