Swift Digest
SE-0305 | Swift Evolution

Package Manager Binary Target Improvements

Proposal
SE-0305
Authors
Anders Bertelrud, Tom Doron
Review Manager
Tom Doron
Status
Implemented (Swift 5.6)

01 何が問題だったのか

SE-0272 で SwiftPM に導入された binaryTarget は、ソースコードをビルドできない(あるいは公開できない)ライブラリをパッケージから配布する仕組みでしたが、対応フォーマットが Apple プラットフォーム向けの XCFramework に限られており、配布できるのも「ライブラリ」だけでした。

一方で SE-0303 のビルドツールプラグインでは、ビルド中に走らせるコマンドラインツールをパッケージから調達できる必要があります。しかし、

  • protoc のような有名なツールは SwiftPM ではそもそもビルドできない
  • ビルドの「prebuild」フェーズで動くツールは、そのビルド自体の一部としてビルドすることが原理的にできない

といった事情から、ビルドツールをソースから SwiftPM でビルドする前提では実用的なエコシステムを作れませんでした。XCFramework はライブラリ専用のフォーマットであり、実行ファイル(コマンドラインツール)を一般的な形で格納する仕組みがなく、しかもクロスプラットフォームで使える設計にもなっていません。

つまり SwiftPM の binaryTarget には、

  • コマンドラインツールなど、ライブラリ以外のプリビルド成果物を扱う手段がない
  • Apple プラットフォーム以外のバイナリを配布する道筋がない

という制約があり、ビルドツールプラグインを現実的に支えるための基盤として不十分でした。

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

binaryTarget が参照できるフォーマットとして、XCFramework に加えて新しい artifact bundle 形式を導入します。binaryTarget の宣言方法自体は変わらず、既存の XCFramework の扱いにも影響しません。ひとつの binaryTarget に XCFramework と artifact bundle を同居させることはできず、いずれか一方になります。

artifact bundle は複数の artifact(プリビルド成果物)をまとめて配布するためのディレクトリで、各 artifact はさらに複数の variant(アーキテクチャ・プラットフォームごとの実体)を持てます。ディレクトリを直接パッケージに含めることも、.zip にまとめてリモートから取得することもでき、後者の場合は従来どおりチェックサムで内容の整合性を検証します。

この Proposal では typeexecutable(コマンドラインツール)に限定していますが、フォーマット自体は将来ライブラリやリソースなど他の種類を追加できるように設計されています。

artifact bundle のディレクトリ構造

拡張子は .artifactbundle、トップレベルに info.json マニフェストと、各 artifact ごとのサブディレクトリを置きます。各 artifact の中には variant ごとのサブディレクトリがあり、variant ディレクトリがそのまま実行ファイルツリーのルートになります。

<name>.artifactbundle
 ├ info.json
 ├ <artifact>
 │  ├ <variant>
 │  │  ├ <executable>
 │  │  └ <other files>
 │  └ <variant>
 │     └ ...
 └ <artifact>
    └ <variant>
       └ ...

artifact 名・variant 名は任意ですが、バンドル内/artifact 内でそれぞれ一意である必要があります。プラグインはこの artifact 名でツールを引けます(SE-0303context.tool(named:))。名前解決はすべてケースセンシティブで、ケースインセンシティブなファイルシステム上でも問題が起きないよう、大文字小文字だけが違う名前は避けることが推奨されます。

info.json マニフェスト

info.json は次のような JSON ファイルです。

{
    "schemaVersion": "1.0",
    "artifacts": {
        "<identifier>": {
            "version": "<version number>",
            "type": "executable",
            "variants": [
                {
                    "path": "<relative-path-to-executable>",
                    "supportedTriples": [ "<triple1>", ... ]
                }
            ]
        }
    }
}
  • schemaVersion: 現時点では "1.0"。将来フォーマットを拡張するための版数
  • artifacts: artifact の識別子をキーにしたマップ
  • 各 artifact の version: 情報用のバージョン文字列(プラグインから参照可能)
  • 各 artifact の type: 本 Proposal では常に "executable"
  • 各 variant の path: バンドル内での実行ファイルの相対パス
  • 各 variant の supportedTriples: この variant が対応する target triple のリスト

variant の選択は host toolchain の target triple で行い、ユニバーサルバイナリのようにひとつの variant が複数の triple をカバーすることもできます。

具体例: protoc を artifact bundle で配布する

Protocol Buffers の protoc を Linux / macOS / Windows 向けに配布するバンドルは、次のようなレイアウトになります。

