ターゲット設定のデフォルト値
Default Target Settings
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift パッケージでは、すべてのターゲットに同じ設定フラグを適用したいことがよくあります。たとえば upcoming feature flag を有効化する設定は、多くのパッケージでプライマリターゲットとテストターゲットの両方に同じものを書くことになります。
let package = Package(
// ...
targets: [
.target(
name: "MyPackage",
swiftSettings: [
.enableUpcomingFeature("ApproachableConcurrency"),
],
),
.testTarget(
name: "MyPackageTests",
dependencies: ["MyPackage"],
swiftSettings: [
.enableUpcomingFeature("ApproachableConcurrency"),
],
),
]
)
ターゲットが 2 つ程度なら重複も許容範囲かもしれませんが、ターゲット数が多いパッケージや複雑なマニフェストでは、どの設定がどこに適用されているのかを把握するのが難しくなります。
これを避けるために、共通設定を定数の配列に切り出して各ターゲットで結合する、という書き方もできます。
let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("ApproachableConcurrency"),
]
let package = Package(
// ...
targets: [
.target(
name: "MyPackage",
swiftSettings: swiftSettings + [.enableUpcomingFeature("Lifetimes")],
),
.testTarget(
name: "MyPackageTests",
dependencies: ["MyPackage"],
swiftSettings: swiftSettings,
),
]
)
しかし、これはすべてのターゲットがほぼ同じ設定を使う場合にしか通用しません。ターゲットごとに少しずつ設定を変えたくなると、マニフェスト内に追加のロジックが必要になります。実際、パッケージを定義したあとで全ターゲットをループして swiftSettings を書き換える、という手法が使われることもあります。
for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(.enableUpcomingFeature("ExistentialAny"))
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
// ...
target.swiftSettings = settings
}
この方法では、ターゲットを書き換える処理と設定配列を組み立てる処理をループの中で管理しなければならず、しかもパッケージ定義の「後」で行うため、設定とそれが影響する対象との距離が離れてしまいます。こうした既存の回避策はいずれも冗長で間違えやすく、コンパイラの挙動差に由来する不具合はとりわけ発見しづらいものです。
02 どのように解決されるのか
Package に、すべてのターゲットへ適用するデフォルト設定を定義するためのプロパティを追加します。Swift の設定だけでなく、C・C++・リンカの設定にもそれぞれ対応します。
public final class Package {
// ...
public var defaultSwiftSettings: [SwiftSetting]?
public var defaultCSettings: [CSetting]?
public var defaultCXXSettings: [CXXSetting]?
public var defaultLinkerSettings: [LinkerSetting]?
}
Package のイニシャライザにも defaultSwiftSettings などの引数が追加され、マニフェストの中で直接デフォルト設定を宣言できます。これにより、先ほどのテンプレートは各ターゲットから重複した設定を取り除けます。
let package = Package(
// ...
targets: [
.target(
name: "MyPackage"
),
.testTarget(
name: "MyPackageTests",
dependencies: ["MyPackage"]
),
],
defaultSwiftSettings: [
.enableUpcomingFeature("ApproachableConcurrency"),
]
)
設定がパッケージ定義の一箇所にまとまり、ターゲットを後から書き換えるようなロジックも不要になるため、マニフェストがより宣言的に読めるようになります。
設定の継承と inherited()
デフォルト設定はターゲットごとに制御できることが重要です。そのために inherited() というプレースホルダー設定が導入されます。設定が評価されるとき、このプレースホルダーは対応するデフォルト設定の値へ置き換えられます。
let package = Package(
// ...
targets: [
.target(
name: "A",
),
.target(
name: "B",
swiftSettings: [
.inherited(),
]
),
.target(
name: "C",
swiftSettings: [
]
),
.target(
name: "D",
swiftSettings: [
.inherited(),
.enableExperimentalFeature("Lifetimes"),
]
),
],
defaultSwiftSettings: [
.enableUpcomingFeature("ApproachableConcurrency"),
]
)
それぞれのターゲットは次のように振る舞います。
- ターゲット
A:swiftSettingsを省略しているため、デフォルト設定がそのまま適用されます。 - ターゲット
B:inherited()を書くことで、明示的にデフォルト設定を継承します。 - ターゲット
C: 空の配列を指定して設定を定義しているため、デフォルト設定は適用されません。 - ターゲット
D:inherited()の位置によって、デフォルト設定と自前の設定の適用順序を制御できます。
ここで注意したいのは、swiftSettings を省略した場合と、明示的に空の配列を指定した場合とで意味が変わる点です。省略するとデフォルト設定が適用され、空配列を指定するとデフォルト設定は適用されません。この違いはデフォルト設定が存在する場合にのみ効いてくるため、既存のマニフェストの挙動には影響しません。cSettings / cxxSettings / linkerSettings についても同じ仕組みが働きます。
この継承の仕組みは、既存の設定定義 API と同じ挙動をそのまま踏襲します。つまり重複や無効な組み合わせも許容され、それらはパッケージ検証の後段やビルドツール側で処理されます。多くの場合、結果は「最後に指定したものが優先される」という意味論になります。
制約
デフォルト設定には、通常のターゲット設定と同じように条件(.when(...) など)を付けられます。一方で、inherited() プレースホルダー自体に条件を付けることは、現時点ではできません。条件付き継承を正しく解決するにはかなりの複雑さが伴うため、当面は対象外とされています。
03 今後の見通し
現時点では inherited() プレースホルダーに条件を付けることはできませんが、条件付きの継承もパッケージ作者にとって有用ではないかと考えられています。これは API 互換性を保ったまま後から追加できる余地があるとされており、将来的な拡張の候補として挙げられています。ただしこれはあくまで構想であり、実現が約束されているものではありません。