Swift Digest
SE-0272 | Swift Evolution

Package Manager Binary Dependencies

Proposal
SE-0272
Authors
Braden Scothern, Daniel Dunbar, Franz Busch
Review Manager
Boris Bügling
Status
Implemented (Swift 5.3)

01 何が問題だったのか

SwiftPM はこれまでソースコードのみを前提としたパッケージしか扱えず、ビルドモデルもかなり固定的でした。このため、次のようなユースケースがうまく取り込めません。

  • ソースコードを公開したくない商用ライブラリのベンダーが、SwiftPM で簡単に配布したい
  • LLVM のように、SwiftPM の標準的なビルドモデルには収まらない複雑なビルド手順を持つコードベースを、利用者が手軽に使えるパッケージとして提供したい
  • 社内向けに配布したい iOS 用ライブラリを、事情があってソース非公開のまま SwiftPM 経由で使いたい

iOS などのコミュニティでは Firebase、GoogleAnalytics、Adjust のようなクローズドソースの依存を使う文化が根付いており、CocoaPods などの既存のパッケージマネージャはこれをサポートしています。SwiftPM でも同じことができないと、これらのコミュニティでの採用が進みません。

Swift 5.1 以前はコンパイラ側に ABI 互換性などの機能が揃っておらず、バイナリパッケージを現実的な形で提供できませんでした。これらの基盤が整ったことで、あらためて SwiftPM にバイナリ依存を導入する余地が生まれました。

ただし、バイナリ依存を入れることには副作用もあります。ソースが見えないことによる可搬性の低下や、ソース提供されているパッケージをわざわざバイナリ化して「ビルド高速化のためのキャッシュ」として使うと、ソースと成果物の対応がゆるくなりマルウェア混入の温床になりかねないといった懸念です。そのためこの Proposal は、あくまで「ソースを配布できない/したくないケース(vendored binaries)」に絞って解決し、ビルド高速化のためのアーティファクトキャッシュや、バイナリ公開を前提とした中央集権的なパッケージ配布方式には踏み込まない方針をとっています。

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

SwiftPM に、バイナリ成果物をターゲットとして宣言できる仕組みが導入されます。バイナリは新しい「binary target」として扱われ、Package.swift の中で URL またはローカルパスと、整合性チェックのためのチェックサムを指定して取り込みます。

binary target の宣言

Target に、バイナリターゲットを宣言する2つの静的メソッドが追加されます。

extension Target {
    /// Declare a binary target with the given url.
    public static func binaryTarget(
        name: String,
        url: String,
        checksum: String
    ) -> Target

    /// Declare a binary target with the given path on disk.
    public static func binaryTarget(
        name: String,
        path: String
    ) -> Target
}

マニフェスト側では、通常のソースターゲットと混在させて次のように記述します。

let package = Package(
    name: "SomePackage",
    platforms: [
        .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2),
    ],
    products: [
        .library(name: "SomePackage", targets: ["SomePackageLib"])
    ],
    targets: [
        .binaryTarget(
            name: "SomePackageLib",
            url: "https://github.com/some/package/releases/download/1.0.0/SomePackage-1.0.0.zip",
            checksum: "839F9F30DC13C30795666DD8F6FB77DD0E097B83D06954073E34FE5154481F7A"
        ),
        .binaryTarget(
            name: "SomeLibOnDisk",
            path: "artifacts/SomeLibOnDisk.zip"
        )
    ]
)

ひとつのパッケージ内でバイナリターゲットとソースターゲットを混在させられるため、たとえばクローズドソースの C ライブラリに対して、オープンソースの Swift バインディングを同じパッケージで提供するといった構成が可能です。

用語として、バイナリターゲットを少なくともひとつ含むプロダクトを「binary product」、binary product を少なくともひとつ含むパッケージを「binary package」と呼びます。

チェックサムの計算

URL 指定のバイナリターゲットで使うチェックサムは、SwiftPM に追加される新しいサブコマンドで計算します。

swift package compute-checksum <file>

