SwiftGenとSwiftLintをSPMのプラグインで実行する
この記事は、「愛好会 開発合宿in沖縄に参加してきた(成果編) SPMベースのプロジェクトを作る」の記事の解説記事の1つです。
この記事では、SwiftGenとSwiftLintをSPMのプラグインで入れる方法をまとめました。
実装環境
- MacBook Pro 14インチ(M1 Pro)
- Xcode14.1
SPMのプラグインとは?
SPMのプラグインは、①ビルドツールプラグインと②コマンドプラグインの2種類があります。
今回メインで使用するのは、①のビルドツールプラグインで、これを使用することで、SPMのパッケージのビルド中にスクリプトを実行することができます。
この機能を使うことで、今までBuildPhase
で実行していたような処理をSPMでも同様に実行できます。
SwiftGenとSwiftLintは公式でプラグインのサポートしているので、複雑な設定なしに、SPMでSwiftGenやSwiftLintを実行できるようになりました!
SwiftGenのプラグインは、SwiftGenPluginという別のリポジトリで管理されています。
現状のビルドツールプラグインで使いにくいなと感じるのは、プラグインの仕様で、開発者が任意のパラメーターをプラグインの処理に渡したり、カスタマイズができないことです。(これはSwiftLintのREADMEにも記載があります)
例えばSwiftLintやSwiftGenで使う設定ファイルのパスを渡すことも難しいので、プラグインが指定したディレクトリの指定ファイル名の設定ファイルしか適用することができません。
またgitのdiffがあるファイルのみSwiftLintを適用するといったことを直接行うことはできません。
Package.swiftにプラグインの情報を追加する
実際にPackage内で使用できるように設定してみましょう。
今回はSPMProjectのリポジトリをサンプルとして紹介しながら、記述していきます。
なおこのサンプルは以下のような構成になっています。
まずはPackage.swift
にプラグインの情報を記載します。
(省略)
dependencies: [
.package(url: "https://github.com/u5-03/SwiftExtensions", from: "1.0.0"),
.package(url: "https://github.com/realm/SwiftLint", branch: "main"), // ①
.package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0") // ②
],
targets: [
.target(
name: "BasePackage",
dependencies: [
"FunctionA",
"FunctionB",
"FunctionC"
],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint"), // ③
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin") // ④
]
),
.target(
name: "FunctionA",
dependencies: [
"CommonPackage"
],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint"),
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
),
.target(
name: "FunctionB",
dependencies: [
"CommonPackage"
],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint"),
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
),
.target(
name: "FunctionC",
dependencies: [
"CommonPackage"
],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint"),
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
),
.target(
name: "CommonPackage",
dependencies: [
.product(name: "SwiftExtensions", package: "SwiftExtensions")
],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint"),
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
)
]
)
上記の①・②でそれぞれSwiftLint, SwiftGenのプラグインのリポジトリのURLをdependencyとして登録しています。
次に③・④でそれをTarget
のplugin
として登録しています。
これはTarget
ごとに設定する必要があるので、このサンプルPackageの場合は5つのTarget
全てに同様の記述をします。
これでPackage.swift
の設定は完了です!
なおSwiftLintについては、ブランチではなく、バージョンを指定すると、以下のようなエラーが出ました。
これはまだ原因がわかっていません。
設定ファイル等の実装をする
SwiftLint
SwiftLintの場合は、.swiftlint.yml
ファイルを実行ディレクトリに置くだけで完了です。
ちなみに設定ファイルを置いていない場合、置き場を間違えた場合、以下のようなエラーが出て、プラグインが実行できません。
エラーのメッセージの意味はよくわかっていないです(SwiftLintのソースコードを軽く追いましたが、わからなかった。。)
これで実行すると、以下のようなアラートが出ますが、これはそのままTrust & Enable All
を選択します。
これで以下のように、Lintの警告が表示されれば、動作確認は完了です!
ちなみに上記の画像には実は生成されたShell
ファイルのパスも書かれています。
/Users/yugo.sugiyama/Library/Developer/Xcode/DerivedData/SPMProject-asvabhmgdwvtmuevvdigpmgsnkwj/Build/Intermediates.noindex/LocalPackages.build/Debug-iphoneos/LocalPackages_BasePackage.build/Script-11304790157053709582.sh
の箇所がそれになります。
これを開いてみると、以下のようなことが書いてあります。
#!/bin/bash
/usr/bin/sandbox-exec -p "(version 1)
(deny default)
(import \"system.sb\")
(allow file-read*)
(allow process*)
(allow file-write*
(subpath \"/private/tmp\")
(subpath \"/private/var/folders/bz/cpl5mvf90g11jn08qzzz41mm0000gn/T\")
)
(deny file-write*
(subpath \"/Users/yugo.sugiyama/Dev/Swift/Sample/SPMProject/LocalPackages\")
)
(allow file-write*
(subpath \"/Users/yugo.sugiyama/Library/Developer/Xcode/DerivedData/SPMProject-asvabhmgdwvtmuevvdigpmgsnkwj/SourcePackages/plugins/localpackages.output/BasePackage/SwiftLintPlugin\")
)
" "/${BUILD_DIR}/${CONFIGURATION}/swiftlint" lint --cache-path /Users/yugo.sugiyama/Library/Developer/Xcode/DerivedData/SPMProject-asvabhmgdwvtmuevvdigpmgsnkwj/SourcePackages/plugins/localpackages.output/BasePackage/SwiftLintPlugin --config /Users/yugo.sugiyama/Dev/Swift/Sample/SPMProject/LocalPackages/.swiftlint.yml /Users/yugo.sugiyama/Dev/Swift/Sample/SPMProject/LocalPackages/Sources/BasePackage/BasePackage.swift /Users/yugo.sugiyama/Dev/Swift/Sample/SPMProject/LocalPackages/Sources/BasePackage/BaseView.swift /Users/yugo.sugiyama/Dev/Swift/Sample/SPMProject/LocalPackages/Sources/BasePackage/Generated/ImageAssets.swift
このコードは、SwiftLintのSwiftLintPlugin.swiftで生成された処理が書かれています。
これを見ると、lintするファイルのパスを個別に渡して、lintを適用していることがわかります。
SwiftGen
SwiftGenの場合は、設定ファイルとしてswiftgen.yml
を使用します。
今回SwiftGenで生成するコードのリソース等は、以下の通りです
- 画像
- 色
- Localizeのファイル
このうち、②・③はCommonPackage
で共通管理し、画像については使用するTarget
ごとに個別管理します。
最初は画像も共通管理しようとしていましたが、xib
やStoryboard
の場合、GUIIからでは別のModule
(Bundle
)の画像を直接表示することはできないのと、WWDCの動画や有識者の方から個別管理が推奨とのお言葉をいただいたので、この方針にしました!(アドバイスありがとうございます!)
そのため、今回は以下の2つのswiftgen.yml
を用意しました。
①については、すべてのTarget
で共通化しているため、input_dir
などのパスについては、$(TARGET_NAME)
の環境変数を使用しています。
input_dir: Sources/${TARGET_NAME}/Resources
output_dir: Sources/${TARGET_NAME}/Generated
xcassets:
- inputs:
- ImageAssets.xcassets
outputs:
- templateName: swift5
output: ImageAssets.swift
params:
forceProvidesNamespaces: true
publicAccess: false
enumName: ImageAssets
SwiftGenのプラグインでは、(1)実行ディレクトリと(2)各Target
のソースコードのディレクトにあるswiftgen.yml
を見つけて、コード生成を実行します。
したがって、共通で実行するものは(1)のディレクトリに、個別に適用したいものは(2)のディレクトリに配置しましょう。
ちなみに①の設定については、画像のリソースがないTarget
であっても、input_dir
のアセットファイルがないと、エラーになってしまうので、今回は空のアセットファイルを配置することで回避しています。
これでPackageをビルドすることで、コードを自動生成することができます。
ちなみにSwiftGenについては、さきほど記載したコマンドプラグイン
の機能も提供されており、コマンドラインから実行することもできます(参考)
以下のように、実行ディレクトリで実行することで、コード生成ができます。
このコマンドの場合、--config
のオプションを使うことで、任意の設定ファイルを指定することができます。
$ swift package --allow-writing-to-package-directory generate-code-for-resources
注意点は、上記の①の共通で利用しているswiftgen.yml
ファイルは、ビルド中にしか利用できない$(TARGET_NAME)
の環境変数が記述されているので、コマンドとしては実行できません。
カスタムのスクリプトを実装する
先ほど記載したように、現状のSPMのプラグインの仕様・制約により、SwiftLintなどのビルドツールプラグインには、パラメーターなどを渡すことができないので、diff
のあるファイルだけLintを適用するといったことは現状できません。
そこでなんとかその処理をカスタムできないかなと試行錯誤しました
方法としては、SwiftLintのSwiftLintPlugin.swiftに記載している内容を自分で書くことで実現できました(コード)
targets: [
// Use
.binaryTarget( // ①
name: "SwiftLintBinary",
url: "https://github.com/realm/SwiftLint/releases/download/0.50.1/SwiftLintBinary-macos.artifactbundle.zip",
checksum: "487c57b5a39b80d64a20a2d052312c3f5ff1a4ea28e3cf5556e43c5b9a184c0c"
),
.plugin( // ②
name: "SwiftLint",
capability: .buildTool(),
dependencies: ["SwiftLintBinary"]
),
.target(
name: "BasePackage",
dependencies: [
"FunctionA",
"FunctionB",
"FunctionC"
],
plugins: [
"SwiftLint", // ③
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
),
まずは①でLocalPackage
のTarget
にbinaryTarget
として、SwiftLintのバイナリを設定します。
そして②でそのバイナリを.plugin
を使用してビルドツールプラグインとして、設定します。
プラグインの名前は今回SwiftLint
としていますが、この名前はのちに記載するプラグイン設定ファイルのディレクトリ名と一致している必要があります。
最後に③で②のプラグインをTarget
に追加します。
ちなみにSPMではLocalの設定を書く場合は、dependenciesも含めて、ダブルクォーテーションで囲んでそのまま記載するようです(あまり今まで気にしていなかった。。)
次に実際のプラグインで実行する処理の内容を設定をします。(コード)
import PackagePlugin
@main
struct SwiftLintPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
// If want to run lint when specific target, use code below.
// if target.name != "CommonPackage" { return [] }
return [
.buildCommand(
displayName: "Linting \(target.name)",
// `targeKt.directory.string` is the directory SwiftLint.swift is located.
// Set custom swiftlint script path
executable: Path("\(target.directory.string)/../../../Scripts/swiftlint.sh"),
arguments: [
// Set `swiftlint` command path
try context.tool(named: "swiftlint").path,
// Set `.swiftlint.yml` file directory path
"\(target.directory.string)/../.."
],
environment: [:]
)
]
}
}
上記が今回試しに作ったカスタムのスクリプトです。
注意点は先ほど書いた通りディレクトリの名前で、必ずPlugins/{Package.swiftで定義した名前}
にしなければいけません
ファイル名やstruct名は任意で問題ないです。
このスクリプトは、Path
を使用してルートディレクトリに配置したScripts/swiftlint.sh
のファイルを実行し、その際にパラメータとして、swiftlint
のコマンドのパスと.swiftlint.yml
のファイルのパスを渡しています。
またこの処理の中ではTarget
名も取得できるので、特定のTarget
では実行しないといった条件分岐を書くことも可能です。
この方法を使うことで、swiftlint.sh
ファイルでは、以下のように今までようにSwiftLint
の処理を書くことができます。
$ $1 lint --no-cache --format --config $2/.swiftlint.yml
ただこの方法は最初に紹介した公式のプラグインを使用するよりも手間なので、可能ならカスタムしないで済むようにしたいところです。。
注意点
時々実装後に思った通りの挙動をしない場合があります。
その場合は、例のごとく、クリーンする、DerivedDataを消すなどしてキャッシュを消しましょう!
自分の場合は、比較的いつもよりクリーンすることが多かったです
しかしある程度小さくPackageを小さくしていれば、クリーンしてもビルド時間がそこまで長くなることはないと思います!
まとめ
今回はSPMのプロジェクトでSPMのプラグインを使って、SwiftGenやSwiftLintを実行する方法をまとめました。
今後はSPMを使って開発することも多いでしょうし、そこで今まで通り、SwiftGenやSwiftLintなどのツールを使えるようになったのは、開発体験の向上にも大きく貢献すると感じました。
参考資料
- Meet Swift Package plugins
- Swift Package Managerのプラグイン機能
- Swift Package ManagerでBuildToolPluginを利用する
- Swift Package ManagerのBuild Tools
- Swift packages: Resources and localization