🛠️
ローカルPackageのCommandPluginをProjectのRun Scriptで実行する
Swift Package CommandPluginを使えばartifact bundleとして配布されている外部のコマンドラインツールやexecutableTarget
として実装した自作のコマンドラインツールをSwift Packageの仕組みを介して実行できます。
CommandPlugin実装の大雑把な流れ
artifact bundleの場合
- 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名"] ), ] )
- 外部のコマンドラインツール(例えばSwiftLintやSwiftFormatなど)のartifact bundleを
-
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 } } }
-
- Terminalで
swift
コマンドを使ってCommandPlugnを実行swift package plugin コマンド名 引数(オプション)
executableTargetの場合
- 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名"] ), ] )
-
-
executableTarget
でコマンドラインツールを実装- コマンドラインツールとしての振る舞いを簡単に実装するにはapple/swift-argument-parserを利用するのがおすすめ
- 全部自前で書くなら
CommandLine.arguments
で引数などを受け取り、FileManager
などを使ってファイル操作を行う
-
CommandPlugin
を実装-
context.tool(named:)
でexecutableTarget
で実装したコマンドラインツールのPathを取得 -
Process.run(_:arguments:)
でコマンドラインを実行 - Processの異常終了をハンドリング
-
- 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 Sandboxing
をNO
にする必要があります。参考: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