Skip to content

Commit 3793551

Browse files
committed
[PIF] Re-implement swift language version handling
Since `toolSwiftVersion` is gone and swift language version is now handled by the build settings we need to adjust `PIFBuilder` to handle that properly. PIF build settings can handle per-target platform specific settings which are now expressible in SwiftPM via `.swiftLanguageVersion` build setting, handling of such settings is implemented in this commit.
1 parent b0e724b commit 3793551

File tree

2 files changed

+208
-39
lines changed

2 files changed

+208
-39
lines changed

Sources/XCBuildSupport/PIFBuilder.swift

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,20 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
357357
addBuildConfiguration(name: "Release", settings: releaseSettings)
358358

359359
for product in package.products.sorted(by: { $0.name < $1.name }) {
360-
try self.addTarget(for: product)
360+
let productScope = observabilityScope.makeChildScope(
361+
description: "Adding \(product.name) product",
362+
metadata: package.underlying.diagnosticsMetadata
363+
)
364+
365+
productScope.trap { try self.addTarget(for: product) }
361366
}
362367

363368
for target in package.targets.sorted(by: { $0.name < $1.name }) {
364-
try self.addTarget(for: target)
369+
let targetScope = observabilityScope.makeChildScope(
370+
description: "Adding \(target.name) module",
371+
metadata: package.underlying.diagnosticsMetadata
372+
)
373+
targetScope.trap { try self.addTarget(for: target) }
365374
}
366375

367376
if self.binaryGroup.children.isEmpty {
@@ -485,10 +494,7 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
485494
settings[.GCC_C_LANGUAGE_STANDARD] = clangTarget.cLanguageStandard
486495
settings[.CLANG_CXX_LANGUAGE_STANDARD] = clangTarget.cxxLanguageStandard
487496
} else if let swiftTarget = mainTarget.underlying as? SwiftTarget {
488-
settings[.SWIFT_VERSION] = try swiftTarget
489-
.computeEffectiveSwiftVersion(supportedSwiftVersions: self.parameters.supportedSwiftVersions)
490-
.description
491-
497+
try settings.addSwiftVersionSettings(target: swiftTarget, parameters: self.parameters)
492498
settings.addCommonSwiftSettings(package: self.package, target: mainTarget, parameters: self.parameters)
493499
}
494500

@@ -676,9 +682,8 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
676682
shouldImpartModuleMap = false
677683
}
678684
} else if let swiftTarget = target.underlying as? SwiftTarget {
679-
settings[.SWIFT_VERSION] = try swiftTarget
680-
.computeEffectiveSwiftVersion(supportedSwiftVersions: self.parameters.supportedSwiftVersions)
681-
.description
685+
try settings.addSwiftVersionSettings(target: swiftTarget, parameters: self.parameters)
686+
682687
// Generate ObjC compatibility header for Swift library targets.
683688
settings[.SWIFT_OBJC_INTERFACE_HEADER_DIR] = "$(OBJROOT)/GeneratedModuleMaps/$(PLATFORM_NAME)"
684689
settings[.SWIFT_OBJC_INTERFACE_HEADER_NAME] = "\(target.name)-Swift.h"
@@ -1575,31 +1580,6 @@ extension Target {
15751580
}
15761581
}
15771582

