デプロイメントターゲットによる条件付きコンパイル
Deployment target conditional compilation
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift では、あるAPIが実行時に利用できるかどうかを if #available(...) で調べられます。
if #available(macOS 15, iOS 18, *) {
useNewImplementation()
} else {
useFallbackImplementation()
}
しかし #available はあくまで 実行時 の分岐です。import する module、typealias、プロトコル適合、stored property、あるいは宣言そのものを切り替えることはできません。これらの選択は module のコンパイル時に決めなければならないためです。
たとえば、アプリケーションのデプロイメントターゲットに合わせて再ビルドされるライブラリが、そのターゲットで利用が保証されるなら Mutex を使い、古いターゲット向けには従来のロック実装を使い分けたい、というケースがあります。これは実行時ではなくコンパイル時に決めたい分岐です。
現状では、プロジェクト側で独自の条件付きコンパイルのフラグを定義することで近い動作を実現できます。
#if USE_SYNCHRONIZATION_MUTEX
// 新しい実装
#else
// 後方互換の実装
#endif
ただしこの方法では、ビルドシステムが USE_SYNCHRONIZATION_MUTEX をデプロイメントターゲットから導き出し、両者を常に同期させ続けなければなりません。もし両者がずれると、選択されるソースがバイナリの最小OS要件を正しく反映しなくなってしまいます。しかも、この情報はコンパイラがターゲットトリプルとして既に把握しているものです。
なお、これはSDKのバージョンチェックとは異なります。2つの module が同じSDKを使っていても、最小OSバージョンの要件は別々でありうるためです。
02 どのように解決されるのか
#if で使える新しい条件 deploymentTargetAtLeast(...) を導入します。コンパイル対象の module の最小デプロイメントターゲットに応じて、コンパイルするソースを選択できます。
#if deploymentTargetAtLeast(macOS 15, iOS 18, *)
import Synchronization
typealias Lock<State> = Mutex<State>
#else
import os
typealias Lock<State> = OSAllocatedUnfairLock<State>
#endif
引数には if #available(...) と同じ、プラットフォームとバージョンの綴りを使います。判定されるのは コンパイル中のプラットフォームに対応する要件だけ です。末尾の * は、列挙されていないその他すべてのプラットフォームで条件が true になることを意味します。
上の例は、コンパイル対象ごとに次のように評価されます。
| コンパイル対象 | 結果 |
|---|---|
| macOS 14 | false |
| macOS 15 以降 | true |
| iOS 17 | false |
| iOS 18 以降 | true |
| Linux | true(* による) |
文法とワイルドカード
* は必須で、1回だけ、必ず最後の引数として書きます。deploymentTargetAtLeast(*) は妥当で、常に true に評価されます。
他の条件付きコンパイルの述語と同様に、!・&&・||・括弧と組み合わせられます。
#if deploymentTargetAtLeast(macOS 15, *) && canImport(Synchronization)
// ...
#endif
バージョンはピリオド区切りの1つ以上の符号なし10進数から成り、コンポーネントごとに比較されます。省略された末尾のコンポーネントは 0 として扱われるため、15 と 15.0 は等しいものとして比較されます。
プラットフォームのマッチング
プラットフォーム名やエイリアス、要件の適用可否は if #available(...) に従います。同じプラットフォームを二度書くことはできません。複数の要件が適用可能な場合は、最も具体的なものが判定に使うバージョンを決めます。たとえば Mac Catalyst では macCatalyst の要件が iOS の要件より優先されます。適用可能な要件がなければ、* によって条件は true になります。
認識できないプラットフォーム名は警告となり、そのコンパイラが知っているどのターゲットともマッチしないため、結果はワイルドカードで決まります。これにより、古いコンパイラが将来のプラットフォーム名に出会っても動作しつつ、綴り間違いは診断できるようになっています。
デプロイメントターゲットの取得と比較
この条件は、現在の module のターゲットトリプルに含まれる最小デプロイメントバージョンを使います。初期実装では、macOS・iOS・tvOS・watchOS・visionOS・Mac Catalyst・Windows・Android について意味のあるバージョンを取得できます。Android はターゲットトリプルの environment バージョン(API レベルを表す)を使います。Linux などのターゲットは、現状ではこの比較に使えるデプロイメントバージョンを持ちません。
要件が現在のプラットフォームを明示的に挙げているのに、コンパイラがそのデプロイメントバージョンを取得できない場合は、コンパイルエラーになります。
// Linux 向けにコンパイルするとエラー。ターゲットがデプロイメントバージョンを持たないため。
#if deploymentTargetAtLeast(Linux 6, *)
// ...
#endif
if #available(...) はこうしたプラットフォームを受け入れてチェックを満たしたものとして扱いますが、これは実行時クエリが動作中のシステムにフォールバックできるためです。一方この条件は、ターゲットがデプロイメントバージョンを持たないと比較する対象がなく、黙って true や false を返すとソースに書かれていない理由で分岐を選ぶことになってしまうため、代わりに診断します。
Linux をワイルドカードに任せれば、同じソースをポータブルに保てます。
// Linux では '*' により true に評価される。
#if deploymentTargetAtLeast(macOS 15, iOS 18, *)
// ...
#endif
コンパイルモデル
deploymentTargetAtLeast(...) は通常のソースレベルの #if 条件です。それを含む module の文脈で評価され、選択された分岐だけがそれ以降のコンパイルに進みます。そのため、コンパイル済みの module には、その module のデプロイメントターゲット向けに選ばれた分岐だけが含まれます。
この条件は、シリアライズやクロスモジュールのインライン化のあとに再評価されるために保持されることはありません。たとえば macOS 14 向けにコンパイルされたライブラリが、あとから macOS 15 を対象とするアプリケーションにリンクされても、その条件は macOS 14 向けに評価されたままです。別のターゲット向けにライブラリを再ビルドすれば、条件はそのターゲット向けに再評価されます。
また、この条件はSDKのバージョンを比較したり、宣言を可視にしたり、通常のアベイラビリティチェックを回避したりするものではありません。選択された分岐のソースは、ビルドに使うSDKに対して従来どおりコンパイルできる必要があります。
実験的機能フラグ
プロトタイプは -enable-experimental-feature DeploymentTargetCondition で有効になります。このフラグは提案されるソース構文の一部ではありません。
03 今後の見通し
元のProposalでは、今後の方向性としていくつかの構想が挙げられています。いずれも将来的な検討事項であり、この提案の一部として実現が約束されているものではありません。
オプティマイザから見えるデプロイメントターゲット条件
if #deploymentTarget(...) のような式を用意し、SIL に保持してクロスモジュールのインライン化のあとにも評価できるようにすれば、最終的なクライアントのデプロイメントターゲットに応じた選択が可能になる、という構想があります。ただしこれは異なるコンパイルモデルを必要とし、この提案の範囲外とされています。
SDK の条件付きコンパイル
SDK のバージョンを露出する別の条件も考えられます。これはデプロイメントターゲットの条件を置き換えるものではなく、補完するものになるとされています。