パッケージマネージャの条件付きプラグイン
Package Manager Conditional Plugin
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SwiftPM のビルドツールプラグイン(SE-0303)は、リンター(SwiftLint)、フォーマッタ(SwiftFormat)、コード生成(SwiftGen、SwiftProtobuf)、ドキュメント生成(DocC)など幅広い用途で使われています。ターゲット宣言の plugins: パラメータでプラグインを指定すると、そのプラグインがビルドのたびに適用されます。
問題は、この plugins: パラメータには条件付き適用のしくみがまったくなかったことです。ターゲットの依存関係は .when(platforms:)(SE-0273)、ビルド設定は BuildSettingCondition、パッケージのトレイト(trait)は .when(traits:)(SE-0450)で条件を付けられるのに、プラグインの適用だけが条件を付けられない唯一のターゲット設定として取り残されていました。
これが実害になるのは、ビルドツールプラグインが「ビルドされる成果物」ではなく「ビルド環境」の一部だからです。パッケージ自体は複数プラットフォームに対応していても、そのプラグインが特定のビルドホストでしか動かない、あるいは特定のターゲットプラットフォーム向けにしか意味のない出力を生成する、というケースは珍しくありません。
具体例: Linux で失敗する SwiftLint
たとえば macOS で開発しているパッケージが、ビルドツールプラグインとして SwiftLint を使っているとします。
.executableTarget(
name: "MyTool",
plugins: [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
)
SwiftLint はビルド済みバイナリを含む artifact bundle を配布しています。このバイナリは新しめの glibc に対してコンパイルされているため、一部の Linux ディストリビューション(Amazon Linux 2 など)でこのパッケージをビルドすると、プラグインのバイナリが実行できず、ビルドがコンパイルに到達する前に失敗してしまいます。
swiftlint: /lib64/libc.so.6: version `GLIBC_2.34' not found
error: failed: PrebuildCommand(...)
SwiftLint は開発者のマシンでのみ意味を持つツールであり、コンパイル結果には何の影響も与えません。それにもかかわらず、「このプラグインは macOS でのみ適用する」とマニフェストに書く手段がありませんでした。
既存の回避策とその限界
現状で唯一の回避策は、Package.swift の中で #if を使ってプラグインの配列を条件付きで組み立てることです。
#if os(Linux)
let lintPlugins: [Target.PluginUsage] = []
#else
let lintPlugins: [Target.PluginUsage] = [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
#endif
しかしこの方法にはいくつもの欠点があります。
- マニフェストAPIの他の部分(
.when(platforms:)や.when(traits:))と一貫しません。 - プラグインを使うすべてのターゲットで、インラインでの宣言をやめて計算済み変数を参照する必要があり、冗長でミスを招きやすくなります。
Package.swiftはホスト上で一度だけパースされるため、#if os(...)はホストプラットフォームに対して評価されます。クロスコンパイル時にターゲットプラットフォームで条件分岐することができません。- ホストごと・トレイトごとに異なるプラグインを使いたい場合、
#ifブロックが増殖してスケールしません。 Package.swiftは宣言的なマニフェストとして設計されているのに、マニフェストのコンパイル時条件分岐はあくまで逃げ道であり、SwiftPM がビルドを計画するときではなくマニフェストのコンパイル時に評価されてしまいます。
とくにトレイトはプラグインとの相性が良い機能です。多くのプラグインは成果物の意味論ではなくワークフロー上のポリシーを表します。たとえば Lint というトレイトを定義し、swift build --traits Lint のように有効にしたときだけリンターを適用する、といった使い方が自然に考えられます。
02 どのように解決されるのか
Target.PluginUsage に、新しい PluginUsageCondition 型を使ったオプショナルな condition パラメータを追加します。この条件で、プラグインを適用するホストプラットフォーム・ターゲットプラットフォーム・有効なトレイトをそれぞれ独立に制約できます。
.executableTarget(
name: "MyTool",
plugins: [
.plugin(
name: "SwiftLintBuildToolPlugin",
package: "SwiftLintPlugins",
condition: .when(hostPlatforms: [.macOS])
),
]
)
これで、先ほどの SwiftLint の例のように「macOS でのみプラグインを適用する」ことをマニフェスト上で宣言的に表現できます。
ホストとターゲットを別軸で指定する
プラグインには「ホスト上で動作し、ターゲット向けの出力を生成する」という二面性があります。SE-0387 の定義では、ホストはコードをビルドするマシン、ターゲットはコードが動作するマシンを指します。そのため条件も両者を別々のパラメータで指定します。
hostPlatforms:はプラグインが動作するホストを制約します(例: macOS 専用バイナリに依存するプラグイン)。targetPlatforms:はプラグインの出力が有効なターゲットを制約します(例: Apple プラットフォームでしかコンパイルできない Metal シェーダのコード生成器)。
.plugin(
name: "MetalShaderGenerator",
package: "MetalShaderGenerator",
condition: .when(targetPlatforms: [.macOS, .iOS, .tvOS, .visionOS])
)
トレイトで指定する
SE-0450 のトレイトを使って、プラグインをトレイトで条件付けることもできます。
.plugin(
name: "SwiftLintBuildToolPlugin",
package: "SwiftLintPlugins",
condition: .when(traits: ["Lint"])
)
こうすると、すべてのビルド・すべてのプラットフォームでプラグインを走らせることなく、swift build --traits Lint でリンターを有効化する、といった opt-in の運用ができます。
フィルタの合成規則
各フィルタは独立しており、条件は「指定したすべてのフィルタが一致したとき」にのみ満たされます。指定しなかった軸には制約がかかりません。when(hostPlatforms:targetPlatforms:traits:) は、少なくとも 1 つの引数を指定する必要があります。
public struct PluginUsageCondition: Sendable {
public static func when(
hostPlatforms: [Platform]? = nil,
targetPlatforms: [Platform]? = nil,
traits: Set<String>? = nil
) -> PluginUsageCondition
}
PluginUsageCondition は BuildSettingCondition(SE-0238)にならい、オプショナル引数を持つ単一のファクトリメソッドを公開する形にしています。これにより、将来フィルタの軸が増えてもオーバーロードが組み合わせ的に増えるのを避けられます。
ビルド計画時の挙動
SwiftPM がビルドを計画し、条件付きのプラグイン利用に出会ったときの挙動は次のとおりです。
- 条件の評価:
hostPlatformsはホストプラットフォームに、targetPlatformsはターゲットプラットフォーム(クロスコンパイルでない場合はホストと同じ)に、traitsは有効なトレイトの集合に対して照合されます。 - プラグインのスキップ: 条件を満たさない場合、そのプラグインは実行されません。プラグインの prebuild/build コマンドはビルドグラフに追加されません。
- 依存解決は変わらない: 条件を満たさない場合でも、プラグインのパッケージ依存自体は SE-0273 と同様に解決・取得されます。これは依存解決にホスト固有のロジックを持ち込まないための設計です。
- バイナリ artifact の扱い: プラグインが現在のプラットフォーム向けに存在しないバイナリ artifact を使っていても、条件でそのプラットフォームが除外されていれば、SwiftPM はエラーを出しません。この提案がない状態では、使われないはずのバイナリが原因でビルドが失敗しえます。
この変更は additive で、condition を指定しない既存のプラグイン利用はこれまでどおり動作します。新しいAPIは新しいtools versionでゲートされます。Package.swift で #if os(...) を使っていたパッケージは、この新APIへ移行することで、よりクリーンで宣言的なマニフェストに書き換えられます。
03 今後の見通し
より細かいプラットフォームのフィルタリング
現在の Platform enum では、Linux ディストリビューションの違いや libc の種類、アーキテクチャを区別できません。.when(hostPlatforms: [.macOS]) は Linux ホストをまとめて除外できるので、前述の「Amazon Linux 2 上の SwiftLint」問題はこれで解決できますが、「glibc 2.34 以上を持つ Linux ホスト」のような条件は表現できません。これには Platform 自体をより表現力のあるものにする必要があり、SE-0387 の Future Directions や Platform Steering Group の作業(SP-0001)で検討されています。PluginUsageCondition は Platform をそのまま利用するため、こうした粒度の向上を追加の提案なしに取り込めると考えられています。
プラグインの configuration 条件
SE-0273 では .when(configuration: .debug) のような configuration 条件が提案されつつも、まだ実装されていません。もし TargetDependencyCondition に configuration 条件が追加されるなら、PluginUsageCondition にも追加すべきとされています。よくある使い方としては、debug ビルドのときだけリンターのプラグインを適用する、といったものが考えられます。
実行中プラグインからのターゲットプラットフォーム認識
この提案は、ターゲットプラットフォームに応じてプラグインを「実行するかどうか」をマニフェスト側で決めるものです。これを補完する方向として、実行中のプラグインに対して PluginContext からホストとターゲットの triple を公開し、プラグインが生成するコマンドをターゲットに応じて変えられるようにする、という構想もあります。この 2 つの方向は自然に組み合わさり、互いに独立しています。