Swift Digest
SE-0146 | Swift Evolution

Package Manager Product Definitions

Proposal
SE-0146
Authors
Anders Bertelrud
Review Manager
Daniel Dunbar
Status
Implemented (Swift 4.0)

01 何が問題だったのか

Swift Package Manager(SwiftPM)には、当時「パッケージが何を作るか(= プロダクト)」をパッケージ作者が明示的に宣言する手段がありませんでした。ターゲットの構成からビルド生成物を暗黙的に推定するルールがあるだけで、マニフェスト(Package.swift)にプロダクトを定義する公式な構文は存在しなかったのです。

プロダクトを宣言できない

暗黙ルールは「main.swift を持つターゲットは実行ファイルになる」「ライブラリターゲットがあればパッケージ名のライブラリをひとつ作る」といった単純な規則に基づいていました。これには次のような問題があります。

  • ビルド生成物の種類を作者が制御できない。ライブラリを静的にしたいか動的にしたいかといった指定ができません。
  • ファイル構成の些細な変更が出力を変えてしまう。たとえばあるモジュールに main.swift を追加しただけで、そのモジュールがライブラリから実行ファイルに変わってしまい、意図しない挙動を招きます。
  • 新しい種類の生成物に拡張しづらい。ルールベースの推定では将来プロダクトの種類が増えたときに破綻します。

依存の粒度がパッケージ単位しかない

もうひとつの問題は、パッケージ間の依存がパッケージ全体への依存としてしか表現できなかったことです。ターゲット粒度で「この公開 API だけに依存したい」「あのコマンドラインツール部分には依存したくない」といった表現ができません。

この制約を回避するには、ひとつの概念的なコンポーネントを無理に複数のパッケージに分割する必要がありました。たとえば「公開ライブラリ API + コマンドラインツール + 両者が共有する内部実装」を持つコンポーネントは、次のように 3 つのパッケージに分けざるを得なかったのです。

  • 公開ライブラリ用パッケージ
  • コマンドラインツール用パッケージ
  • 両者が共有する内部実装用の、非公開パッケージ

しかし、これは本来ひとつのパッケージとしてまとめ、共通のバージョン番号で管理したいものです。SwiftPM の表現力不足のために、作者の意図に反したパッケージ分割が強いられていました。

ターゲットの可視性だけでは足りない

「ターゲットを public / private で区別できればよいのでは」という案もありましたが、それだけではどんな種類の生成物を作るかを作者が指定するという問題は解決しません。可視性制御と、プロダクトという概念(= パッケージが外部に提供する正式な出力)は別物であり、後者がどうしても必要でした。

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

SwiftPM にプロダクト(product) という概念を正式に導入します。パッケージが外部に公開する出力をマニフェストで明示的に宣言できるようにし、さらに他パッケージのプロダクト単位で依存を張れるようにします。

プロダクトを宣言する

Package() イニシャライザに products パラメータが追加されます。プロダクトは「型(実行ファイル / ライブラリ)」「一意な名前」「実装となる root ターゲットのリスト」から成り、ライブラリであれば静的か動的かといった追加属性も指定できます。

let package = Package(
    name: "MyServer",
    targets: [
        Target(name: "Utils"),
        Target(name: "HTTP", dependencies: ["Utils"]),
        Target(name: "ClientAPI", dependencies: ["HTTP", "Utils"]),
        Target(name: "ServerAPI", dependencies: ["HTTP"]),
        Target(name: "ServerDaemon", dependencies: ["ServerAPI"]),
    ],
    products: [
        .Library(name: "ClientLib", type: .static, targets: ["ClientAPI"]),
        .Library(name: "ServerLib", type: .dynamic, targets: ["ServerAPI"]),
        .Executable(name: "myserver", targets: ["ServerDaemon"]),
    ]
)

