Package Manager Revised Dependency Resolution
01 何が問題だったのか
Swift Package Manager(SwiftPM)には、SE-0145 で導入された「pinning」機能がありました。pinning は、依存パッケージのバージョンを特定のものに固定するための仕組みです。しかし、この設計には次のような問題がありました。
ひとつの仕組みで二つの用途を兼ねていた
pinning は、本来性質の異なる二つの用途を一つの機能で扱おうとしていました。
- 解決済みバージョンの記録と共有: チーム内で同じバージョンの依存パッケージを使うため、解決結果を記録してリポジトリに入れておきたい
- 特定バージョンへの固定: 挙動の悪いパッケージを特定バージョンに留め置き、
swift package updateでも更新されないようにしたい
この二つを兼ねるために、パッケージ単位で「autopinning」のオン・オフを切り替える設定が用意されていました。デフォルトではオンで、解決結果が自動的に Package.pins に記録されます。autopinning がオンだと swift package pin で特定バージョンに固定しても、swift package update を実行すれば全パッケージが最新に更新されてしまい、意図的な pin を維持することができません。特定の pin を残したいなら autopinning をオフにする必要があり、二つの機能が相互排他的で混乱を招いていました。
依存関係解決のタイミングが曖昧だった
Package.swift や Package.pins のバージョン要求が変更されても、SwiftPM は変更を自動的には検知しませんでした。swift package update を明示的に実行するか、チェックアウトを持たない新規ユーザーがビルドしようとするまで、解決の更新もエラー報告も行われません。要求が満たせなくなっていてもしばらく気付けないなど、いつ解決が走るのかが不明瞭でした。
fetch コマンドの名前が実態と合わなかった
swift package fetch コマンドは、名前に反してネットワークから必ず取得を行うわけではありませんでした。git の世界では fetch は「リモートから最新内容を取得する」動作を意味するため、この命名はユーザーを混乱させていました。
02 どのように解決されるのか
pinning 機能を一旦取り除き、「解決済みバージョンの記録」と「特定バージョンへの固定」を別々の仕組みとして分離します。このProposalでは前者にあたる resolved versions を導入し、後者の pinning は将来的に純粋な追加機能として再導入する余地を残しています。あわせて、fetch コマンドを resolve コマンドへ置き換え、依存関係解決のタイミングを明確化します。
削除される機能
次の要素は削除されます。
swift package pin/swift package unpinコマンドswift package updateの--repinフラグPackage.pinsファイル
Package.resolved と resolved versions
解決済みバージョンは常にトップレベルパッケージの Package.resolved に記録されます。このファイルが存在する場合、SwiftPM は記録されたバージョンを使って依存関係を解決します(最新版を取りに行くのではなく、記録に従う)。
MyApp/
├── Package.swift
├── Package.resolved ← 解決済みバージョンが記録される
└── Sources/
Package.resolved をリポジトリにコミットすると、チーム全員が同じバージョンの依存パッケージを使えます。.gitignore に追加した場合は、各ユーザーが個別に swift package update のタイミングで更新することになり、新規ユーザーは最新の適格バージョンから始まります。なお、ライブラリのようにほかのパッケージから依存される側の Package.resolved は、クライアントパッケージには影響しません。
Package.resolved には、バージョンに加えて各依存パッケージの Git リビジョンも記録されます。将来的には、記録済みバージョンが別のリビジョンに解決されてしまった場合に警告を出すといった活用が見込まれます。
swift package resolve コマンド
非推奨となる swift package fetch に代わり、swift package resolve が導入されます。挙動は次のとおりです。
Package.swiftとPackage.resolvedの両方を踏まえて依存関係を解決する- 解決できない場合はエラーを返す
Package.resolvedに記録されたバージョンがまだ適格ならそれを使う- 解決後は、チェックアウトされているバージョンと
Package.resolvedの記録が一致する - ネットワークアクセスは本当に必要なときにしか行わない(既に解決済みで要求も変わっていなければ、ネットワークは叩かない)
swift build、swift test、swift package generate-xcodeproj は、実行前に暗黙的に resolve を走らせます。解決できなければエラーで中断します。
# 通常のビルド。内部で resolve が走るが、変更がなければネットワークには触れない
$ swift build
# 明示的に解決だけを行う
$ swift package resolve
# すべての依存を最新の適格バージョンに更新し、Package.resolved も書き換える
$ swift package update
swift package show-dependencies も暗黙の resolve を行いますが、解決に失敗しても分かっている範囲の依存情報は表示します。swift package edit は、解決に失敗していても対象のパッケージさえ特定・取得できていれば編集を許可します(これは、編集を通じて解決不能なグラフを修正できるようにするためです)。swift package unedit は編集を解除したうえで resolve を行います。
バージョン要求変更時の挙動
Package.swift のバージョン要求が変更され、Package.resolved に記録されたバージョンが適格でなくなった場合、resolve は該当パッケージを最新の適格バージョンへ自動的に更新します。その影響で再解決が必要になった他のパッケージについては、可能な限り既存の記録バージョンを維持し、どうしても合わなければ同様に更新します。新しく依存グラフに加わったパッケージは追加され、外れたパッケージは Package.resolved から取り除かれます。
edit モードとの関係
依存パッケージが edit モードにある間は、Package.resolved に記録されたバージョンと異なるバージョンがチェックアウトされていても構いません。edit 中のパッケージのバージョンは自動的には書き換えられません。edit モードのままで swift package update を実行すると、edit 中のパッケージおよびその配下のパッケージの記録バージョンが Package.resolved から取り除かれ、edit モードを抜けた後の再解決で新しく記録され直します。
ファイル拡張子として .lock を採用しなかった理由
他のパッケージマネージャ(Cargo、Bundler、Yarn など)では .lock 拡張子が一般的ですが、SwiftPM はあえて Package.resolved という名前を採用しています。
swift package updateやPackage.swiftの変更で簡単に書き換わるため、「lock」と呼ぶほど強い固定を表していない- 将来的に pinning を再導入したとき、「lock」と「pin」の違いが分かりにくくなる
- 「lock」は POSIX ファイルロックや並行処理のロックと用語が衝突している
Future Directions
このProposal では pinning を完全に削除しますが、将来的に「純粋な追加機能」として再導入する可能性が示されています。再導入される pinning は次のようなものが想定されています。
swift package pinで明示的に指定したときだけPackage.pinsに記録される- pin されたパッケージは
swift package updateの対象外になり、更新するにはまず unpin する必要がある - resolved versions と併用でき、必要な人だけが使う
なお、これはあくまで将来の見通しであり、具体的な仕様や時期を約束するものではありません。