Swift Digest
SE-0226 | Swift Evolution

Package Manager Target Based Dependency Resolution

Proposal
SE-0226
Authors
Ankit Aggarwal
Review Manager
Boris Bügling
Status
Partially implemented (Swift 5.2): Implemented the manifest API to disregard targets not concerned by any dependency products, which avoids building dependency test targets.

01 何が問題だったのか

Swift Package Manager(SwiftPM)の依存解決は、これまでパッケージに書かれたすべての依存を無条件に解決していました。たとえあるパッケージが、テスト用のモックライブラリや、ライブラリ本体とは別にサンプル用に作ったコマンドラインツール(引数パーサなど)を依存として持っていても、そのパッケージをライブラリとして利用するだけの側まで、それらをクローンしてバージョン制約に従わせる必要がありました。

使わない依存まで巻き込まれる

例として、irc というライブラリパッケージを考えます。このパッケージは次の 3 つの依存を宣言しているとします。

  • swift-nio: 本体ライブラリが使う
  • ArgParse: 同梱のサンプル実行ファイル irc-sample だけが使う
  • TestUtilities: irc 自身のテストターゲットだけが使う

別のパッケージ irc-client が、この irc のライブラリプロダクトだけを利用したい場合でも、これまでの SwiftPM は irc のマニフェストに書かれた ArgParseTestUtilities も合わせて解決しようとしていました。irc-clientirc-sampleircTests もビルドしないにもかかわらず、それらのためだけに宣言された依存が丸ごと巻き込まれてしまうのです。

依存ヘル(dependency hell)の原因になる

使われない依存まで解決対象に含めることには、性能面の問題だけでなく、依存のバージョン解決を不必要に難しくするという深刻な副作用がありました。別々のパッケージがテスト用やサンプル用に互いに非互換なバージョンの同じライブラリを使っていると、本体ライブラリの利用には何の関係もないのに、バージョン解決が失敗してビルドできなくなります。

また、使わないリポジトリまでクローンしなければならない分、解決プロセス自体も遅くなります。

パッケージ名と URL の紐付けが曖昧

この問題に取り組もうとすると、もうひとつ別の課題が浮かび上がります。「ターゲットが必要とするプロダクトを提供しているのはどのパッケージか」を、依存をクローンせずに判別する手段が必要になるのです。

従来のターゲット依存の書き方では、次のようにプロダクト名だけを書くことができました。

.target(
    name: "foo",
    dependencies: ["bar"]
),

この書き方では、bar がどのパッケージから提供されているのかをマニフェストから直接読み取ることができません。そのため SwiftPM は、どのプロダクトがどのパッケージに属するのかを確かめるためだけに、結局すべての依存をクローンする羽目になっていました。つまり「使われる依存だけを解決する」ためには、マニフェスト側でプロダクトと提供元パッケージの対応をクローンする前に分かるようにする必要があったのです。

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

SwiftPM の依存解決を、パッケージグラフで実際に使われているプロダクトに必要な依存だけを解決する方式に変更します。あるパッケージが持つ依存のうち、利用側のパッケージグラフで参照されていないプロダクト(たとえばサンプル実行ファイルやテストターゲットのためだけに宣言された依存)は、クローンも解決も行われません。

これを実現するために、ターゲット依存の宣言にプロダクトを提供するパッケージ名を明示する新しい書き方が導入されます。

.product(name:package:) でパッケージを明示する

ターゲットがほかのパッケージのプロダクトに依存するときは、Target.Dependency.product(name:package:) を使ってプロダクト名と提供元パッケージ名の両方を指定します。

.target(
    name: "foo",
    dependencies: [.product(name: "bar", package: "bar-package")]
),

package 引数を必須にすることで、SwiftPM はマニフェストを読んだ時点でプロダクトと提供元パッケージの対応関係を確定できます。これにより、依存を実際にクローンする前に「このターゲットをビルドするにはどのパッケージが必要か」を判断でき、使われない依存を解決対象から除外できます。

