Swift Digest
SE-0063 | Swift Evolution

SwiftPM System Module Search Paths

Proposal
SE-0063
Authors
Max Howell
Review Manager
Anders Bertelrud
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift Package Manager(SwiftPM)では、Swiftから import できるC系ライブラリを「system module」としてパッケージ化できます。具体的には、OS側(apt / pacman / brew などのシステムパッケージャ)でインストールされた既存のCライブラリに対して、clangのモジュールマップmodule.modulemap)を書いたSwiftPMパッケージを用意することで、Swiftコードから import Gtk のような形で扱えるようにする仕組みです。

この仕組み自体は動いていたものの、次のような問題が山積みで、実用上かなり扱いづらい状況でした。

モジュールマップが絶対パスを要求する

module.modulemap 内でヘッダを指定するときは絶対パスで書く必要があります。一方、ライブラリのインストール先はプラットフォームごとにばらばらです。たとえばモジュールマップの慣習として「プレフィックスは /usr」を前提にヘッダを記述することが多いものの、macOSのHomebrewやMacPortsは /usr 以外のプレフィックスにインストールします。結果として、同じパッケージをmacOSとLinuxの両方で使えるようにすることが困難でした。

検索パスが足りない

SwiftPMはビルド時に /usr/lib:/usr/local/lib をリンカ検索パスに、/usr/include:/usr/local/include をCのインクルード検索パスに加えます。しかし、多くのライブラリはこれだけでは不十分です。たとえばGTKのモジュールマップを解決するには、gtk.h が引く周辺ヘッダを見つけるために -I/usr/include/gtk のような追加パスが必要になります。

システムライブラリのインストールはユーザー任せ

system moduleのSwiftPMパッケージを取り込んでも、肝心のCライブラリ本体は別途OS側でインストールしておく必要があります。ところが、そのインストール名(パッケージャ上のパッケージ名)はディストリビューションによって異なり、直感的でもありません。たとえばDebian系でGTK3をインストールするためのパッケージ名は libgtk-3-0-dev で、これを知らないとビルドエラーの原因に辿り着けません。

これらが組み合わさることで、「SwiftPMのsystem moduleは用意したが、環境ごとにモジュールマップや追加フラグを書き分けないと動かないし、動かない理由もわかりにくい」という状態になっていました。

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

SwiftPMに、C系ライブラリが提供する pkg-config の設定ファイル(.pc ファイル)を読み取る機能を追加します。あわせて、Package.swift に「どのシステムパッケージャで、どの名前でインストールすればよいか」を書けるようにし、ビルドに失敗したときに適切なインストール手順をユーザーに案内できるようにします。

.pc ファイルから検索パスとフラグを取り込む

pkg-config は、C系ライブラリのインストール先・コンパイル時フラグ・リンク時フラグをプラットフォーム横断的に記述するためのデファクト標準です。多くのシステムライブラリは、インストール時にあわせて <ライブラリ名>.pc ファイルを配置します。

SwiftPMは、system moduleのパッケージをビルドするときにこの .pc ファイルを読み、次のように扱います。

  • 記述されたインストールプレフィックスを使って、モジュールマップに書かれたヘッダパスを解釈し直します。これにより、/usr を前提に書かれたモジュールマップがHomebrewやMacPortsのプレフィックスにも追従できるようになります。
  • .pc が示す追加のインクルードパスやリンカフラグを、ビルド時のフラグに反映します。-I/usr/include/gtk のような追加パスもここから供給されます。

pkg-config への外部依存を増やしたくないため、SwiftPMは自前で .pc ファイルを読み解きます。その際、pkg-config の仕様に従って標準の検索パスを辿り、環境変数 PKG_CONFIG_PATH による上書きにも従います。

読みに行く .pc ファイルの特定

慣習では .pc ファイルの名前はライブラリのリンク名と一致するため、SwiftPMはパッケージに含まれる .modulemap を解析して、そこから .pc のファイル名を推測します。ただし、この慣習に従わないライブラリもあります(たとえばUbuntuのGTK-3)。その場合に備えて、Package.swift で明示的に名前を上書きできます。

let package = Package(
    name: "CFoo",
    pkgConfigName: "gtk-3"
)

この例では gtk-3.pc を探しに行きます。

インストール方法をユーザーに案内する

Package.swift では、対応するシステムパッケージ名をパッケージャごとに宣言できます。

let package = Package(
    name: "CFoo",
    pkgConfigName: "foo",
    providers: [
        .Brew(installName: "foo"),
        .Apt(installName: "libfoo-dev"),
    ],
)

こう書いておくと、依存モジュールのビルドが失敗したときに、SwiftPMが現在の環境に合ったパッケージャを判定して、次のようなヒントを添えて失敗を報告できます。

error: failed to build module `bar'
note: you may need to install `foo' using your system-packager:

    apt-get install libfoo-dev

providers は明示的なenumで表現されるので、新しいシステムパッケージャへの対応も後から追加できます。将来的に、同じ apt でもディストリビューションやリリースごとにパッケージ名が違うといった事情に対応するため、enumのケースを後方互換な形で拡張していくことも想定されています。ただし初期バージョンは単純な「パッケージャ種別とインストール名」の組だけを扱います。

既存コードへの影響

この機能はSwiftPMのsystem moduleまわりを改善するもので、既存のパッケージのビルドを壊すような変更は含みません。従来どおり素のモジュールマップだけで動いていたパッケージは引き続きそのまま動き、pkgConfigNameproviders を追記することで段階的に恩恵を受けられます。

扱わない/将来に委ねる領域

Future Directionsとして、次のような方向性が示されています。いずれも今回のスコープ外であり、今後の議論に委ねられます。

  • pkg-config を唯一の真実の源とせず、システムパッケージャ自身が直接パス情報を提供する仕組み。たとえばHomebrewはライブラリごとに独立したディレクトリにインストールするので、そのディレクトリ情報を直接使えば、共通の検索パスに由来する取り違えのリスクを減らせます。この場合、pkg-config は複数ある情報源のひとつ、フォールバックとしての位置づけになります。
  • .pc を持たない古めのライブラリでよく使われる、foo-config のような問い合わせ用ツールへの対応。現時点では実例の数と重要度を見てから判断する方針です。
  • macOSのシステムライブラリで .pc が用意されていないものへの対応。こちらも影響範囲を見てから検討されます。

なお、SwiftPM自身がシステムパッケージャを呼び出して依存ライブラリを自動インストールする機能は、セキュリティ上の懸念から導入しない方針です。代わりに、providers から得られる情報をJSONなどで出力し、別ツールで自動化する余地を残すにとどめます。