🛠️

Swift Package ManagerでBuildToolPluginを作る

に公開
  1. GitHub でリポジトリを新規に作って、ローカルにcloneする
  2. cloneしたリポジトリのルートディレクトリで SPM の初期化をする
    $ swift package init --type build-tool-plugin
    
  3. 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はここでは削除しておいた方が良い
  4. Pluginsディレクトリを作り、ファイルを配置する
    .
    ├── Plugins
    │   └── LineCountPlugin
    │       └── main.swift
    ├── Sources
    │   ├── LineCounter
    │   │   └── main.swift
    │   └── LineCounterCore
    │       └── LineCounterCore.swift
    └── Tests
        └── LineCounterCoreTests
            └── LineCounterCoreTests.swift
    
  5. 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の実装自体はできているが、動作確認をしたいため一旦ダミーのライブラリを作る。

  1. 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"]
    +       ),
        ]
    )
    
  2. 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

のように出力され、プラグインが実行されたことが分かる。

本例のソースコード

https://github.com/Kyome22/LineCountPlugin/tree/main

注意点

BuildToolPluginPluginContext.tool(named:)でコマンドの実行ファイルを見つけることになるが、罠があるので注意が必要。

productsexecutableTargetexecutableの名前に差がある場合、

let package = Package(
    name: "LineCounter",
    products: [
        .executable(
            name: "lc",
            targets: ["LineCounter"]
        )
    ],
    targets: [
        .executableTarget(name: "LineCounter")
    ]
)

executableの名前で実行ファイルが生成される。productsexecutableが含まれていない場合はexecutableTargetの名前で実行ファイルが生成される。そのため、2つの名前の一致性については注意が必要だ。

また、実行環境であるmacにすでに実行ファイルと同名のコマンドがインストールされていた場合(例えばbrewなどで)、そちらが優先して使われるため、自作したexecutableを確実に使用したい場合は、同名の著名なコマンドが存在しないことを確認した方が良い。

関連

https://zenn.dev/kyome/articles/6a451efbcdc0ed

https://zenn.dev/kyome/articles/06e0b94559b92c

Discussion