Package Manager Target Specific Build Settings
01 何が問題だったのか
Swift Package Manager(SwiftPM)は当初、ビルドツール(コンパイラやリンカ)の呼び出し方をマニフェストからカスタマイズする手段をほとんど持っていませんでした。そのため、C/C++ や Objective-C を含むパッケージの作者は、少し込み入ったビルド要件を満たすためだけに、かなり不格好な回避策を強いられていました。
たとえば次のようなケースです。
- プロジェクト内の複数ディレクトリに分かれたヘッダを
#includeしたいが、SwiftPM は各ターゲットのincludeディレクトリしかヘッダ検索パスに追加しないため、プライベートヘッダをincludeにシンボリックリンクしたり、ソースの#include文をいじる必要がありました。 - 特定のコンパイル時条件(
-Dで渡すマクロや、Swift の#if用のコンパイル条件)を有効にしたいが、マニフェストには指定手段がなく、ラッパースクリプトや環境変数に頼るしかありませんでした。 - システムに入っているライブラリやフレームワークにリンクしたい場合、「システムライブラリターゲット」や空の C ターゲットを作って module map を手書きする、といった手続きが必要でした。プラットフォームによってリンクするライブラリを切り替える、といった条件付けの手段もありません。
- 上記のどれにも当てはまらない、実験的にどうしてもコンパイラへ直接フラグを渡したい、というケースに至っては、SwiftPM コマンドラインの
-Xcc/-Xswiftc/-Xlinkerを利用者側で渡してもらう以外に方法がなく、パッケージ単体で自分のビルド要件を表現できていませんでした。
これらの設定項目自体は、ビルドシステムとしてはごく基本的なものです。にもかかわらず、SwiftPM で素直に書ける API が用意されていないために、パッケージ化そのものを諦める・他のビルドシステムを併用する、といった判断につながりやすい状況でした。
02 どのように解決されるのか
PackageDescription のターゲット定義に、ターゲット固有のビルド設定を宣言するための 4 つの引数 cSettings / cxxSettings / swiftSettings / linkerSettings を追加します。ここで指定した設定はそのターゲットのコンパイル・リンクにのみ適用され、他のターゲットや依存先には波及しません。
.target(
name: "MyTool",
dependencies: ["Yams"],
cSettings: [
.define("BAR"),
.headerSearchPath("path/relative/to/my/target"),
.define("DISABLE_SOMETHING", .when(platforms: [.iOS], configuration: .release)),
.define("ENABLE_SOMETHING", .when(configuration: .release)),
],
swiftSettings: [
.define("API_VERSION_5"),
],
linkerSettings: [
.linkedLibrary("z"),
.linkedFramework("CoreData"),
.linkedLibrary("openssl", .when(platforms: [.linux])),
.linkedFramework("CoreData", .when(platforms: [.macOS], configuration: .debug)),
]
)
追加されるビルド設定
この提案で導入されるのは次の設定項目です。いずれも静的メソッドとして、各 *Setting 型(CSetting / CXXSetting / SwiftSetting / LinkerSetting)に用意されます。
headerSearchPath(_:)(C / C++): ターゲットからの相対パスをヘッダ検索パスに追加します。絶対パスやパッケージ外を指すパスは禁止されます。あくまでそのターゲット内部のビルドに適用されるため、公開ヘッダの検索パスの用途には向きません。define(_:to:)(C / C++):-D<name>=<value>をコンパイラに渡します。値は省略可能です。define(_:)(Swift): Swift 側のコンパイル条件を有効化します。C / C++ 側と違って値は持ちません(#if FOOのFOOを立てるイメージです)。linkedLibrary(_:)(Linker): システムライブラリへのリンクを追加します。これまで空の C ターゲットや手書き module map で実現していた用途を置き換えられます。linkedFramework(_:)(Linker): フレームワークへのリンクを追加します。Swift や C / Objective-C のターゲットはフレームワークを自動リンクしますが、C++ を含むターゲットでは自動リンクが効かないため、この設定が必要になります。Apple プラットフォーム専用であることが多いので、後述の.whenでプラットフォーム条件を付けるのが推奨されます。unsafeFlags(_:)(全設定共通): 任意のコマンドラインフラグをそのままビルドツールへ渡す、いわゆるエスケープハッチです。
.when による条件付け
各設定の末尾には BuildSettingCondition を受け取る引数があり、.when(platforms:configuration:) でプラットフォームやビルド構成(debug / release)ごとに適用を絞り込めます。platforms と configuration の両方を省略すると意味のない条件になるため、マニフェスト解析時にエラーとして診断されます。
unsafeFlags の位置付け
unsafeFlags は「このフラグは本来 SwiftPM の管理下に置くべきだが、現状は API が用意されていない」といった場面のための逃げ道です。コンパイラの挙動を大きく変えたり(たとえば Swift の -wmo)、ツールチェーンの探索パスを差し替えたりするようなフラグは、ビルドの再現性やセキュリティの観点から安全性を SwiftPM 側で判断できません。
そのため、unsafeFlags を含むターゲットを持つプロダクトは、他のパッケージから依存として利用することができません。アプリやローカルパッケージで実験的に使う分には問題ありませんが、公開パッケージで使うと利用者側がその依存を解決できなくなる、という制約があります。
Swift ターゲットと C 系設定の共存
ひとつの Swift ターゲットに swiftSettings と cSettings の両方を指定した場合、C 側のフラグは Swift コンパイラに -Xcc プレフィックス付きで渡されます。C 系ターゲットに cSettings と cxxSettings の両方を指定した場合も同様に、C のフラグは C++ コンパイラにも -Xcc 経由で渡されます。コマンドライン SwiftPM における -Xcc / -Xswiftc / -Xlinker の挙動と揃えた設計です。
既存パッケージへの影響
新しい API は追加のみで、既存パッケージの挙動は変わりません。これらの設定を利用したいパッケージは、マニフェスト冒頭の // swift-tools-version:... を本提案が実装された Swift 5.0 以上に引き上げる必要があります。
Future Directions
本提案は「よく使われる基本的なビルド設定」を宣言的に扱えるようにするための土台を作ることに主眼があります。将来的には、より多様な条件式・マクロ展開・設定の継承などを含む、よりリッチなビルド設定モデルに発展させていくことが想定されています(あくまで見通しであり、具体的な API 形状や時期は今後の提案に委ねられます)。