Swift Package プラグインの全体像

## 概要

Swift 5.6 から Swift Package にプラグインが追加され、 SPM から外部ツールの実行を含めた色々な処理ができるようになりました。 Swift 5.8 の時点では以下２種類のプラグインがサポートされています。

- ビルドツールプラグイン
- コマンドプラグイン

この記事では、まだプラグインに触れたことがない方でも、それぞれのプラグインで何ができて何ができないのかがだいたい理解できるようにまとめます。プラグインの実装方法の詳細などはこの記事では扱わないことにします。

## TL;DR

| | ビルドツールプラグイン | コマンドプラグイン |
| --- | --- | --- |
| 実行タイミング | ビルド時 | 手動 (CLI / IDE から) |
| 利用例 | ・swiftgen や swift-protobuf などを使ったコード生成 <br>・swiftlint などを使った lint | ・docc などを使ったドキュメント生成 <br>・swift-format や swiftlint などを使ったコードフォーマット |
| 何に相当するか | Xcode target の build phases | homebrew / mint などのツールで入れていた CLI |
| 制限 | ・ネットワークにアクセスできない <br>・package 内にファイル書き込みできない | ・ネットワークにアクセスできない |

## 参考リンク

- [SE-0303 Package Manager Extensible Build Tools](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md)
- [SE-0332 Package Manager Command Plugins](https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md)
- [Meet Swift Package plugins - WWDC22](https://developer.apple.com/videos/play/wwdc2022/110359/)
- [Swift Package Managerのプラグイン機能](https://qiita.com/usamik26/items/1c2cec0903fea2e03344)
- [SPM Build tools を用いて SwiftGen を導入する上での罠とTips](https://www.youtube.com/watch?v=h_q2M20FD1Y&t=637s)

## プラグインの概要

Swift package プラグインはビルド時や手動実行時に swift package から外部ツールを実行するための仕組みです。 プラグインはツール側で提供してくれていることもありますが、ツールのバイナリさえあれば自作することもできます。

例えば、以下のツールでは公式がプラグインを配布してくれています。提供されるプラグインはツールのレポジトリに同梱されていることが多いですが、別レポジトリに分けられていることもあり、以下の中では SwiftGen のプラグインは別レポジトリになっています。いずれの場合でもプラグインの実装は `Plugins` ディレクトリの中にあります。

- [apple/swift-format](https://github.com/apple/swift-format)
- [realm/SwiftLint](https://github.com/realm/SwiftLint)
- [nicklockwood/SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
- [SwiftGen/SwiftGenPlugin](https://github.com/SwiftGen/SwiftGenPlugin)

使いたいツールのプラグインが提供されていない場合は、ツールのレポジトリに対してプラグインを追加する PR を出すか、そのツールのバイナリに依存するプラグインを外部レポジトリに自作することもできます。プラグインの作り方はそれだけで１つの記事になるような内容なので、この記事ではこれ以上詳細については書かないことにします。

## ビルドツールプラグインの使い方

もしすでに使いたいツールのプラグインが提供されている場合、自分の swift package からそのプラグインを利用するのは非常に簡単です。

まずはビルドツールプラグインの使い方から見ていきます。ビルドツールプラグインはビルド時に自動的に実行されるプラグインで、主な用途としてコード生成などが想定されています。今回は別の例として、開発している target をビルドするたびに lint をかけるために SwiftLint のプラグインを使いたいとしましょう。

SwiftLint の `Package.swift` を見にいくと、以下のように `SwiftLintPlugin` という名前で `capability` が `buildTool()` のプラグイン、すなわちビルドツールプラグインが提供されていることがわかります。

[realm/SwiftLint - Package.swift#L40-L46](https://github.com/realm/SwiftLint/blob/cfd9a26e33823692d0531bffc8486544991c62f5/Package.swift#L40-L46)

このプラグインを使うためにやることは、自分が開発している package の `Package.swift`  の 

- `dependencies` に SwiftLint の package を書く
- lint をかけたい target の `plugins`  に SwiftLintPlugin を設定する

の２つのみです。

```diff_swift
// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    // ...
    dependencies: [
+       .package(url: "https://github.com/realm/SwiftLint", branch: "main")
    ],
    targets: [
        .target(
            name: "MyPackage",
            dependencies: [],
+           plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
        ),
     ]
)
```

これだけでビルドのたびにプラグインが実行されます。SwiftLint の場合は、ルールに引っ掛かるようなコードを書くと warning を表示してくれます。MyPackage 内のコードを適当にめちゃくちゃにしてビルドすると以下のように SwiftLint が warning や error を出してくれることが確認できます。

![image](https://res.craft.do/user/full/9dc82df4-0673-e42b-764f-aa9bf1431789/doc/BAF031CD-EA2F-424A-9066-B4CC6FE9005B/C5DE4909-BDF7-4090-A592-82414A163DBC_2/aPVLtho0ELYUVOlFge9lQTqjNcrMIiyeTOmVexuwTU4z/Image.png)

ビルドツールプラグインはビルドをきっかけ走りますが、とにかく毎回のビルドで走るのか必要なときだけ走るのかはプラグイン側の実装次第です。プラグイン側で入力ファイルと出力ファイルを明確に定義すれば、入力ファイルが前回のビルドから変化した場合や出力ファイルが存在しない場合のみプラグインを実行してくれます。

## コマンドプラグインの使い方

ビルドのたびではなく、手動で任意のタイミングで外部ツールを実行したいときに使うのがコマンドプラグインです。主な用途としてはコードフォーマットやドキュメント生成などが想定されています。[nicklockwood/SwiftFormat](https://github.com/nicklockwood/SwiftFormat) を例にコマンドプラグインの使い方を見ていきます。

SwiftFormat の `Package.swift` を見ると、プラグインとして `SwiftFormatPlugin` が提供されていることがわかります。コマンドプラグインでは `capability` が `command` となっています。

[nicklockwood/SwiftFormat - Package@swift-5.6.swift#L15-L22](https://github.com/nicklockwood/SwiftFormat/blob/7d3682f70f790805ecab81a685b711f6842f4698/Package%40swift-5.6.swift#L15-L22)

コマンドプラグインを使うためには、以下のように `dependencies` に package を追加します。特定の target のビルド時に実行するビルドツールプラグインと異なり、コマンドプラグインは任意のタイミングで任意の対象に対して実行できるので、 `target` に対して何か記述を追加するということはありません。

```diff_swift
let package = Package(
    // ...
    dependencies: [
        .package(url: "https://github.com/realm/SwiftLint", branch: "main"),
+       .package(url: "https://github.com/nicklockwood/SwiftFormat", branch: "master"),
    ],
    // ...
)
```

コマンドプラグインは CLI から実行することができます。コマンドラインから `swift package plugin --list` を実行すると利用可能なコマンドプラグインの一覧が表示されますが、 SwiftFormat を `dependencies` に追加することで `SwiftFormatPlugin` が `swiftformat` コマンドで実行できるようになっていることがわかります。

```shell-session
$ swift package plugin --list
‘generate-manual’ (plugin ‘GenerateManual’ in package ‘swift-argument-parser’)
‘swiftformat’ (plugin ‘SwiftFormatPlugin’ in package ‘SwiftFormat’)
```

実際に実行すると、プラグインが package 内にファイル書き込みすることに対する許可が求められます。コードフォーマットのためにはファイル書き込みが必要なことから、 `SwiftFormatPlugin` がそのためのパーミッションを設定しているためです。

```shell-session
$ swift package plugin swiftformat
Plugin ‘SwiftFormatPlugin’ wants permission to write to the package directory.
Stated reason: “This command reformats source files”.
Allow this plugin to write to the package directory? (yes/no)
```

上記の質問に `yes` と答えることでフォーマットが実行されます。

ただ、ローカルで手動実行する際には毎回プロンプトに対して yes と答えていても面倒ということ以外に問題はないですが、 CI ではインタラクティブに回答することができないのでこの質問を回避する必要があります。以下のように `--allow-writing-to-package-directory` オプションをつけて最初から package への書き込みを許可することで質問なしでいきなりフォーマットを実行することができます。

```shell-session
$ swift package plugin --allow-writing-to-package-directory swiftformat
```

コマンドプラグインは、 CLI 以外に Xcode からも実行することができます。 `dependencies` に SwiftFormat を追加した状態で package を右クリックすると、 `SwiftFormatPlugin` の項目が追加されています。これをクリックすることで、 package に対してコードフォーマットがかかります。

![image](https://res.craft.do/user/full/9dc82df4-0673-e42b-764f-aa9bf1431789/doc/BAF031CD-EA2F-424A-9066-B4CC6FE9005B/F8AEDE6F-745D-493F-AA03-20C6C1418337_2/OM02i5wR6nvSJg5glZxs3Fp8MAi3ph2VxE2nKZGowRwz/Image.png)

コマンドプラグインには、実行時に引数を渡せるようになっています。 CLI からは通常のコマンドライン引数で、 Xcode からの実行では以下の UI から引数を指定できます。

![image](https://res.craft.do/user/full/9dc82df4-0673-e42b-764f-aa9bf1431789/doc/BAF031CD-EA2F-424A-9066-B4CC6FE9005B/6450EEF0-C8D0-430A-B3B1-79231E239497_2/cZkwKxTOSAUo0xrnzfopiLO1AmiZxh4NmnW8vGJDl44z/Image.png)

受け取った引数をどう扱うかはプラグインの実装次第ですが、ちゃんと作られたプラグインでは不自由なくプラグインを利用できるように引数が指定できるようになっているはずです。例えば、SwiftFormat のコマンドプラグインではプラグインに渡した引数がそのままツールに渡されるようになっており、直接 SwiftFormat のバイナリを実行するのと同じ使い勝手が実現されています。

[nicklockwood/SwiftFormat - SwiftFormatPlugin.swift#L57](https://github.com/nicklockwood/SwiftFormat/blob/7d3682f70f790805ecab81a685b711f6842f4698/Plugins/SwiftFormatPlugin/SwiftFormatPlugin.swift#L57)

## プラグインの制限

プラグインができることにはいくつかの制限がかけられています。プラグインを開発フローに組み込む上では、プラグインに何ができるかだけでなく何ができないかを理解しておく必要があります。

### package 内へのファイル書き込み

ビルドツールプラグインには package 内にファイル書き込みができないという制限があります。

例えば、Xcode target の build phase でよくやる処理としてコードフォーマットがあると思いますが、コードフォーマットをするということは当然 package 内のファイルに対して書き込みが必要になるので、 swift package のプラグインではビルドごとに自動的にコードフォーマットをかけることはできません。

ビルドツールプラグインの主な用途としてコード生成が想定されていますが、 package 内にファイル書き込みができない以上どこにコードを生成すればいいのか不思議に思えます。実は、ビルドツールプラグインが書き込めるディレクトリとして、 target とプラグインの組み合わせに対してユニークなワークディレクトリが用意されています。ワークディレクトリは、 Xcode からビルドした場合は `DerivedData` 内に、 CLI からビルドした場合は `.build` ディレクトリ内に生成されます。 SwiftGen や SwiftProtobuf はワークディレクトリに成果物を出力することで、ビルドツールプラグインの package 内に書き込めないという制限を回避しつつ成果物を package から利用できるようにしています。

コマンドプラグインでは制限が緩められていて、プラグイン側で明示的にパーミッションを指定していれば package 内のファイルに書き込みが可能です。そのため、コマンドプラグインではコードフォーマットや linter の auto correct をかけることができます。

### ネットワーク通信

ビルドツールプラグインとコマンドプラグインのどちらもも、ネットワーク通信することはできません。 iOS 開発のフローの中でやりそうな処理で言うと、例えば dSYM ファイルを Firebase にアップロードするようなことは現状のプラグインではできないことになると思います。

ネットワーク通信が制限されているのはセキュリティの観点からだと思いますが、プロポーザルによると、現在のコマンドプラグインからのファイル書き込みと同様に、将来的にはプラグインのパーミッションを設定することでネットワーク通信ができるようになるかもしれません。

## プラグインはこれまでの何に相当するものか

最後に、 swift package のプラグインがプラグイン登場以前のどういう概念に相当するものなのかを見ていきます。

ビルドツールプラグインはビルドをきっかけに任意の処理を実行することができるので、 Xcode の target の build phases に対応するということができると思います。プラグイン登場以前は swift package のビルド時に何らかの処理をする方法がなかったので、ビルドツールプラグインの登場によってできることが大きく広がったと言えます。ただ、現状はファイル書き込みやネットワークアクセスの点で build phases よりもできることが少ない点には注意する必要があります。

コマンドプラグインでは外部ツールを実行することができるので、これまで homebrew や mint などのサードパーティツールを用いて導入していた CLI ツールに相当するものだと言えます。とくに、 swift package の開発や swift package を中心にしたアプリ開発においては、ほとんどの場合 homebrew や mint は利用しなくてよくなるのではないかと期待しています。

apple/swift-format や realm/SwiftLint など主要なツールでは公式にプラグインが提供されていますし、もし提供されていない場合でもプラグインを自作することができます。ツール類をプラグインでまかなうことで、開発が swift package で完結することになります。
もちろん、コマンドプラグインにはネットワークにアクセスできないなどの制限があるので置き換えが効かない場合もありますが、ほとんどのユースケースはカバーできそうです。サードパーティかつグローバルな環境に影響を及ぼす可能性のある homebrew や mint を使わず、 swift package でツール類の環境構築ができれば、とくにチーム開発においては大きなメリットがあると思います。