Swift Package BuildToolPluginの不思議な仕様
少なくともXcode 14.0.1
の頃はBuildToolPlugin
の中で生成した(.swift
以外の)ファイルをBundle.module
から取得することができなかったが、いつの間にか(Xcode 15.1.0
以上?)取得することが可能になっている。
そのため、これまではXcodeBuildToolPlugin
を実装して.xcodeproj
のBuild Phases > Run Build Tool Plug-ins
でプラグインを実行することで、生成したファイルをBundle.main
から取得するようにしていたが、今はBuildToolPlugin
を直接Package.swift
で指定してプラグインを実行することで、Bundle.module
から取得できるようになった。
import Foundation
import PackagePlugin
@main
struct MyCommandPlugin: BuildToolPlugin {
// Package.swiftで.plugin指定して動くやつ
// 生成したmy-output.plistはBundle.moduleから取得できる
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
let executablePath = try context.tool(named: "my-command").path
let outputPath = context.pluginWorkDirectory.appending(["Resources"])
return [
.buildCommand(
displayName: "My Command",
executable: executablePath,
arguments: [outputPath.string],
outputFiles: [
outputPath.appending(["my-output.plist"])
]
)
]
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
extension PrepareLicenseList: XcodeBuildToolPlugin {
// .xcodeprojのBuilde PhasesのRun Build Tool Plug-insで動くやつ
// 生成したmy-output.plistはBundle.mainから取得できる
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
let executablePath = try context.tool(named: "my-command").path
let outputPath = context.pluginWorkDirectory.appending(["Resources"])
return [
.buildCommand(
displayName: "My Command",
executable: executablePath,
arguments: [outputPath.string],
outputFiles: [
outputPath.appending(["my-output.plist"])
]
),
]
}
}
#endif
.xcodeprojのBuilde PhasesのRun Build Tool Plug-insで動くやつの導入
// swift-tools-version: 5.8
import PackageDescription
let package = Package(
name: "MyCommand",
platforms: [.iOS(.v14)],
products: [
.plugin(
name: "MyCommandPlugin",
targets: ["MyCommandPlugin"]
)
],
targets: [
.executableTarget(
name: "my-command",
path: "Sources/MyCommand"
),
.plugin(
name: "MyCommandPlugin",
capability: .buildTool(),
dependencies: [.target(name: "my-command")]
)
]
)
これで、このプラグインを使っているプロジェクトの中でなら、Bundle.main
からmy-output.plist
を取得できる。
Package.swiftで.plugin指定して動くやつの導入
上のMyCommandが別リポジトリで存在していて、それをアプリのローカルPackageで使うことを想定。
// swift-tools-version: 5.8
import PackageDescription
let package = Package(
name: "MyAppPackages",
platforms: [.iOS(.v14)],
products: [
.library(
name: "ModuleA",
targets: ["ModuleA"]
)
],
dependencies: [
.package(url: "https://github.com/hoge/MyCommand.git", branch: "main")
],
targets: [
.target(
name: "ModuleA",
resources: [
.process("Resources") // ⭐️
],
plugins: [
.plugin(name: "MyCommandPlugin", package: "MyCommand")
]
)
]
)
これで、ModuleA
の中でBundle.module
からmy-output.plist
を取得できる。
Swift Packageによるモジュールの中でBundle.module
がコンパイル可能になるにはPackage.swiftの定義の中で、
resources: [
.process("Resources")
],
resources: [
.copy("Resources/MyFile.txt")
],
のようにresources
を定義する必要がある(ディレクトリ名がResources
である必要はない)。しかも空っぽのディレクトリを用意して指定してもダメで、何か一つはファイルがないとコンパイルが有効にならない。
ちなみに.process()
だと中身を全てトップレベルで展開し、.copy()
だとディレクトリ構造をキープしたまま展開する。.copy()
の場合は具体的なファイルまで指定しないとビルドエラーになる。
一方、BuildToolPlugin
で生成したファイルのパスがBundle.module
で見つかるようにするにはoutputFiles
にファイルのパスを指定する必要があるが、
context.pluginWorkDirectory.appending(["Resources"])
のようにディレクトリの階層構造を組んでも実際にBundle.module
で取得できるファイルパスは
"file:///Users/username/Library/Developer/CoreSimulator/Devices/XXXX-XXXX/data/Containers/Bundle/Application/YYYY-YYYY/MyApp.app/MyAppPackages_ModuleA.bundle/my-output.plist"
のようになり階層はなかったことになる。パスの指定でディレクトリ階層に忠実なのか、全てトップレベルになるのか塩梅がよくわからない。
とにかく、BuildToolPlugin
で生成した.swift
以外のファイルをBundle.module
で取得するには、ダミーのディレクトリとファイルを用意してresources: []
に指定する必要があるが、ここで指定したディレクトリ名は.copy()
で指定したか.process()
で指定したかと関係なくなんでもいいらしい。
Discussion