Swift Digest
SE-0271 | Swift Evolution

Package Manager Resources

Proposal
SE-0271
Authors
Anders Bertelrud, Ankit Aggarwal
Review Manager
Boris Buegling
Status
Implemented (Swift 5.3)

01 何が問題だったのか

Swift のパッケージは、多くの場合ソースコードを中心に構成されますが、実行時に必要となるリソースファイル(画像、サウンド、ストーリーボード、Metal シェーダ、データファイルなど)を含めたい場合もあります。SwiftPM にはこうしたリソースをパッケージに同梱し、ビルド成果物に取り込む仕組みがありませんでした。

リソースの扱いは単純に「ファイルをコピーする」だけでは足りません。プラットフォームによってはリソースを最適化したり、特殊なコンパイル処理を行ったりする必要があります。また、次のような事情から、ソースコード側からリソースの在りかを固定的に決め打ちすることもできません。

  • 同じパッケージが、あるときは動的ライブラリやフレームワークとしてアプリケーションバンドルに埋め込まれ、別のときはクライアント実行ファイルに静的リンクされる
  • プラットフォームによってはそもそも「バンドル」という単位が別ファイルとして存在しない
  • ビルドシステム側の都合で、コードとリソースが必ずしも同じバンドルに同居するとは限らない

そのため、パッケージ側ではリソースを プラットフォームに依存しない形で宣言 でき、実行時には 配置場所を前提にしない統一的な API でアクセスできる必要があります。

加えて、ソースディレクトリ内にあるファイルのうち何をリソースとして扱うかも悩ましい問題です。.storyboard.xib.metal のように用途が明確な拡張子だけならまだしも、.md / .png / .jpg / .txt / .pdf のようにリソースである場合もあれば単なる設計メモである場合もある拡張子については、暗黙的にリソースへ含めてしまうと、意図せず内部ドキュメントを成果物に同梱してしまう事故が起きます。実際にこうした事故は過去にも発生しており、「暗黙に全部入れる」方式では安全性が保てません。

さらに、リソースの置き場所にも制約を付けたくありません。既存の CocoaPods や Xcode プロジェクトの多くでは、ストーリーボードや XIB をそれを使うソースコードの近くに置くなど、機能単位でソースとリソースを混在させる構成が一般的です。「リソースは専用ディレクトリにまとめる」方式では、こうしたレイアウトをそのまま取り込めません。

つまり、プラットフォーム非依存にリソースを宣言でき、誤同梱のリスクを抑えつつ、既存のソース構成を崩さずに採用できて、実行時には統一的な方法でアクセスできる、というリソースの仕組みが SwiftPM に欠けていた、というのが問題です。

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

SwiftPM にリソースを扱うための仕組みが導入されます。リソースはソースファイルと同様にターゲットの一部として扱われ、マニフェストで宣言します。実行時には、ターゲットごとに生成されるバンドルに、統一された API からアクセスできます。

ターゲットに対するリソースの宣言

target / testTargetresources パラメータが追加されます。Resource 型は .process(_:).copy(_:) の2種類のルールを提供します。

.target(
    name: "MyLibrary",
    resources: [
        .process("Assets"),            // ディレクトリ配下に再帰的に process ルールを適用
        .copy("DefaultSettings.plist") // そのままコピー
    ]
)
  • .process(_:) は、ビルド対象プラットフォームに合わせた処理(画像最適化など)を行うルールです。特別な処理が定義されていないファイルはそのままコピーされます。ディレクトリを指定するとその中のファイルに再帰的に適用され、バンドル内ではフラットに配置されます。
  • .copy(_:) は、ファイルやディレクトリをそのままの構造でコピーするルールです。ディレクトリ構造を保ったままバンドルに含めたい場合に使います。

ターゲット内の すべてのファイルは必ずいずれかの役割を持つ 必要があります。SwiftPM がルールを自動判定できないファイル(たとえば README.md など、ソースでもビルトインの既知リソースでもないもの)がターゲット内に残っていると、SwiftPM はエラーを出します。そのようなファイルは、exclude で除外するか、resources に追加してルールを明示する必要があります。これにより、意図しないファイルが暗黙に成果物へ同梱される事故を防げます。

