🛠️

ローカルPackageのCommandPluginをProjectのRun Scriptで実行する

2024/06/01に公開

Swift Package CommandPluginを使えばartifact bundleとして配布されている外部のコマンドラインツールやexecutableTargetとして実装した自作のコマンドラインツールをSwift Packageの仕組みを介して実行できます。

CommandPlugin実装の大雑把な流れ

artifact bundleの場合

  1. Package.swiftを編集
    • 外部のコマンドラインツール(例えばSwiftLintやSwiftFormatなど)のartifact bundleをbinaryTargetで取り込む
    • PluginCapability.commandにして.pluginのTargetを定義
    // swift-tools-version: 5.10
    import PackageDescription
    
    let package = Package(
        name: "PluginPackages",
        platforms: [
            .macOS(.v14),
        ],
        products: [],
        targets: [
            .binaryTarget(
                name: "artifact bundleとして配布されているbinaryTarget名",
                url: "https://github.com/owner/repository/releases/download/x.y.z/artifactbundle.zip",
                checksum: "最初は空文字にしておいて、Xcodeに提示されたchecksumを入れると良い"
            ),
            .plugin(
                name: "実装するCommandPluginのフォルダ名",
                capability: .command(
                    intent: .custom(verb: "コマンド名", description: ""),
                    permissions: []
                ),
                dependencies: ["上のbinaryTarget名"]
            ),
        ]
    )
    
  2. CommandPluginを実装
    • context.tool(named:)でコマンドラインツールのPathを取得
    • Process.run(_:arguments:)でコマンドラインを実行
    • Processの異常終了をハンドリング
    import Foundation
    import PackagePlugin
    
    @main
    struct MyCommandPlugin: CommandPlugin {
        func performCommand(context: PluginContext, arguments: [String]) async throws {
            let tool = try context.tool(named: "コマンドラインツール名")
            let executableURL = URL(fileURLWithPath: tool.path.string)
    
            let process = try Process.run(executableURL, arguments: arguments)
            process.waitUntilExit()
    
            guard process.terminationReason == .exit else {
                Diagnostics.error("Termination Other Than Exit")
                return
            }
            guard process.terminationStatus == EXIT_SUCCESS else {
                Diagnostics.error("Command Failed")
                return
            }
        }
    }
    
  3. Terminalでswiftコマンドを使ってCommandPlugnを実行
    swift package plugin コマンド名 引数(オプション)
    

executableTargetの場合

  1. Package.swiftを編集
    • executableTargetを定義
    • PluginCapability.commandにして.pluginのTargetを定義
    // swift-tools-version: 5.10
    import PackageDescription
    
    let package = Package(
        name: "PluginPackages",
        platforms: [
            .macOS(.v14),
        ],
        products: [],
        targets: [
            .executableTarget(
                name: "任意のフォルダ名前",
            ),
            .plugin(
                name: "実装するCommandPluginのフォルダ名",
                capability: .command(
                    intent: .custom(verb: "コマンド名", description: ""),
                    permissions: []
                ),
                dependencies: ["上のexecutableTarget名"]
            ),
        ]
    )
    
  2. executableTargetでコマンドラインツールを実装
    • コマンドラインツールとしての振る舞いを簡単に実装するにはapple/swift-argument-parserを利用するのがおすすめ
    • 全部自前で書くならCommandLine.argumentsで引数などを受け取り、FileManagerなどを使ってファイル操作を行う
  3. CommandPluginを実装
    • context.tool(named:)executableTargetで実装したコマンドラインツールのPathを取得
    • Process.run(_:arguments:)でコマンドラインを実行
    • Processの異常終了をハンドリング
  4. Terminalでswiftコマンドを使ってCommandPlugnを実行
    swift package plugin コマンド名 引数(オプション)
    

本題:CommandPluginをProjectのRun Scriptで実行する

Xcode ProjectのBuild Phases > Run Scriptでシェルスクリプトが実行できるので、そこでswift package pluginコマンドを叩きます。

xcrun --sdk macosx \
  swift package --package-path ローカルパッケージのパス \
  plugin --allow-writing-to-directory 編集したいディレクトリのパス \
  コマンド名 引数(オプション)
  • CommandPluginはmacOSで動作させるため、xcrun --sdk macosxでmacOSのSDKを指定する
  • ディレクトリの構成がプロジェクトによってまちまちだと思うので、ローカルPackagesのパスを指定するには--package-pathオプションを使う
  • 編集したいディレクトリのパスを指定するには--allow-writing-to-directoryオプションを使う

ただし、Xcode 15からシェルスクリプトの実行にSandBoxが適用されるようになっており、プロジェクト内のファイルを編集する場合はBuild SettingsのUser Script SandboxingNOにする必要があります。参考:ENABLE_USER_SCRIPT_SANDBOXINGとは何なのか?

プロジェクト内のファイルを編集しないけれどRun Scriptでswift package pluginを実行したい場合は--disable-sandboxのオプションをつけます。参考:What does –disable-sandbox do for Swift Package Manager?

xcrun --sdk macosx \
  swift package --disable-sandbox --package-path ローカルパッケージのパス \
  plugin コマンド名 引数(オプション)

Discussion