Package Manager Custom Target Layouts
01 何が問題だったのか
Swift Package Manager はディスク上のディレクトリ構成から自動的にターゲットを推論する、規約ベースの仕組みを採用してきました。新規に作るシンプルなパッケージにとってはこの規約は便利ですが、そのまま当てはまらないプロジェクトや、規約がもたらす暗黙性によって以下のような問題が生じていました。
規約に合わないプロジェクトをパッケージ化しづらい
既存のC言語ライブラリや、独自のディレクトリ構成を持つ大規模プロジェクトをそのまま Swift Package として取り込もうとすると、SwiftPM が要求するディレクトリ構成に合わせてソースを再配置しなければなりませんでした。たとえば src/ 配下にソースを置く C ライブラリや、独自の階層を持つビルドツールなどを、現状維持のままパッケージ化する手段が用意されていませんでした。
ターゲットがディスクから暗黙に推論される
SwiftPM はターゲットをディスク上のディレクトリから推論していたため、Package.swift を読んだだけではそのパッケージがどんなターゲットを提供しているかが分かりませんでした。規約を少しでも外すと、意図しないターゲットが作られたり、期待したターゲットが作られなかったりし、原因の特定も困難でした。
テストターゲットの命名規約が強制されていた
テストターゲットはディレクトリ名に Tests サフィックスを付けることで識別される規約でした。そのためテストターゲットの名前付けに制約がかかり、かつ「名前にサフィックスが付いているかどうか」という暗黙のシグナルでターゲットの種類が決まる、見通しの悪い仕組みになっていました。
C ターゲットの公開ヘッダのパスを変えられない
C 言語系ターゲットでは include ディレクトリが公開ヘッダ置き場として固定されており、既存の C プロジェクトのようにヘッダを別の場所に置きたいケースに対応できませんでした。
02 どのように解決されるのか
SwiftPM のターゲット定義を明示化し、ディスク上のレイアウトをマニフェストからカスタマイズできるようにします。これは Swift 4 のマニフェスト API(// swift-tools-version:4.0 以降)に対する変更で、Swift 3 用のマニフェストには影響しません。
ターゲットは明示宣言が必須に
ディスクからのターゲット自動推論は廃止され、すべてのターゲットは Package.swift の targets に明示的に宣言する必要があります。依存関係やビルド設定を使う際にはいずれ宣言が必要だったこと、宣言があればエラー診断も親切にできることから、常に書かせる方向に統一されました。
テストターゲットは .testTarget で宣言
テストターゲットを判別するための「Tests サフィックス」ルールは廃止され、代わりに Target クラスのファクトリメソッド testTarget で明示的に宣言します。
.testTarget(name: "FooTests", dependencies: ["Foo"])
既定の探索パス
ターゲットに path を指定しない場合は、ターゲット名と同名のディレクトリを以下の順に探索します(大文字小文字は区別)。
- 通常ターゲット: パッケージルート、
Sources、Source、src、srcs - テストターゲット:
Tests、パッケージルート、Sources、Source、src、srcs
複数のパスで同名ディレクトリが見つかった場合はエラーとなり、後述の path で明示する必要があります。
path / sources / exclude でレイアウトを制御
Target に3つのプロパティが追加され、ターゲットのソース配置を自由に指定できるようになります。
path: ターゲットのソースが置かれたトップレベルディレクトリをパッケージルートからの相対パスで指定します。パッケージルートの外(../Fooや/Foo)は不可。既定値はnil(上述の探索パスから検索)で、""または"."を指定するとパッケージルート直下をターゲットとみなします。sources: ターゲットに含めるソースを、pathからの相対パスで列挙します。ディレクトリを指定すると再帰的にソースファイルが収集されます。既定のnilは「path配下のすべての有効なソース」を意味します。exclude:path配下から除外するファイル・ディレクトリを指定します。sourcesより優先されます。
二つのターゲットのパスが重なっている場合はエラーですが、exclude で解消できます。
// エラー: Bar のパスと BarTests のパスが重なっている
.target(name: "Bar", path: "Sources/Bar"),
.testTarget(name: "BarTests", dependencies: ["Bar"], path: "Sources/Bar/Tests"),
// OK: Bar 側で Tests を除外すれば重なりが解消する
.target(name: "Bar", path: "Sources/Bar", exclude: ["Tests"]),
.testTarget(name: "BarTests", dependencies: ["Bar"], path: "Sources/Bar/Tests"),
C ターゲットの公開ヘッダパスを指定できる
C 系ターゲットには publicHeadersPath プロパティが追加されました。path からの相対パスで、既定値は include です。既存の C プロジェクトのヘッダ構成に合わせてパッケージ化できるようになりますが、modulemap 生成などの関係で公開ヘッダディレクトリは依然として単一である必要があります。
Package の exclude は廃止
ターゲット単位で exclude を持てるようになったため、Package クラスの exclude プロパティは削除されます。
既存パッケージへの影響と移行
Swift 3 のマニフェストでは、次のようなフラットなレイアウトが許容されていました。
- ソースファイルをパッケージルート直下に置く
- ソースファイルを
Sources/直下に置く
Swift 4 のマニフェストにアップデートすると、ターゲット名と同名のディレクトリが既定で要求されるため、これらのフラットレイアウトを続けたい場合は path を明示する必要があります。たとえば、
Package.swift
Sources/main.swift
Sources/foo.swift
というレイアウトの Foo パッケージは、次のように書き直します。
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "Foo",
targets: [
.target(name: "Foo", path: "Sources"),
]
)
カスタムレイアウトの例
典型的なユースケースは次のように表現できます。
Swift のみ・複数ターゲット・テストのフィクスチャ除外:
let package = Package(
name: "SwiftyJSON",
targets: [
.target(
name: "Utility",
path: "Sources/BasicCode"
),
.target(
name: "SwiftyJSON",
dependencies: ["Utility"],
path: "SJ",
sources: ["SwiftyJSON.swift"]
),
.testTarget(
name: "AllTests",
dependencies: ["Utility", "SwiftyJSON"],
path: "Tests",
exclude: ["Fixtures"]
),
]
)
C ライブラリ(LibYAML)のように、ソースがサブディレクトリに置かれているケース:
let package = Package(
name: "LibYAML",
targets: [
.target(
name: "libyaml",
sources: ["src"]
)
]
)
独自の深い階層を持つプロジェクト(swift-build-tool)でも、path: "." とした上で sources に必要なディレクトリとファイルだけを列挙することで、既存のレイアウトのままパッケージ化できます。