.storyboard / .xib / .xcassets / .metal / CoreData モデルのような、用途が明確な既知のファイル形式については、SwiftPM がビルトインのルールで自動的にリソースとして認識するため、resources への明示的な記述は不要です。また、libSwiftPM にはクライアント(たとえば Xcode)が独自のファイル形式を登録できる API が用意され、登録された形式も自動検出の対象になります。

ターゲットごとのリソースバンドル

リソースをひとつ以上持つターゲットについては、SwiftPM がターゲットごとに Foundation 形式のバンドルを生成します。バンドル名や識別子はパッケージ名とターゲット名から決まりますが、これは実装詳細であり、パッケージのコードが名前を直接知る必要はありません。

バンドルの配置場所もプラットフォームによって異なります。Linux ではクライアントの実行ファイルの隣に置かれ、macOS などではクライアントのメインバンドル内にネストされるなど、ビルドシステムが適切な場所を選びます。ここで重要なのは、コードがバンドルの配置場所を前提にしないことで、ビルドシステムがリソースの配置を自由に決められるようにしている点です。

実行時のアクセス: Bundle.module

SwiftPM は、リソースを持つモジュールごとに Bundle に対する internal な静的プロパティ module を自動生成します。

extension Bundle {
    /// The bundle associated with the current Swift module.
    static let module: Bundle = { ... }()
}

module は internal なので、同じモジュール内のコードからのみ参照でき、他モジュールの Bundle.module と衝突することもありません。初回アクセス時にバンドルが読み込まれ、以降はキャッシュされます。リソースを持たないモジュールについては、このプロパティ自体が生成されません。

利用側は、既存の Bundle ベースの API にそのまま Bundle.module を渡すだけで、モジュールのリソースにアクセスできます。

// plist ファイルのパスを取得する
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")

// アセットカタログから画像を読み込む
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))

// コンパイル済み Metal シェーダから関数を取り出す
let shader = try mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")

// テクスチャを読み込む
let texture = MTKTextureLoader(device: mtlDevice).newTexture(name: "Grass", scaleFactor: 1.0, bundle: Bundle.module, options: options)

プロパティ名が常に module で固定のため、コードを別モジュールへ移動しても(リソースも一緒に移動すれば)ソースを書き換える必要はありません。

従来の Cocoa では、Bundle(for: SomeClass.self) で同じバンドルにあることを前提にするか、Bundle(identifier:) で識別子をハードコードするしかありませんでした。前者はコードが静的リンクされたときに破綻し、後者は識別子の重複や未ロード時の扱いに弱いという問題があります。Bundle.module は、これらの問題を避けつつ、配置場所の詳細をパッケージ側で気にしなくて済むようにします。

コードを持たずリソースだけを提供したいパッケージでも、ターゲットに Bundle.module を公開する薄いプロパティを用意することで、バンドル自体をクライアントへ明示的に渡せます。

Objective-C からアクセスしたい場合のために、ビルドシステムは SWIFTPM_MODULE_BUNDLE というプリプロセッサマクロも定義します。

tools version による既存パッケージへの影響

この仕組みは、本 Proposal が実装されたバージョンの tools version 以降でのみ有効です。既存パッケージが tools version を更新した際には、それまで暗黙に無視されていたファイルについて、exclude するかリソースとして扱うかを明示的に決める必要が出てくる場合があります。

今後の展望

本 Proposal では、既存の Bundle ベースの API をそのまま使える素直な仕組みを優先し、バンドルへのアクセス手段のみを提供しています。

将来的には、SwiftPM がリソースの名前と型を把握していることを活かし、リソースごとに型付きのアクセサを生成してビルド時に存在や型のミスマッチを検出できるようにすること、sources / exclude / resources の配列で fnmatch() セマンティクスの glob パターンを使えるようにすることなどが、別 Proposal の候補として挙げられています(これらはあくまで方向性であり、実現を約束するものではありません)。また、ローカライズ対応も本 Proposal のリソース機構の上に別途積み上げる形で、別 Proposal にて扱われる予定です。