パッケージ名とプロダクト名が同じような小さなパッケージでは、従来どおり文字列リテラルだけで書く短縮形も引き続き使えます。

.target(
    name: "foo",
    dependencies: ["bar"]
),

この byName 形式は、パッケージ名とプロダクト名が一致する場合の略記として保持されます。

実際の解決の様子

具体例として、前述の irc パッケージと irc-client パッケージを考えます。

// IRC package
let package = Package(
    name: "irc",
    products: [
        .library(name: "irc", targets: ["irc"]),
        .executable(name: "irc-sample", targets: ["irc-sample"]),
    ],
    dependencies: [
       .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
       .package(url: "https://github.com/swift/ArgParse.git", from: "1.0.0"),
       .package(url: "https://github.com/swift/TestUtilities.git", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "irc",
            dependencies: [.product(name: "NIO", package: "swift-nio")]
        ),
        .target(
            name: "irc-sample",
            dependencies: ["irc", "ArgParse"]
        ),
        .testTarget(
            name: "ircTests",
            dependencies: [
                "irc",
                .product(name: "Nimble", package: "TestUtilities"),
            ]
        )
    ]
)

// IRC Client package
let package = Package(
    name: "irc-client",
    products: [
        .executable(name: "irc-client", targets: ["irc-client"]),
    ],
    dependencies: [
       .package(url: "https://github.com/swift/irc.git", from: "1.0.0"),
    ],
    targets: [
        .target(name: "irc-client", dependencies: ["irc"]),
    ]
)

irc-client を解決するとき、SwiftPM がクローンするのは ircswift-nio だけです。ArgParseirc-sample でしか使われず、NimbleTestUtilities)は irc のテストターゲットでしか使われないため、どちらも irc-client のビルドには不要と判断され、解決対象から外れます。Package.resolved にもこれらのエントリは現れません。

パッケージ識別子(name 属性)の扱い

当初の実装(Swift 5.2)では、ターゲット依存に書いたパッケージ名と、実際の依存宣言を突き合わせるために、依存宣言側にも name 属性を書く必要がありました。

.package(name: "swift-nio", url: "https://github.com/apple/swift-nio.git", from: "1.0.0")

ただし、この name は依存先パッケージのマニフェストに書かれている name と一致させる必要があり、利用者にとっては「その名前を知るためだけに相手のマニフェストを見に行く」という不便さがありました。

この点を改善するため、5.4 より後のバージョンでは、name を明示しなくても依存 URL の末尾のパスコンポーネント(ローカル依存の場合はパスの末尾)からパッケージ識別子を機械的に決めるように変更されます。たとえば https://github.com/apple/swift-nio.git からは swift-nio という識別子が得られ、.product(name: "NIO", package: "swift-nio") のように書けば済みます。この URL は入力されたままの形で使われ、パーセントエンコーディングやミラー設定の影響は受けません。

併せて、Package.Dependency.package(...)name: 引数の利用には警告が出るようになり、最終的には非推奨化される予定です(speculative)。

既存パッケージへの影響

この挙動は tools-version で切り替えられるため、古いバージョンの tools-version を宣言しているパッケージには影響しません。古いパッケージは従来どおりすべての依存を解決し、新しい tools-version のパッケージだけが「使われるプロダクトに必要な依存だけを解決する」新方式に従います。ひとつの依存グラフの中に両方のパッケージが混在していても問題ありません。

なお、既に解決済みの依存のあるターゲットに対して後からプロダクト依存を追加すると、それがきっかけで追加の依存解決が走る可能性があります。これは従来の実装でも起こり得たことで、この提案で新しく発生する問題ではありません。

実装状況

この提案は「Partially implemented (Swift 5.2)」というステータスで、マニフェスト API と、使われるプロダクトに関係しないターゲット(テストターゲットなど)をビルド対象から外す部分が先行して実装されています。これにより、依存パッケージのテストターゲットをビルドしないといった効果はすでに得られています。ターゲットベースでの依存解決そのものの完全な実装は、段階的に進められています。