protoc.artifactbundle
├── info.json
├── protoc-3.15.6-linux-gnu
│   ├── bin
│   │   └── protoc
│   └── include
│       └── etc.proto
├── protoc-3.15.6-macos
│   ├── bin
│   │   └── protoc
│   └── include
│       └── etc.proto
└── protoc-3.15.6-windows
    ├── bin
    │   └── protoc.exe
    └── include
        └── etc.proto

対応する info.json は次のとおりです。

{
    "schemaVersion": "1.0",
    "artifacts": {
        "protoc": {
            "type": "executable",
            "version": "3.15.6",
            "variants": [
                {
                    "path": "protoc-3.15.6-linux-gnu/bin/protoc",
                    "supportedTriples": ["x86_64-unknown-linux-gnu"]
                },
                {
                    "path": "protoc-3.15.6-macos/bin/protoc",
                    "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
                },
                {
                    "path": "protoc-3.15.6-windows/bin/protoc.exe",
                    "supportedTriples": ["x86_64-unknown-windows"]
                }
            ]
        }
    }
}

この macOS 向け variant はユニバーサルバイナリなので、ひとつの variant が x86_64 と arm64 の両方の triple を担当しています。バンドル内の include/ のような付随ファイルも、プラグインから実行ファイルと同様に参照できます。

artifact bundle index: variant 単位での分割ダウンロード

すべての variant を含んだバンドルは、プラットフォームが増えるほど .zip のサイズが膨らみ、ダウンロードの大半が使われないままになります。これを避けるため、バンドルを「variant のサブセットだけを含む複数のバンドル」に分割し、そのインデックスを指す artifact bundle index を用意できます。

artifact bundle index は .artifactbundleindex 拡張子の JSON ファイルで、次のような内容を持ちます。

{
    "schemaVersion": "1.0",
    "bundles": [
        {
            "fileName": "<name of .zip file containing bundle>",
            "checksum": "<checksum of .zip file>",
            "supportedTriples": [ "<triple1>", ... ]
        }
    ]
}
  • bundles[].fileName: バンドル .zip のファイル名(同じディレクトリ内に置かれる想定)
  • bundles[].checksum: その .zip に対する swift package compute-checksum の結果
  • bundles[].supportedTriples: そのバンドル内の variant が対応する target triple 一覧

binaryTargeturl.artifactbundleindex を指定したときは、チェックサムとしてはインデックスファイル自体のチェックサムを書きます。SwiftPM は host の triple を見て必要なバンドルだけをダウンロードし、ダウンロードした .zip の整合性はインデックス内に書かれた各 checksum で検証します。

用途としては、例えば Apple プラットフォーム用・Windows 用・各種 Linux 用、といった単位で .zip を分けておき、利用側の環境に必要なものだけを取得する、といった構成が可能です。

SwiftPM 側の処理の流れ

SwiftPM は .zip を取得・チェックサム検証した後に展開し、

  1. .xcframework があれば従来どおり XCFramework として扱う
  2. そうでなく .artifactbundle があれば artifact bundle として扱う

という順で判定します。ひとつの .zip に両方が含まれるのはエラーです。.artifactbundle を見つけた場合は info.json を読み、未知の schemaVersion ならエラーにします。登録された artifact はプラグインからの tool(named:) 呼び出しに応じて解決され、展開後の実行ファイルパスとしてプラグインに渡されます。

対応バージョンと既存パッケージへの影響

artifact bundle は、このProposal が実装された SwiftPM の tools version を指定したパッケージでのみ有効になります。既存パッケージの挙動は変わりません。

Future Directions

この Proposal は executable の artifact bundle 配布にフォーカスしていますが、フォーマットの設計自体は以下のような将来の拡張を見据えています(いずれも実現を約束するものではありません)。

  • Linux 向けバイナリ互換性。Linux は単一プラットフォームではないため、manylinuxPEP 513)のような考え方を取り入れ、非 ABI 安定な依存を静的リンクするなどの方針で「多くの Linux で動くバイナリ」を作れるようにする
  • シェルスクリプトなどスクリプト形式の実行ファイル向けに、variant selector をアーキテクチャに対して * を許すような形に拡張する
  • Darwin 以外のプラットフォーム向けのバイナリライブラリ配布。executable より ABI 互換性の問題がさらに大きいため、別 Proposal で設計が必要
  • 3D モデル・テクスチャ・フォントなど、任意のバイナリ成果物の配布。.artifactbundle の形自体は対応可能で、プラグイン API やリソースバンドルとの橋渡しを別途設計する余地がある