Package Creation
01 何が問題だったのか
SwiftPM で新しいパッケージを用意するためのコマンドは swift package init しかなく、このコマンドが次の 2 つの異なるユースケースを兼任していました。
- 空のディレクトリに、新しいパッケージを一から作る
- 既存のソースコードが入ったディレクトリを、パッケージ化する
両方を 1 つのコマンドで賄っているため、デフォルト挙動がどちらのユースケースにも中途半端になっており、特に Swift を触り始めたばかりのユーザーにとって直感に反する結果を生んでいました。
新規作成の場合、ユーザーはまず自分で空のディレクトリを作り、そこに cd してから init を実行する必要があります。
$ mkdir MyApp
$ cd MyApp
$ swift package init
生成されるディレクトリ構成は次のようなもので、デフォルトでは ライブラリ として、しかもパッケージ名にはカレントディレクトリ名が使われます。
.
├── .gitignore
├── Package.swift
├── README.md
├── Sources
│ └── MyApp
│ └── MyApp.swift
└── Tests
└── MyAppTests
└── MyAppTests.swift
ところが新規ユーザーの多くは「Swift を試すために、とりあえず動く小さなプログラムを作りたい」という動機で来るため、期待しているのは実行可能なプログラム(executable)であってライブラリではありません。また、「ディレクトリをあらかじめ作って、その名前がパッケージ名になる」という前提知識も、コマンド名や挙動からは読み取れません。
一方で、既存のソース群をパッケージ化したい既存ユーザーにとっては、ディレクトリ名をそのまま使って不足しているファイル(Sources/・Tests/・Package.swift など)を補う現在の挙動は理にかなっています。このため init の挙動を新規作成寄りに変えてしまうと、既存の自動化やワークフローを壊してしまうというジレンマもありました。
さらに、swift package init が生成するディレクトリ構成は固定で、ユーザーやチームが普段使っている独自のひな形(ライセンスファイル、独自の Sources レイアウト、依存パッケージの初期セットなど)を反映する仕組みもありませんでした。SwiftPM 自体はパッケージのディレクトリ構成についてかなり柔軟なのに、ひな形作成の段階ではその柔軟さを活かせていない状態でした。
02 どのように解決されるのか
新しいコマンド swift package create を追加し、swift package init と役割を明確に分けます。
swift package createは 新しいパッケージを一から作る ためのコマンドswift package initは 既存のソースディレクトリをパッケージ化する ためのコマンド
加えて、両コマンドにユーザー定義の テンプレート機能 を導入し、ひな形のディレクトリ構成を自分たちのスタイルに揃えられるようにします。
swift package create の基本
create では、ディレクトリを作るところからコマンドが面倒を見ます。引数にパッケージ名を渡せば、その名前のディレクトリが新しく作られ、必要なファイルがその中に展開されます。
$ swift package create MyApp
生成される構成は次のとおりです。
.
├── Package.swift
├── Sources
│ └── MyApp
│ └── MyApp.swift
└── Tests
└── MyAppTests
└── MyAppTests.swift
init と違い、デフォルトでは executable として作られます。そのため、そのまますぐに実行して動作を確認できます。
$ cd MyApp
$ swift run
[3/3] Linking MyApp
Hello, world!
パッケージの種類は --type オプションで切り替えられます。指定できるのは library・system-module・executable です。
$ swift package create MyLib --type library
$ swift package create SysMod --type system-module
ユーザー定義テンプレート
~/.swiftpm/configuration/templates/new-package/<テンプレート名> に Swift パッケージの形でひな形を置いておくと、--template オプションでそれを指定してパッケージを作れます。SwiftPM はテンプレートディレクトリをコピーし、テキストファイル内の特定のプレースホルダを置換し、テンプレート由来の .git 情報を取り除いた上で、新しいパッケージとして配置します。
使えるプレースホルダは次の 2 つです。
___NAME___:--name(または引数)で指定された名前にそのまま置換される___NAME_AS_C99___: 上記を C99 の識別子として有効になるよう変換した名前
たとえば、次のようなテンプレートを test という名前で登録しておいたとします。
.
├── .git
├── .gitignore
├── Package.swift
├── README.md
├── LICENSE.md
└── src
└── MyApp.swift
Package.swift(抜粋):
import PackageDescription
let package = Package(
name: "___NAME___",
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.0.0"),
],
targets: [
.executableTarget(
name: "___NAME_AS_C99___",
sources: "src",
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "Crypto", package: "swift-crypto"),
]
),
]
)
この状態で次を実行すると、
$ swift package create HelloWorld --template test
.git を除いた構成がコピーされた上で、___NAME___ などが HelloWorld に置き換えられたパッケージが得られます。依存関係や独自のディレクトリレイアウト、README や LICENSE までまるごとテンプレート化できるのがポイントです。
デフォルトテンプレートと配布
テンプレート名を default にしておく(~/.swiftpm/configuration/templates/new-package/default)と、--template を指定せずに swift package create / swift package init を実行したときに自動的にそのテンプレートが使われます。デフォルトのひな形を自分たちのスタイルに置き換えられる、ということです。
テンプレートは Git リポジトリとして共有する前提で、次のコマンドも追加されます。
swift package add-template <url> [--name <name>]: URL からテンプレートをgit cloneして登録するswift package update-template <name>: 登録済みテンプレートをgit updateで更新する
swift package init 側の変更
init は「既存のソースをパッケージ化する」用途に寄せるため、いくつか挙動が調整されます。
README.mdと.gitignoreを デフォルトでは生成しない(既存ソースのディレクトリへの影響を抑えるため)- 空のディレクトリで
initが実行されたときは、従来どおり新しいパッケージを作りつつ、「次からはswift package createのほうが適している」という diagnostics メッセージを表示する --templateオプションをinit側でも受け付け、テンプレート機能を利用できるようにする
今後の展望(Future Iterations)
ディレクトリコピー + 文字列置換だけでは表現しきれない、より動的なパッケージ生成については、将来的に スクリプトで手続き的にパッケージを構築する仕組み を導入する案が挙げられています(speculative)。SwiftPM がパッケージ生成用の API を提供し、Swift スクリプトが入力オプションや外部条件に応じて生成内容を決める、というイメージです。今回の提案はあくまでコピーベースのテンプレートにとどめつつ、その先の拡張余地を残す位置づけになっています。
ステータスについての注記
本 Proposal は Returned for revision となっており、上記の設計そのままでは採用されていません。swift package create の導入というゴールと、テンプレート機能の具体的な位置づけ(デフォルトテンプレートの扱いや共有方法など)が再検討の対象となった、という点を踏まえて読む必要があります。