Swift Digest
SE-0292 | Swift Evolution

Package Registry Service

Proposal
SE-0292
Authors
Bryan Clark, Whitney Imura, Mattt Zmuda
Review Manager
Tom Doron
Status
Implemented (Swift 5.7)

01 何が問題だったのか

Swift Package Manager(SwiftPM)はこれまで、依存パッケージを Git リポジトリの URL で指定し、Git 経由でクローンすることで取得してきました。しかし、汎用的なバージョン管理ツールである Git は、パッケージ配布のワークフローには必ずしも適していません。

Git に依存することの問題点

  • 再現性: Git のタグは後からいつでも別のコミットに付け替えることができます。同じ 1.2.0 を指していたはずが、いつビルドしたかによって取得されるコードが変わり、ビルド結果が再現できなくなる恐れがあります。
  • 可用性: リポジトリが移動・削除されると、以降のビルドが失敗します。
  • 効率: クローンではパッケージの全バージョン(全履歴)を取得してしまいますが、実際に使うのはその中の1バージョンだけです。
  • 速度: 履歴が大きなリポジトリほどクローンに時間がかかり、サーバ・クライアントの双方にコストがかかります。HTTP と CDN を使って同じ内容を配信する場合に比べて、かなり遅くなることがあります。

他の言語エコシステムには RubyGems、PyPI、npm、crates.io といったパッケージレジストリがあり、Swift でも CocoaPods のインデックスが広く使われてきました。SwiftPM 用のパッケージレジストリがあれば、より高速で信頼性の高い依存解決が可能になり、検索やセキュリティ監査、オフラインキャッシュなど周辺機能の基盤にもなります。

パッケージ識別の曖昧さ

従来の SwiftPM では、パッケージの同一性は URL の末尾のパスコンポーネント(大文字小文字を区別しない)から導出されていました。このやり方には次のような問題があります。

  • 同名だが別物のパッケージが同じ識別子と見なされて衝突する。
  • 同じパッケージが異なる URL で書かれていると、別のパッケージとして重複してしまう。

URL に頼らずパッケージを一意に識別し、かつレジストリから配信する仕組みを、標準化された形で持つ必要がありました。

02 どのように解決されるのか

SwiftPM が Git に加えて、パッケージレジストリ(package registry)と呼ばれる Web サービス経由でも依存を取得できるようにするための、標準インターフェイスを定義します。あわせて、パッケージを URL ではなくスコープ付き識別子で指定できるようにし、SwiftPM 側にはレジストリ連携のためのサブコマンドと設定が追加されます。

レジストリが提供する Web API

レジストリは次のような REST エンドポイントを備えた HTTP サービスとして定義されます。

メソッド パス 用途
GET /{scope}/{name} リリース一覧の取得
GET /{scope}/{name}/{version} リリースのメタデータ取得
GET /{scope}/{name}/{version}/Package.swift{?swift-version} マニフェスト取得
GET /{scope}/{name}/{version}.zip ソースアーカイブのダウンロード
GET /identifiers{?url} URL に紐づくパッケージ識別子の検索

SwiftPM は依存解決時に、まず /{scope}/{name} で利用可能なリリースを問い合わせ、バージョン要件を満たすリリースが見つかれば /{scope}/{name}/{version}/Package.swift でそのリリースのマニフェストを取得します。依存グラフが確定したら、各依存について /{scope}/{name}/{version}.zip を取得してソースを展開します。

これは Git ベースの解決と対応づけると次のような関係になります。

SwiftPM の操作 Git での実装 レジストリでの実装
パッケージ内容の取得 git clone && git checkout GET /{scope}/{name}/{version}.zip
利用可能なタグの一覧 git tag GET /{scope}/{name}
マニフェストの取得 git clone GET /{scope}/{name}/{version}/Package.swift

公式の仕様書と OpenAPI 定義、Swift 製のリファレンス実装が併せて提供され、第三者が独自のレジストリを立てられるようになっています。

スコープ付きパッケージ識別子

