Package Manager Manifest API Redesign
01 何が問題だったのか
Swift Package Manager(SwiftPM)の Package.swift マニフェスト API は、Swift の API Design Guidelines が整備される前に設計されたもので、Swift Evolution によるレビューも経ていませんでした。そのため、Swift らしい命名・形になっていない箇所や、後々の拡張に不都合な箇所がいくつも残っていました。
命名規約に従っていない
列挙型のケース名が Swift の慣例(小文字始まりのキャメルケース)に従っておらず、Target.Dependency や SystemPackageProvider では次のように大文字始まりのケース名が使われていました。
// 旧API
enum Dependency {
case Target(name: String)
case Product(name: String, package: String?)
case ByName(name: String)
}
enum SystemPackageProvider {
case Brew(String)
case Apt(String)
}
宣言後にパッケージを調整できない
Package と Target のプロパティの多くがイミュータブルな let として宣言されており、初期化後にプラットフォーム別の調整などができませんでした。たとえば「Linux のときだけ依存先を差し替える」といった条件分岐が書きづらい状況でした。
拡張しづらい初期化方法
Target や Product の生成に直接イニシャライザを使う設計は、列挙型ケースのようなショートハンド(.target(...) のようなドット構文)との併用がしづらく、将来のオーバーロード追加にも不向きでした。また、Version には意味が曖昧な successor() / predecessor() メソッドが存在し、buildMetadataIdentifier は SemVer 2.0 の仕様と異なり単一の文字列として保持されていました。
バージョン指定が直感的でない
パッケージ依存のバージョン指定では、メジャー/マイナーバージョンを引数に取るファクトリ(majorVersion:、minor:)や、正確なバージョン、範囲を指定する複数のイニシャライザが乱立していました。
// 旧API
.Package(url: "/SwiftyJSON", majorVersion: 1)
.Package(url: "/SwiftyJSON", majorVersion: 1, minor: 2)
.Package(url: "/SwiftyJSON", "1.2.3")
.Package(url: "/SwiftyJSON", versions: "1.2.3"..<"1.2.6")
他のパッケージマネージャが採用するキャレット(^)・チルダ(~)演算子のような、「次のメジャーバージョン未満まで許容する」というSemVer と相性のよい指定を簡潔に書く手段もありませんでした。
暗黙のテストターゲット依存
テストターゲット FooTests が何も依存を宣言していない場合、同名の Foo ターゲットがあれば自動的にそれに依存するという暗黙ルールがありました。このルールは学習しないとわからず、依存を可視化しづらく、FooTests から Foo への依存を外す手段もないなど、扱いに困る仕様でした。
今のうちに直したい
Swift 4 以降 SwiftPM の利用者は大きく増えていくと見込まれており、ユーザが少ないうちに API を整理しておく必要がありました。
02 どのように解決されるのか
Package.swift マニフェスト API を全面的に見直し、API Design Guidelines に沿った形に整理します。変更は新しい PackageDescription v4 ライブラリに対して加えられ、既存パッケージは Swift 4 ツールに同梱される v3 ライブラリのまま動きます。v4 への移行は SE-0152 のツールバージョン指定で opt-in します。
列挙ケースを lowerCamelCase 化
Target.Dependency と SystemPackageProvider のケース名が小文字始まりに統一されます。また SystemPackageProvider のペイロードは複数のシステムパッケージを渡せるよう配列化されます。
enum Dependency {
case target(name: String)
case product(name: String, package: String? = nil)
case byName(name: String)
}
enum SystemPackageProvider {
case brew([String])
case apt([String])
}
product ケースの package 引数にはデフォルト値 nil が与えられ、.product(name: "Foo") のように短く書けます。
ファクトリメソッドへの統一
最上位の Package を除き、Target や Product といったオブジェクトはファクトリメソッド経由で生成するよう統一されます。これにより、列挙型ケースと同じドット構文で書けるようになり、将来のオーバーロード追加にも対応しやすくなります。
Target には .target(name:dependencies:) のようなファクトリメソッドが追加され、直接のイニシャライザは private になります。プロダクトも同様に Product クラスにネストされた Executable / Library として用意され、Product.library(...) / Product.executable(...) というファクトリメソッドから生成します。
Package と Target のプロパティをミュータブルに
宣言後に内容を書き換えられるよう、Package と Target のプロパティはすべて var になります。これにより、プラットフォームごとの条件分岐で依存などを差し替えることが自然に書けます。
let package = Package(
name: "FooPackage",
targets: [
.target(name: "Foo", dependencies: ["Bar"]),
]
)
#if os(Linux)
package.targets[0].dependencies = ["BarLinux"]
#endif
Package イニシャライザの引数順の整理
Package の引数は興味のある順に並び替えられ、name、pkgConfig、providers、products、dependencies、targets、swiftLanguageVersions の順になります。products と dependencies がパッケージの顔として先に来て、細部である targets はファイルの末尾近くに置かれます。
バージョン指定の刷新
新しい API では、パッケージ依存のバージョン指定は .package(url:...) 形式のファクトリメソッドを通じて行います。SemVer を正しく使うことを促すため、「次のメジャーバージョンまで許容する」を最も簡潔に書けるデフォルトにしています。
// 1.0.0 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.0.0")
// 1.5.8 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.5.8")
より細かい指定が必要な場合は Requirement 列挙型に対する各種ファクトリを使います。
// 1.5.8 ..< 2.0.0(キャレット相当)
.package(url: "/SwiftyJSON", .upToNextMajor(from: "1.5.8"))
// 1.5.8 ..< 1.6.0(チルダ相当)
.package(url: "/SwiftyJSON", .upToNextMinor(from: "1.5.8"))
// ちょうど1.5.8
.package(url: "/SwiftyJSON", .exact("1.5.8"))
// 任意の開区間・閉区間
.package(url: "/SwiftyJSON", "1.2.3"..<"1.2.6")
.package(url: "/SwiftyJSON", "1.2.3"..."1.2.8")
// ブランチやリビジョンの指定(SE-0150の調整)
.package(url: "/SwiftyJSON", .branch("develop"))
.package(url: "/SwiftyJSON", .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"))
旧来の majorVersion:・minor: などを取るファクトリ、および位置引数でバージョンや範囲を渡す形はすべて撤廃されます。
暗黙のテストターゲット依存の撤廃
FooTests が Foo に暗黙で依存するルールは削除され、必要ならマニフェストで明示的に宣言することになります。暗黙ルールによる混乱や、依存解析・機械的な依存編集での不都合が解消されます。
Version の整理
Version から successor() / predecessor() が削除され、buildMetadataIdentifier は SemVer 2.0 に合わせて 配列([String]) になります。v3 API との互換のため、従来の単一文字列プロパティは computed property として残され、文字列を受け取るイニシャライザも維持されます。
移行のしかた
v3 → v4 への自動マイグレーションは提供されません。可能な範囲で旧 API に @unavailable を付け、エラーメッセージから移行先がわかるようにはなりますが、列挙ケース名の変更など一部は手作業で書き換える必要があります。
- 既存パッケージは宣言しなければ Swift 3 ツールバージョン扱いとなり、v3 API のままビルドされます。
- Swift 4 ツールで
swift package initを使って作る新規パッケージは、デフォルトで v4 API を採用します。 - 既存パッケージを v4 API に移行するには、
swift package tools-version --set-currentでツールバージョンを 4.0 以上に引き上げ、この提案で示された変更に合わせてマニフェストを書き換えます。 - Swift 3 と Swift 4 の両ツールをサポートしたいパッケージは、SE-0151 に従って v3 API と Swift 3 言語モードにとどめる必要があります。
完成形のマニフェスト例
新しい API で書いた典型的なマニフェストは次のようになります。
let package = Package(
name: "Paper",
products: [
.executable(name: "tool", targets: ["tool"]),
.library(name: "Paper", targets: ["Paper"]),
.library(name: "PaperStatic", type: .static, targets: ["Paper"]),
.library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"]),
],
dependencies: [
.package(url: "http://github.com/SwiftyJSON/SwiftyJSON", from: "1.2.3"),
.package(url: "../CHTTPParser", .upToNextMinor(from: "2.2.0")),
.package(url: "http://some/other/lib", .exact("1.2.3")),
],
targets: [
.target(
name: "tool",
dependencies: [
"Paper",
"SwiftyJSON",
]),
.target(
name: "Paper",
dependencies: [
"Basic",
.target(name: "Utility"),
.product(name: "CHTTPParser"),
]),
]
)
システムパッケージのマニフェストもシンプルに書けます。
let package = Package(
name: "Copenssl",
pkgConfig: "openssl",
providers: [
.brew(["openssl"]),
.apt(["openssl", "libssl-dev"]),
]
)
今後の見通し
この提案は公開済み API の整理のみを扱い、新機能は含みません。ただし、ここで整えた基盤の上に、今後次のような機能が個別の提案として検討される見込みです(いずれも本提案の範囲外で、実現を約束するものではありません)。
- カスタムレイアウト対応に合わせた
excludeプロパティの撤廃。 - バージョン範囲の部分的な除外(
.excluding(...)のような指定)。 - ターゲット依存の箇所でパッケージ依存をインラインに書けるようにする、宣言の簡略化。
swiftLanguageVersionsを置き換えるビルド設定機構の導入。