このコマンドはパッケージの tools version に紐付いており、将来アルゴリズムを進化させても既存パッケージとの互換性を壊さずに済むようになっています。

成果物のフォーマット

初期実装では対象を Apple プラットフォームに限定し、成果物のフォーマットには XCFramework を採用します。XCFramework は動的/静的リンクの双方に対応し、Apple 各プラットフォーム向けのバイナリをひとつのアーティファクトにまとめられるため、既存の配布資産をそのまま再利用しやすいという利点があります。

  • URL 指定の場合は、.zip直下XCFramework が配置されている必要があります。また、XCFramework のモジュール名はマニフェストの name と一致している必要があります。
  • ローカルパス指定の場合は、.zip に加えて生の XCFramework ディレクトリも許容されます。

解決時点では SwiftPM はアーティファクトの中身を検証しません。正しい形式で提供する責任はベンダー側にあります。

非 Apple プラットフォームでの扱い

非 Apple プラットフォーム向けに解決しようとした場合、SwiftPM はエラーを出し、そのバイナリ依存が現在のプラットフォームでは無効であることを明示します。黙って無視する挙動は混乱を招くため採用されていません。

また、バイナリ依存を直接エクスポートするプロダクトに対して type を指定した場合(たとえば .product(name: "MyBinaryLib", type: .static, targets: ["MyBinaryLib"]) のような宣言)は、解決時にエラーになります。

Package.resolved とチェックサムの固定

バイナリ依存については、Package.resolved でチェックサムも併せて固定されます。同じバージョンのまま成果物の中身だけが差し替えられた場合、解決時に SwiftPM がチェックサムの不一致を検知してエラーを出すため、ベンダーが気づかれずにバイナリを入れ替えることを防げます。

ミラーリング

パッケージ URL に対する --package-url オプションが非推奨になり、代わりに --original-url が導入されます。これはパッケージ URL とアーティファクト URL の両方に使え、バイナリアーティファクトもミラーできるようになります。

swift package config set-mirror \
    --original-url <original URL> \
    --mirror-url <mirror URL>

# 例:
swift package config set-mirror \
    --original-url https://github.com/Core/core/releases/download/1.0.0/core.zip \
    --mirror-url https://mygithub.com/myOrg/core/releases/download/1.0.0/core.zip

ミラーを外すコマンドも用意されています。

swift package config unset-mirror \
    --original-url https://github.com/Core/core/releases/download/1.0.0/core.zip

--mirror-url--all といった他の解除方法は、パッケージ URL のときと同じように使えます。

セキュリティ上の考え方

バイナリ依存だからといって、ソース依存と比べて本質的に信頼度が低いわけではありません。ソースを公開していても悪意あるコードは仕込めるため、「ソースが見えること」は信頼の根拠として十分ではないからです。一方で、可搬性が落ちるなど、バイナリ依存を増やすこと自体のコストはあります。

安全側に倒すため、本 Proposal では次の仕組みを導入しています。

  • binaryTarget の宣言時にチェックサムを必須にする。これにより、マニフェストを提供する git リポジトリと、バイナリを提供するサーバーの両方を同時に侵害しない限り成果物を差し替えられない。
  • 解決後のチェックサムを Package.resolved に保存する。これにより、同一バージョンのままベンダーがバイナリを差し替える攻撃を検知できる。

既存パッケージへの影響

純粋な追加機能なので、既存パッケージには影響しません。

今後の展望

Apple 以外のプラットフォームへの対応は、ABI の保証がプラットフォームごとに異なる(corelibs-foundation の ABI、浮動小数点ハードウェアの有無など)ため自明ではなく、本 Proposal では扱いません。将来的には、ArtifactArtifactCondition のような仕組みで、対象となる LLVM トリプル(アーキテクチャ・ベンダー・OS)ごとに使えるアーティファクトを宣言できるようにする方向性が示唆されています。ただしこれはあくまで設計の一例で、具体的な API は今後の別 Proposal で検討されます。