パッケージは scope.package-name という形のスコープ付き識別子で表されます。

  • スコープはレジストリ内での名前空間で、英数字とハイフンのみ、先頭末尾や連続したハイフンは不可、最大 39 文字。
  • パッケージ名はスコープ内での一意な名前で、英数字・アンダースコア・ハイフン、先頭末尾や連続した記号は不可、最大 100 文字。
  • 比較はロケールに依存しないケースフォールディングで行われます。

スコープの文字制約は、キリル文字 “А” を “A” と誤認させるようなホモグラフ攻撃や、llvm--swift / llvm_swift のような見分けのつきにくい名前、apple.com のようなドメイン風の名前によるタイポスクワッティングを防ぐためのものです。

マニフェストでの宣言方法

PackageDescription に、識別子で依存を宣言するための新しいメソッドが追加されます。

extension Package.Dependency {
    public static func package(
        id: String,
        _ requirement: Package.Dependency.VersionBasedRequirement
    ) -> Package.Dependency
}

Package.swift 内では次のように書きます。

dependencies: [
    .package(id: "mona.LinkedList", .upToNextMinor(from: "1.1.0")),
    .package(id: "mona.RegEx", .exact("2.0.0"))
]

id: で宣言された依存はバージョンベースの要件しか持てません(ブランチ指定やコミット指定は不可)。これを表す新しい型 Package.Dependency.VersionBasedRequirement が導入されます。

ターゲット側での参照では、識別子がそのままパッケージ名として使われます。

targets: [
    .target(
        name: "MyLibrary",
        dependencies: [
            .product(name: "LinkedList", package: "mona.LinkedList")
        ]
    )
]

パス指定や、識別子と紐づかない URL 指定の依存は、従来どおり URL の末尾のパスコンポーネントから識別子が合成されます。

URL 宣言と識別子宣言の突き合わせ

同じ依存グラフ内に .package(id: "mona.LinkedList", ...).package(url: "https://github.com/mona/LinkedList", ...) の両方が現れた場合、SwiftPM はこれを同一のパッケージとして扱う必要があります。

レジストリはリリース一覧のレスポンスの Link ヘッダで、そのパッケージの正規 URL(rel="canonical")と別名 URL(rel="alternate")を返します。

Link: <https://github.com/mona/LinkedList>; rel="canonical",
      <ssh://git@github.com:mona/LinkedList.git>; rel="alternate"

SwiftPM はこの情報を使って URL と識別子を対応づけます。git@github.com:mona/LinkedList.git のような “scp スタイル” の URL は ssh:///git@github.com/mona/LinkedList のような明示的な URL と等価とみなすなど、URL の正規化も行われます。必要に応じて GET /identifiers{?url} でレジストリに問い合わせて対応づけを得ることもできます。

ソースアーカイブと整合性チェック

レジストリからダウンロードされるリリースは Zip アーカイブで、SHA-256 のチェックサムで完全性を検証します。Package.resolved に記録されたチェックサム、サーバから返ってきたチェックサム、実際にダウンロードしたアーカイブから計算したチェックサムのいずれかが食い違うと、SwiftPM はダウンロードを拒否します。

$ swift build
error: checksum of downloaded source archive of dependency 'mona.LinkedList' (c2b934fe...) does not match checksum specified by the manifest (ed008d5a...)

初回取得時にはサーバから得たチェックサムが Package.resolved に保存され、以降はそれを基準に検証されます(Trust on First Use 型のモデル)。

チェックサムの計算を検証する標準手段として、swift package compute-checksum が使えます。

$ swift package compute-checksum LinkedList-1.2.0.zip
1feec3d8d144814e99e694cd1d785928878d8d6892c4e59d12569e179252c535

アーカイブ作成のための archive-source サブコマンド

他のパッケージマネージャの経験上、チェックサム不一致は攻撃よりも「アーカイブの作り方やチェックサムの計算方法が食い違っていた」ことが原因である場合が多く、SwiftPM はソースアーカイブを作るための標準的な手段として swift package archive-source を提供します。

$ swift package archive-source
Created LinkedList.zip

$ git checkout 1.2.0
$ swift package archive-source --output="LinkedList-1.2.0.zip"
# Created LinkedList-1.2.0.zip

