🛠️

Build Tool Plugin で swift-format をビルド時実行させる

に公開

はじめに

筆者はiOS開発に2年以上のブランクがあり、ビルドスクリプトは Build Phases > Run Script でしか定義したことがありませんでした。

Xcode15以降はENABLE_USER_SCRIPT_SANDBOXING=YESがデフォルトのようです。
これを NO にすれば従来の Run Script でも動作はしたのですが、セキュリティ上 YES が推奨されている背景もあり、設定を変更せずに安全にビルド時ワークフローを定義できる Build Tool Plugin を導入することにしました。

USER_SCRIPT_SANDBOXING については、以下の記事が参考になりました。🙏
https://zenn.dev/miharun/articles/c6db02550ed3f8

Build Tool Plugin とは

Xcode14から導入された、Swiftを使ってビルド時のワークフローを定義できる仕組みのことです。
Swift Package Manager で設定可能です。

https://developer.apple.com/videos/play/wwdc2022/110401/?time=22

.swift-format を作成する

今回はビルド時にリント解析をかけたかったので、Apple公式のフォーマッター/リンターである swift-format を採用しました。
Xcode 16以降であれば、特別なインストールをしなくてもツールチェーンに組み込まれているようです。

https://github.com/swiftlang/swift-format

.swift-formatはルール適用のための構成ファイルです。
プロジェクトルートに.swift-formatを作成します。
デフォルトの設定は以下のコマンドで出力できます。

swift format dump-configuration

swift-format 用 Build Tool Plugin を作成する

メニューから File > New > Package > Build Tool Plug-in を選択して作成します。

コマンドラインからは以下を実行します。

swift package init --type build-tool-plugin --name ${YOUR_PLUGIN}

Pluginのパッケージが作成されたら、Plugins/YourPlugin.swiftファイルに実行したいコマンドを書いていきます。

以下がXcodeプロジェクトビルド時に実行される関数になります。

extension YourPlugin: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
        // ここにビルドコマンドの組み立てを記述していく
    }
}
swift-format用に書いた実際のプログラム
import PackagePlugin
import struct Foundation.URL

@main
struct SwiftFormatPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        // SwiftPMターゲット用のダミー(今回はXcodeプロジェクトでのみ動かすため空配列を返している)
        return []
    }
}

#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin

extension SwiftFormatPlugin: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
        let directoryURL = context.xcodeProject.directoryURL;
        let swiftFormatFile = directoryURL.appending(path: ".swift-format")
        // ⚠️ ご自身のプロジェクトのソースコードが格納されているフォルダ名に書き換えてください
        let sourceFiles = directoryURL.appending(path: "YourApp")
        return [
          .buildCommand(
            displayName: "Run swift-format",
            executable: try context.tool(named: "swift").url,
            arguments: [
              "format",
              "lint",
              "--strict",
              "--configuration",
              swiftFormatFile.path(),
              "-r",
              sourceFiles.path(),
            ],
            inputFiles: [],
            outputFiles: []
          )
        ]
    }
}

#endif

https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/writingbuildtoolplugin/

Xcode Project に作成した Build Tool Plugin を組み込む

最後に、アプリのターゲット設定を開き、Build Phases > Run Build Tool Plug-ins に先ほど作成した Build Tool Plugin を追加します。

XcodeGenを採用している場合は、project.ymlに以下を追加します。


packages:
+  SwiftFormatPlugin:
+    path: {YOUR_PACKAGE_PATH}/SwiftFormatPlugin #Package.swiftへの相対パス

targets:
  YourApp:
+   buildToolPlugins:
+     - plugin: SwiftFormatPlugin
+       package: SwiftFormatPlugin #packagesで定義したパッケージ名

これでプロジェクトをビルド(⌘ + B)した際、自動的に swift-format が走り、規約違反があればXcode上に直接エラーが表示されるようになりました!🎉

Discussion