1578-
extension SwiftTarget {
1579-
func computeEffectiveSwiftVersion(supportedSwiftVersions: [SwiftLanguageVersion]) throws -> SwiftLanguageVersion {
1580-
// We have to normalize to two component strings to match the results from XCBuild w.r.t. to hashing of
1581-
// `SwiftLanguageVersion` instances.
1582-
let normalizedDeclaredVersions = Set(self.declaredSwiftVersions.compactMap {
1583-
SwiftLanguageVersion(string: "\($0.major).\($0.minor)")
1584-
})
1585-
// If we were able to determine the list of versions supported by XCBuild, cross-reference with the package's
1586-
// Swift versions in case the preferred version isn't available.
1587-
if !supportedSwiftVersions.isEmpty, !supportedSwiftVersions.contains(self.toolSwiftVersion) {
1588-
let declaredVersions = Array(normalizedDeclaredVersions.intersection(supportedSwiftVersions)).sorted(by: >)
1589-
if let swiftVersion = declaredVersions.first {
1590-
return swiftVersion
1591-
} else {
1592-
throw PIFGenerationError.unsupportedSwiftLanguageVersion(
1593-
targetName: self.name,
1594-
version: self.toolSwiftVersion,
1595-
supportedVersions: supportedSwiftVersions
1596-
)
1597-
}
1598-
}
1599-
return self.toolSwiftVersion
1600-
}
1601-
}
1602-
16031583
extension ProductType {
16041584
var targetType: Target.Kind {
16051585
switch self {
@@ -1828,6 +1808,93 @@ extension PIF.PlatformFilter {
18281808
}
18291809

18301810
extension PIF.BuildSettings {
1811+
fileprivate mutating func addSwiftVersionSettings(
1812+
target: SwiftTarget,
1813+
parameters: PIFBuilderParameters
1814+
) throws {
1815+
guard let versionAssignments = target.buildSettings.assignments[.SWIFT_VERSION] else {
1816+
// This should never happens in practice because there is always a default tools version based value.
1817+
return
1818+
}
1819+
1820+
func isSupportedVersion(_ version: SwiftLanguageVersion) -> Bool {
1821+
parameters.supportedSwiftVersions.isEmpty || parameters.supportedSwiftVersions.contains(version)
1822+
}
1823+
1824+
func computeEffectiveSwiftVersions(for versions: [SwiftLanguageVersion]) -> [String] {
1825+
versions
1826+
.filter { target.declaredSwiftVersions.contains($0) }
1827+
.filter { isSupportedVersion($0) }.map(\.description)
1828+
}
1829+
1830+
func computeEffectiveTargetVersion(for assignment: BuildSettings.Assignment) throws -> String {
1831+
let versions = assignment.values.compactMap { SwiftLanguageVersion(string: $0) }
1832+
if let effectiveVersion = computeEffectiveSwiftVersions(for: versions).last {
1833+
return effectiveVersion
1834+
}
1835+
1836+
throw PIFGenerationError.unsupportedSwiftLanguageVersions(
1837+
targetName: target.name,
1838+
versions: versions,
1839+
supportedVersions: parameters.supportedSwiftVersions
1840+
)
1841+
}
1842+
1843+
var toolsSwiftVersion: SwiftLanguageVersion? = nil
1844+
// First, check whether there are any target specific settings.
1845+
for assignment in versionAssignments {
1846+
if assignment.default {
1847+
toolsSwiftVersion = assignment.values.first.flatMap { .init(string: $0) }
1848+
continue
1849+
}
1850+
1851+
if assignment.conditions.isEmpty {
1852+
self[.SWIFT_VERSION] = try computeEffectiveTargetVersion(for: assignment)
1853+
continue
1854+
}
1855+
1856+
for condition in assignment.conditions {
1857+
if let platforms = condition.platformsCondition {
1858+
for platform: Platform in platforms.platforms.compactMap({ .init(rawValue: $0.name) }) {
1859+
self[.SWIFT_VERSION, for: platform] = try computeEffectiveTargetVersion(for: assignment)
1860+
}
1861+
}
1862+
}
1863+
}
1864+
1865+
// If there were no target specific assignments, let's add a fallback tools version based value.
1866+
if let toolsSwiftVersion, self[.SWIFT_VERSION] == nil {
1867+
// Use tools based version if it's supported.
1868+
if isSupportedVersion(toolsSwiftVersion) {
1869+
self[.SWIFT_VERSION] = toolsSwiftVersion.description
1870+
return
1871+
}
1872+
1873+
// Otherwise pick the newest supported tools version based value.
1874+
1875+
// We have to normalize to two component strings to match the results from XCBuild w.r.t. to hashing of
1876+
// `SwiftLanguageVersion` instances.
1877+
let normalizedDeclaredVersions = Set(target.declaredSwiftVersions.compactMap {
1878+
SwiftLanguageVersion(string: "\($0.major).\($0.minor)")
1879+
})
1880+
1881+
let declaredSwiftVersions = Array(
1882+
normalizedDeclaredVersions
1883+
.intersection(parameters.supportedSwiftVersions)
1884+
).sorted(by: >)
1885+
if let swiftVersion = declaredSwiftVersions.first {
1886+
self[.SWIFT_VERSION] = swiftVersion.description
1887+
return
1888+
}
1889+
1890+
throw PIFGenerationError.unsupportedSwiftLanguageVersions(
1891+
targetName: target.name,
1892+
versions: Array(normalizedDeclaredVersions),
1893+
supportedVersions: parameters.supportedSwiftVersions
1894+
)
1895+
}
1896+
}
1897+
18311898
fileprivate mutating func addCommonSwiftSettings(
18321899
package: ResolvedPackage,
18331900
target: ResolvedModule,
@@ -1859,9 +1926,22 @@ extension PIF.BuildSettings.Platform {
18591926
}
18601927

18611928
public enum PIFGenerationError: Error {
1862-
case unsupportedSwiftLanguageVersion(
1929+
case unsupportedSwiftLanguageVersions(
18631930
targetName: String,
1864-
version: SwiftLanguageVersion,
1931+
versions: [SwiftLanguageVersion],
18651932
supportedVersions: [SwiftLanguageVersion]
18661933
)
18671934
}
1935+
1936+
extension PIFGenerationError: CustomStringConvertible {
1937+
public var description: String {
1938+
switch self {
1939+
case .unsupportedSwiftLanguageVersions(
1940+
targetName: let target,
1941+
versions: let given,
1942+
supportedVersions: let supported
1943+
):
1944+
"Some of the Swift language versions used in target '\(target)' settings are supported. (given: \(given), supported: \(supported))"
1945+
}
1946+
}
1947+
}

Tests/XCBuildSupportTests/PIFBuilderTests.swift

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2915,7 +2915,8 @@ class PIFBuilderTests: XCTestCase {
29152915
#endif
29162916
let fs = InMemoryFileSystem(
29172917
emptyFiles:
2918-
"/Foo/Sources/foo/main.swift"
2918+
"/Foo/Sources/foo/main.swift",
2919+
"/Foo/Sources/bar/main.swift"
29192920
)
29202921

29212922
let observability = ObservabilitySystem.makeForTesting()
@@ -2929,6 +2930,13 @@ class PIFBuilderTests: XCTestCase {
29292930
swiftLanguageVersions: [.v4_2, .v5],
29302931
targets: [
29312932
.init(name: "foo", dependencies: []),
2933+
.init(name: "bar", dependencies: [], settings: [
2934+
.init(
2935+
tool: .swift,
2936+
kind: .swiftLanguageVersion(.v4_2),
2937+
condition: .init(platformNames: ["linux"])
2938+
),
2939+
]),
29322940
]
29332941
),
29342942
],
@@ -2938,7 +2946,7 @@ class PIFBuilderTests: XCTestCase {
29382946

29392947
let builder = PIFBuilder(
29402948
graph: graph,
2941-
parameters: .mock(supportedSwiftVersions: [.v4_2]),
2949+
parameters: .mock(supportedSwiftVersions: [.v4_2, .v5]),
29422950
fileSystem: fs,
29432951
observabilityScope: observability.topScope
29442952
)
@@ -2951,13 +2959,94 @@ class PIFBuilderTests: XCTestCase {
29512959
project.checkTarget("PACKAGE-PRODUCT:foo") { target in
29522960
target.checkBuildConfiguration("Debug") { configuration in
29532961
configuration.checkBuildSettings { settings in
2954-
XCTAssertEqual(settings[.SWIFT_VERSION], "4.2")
2962+
XCTAssertEqual(settings[.SWIFT_VERSION], "5")
2963+
}
2964+
}
2965+
}
2966+
2967+
project.checkTarget("PACKAGE-PRODUCT:bar") { target in
2968+
target.checkBuildConfiguration("Debug") { configuration in
2969+
configuration.checkBuildSettings { settings in
2970+
XCTAssertEqual(settings[.SWIFT_VERSION], "5")
2971+
XCTAssertEqual(settings[.SWIFT_VERSION, for: .linux], "4.2")
29552972
}
29562973
}
29572974
}
29582975
}
29592976
}
29602977
}
2978+
2979+
func testPerTargetSwiftVersions() throws {
2980+
#if !os(macOS)
2981+
try XCTSkipIf(true, "test is only supported on macOS")
2982+
#endif
2983+
let fs = InMemoryFileSystem(
2984+
emptyFiles:
2985+
"/Foo/Sources/foo/main.swift",
2986+
"/Foo/Sources/bar/main.swift",
2987+
"/Foo/Sources/baz/main.swift"
2988+
)
2989+
2990+
let observability = ObservabilitySystem.makeForTesting()
2991+
let graph = try loadModulesGraph(
2992+
fileSystem: fs,
2993+
manifests: [
2994+
Manifest.createRootManifest(
2995+
displayName: "Foo",
2996+
path: "/Foo",
2997+
toolsVersion: .v5_3,
2998+
swiftLanguageVersions: [.v4_2, .v5],
2999+
targets: [
3000+
.init(name: "foo", dependencies: [], settings: [
3001+
.init(
3002+
tool: .swift,
3003+
kind: .swiftLanguageVersion(.v4_2)
3004+
),
3005+
]),
3006+
.init(name: "bar", dependencies: [], settings: [
3007+
.init(
3008+
tool: .swift,
3009+
kind: .swiftLanguageVersion(.v6)
3010+
),
3011+
]),
3012+
.init(name: "baz", dependencies: [], settings: [
3013+
.init(
3014+
tool: .swift,
3015+
kind: .swiftLanguageVersion(.v3),
3016+
condition: .init(platformNames: ["linux"])
3017+
),
3018+
.init(
3019+
tool: .swift,
3020+
kind: .swiftLanguageVersion(.v4_2),
3021+
condition: .init(platformNames: ["macOS"])
3022+
),
3023+
]),
3024+
]
3025+
),
3026+
],
3027+
shouldCreateMultipleTestProducts: true,
3028+
observabilityScope: observability.topScope
3029+
)
3030+
3031+
let builder = PIFBuilder(
3032+
graph: graph,
3033+
parameters: .mock(supportedSwiftVersions: [.v4_2, .v5]),
3034+
fileSystem: fs,
3035+
observabilityScope: observability.topScope
3036+
)
3037+
let _ = try builder.construct()
3038+
3039+
testDiagnostics(observability.diagnostics) { result in
3040+
result.check(
3041+
diagnostic: "Some of the Swift language versions used in target 'bar' settings are supported. (given: [6], supported: [4.2, 5])",
3042+
severity: .error
3043+
)
3044+
result.check(
3045+
diagnostic: "Some of the Swift language versions used in target 'baz' settings are supported. (given: [3], supported: [4.2, 5])",
3046+
severity: .error
3047+
)
3048+
}
3049+
}
29613050
}
29623051

29633052
extension PIFBuilderParameters {

0 commit comments

Comments
 (0)