Swift Package ManagerのBuildToolPluginでSwiftGenを利用する

# はじめに
* Siwft 5.6 から [Package Manager Extensible Build Tools](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) が利用できるようになりました
* これにより、SwiftPM のモジュールのビルド時に任意のスクリプトを実行する事ができるようになりました
* そこで、SwiftGen を利用して、Asset Catalog からコードを自動生成する事を試してみました
* Colors.xcassets にある aka のカラーアセットから SwiftGen を利用して、`Assets.aka.color` を作成し、参照したいです

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/6f9f2296-df64-a443-119b-87fbafca7659.png)

# Swift Package Manager の BuildToolPlugin で SwiftGen を利用する
## Artifact の実装
* Plugin で swiftgen を利用するために artifactbundle を準備します
* Artifact は、[Package Manager Binary Target Improvements](https://github.com/apple/swift-evolution/blob/main/proposals/0305-swiftpm-binary-target-improvements.md) で導入された仕組みで、SwiftPM でビルド済みツールがサポートされます
* swiftgen.artifactbundle を作成し、Info.json とビルド済みの swiftgen を追加しました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/d95e37c7-70f0-2388-e9be-4569c1616cf7.png)

* Info.jon は今回は、macos のみサポートとしました
* これで swiftgen.artifactbundle を利用できるようになりました

```json
{
    "schemaVersion": "1.0",
    "artifacts": {
        "swiftgen": {
            "type": "executable",
            "version": "6.5.1",
            "variants": [
                {
                    "path": "swiftgen-6.5.1-macos/bin/swiftgen",
                    "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
                }
            ]
        }
    }
}
```

## Package.swift の実装
* `binaryTarget` を利用して、swiftgen を取り込みます
* swiftgen を利用するプラグイン SwiftGenPlugin を作成します

```swift
let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary",
                 targets: ["MyLibrary"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(name: "swiftgen",
                      path: "Artifacts/swiftgen.artifactbundle"),
        .plugin(name: "SwiftGenPlugin",
                capability: .buildTool(),
                dependencies: ["swiftgen"]),
        .target(name: "MyLibrary",
                resources: [.process("ResourceFiles")],
                plugins: [.plugin(name: "SwiftGenPlugin")]),
    ]
)
```

## Plugin の実装

* Plugins/SwiftGenPlugin を作成し、SwiftGenPlugin を作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/bc93f591-628b-c3da-fa3f-796c192fcd3d.png)

* `context.tool(named: "swiftgen")` でバイナリのパスを取得します
* context と target から設定ファイルや出力パスなど適当に用意します
* pluginWorkDirectory 以下以外を出力先に設定しても、パーミッションが無いと言われてエラーになるので注意が必要です

```swift
@main struct SwiftGenPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        let outputFilesDirectory = context.pluginWorkDirectory
        return [
            .prebuildCommand(displayName: "SwiftGen",
                             executable: try context.tool(named: "swiftgen").path,
                             arguments: [
                                "config", "run",
                                "--config", target.directory.appending("ResourceFiles/swiftgen.yml").string,
                             ],
                             environment: [
                                "OUT_DIR": outputFilesDirectory.string,
                             ],
                             outputFilesDirectory: outputFilesDirectory)
        ]
    }
}
```

* swiftgen.yml は、出力パスを OUT_DIR の環境変数を参照するようにし、SwiftGenPlugin から設定できるようにします

```yaml
xcassets:
  inputs:
    - Colors.xcassets
  outputs:
    - templateName: swift5
      output: ${OUT_DIR}/Asset.generated.swift
```

## 自動生成されたコードの利用

* 自動生成されたコードはナビゲーションエリアに見えないので、もどかしいですが、利用できるようになってます

```swift
public struct MyLibrary {
    public let text = "Hello, World!"
    public let color: UIColor = Asset.aka.color
}
```

# まとめ
* Swift 5.6 で追加された Plugin を使って SwiftGen を利用してみました
* ビルド済みツールを Artifact で導入できるのがとても良かったです
    * 試してないですが、linux などターゲットでバイナリ分ける事もできそうなので便利そうです
* これで SwiftPM モジュールのアプリを作るのが捗りそうです