🛠️
ローカル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