挙動は git archive --format zip と等価で、export-ignore 属性が付いたファイル(デフォルトで .git.build などの隠しファイル)は除外されます。

レジストリの設定: swift package-registry サブコマンド

プロジェクトやユーザー単位でレジストリの URL を設定するために、swift package-registry サブコマンドが追加されます。

$ swift package-registry set https://internal.example.com/

この操作で .swiftpm/configuration/registries.json が作成・更新されます。設定はデフォルトレジストリ(キー [default])とスコープ単位の設定のどちらでも持つことができ、--scope を付ければ特定スコープ専用のレジストリを指定できます。

$ swift package-registry set https://internal.example.com/ --scope example

これにより、example.PriorityQueue のようにそのスコープに属するパッケージだけを別のレジストリに振り向けられます。スコープ付きの設定は、デフォルトレジストリの設定よりも優先されます。

unset で登録解除、--global を付ければ ~/.swiftpm/configuration/registries.json に対するユーザー全体の設定として書き込めます。ローカルの設定はグローバル設定より優先され、全体としての優先順位は次のようになります。

  1. ./Package.swift
  2. ./Package.resolved
  3. ./.swiftpm/configuration/registries.json
  4. ~/.swiftpm/configuration/registries.json

レジストリが要求する場合は --login--password で認証情報を渡せます。パスワードは .netrc に保存され(プロジェクト直下かユーザーホーム)、registries.json には login のみが記録されます。レジストリが未設定なのに識別子ベースの依存を解決しようとするとエラーになります。

ミラー指定の識別子対応

既存の swift package config set-mirror サブコマンドは --original-url に加えて --package-identifier を受け付けるようになります。

$ swift package config set-mirror \
    --package-identifier mona.LinkedList \
    --mirror-url https://github.com/OctoCorp/SwiftLinkedList.git

これにより、mona.LinkedList という識別子で宣言された依存を、Git の任意の URL に差し替えて取得させることができます。

セキュリティ上の考え方

パッケージレジストリ経由の配布では、HTTPS の使用と整合性チェックサムの検証が仕様として必須化されます。これに加えて、識別子の文字制限によるホモグラフ攻撃・タイポスクワッティング対策、改竄対策としての Trust on First Use、資格情報を .netrc に分離して誤コミットを避ける運用などが組み合わさって、Git 直接取得よりも強いセキュリティ特性が得られる設計になっています。レジストリ側での認証方式(OAuth 2.0 などの採用)は各レジストリの裁量に委ねられます。

既存パッケージへの影響

URL ベースで依存を宣言している既存パッケージはこれまで通り Git 経由で解決されるため、この提案によって壊れることはありません。レジストリの利用はあくまでオプトインで、識別子形式で依存を宣言し、レジストリを設定したときに初めて有効になります。

今後の見通し

この提案はレジストリの基礎を定めるものであり、次のような拡張が将来的な方向性として示されています(いずれも現時点では speculative な見通しです)。

  • パッケージの公開: 本提案はリリースをどのように公開するかは定めておらず、”push” 型・”pull” 型いずれの公開モデルも取り得ます。業界関係者と共同で、公開のための標準を策定していくことが想定されています。
  • パッケージ削除のポリシー: 左 pad 事件のような事故を避けるため、レジストリには強い耐久性が期待される一方で、誤公開・脆弱性開示・法的要請など正当な削除理由もあります。仕様としてどこまで定めるかは今後の検討課題とされています。
  • URL の正規化: 無意味な表記揺れを吸収する URL 正規化の強化。
  • オフラインキャッシュ: ネットワーク非接続でもビルドできるキャッシュ機構。
  • バイナリフレームワーク配布: XCFramework やアーティファクトアーカイブの配布へのレジストリ仕様の拡張。
  • パッケージ編集コマンドの拡張: swift package add-dependency mona.LinkedList のように識別子でコマンドラインから依存を追加できる機能。
  • マニフェスト移行ツール: swift package-registry migrate のように、URL ベースの依存宣言を識別子ベースへ書き換える支援ツール。
  • セキュリティ監査と検索 API: CVE 情報をリリース一覧に含めて swift package audit を提供すること、swift package search のようなパッケージ検索のためのエンドポイントの追加。