Package Manager Version Pinning
01 何が問題だったのか
Swift Package Manager(SwiftPM)の依存解決は、Package.swift に書かれたセマンティックバージョン制約を満たす中から適切なバージョンを自動で選びます。この仕組みは「セマンティックバージョンに従っていればパッチ・マイナー更新は互換であり、常に最新を取りにいってよい」という前提に依存しています。しかし現実のプロジェクトでは、その範囲内のどのバージョンが選ばれるかを、バージョン制約とは独立に固定したい場面があります。
「works for me」を避けたいチーム開発
同じリポジトリを複数人や CI で開発しているとき、依存解決が時期によって異なるバージョンを選んでしまうと、「自分の環境ではビルドできるのに他の人の環境では失敗する」といった状況が生まれます。制約の範囲内での自動更新が入るたびに、再現性のない挙動差が生じる余地があります。
テストが難しい依存や壊れやすい依存
上流の依存パッケージが壊れやすかったり、問題の切り分けに手間がかかったりする場合、セマンティックバージョンを信じて自動で上げるのではなく、自分たちが検証済みの特定のバージョンに据え置きたいことがあります。セマンティックバージョン運用はそのまま守りつつ、どのバージョンを推奨するかを明示的に宣言する手段が欲しくなります。
デプロイ時の再現性
本番向けのリリースをビルドするときは、後から同じビルドを再現できることが重要です。そのためには、ビルドに使われた依存パッケージのバージョンを正確に記録し、あとから同じ構成を取り戻せるようにする必要があります。
これまでの SwiftPM の振る舞い
SwiftPM は、ローカルにチェックアウト済みの依存パッケージを、明示的な swift package update が無い限り勝手に更新することはありません。その意味では、各開発者の手元は事実上すでに「ピン留めされた」状態で動いています。しかし、そのピン留め情報をチームメンバーと共有する手段が無いため、異なる環境どうしで同じ依存構成を再現できませんでした。
つまり、セマンティックバージョン制約を上書きするわけではなく、その制約の範囲内で選ばれた具体的なバージョンを記録・共有する仕組みが欠けていたのがこの提案以前の状況です。
02 どのように解決されるのか
SwiftPM に、パッケージごとに使用する依存バージョンを固定するための pin(ピン留め) 機構を追加します。Package.swift のマニフェスト側の制約はそのまま活かしつつ、その制約の範囲内でどの具体バージョンを使うかを Package.pins というファイルに記録し、必要に応じてチームで共有できるようにします。
Package.pins ファイル
Package.swift の隣に置かれる任意ファイルとして Package.pins を導入します。ここにはパッケージ識別子、ピン留めされたバージョン、解決されたタグのコミットハッシュ(SHA)といった情報が記録されます。ファイル形式は実装依存の JSON です。
このファイルは、ソース管理にコミットするかどうかを利用者が選べます。チームで同じ依存バージョンを共有したいアプリケーションではコミットし、他者に取り込まれるライブラリでは .gitignore に入れて共有しない、という使い分けが想定されています(ライブラリの pin はダウンストリームには継承されないため、コミットすると混乱のもとになるためです)。
Package.pins が存在するとき、SwiftPM は依存解決の際に pin を尊重します。ただし、マニフェストのバージョン制約を上書きすることはありません。両者が矛盾している場合は警告になります。また、あるパッケージの Package.pins は、そのパッケージを依存として使う側の依存解決には影響しません(ライブラリ B が持つ pin はアプリケーション A の依存解決から見えません)。
自動ピン留め(automatic pinning)
デフォルトでは 自動ピン留め が有効です。Package.pins が存在しなくても、SwiftPM は依存解決のたびに選んだバージョンを自動で Package.pins に書き出します。このファイルをコミットしなければ、従来の SwiftPM とほぼ同じ挙動になります(例外は後述の update の振る舞いです)。コミットすれば、チーム全員が同じ依存バージョンを共有できます。
自動ピン留めをオフにしたい場合は明示的に切り替えます。
swift package pin --disable-autopin
swift package pin --enable-autopin
この設定も Package.pins に記録されるので、ソース管理にコミットしておけばチーム全員に反映されます。
個別の pin / unpin
自動ピン留めを使っていても、特定のパッケージだけ意図を持って固定したい場合があります。そのために次のコマンドが追加されます。
swift package pin --all
swift package pin Foo
swift package pin Foo 1.2.3
swift package pin Foo --message "Foo 1.3 のパッチは不安定なので当面据え置き"
--all は解決済みのすべての依存を現在のバージョンで pin します。単独のパッケージ名を渡した場合は、そのパッケージを現在のバージョンで pin します。バージョンを明示すれば、そのバージョンで pin します(指定バージョンはマニフェスト制約の範囲内で解決可能である必要があります)。--message は「なぜ pin したのか」を記録するメモです。
逆に pin を外すのが unpin です。
swift package unpin --all
swift package unpin Foo
ただし、自動ピン留めが有効な状態では pin は常に自動更新されるため、unpin はエラーになります。
ここでの pin はパッケージの推移的閉包に対して行える点が重要です。Package.swift に直接書かれていない間接依存も pin の対象にでき、マニフェスト制約だけでは指定できない粒度の制御が可能になります。
swift package update の挙動
pin があると、swift package update の振る舞いも変わります。
- 既定では、pin されていないパッケージだけを、既存の pin と両立するバージョンの中で最新まで更新します。
- 更新できる unpinned パッケージが無ければ警告します。
--repinを付けると、pin の制約を一時的に外してすべてのパッケージを最新まで更新し、もともと pin されていたパッケージは新しく解決されたバージョンで pin し直します。
swift package update
swift package update --repin
自動ピン留めが有効な場合、すべての依存が常に pin されている状態なので、--repin なしの update は意味を持ちません。そのため自動ピン留め時は update が暗黙的に --repin として扱われます。
その他の補助機能
swift package show-dependencies は各依存が pin されているかを表示するように拡張されます。swift build や swift package update は、pin が効いている旨のログを出して、利用者が現在の挙動を理解しやすくします。pin コマンド実行時に依存グラフがまだ取得されていなければ、フェッチと解決を自動で行います。
今後の拡張として検討される方向性
Package.pins の形式は将来の拡張余地を残す目的であえて実装詳細としています。たとえば、解決されたタグの SHA を pin ファイルに含めることで、パッケージグラフの一部に対する中間者攻撃(man-in-the-middle)への防御手段に発展させる可能性が議論されています。いずれもスコープ外であり、実現を約束するものではありません。
「lock」ではなく「pin」という呼称
他の多くのパッケージマネージャでは同種の機能を「lockfile」と呼びますが、SwiftPM ではあえて pin という語を採用しています。理由は二つあります。
- 「lock」は POSIX ファイルロックや並行プログラミングのロックと重なる overloaded な語で、将来 SwiftPM がファイルロックを導入した際の診断メッセージなどと衝突しやすいこと。
- マニフェストに書くバージョン制約は「要件(requirement)」、ここで導入するのはそれとは別のワークフロー上の一時的な束縛であり、「pin」のほうが意味的にも妥当であること。
既存の lockfile の概念に慣れた利用者には戸惑いがあるかもしれませんが、Swift エコシステム全体の長期的な一貫性を優先した判断です。