🛠️
Swift Package ManagerでBuildToolPluginを作る
- GitHub でリポジトリを新規に作って、ローカルに
clone
する -
clone
したリポジトリのルートディレクトリで SPM の初期化をする$ swift package init --type build-tool-plugin
-
Package.swift
を編集する
(基礎編/応用編の続きを想定するため、すでにexecutableTarget()
が実装済みであるとする)Package.swiftの例// swift-tools-version: 6.0 import PackageDescription let package = Package( - name: "LineCounter", + name: "LineCountPlugin", products: [ - .executable( - name: "lc", - targets: ["LineCounter"] - ), + .plugin( + name: "LineCountPlugin", + targets: ["LineCountPlugin"] + ) ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"), ], targets: [ .target(name: "LineCounterCore"), .executableTarget( name: "LineCounter", dependencies: [ "LineCounterCore", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), .testTarget( name: "LineCounterCoreTests", dependencies: ["LineCounterCore"], resources: [.process("Resources")] ), + .plugin( + name: "LineCountPlugin", + capability: .buildTool(), + dependencies: ["LineCounter"] + ) ] )
products
内のexecutable
はここでは削除しておいた方が良い -
Plugins
ディレクトリを作り、ファイルを配置する. ├── Plugins │ └── LineCountPlugin │ └── main.swift ├── Sources │ ├── LineCounter │ │ └── main.swift │ └── LineCounterCore │ └── LineCounterCore.swift └── Tests └── LineCounterCoreTests └── LineCounterCoreTests.swift
-
BuildToolPlugin
を実装するmain.swiftの例import Foundation import PackagePlugin @main struct LineCountPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { [ .buildCommand( displayName: "Line Count", executable: try context.tool(named: "LineCounter").url, arguments: [ context.package.directoryURL.appending(path: "Package.swift").path() ], outputFiles: [] ) ] } }
ここまででBuildToolPlugin
の実装自体はできているが、動作確認をしたいため一旦ダミーのライブラリを作る。
-
Package.swift
を編集するPackage.swiftの例// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "LineCountPlugin", products: [ .plugin( name: "LineCountPlugin", targets: ["LineCountPlugin"] ), + .library( + name: "Dummy", + targets: ["Dummy"] + ) ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"), ], targets: [ .target(name: "LineCounterCore"), .executableTarget( name: "LineCounter", dependencies: [ "LineCounterCore", .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), .testTarget( name: "LineCounterCoreTests", dependencies: ["LineCounterCore"], resources: [.process("Resources")] ), .plugin( name: "LineCountPlugin", capability: .buildTool(), dependencies: ["LineCounter"] ), + .target( + name: "Dummy", + plugins: ["LineCountPlugin"] + ), ] )
-
Sources
ディレクトリ内にDummy
ディレクトリを作り、ファイルを配置する. ├── Plugins │ └── LineCountPlugin │ └── main.swift ├── Sources │ ├── Dummy │ │ └── File.swift │ ├── LineCounter │ │ └── main.swift │ └── LineCounterCore │ └── LineCounterCore.swift └── Tests └── LineCounterCoreTests └── LineCounterCoreTests.swift
この状態で全体をビルドすると、ビルドのログに
Run custom shell script 'Line Count' 0.1 seconds
45 /Users/user/Library/.../LineCountPlugin/Package.swift
のように出力され、プラグインが実行されたことが分かる。
本例のソースコード
注意点
BuildToolPlugin
のPluginContext.tool(named:)
でコマンドの実行ファイルを見つけることになるが、罠があるので注意が必要。
products
でexecutableTarget
とexecutable
の名前に差がある場合、
let package = Package(
name: "LineCounter",
products: [
.executable(
name: "lc",
targets: ["LineCounter"]
)
],
targets: [
.executableTarget(name: "LineCounter")
]
)
executable
の名前で実行ファイルが生成される。products
にexecutable
が含まれていない場合はexecutableTarget
の名前で実行ファイルが生成される。そのため、2つの名前の一致性については注意が必要だ。
また、実行環境であるmacにすでに実行ファイルと同名のコマンドがインストールされていた場合(例えばbrewなどで)、そちらが優先して使われるため、自作したexecutable
を確実に使用したい場合は、同名の著名なコマンドが存在しないことを確認した方が良い。
関連
Discussion