プロダクトに列挙する targetsroot ターゲット であり、クライアントに見せるインターフェースを持つモジュールを指します。そのターゲットが依存している他ターゲット(上の例なら HTTPUtils)も一緒にコンパイル・リンクはされますが、クライアントからはインターフェースが見えないことを意図しています(現状の Swift コンパイラにはこのモジュール可視性制御が無いため、あくまで作者の意図表明ですが、将来的なコンパイラ対応が期待されています)。

ライブラリは .static / .dynamic を明示できますが、可能な限り省略することが推奨されます。省略すればビルドシステムがプラットフォームに適したデフォルトを選びます。

同じターゲットを複数のプロダクトに含めることもできます。ただし、すべてのターゲットがすべてのプロダクトで使えるわけではありません(たとえばテストターゲットはライブラリプロダクトに含められません)。テストはプロダクトとして扱われず、宣言する必要もありません。

初期はプロダクトの型として 実行ファイル(Executableライブラリ(Library が用意されます。Product は以下のようにネストされた型として定義されます。

public enum Product {
    public class Executable {
        public init(name: String, targets: [String])
    }

    public class Library {
        public enum LibType {
            case .static
            case .dynamic
        }
        public init(name: String, type: LibType? = nil, targets: [String])
    }
}

プロダクト単位の依存

ターゲットの依存宣言を拡張し、他パッケージのプロダクトに依存を張れるようになります。Targetdependencies は従来同パッケージ内のターゲットを指す文字列を並べるものでしたが、そこに .product(name:package:) を書けるようになります。

let package = Package(
    name: "MyClientLib",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", majorVersion: 3),
    ],
    targets: [
        Target(name: "MyUtils"),
        Target(name: "MyClientLib", dependencies: [
            .target(name: "MyUtils"),
            .product(name: "Alamofire", package: "Alamofire")
        ])
    ]
)

package 引数は省略可能です。プロダクト名が一意であれば書かなくてよく、同名プロダクトが複数の依存パッケージに存在する場合のみ明示が必要です。

従来どおり、依存は文字列ショートハンドでも書けます。その場合、SwiftPM はまず同一パッケージ内のターゲット名として解決を試み、見つからなければ依存パッケージのプロダクト名として解決します。内部では TargetDependency に新たに ByName ケースが追加され、パッケージグラフ解決時にターゲット/プロダクトのいずれかへ束縛されます。

// 次の 2 つは等価
Target(name: "Foo", dependencies: ["Bar", "Baz"])
Target(name: "Foo", dependencies: [.target(name: "Bar"), .target(name: "Baz")])

なお、参照できるプロダクトは直接の依存パッケージのもののみです。間接依存のプロダクトは見えません。

暗黙プロダクトと後方互換性

既存パッケージへの影響を最小化するため、暗黙プロダクトの推定ルールは残されます。

  • PackageDescription API の バージョン 3 を使っているパッケージでは、products を省略すると従来どおりの推定ルールが適用されます(実行モジュールごとに実行ファイルプロダクト、ライブラリターゲットがあればパッケージ名のライブラリプロダクトをひとつ)。また、直接依存している他パッケージに対しては暗黙の依存が張られます。
  • PackageDescription API の バージョン 4 に移行すると、プロダクトおよびプロダクト依存の宣言は作者の責任になります。swift package init は新規パッケージ作成時に適切なプロダクト定義を自動で追加し、既存パッケージの移行時には「このパッケージはプロダクトを定義していません」という警告と fix-it による補助が提供されます。

swift package describe は暗黙的に推定されたプロダクトも表示するので、移行時の確認に役立ちます。

この変更がもたらす実利

プロダクトの導入により、ひとつのパッケージ内で「公開ライブラリ」「コマンドラインツール」「内部共有実装」といった概念的に別物の出力を、それぞれ独立した依存先として宣言できるようになります。これまでのようにパッケージを無理に分割する必要がなくなり、単一のバージョン番号のもとで一貫した管理が可能になります。また、ライブラリの静的/動的の別を作者自身が制御できるようになり、将来新しいプロダクトの種類が追加されたときも同じ枠組みで拡張できます。