diff --git a/Sources/SWBCore/PlatformRegistry.swift b/Sources/SWBCore/PlatformRegistry.swift index ae9edb6c..b644283e 100644 --- a/Sources/SWBCore/PlatformRegistry.swift +++ b/Sources/SWBCore/PlatformRegistry.swift @@ -73,9 +73,12 @@ public final class Platform: Sendable { /// Minimum OS version for Swift-in-the-OS support. If this is `nil`, the platform does not support Swift-in-the-OS at all. fileprivate(set) var minimumOSForSwiftInTheOS: Version? = nil - /// Minimum OS version for built-in Swift concurrency support. If this is `nil`, the platform does not support Swift concurrency at all. + /// Minimum OS version for Swift concurrency (Swift 5.5). If this is `nil`, the platform does not support Swift concurrency at all. fileprivate(set) var minimumOSForSwiftConcurrency: Version? = nil + /// Minimum OS version for Span in the standard library (Swift 6.2). If this is `nil`, the platform does not support Swift concurrency at all. + fileprivate(set) var minimumOSForSwiftSpan: Version? = nil + /// The canonical name of the public SDK for this platform. /// - remark: This does not mean that this SDK exists, just that this is its canonical name if it does exist. @_spi(Testing) public let sdkCanonicalName: String? @@ -244,6 +247,11 @@ extension Platform { return self.minimumOSForSwiftConcurrency ?? self.correspondingDevicePlatform?.minimumOSForSwiftConcurrency ?? nil } + /// Determines the deployment version to use for Swift Span support. + fileprivate func swiftOSSpanVersion(_ deploymentTarget: StringMacroDeclaration) -> Version? { + return self.minimumOSForSwiftSpan ?? self.correspondingDevicePlatform?.minimumOSForSwiftSpan ?? nil + } + /// Determines if the platform supports Swift in the OS. public func supportsSwiftInTheOS(_ scope: MacroEvaluationScope, forceNextMajorVersion: Bool = false, considerTargetDeviceOSVersion: Bool = true) -> Bool { guard let deploymentTarget = self.deploymentTargetMacro else { return false } @@ -265,7 +273,7 @@ extension Platform { return version >= minimumSwiftInTheOSVersion } - /// Determines if the platform natively supports Swift concurrency. If `false`, then the Swift back-compat concurrency libs needs to be copied into the app/framework's bundle. + /// Determines if the platform natively supports Swift concurrency. If `false`, then the Swift concurrency back-compat concurrency libs needs to be copied into the app/framework's bundle. public func supportsSwiftConcurrencyNatively(_ scope: MacroEvaluationScope, forceNextMajorVersion: Bool = false, considerTargetDeviceOSVersion: Bool = true) -> Bool? { guard let deploymentTarget = self.deploymentTargetMacro else { return false } @@ -287,6 +295,29 @@ extension Platform { return version >= minimumSwiftConcurrencyVersion } + + /// Determines if the platform natively supports Swift 6.2's Span type. If `false`, then the Swift Span back-compat concurrency libs needs to be copied into the app/framework's bundle. + public func supportsSwiftSpanNatively(_ scope: MacroEvaluationScope, forceNextMajorVersion: Bool = false, considerTargetDeviceOSVersion: Bool = true) -> Bool? { + guard let deploymentTarget = self.deploymentTargetMacro else { return false } + + // If we have target device info and its platform matches the build platform, compare the device OS version + let targetDeviceVersion: Version? + if considerTargetDeviceOSVersion && scope.evaluate(BuiltinMacros.TARGET_DEVICE_PLATFORM_NAME) == self.name { + targetDeviceVersion = try? Version(scope.evaluate(BuiltinMacros.TARGET_DEVICE_OS_VERSION)) + } else { + targetDeviceVersion = nil + } + + // Otherwise fall back to comparing the minimum deployment target + let deploymentTargetVersion = try? Version(scope.evaluate(deploymentTarget)) + + guard let version = targetDeviceVersion ?? deploymentTargetVersion else { return false } + + // Return `nil` here as there is no metadata for the platform to allow downstream clients to be aware of this. + guard let minimumSwiftSpanVersion = swiftOSSpanVersion(deploymentTarget) else { return nil } + + return version >= minimumSwiftSpanVersion + } } extension Platform: CustomStringConvertible { @@ -676,6 +707,7 @@ public final class PlatformRegistry { if let variant = platform.defaultSDKVariant { platform.minimumOSForSwiftInTheOS = variant.minimumOSForSwiftInTheOS platform.minimumOSForSwiftConcurrency = variant.minimumOSForSwiftConcurrency + platform.minimumOSForSwiftSpan = variant.minimumOSForSwiftSpan } } diff --git a/Sources/SWBCore/SDKRegistry.swift b/Sources/SWBCore/SDKRegistry.swift index 6764af75..df0b7058 100644 --- a/Sources/SWBCore/SDKRegistry.swift +++ b/Sources/SWBCore/SDKRegistry.swift @@ -313,9 +313,12 @@ public final class SDKVariant: PlatformInfoProvider, Sendable { /// Minimum OS version for Swift-in-the-OS support. If this is `nil`, the platform does not support Swift-in-the-OS at all. public let minimumOSForSwiftInTheOS: Version? - /// Minimum OS version for built-in Swift concurrency support. If this is `nil`, the platform does not support Swift concurrency at all. + /// Minimum OS version for built-in Swift concurrency support (Swift 5.5). If this is `nil`, the platform does not support Swift concurrency at all. public let minimumOSForSwiftConcurrency: Version? + /// Minimum OS version for built-in Swift Span support (Swift 6.2). If this is `nil`, the platform does not support Swift Span at all. + public let minimumOSForSwiftSpan: Version? + /// The path prefix under which all built content produced by this SDK variant should be installed, relative to the system root. /// /// Empty string if content should be installed directly into the system root (default). @@ -392,9 +395,10 @@ public final class SDKVariant: PlatformInfoProvider, Sendable { self.clangRuntimeLibraryPlatformName = supportedTargetDict["ClangRuntimeLibraryPlatformName"]?.stringValue ?? Self.fallbackClangRuntimeLibraryPlatformName(variantName: name) - let (os, concurrency) = Self.fallbackSwiftVersions(variantName: name) + let (os, concurrency, span) = Self.fallbackSwiftVersions(variantName: name) self.minimumOSForSwiftInTheOS = try (supportedTargetDict["SwiftOSRuntimeMinimumDeploymentTarget"]?.stringValue ?? os).map { try Version($0) } self.minimumOSForSwiftConcurrency = try (supportedTargetDict["SwiftConcurrencyMinimumDeploymentTarget"]?.stringValue ?? concurrency).map { try Version($0) } + self.minimumOSForSwiftSpan = try (supportedTargetDict["SwiftSpanMinimumDeploymentTarget"]?.stringValue ?? span).map { try Version($0) } self.systemPrefix = supportedTargetDict["SystemPrefix"]?.stringValue ?? { switch name { @@ -445,12 +449,12 @@ public final class SDKVariant: PlatformInfoProvider, Sendable { } } - private static func fallbackSwiftVersions(variantName name: String) -> (String?, String?) { + private static func fallbackSwiftVersions(variantName name: String) -> (os: String?, concurrency: String?, span: String?) { switch name { case "macos", "macosx": - return ("10.14.4", "12.0") + return ("10.14.4", "12.0", nil) default: - return (nil, nil) + return (nil, nil, nil) } } diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftStdLibTool.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftStdLibTool.swift index 67618493..6295fe29 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftStdLibTool.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftStdLibTool.swift @@ -22,7 +22,7 @@ public final class SwiftStdLibToolSpec : GenericCommandLineToolSpec, SpecIdentif } /// Construct a new task to run the Swift standard library tool. - public func constructSwiftStdLibraryToolTask(_ cbc:CommandBuildContext, _ delegate: any TaskGenerationDelegate, foldersToScan: MacroStringListExpression?, filterForSwiftOS: Bool, backDeploySwiftConcurrency: Bool) async { + public func constructSwiftStdLibraryToolTask(_ cbc:CommandBuildContext, _ delegate: any TaskGenerationDelegate, foldersToScan: MacroStringListExpression?, filterForSwiftOS: Bool, backDeploySwiftConcurrency: Bool, backDeploySwiftSpan: Bool) async { precondition(cbc.outputs.isEmpty, "Unexpected output paths \(cbc.outputs.map { "'\($0.str)'" }) passed to \(type(of: self)).") let input = cbc.input @@ -85,6 +85,10 @@ public final class SwiftStdLibToolSpec : GenericCommandLineToolSpec, SpecIdentif commandLine.append("--back-deploy-swift-concurrency") } + if backDeploySwiftSpan { + commandLine.append("--back-deploy-swift-span") + } + let outputs = [delegate.createVirtualNode("CopySwiftStdlib \(wrapperPathString.str)")] delegate.createTask(type: self, dependencyData: .dependencyInfo(dependencyInfoFilePath), ruleInfo: ruleInfo, commandLine: commandLine, environment: EnvironmentBindings(environment.map { ($0, $1) }), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: [ delegate.createNode(input.absolutePath) ], outputs: outputs, mustPrecede: [], action: action, execDescription: resolveExecutionDescription(cbc, delegate, lookup: lookup), enableSandboxing: enableSandboxing) diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift index bba8b051..9a9e5512 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift @@ -104,10 +104,13 @@ final class SwiftStandardLibrariesTaskProducer: PhasedTaskProducer, TaskProducer let supportsConcurrencyNatively = context.platform?.supportsSwiftConcurrencyNatively(scope) let backDeploySwiftConcurrency = supportsConcurrencyNatively != nil && supportsConcurrencyNatively != true + let supportsSpanNatively = context.platform?.supportsSwiftSpanNatively(scope) + let backDeploySwiftSpan = supportsSpanNatively != nil && supportsSpanNatively != true + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: [ input ]) let foldersToScanExpr: MacroStringListExpression? = foldersToScan.count > 0 ? scope.namespace.parseLiteralStringList(foldersToScan): nil await appendGeneratedTasks(&tasks) { delegate in - await context.swiftStdlibToolSpec.constructSwiftStdLibraryToolTask(cbc, delegate, foldersToScan: foldersToScanExpr, filterForSwiftOS: filterForSwiftOS, backDeploySwiftConcurrency: backDeploySwiftConcurrency) + await context.swiftStdlibToolSpec.constructSwiftStdLibraryToolTask(cbc, delegate, foldersToScan: foldersToScanExpr, filterForSwiftOS: filterForSwiftOS, backDeploySwiftConcurrency: backDeploySwiftConcurrency, backDeploySwiftSpan: backDeploySwiftSpan) } } diff --git a/Sources/SWBTaskExecution/TaskActions/EmbedSwiftStdLibTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/EmbedSwiftStdLibTaskAction.swift index 1552d691..6b1c261d 100644 --- a/Sources/SWBTaskExecution/TaskActions/EmbedSwiftStdLibTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/EmbedSwiftStdLibTaskAction.swift @@ -160,12 +160,18 @@ public final class EmbedSwiftStdLibTaskAction: TaskAction { // If true, then the Swift concurrency dylibs should be copied into the app/framework's bundles. var backDeploySwiftConcurrency = false + // If true, then the Swift Span dylibs should be copied into the app/framework's bundles. + var backDeploySwiftSpan = false + // The allowed list of libraries that should *not* be filtered when `filterForSwiftOS=true`. let allowedLibsForSwiftOS = ["libswiftXCTest" ] // The allowed list of libraries that should *not* be filtered when `backDeploySwiftConcurrency=true`. let allowedLibsForSwiftConcurrency = ["libswift_Concurrency"] + // The allowed list of libraries that should *not* be filtered when `backDeploySwiftSpan=true`. + let allowedLibsForSwiftSpan = ["libswiftCompatibilitySpan"] + func absolutePath(_ path: Path) -> Path { return path.isAbsolute ? path : task.workingDirectory.join(path) } @@ -368,6 +374,9 @@ public final class EmbedSwiftStdLibTaskAction: TaskAction { case "--back-deploy-swift-concurrency": self.backDeploySwiftConcurrency = true + case "--back-deploy-swift-span": + self.backDeploySwiftSpan = true + default: throw StubError.error("unrecognized argument: \(arg)") } @@ -787,6 +796,9 @@ public final class EmbedSwiftStdLibTaskAction: TaskAction { if backDeploySwiftConcurrency && allowedLibsForSwiftConcurrency.contains(item) { shouldInclude = true } + if backDeploySwiftSpan && allowedLibsForSwiftSpan.contains(item) { + shouldInclude = true + } return shouldInclude } diff --git a/Tests/SWBCoreTests/CommandLineSpecTests.swift b/Tests/SWBCoreTests/CommandLineSpecTests.swift index 61cabff6..3529be14 100644 --- a/Tests/SWBCoreTests/CommandLineSpecTests.swift +++ b/Tests/SWBCoreTests/CommandLineSpecTests.swift @@ -681,7 +681,7 @@ import SWBMacro let cbc = CommandBuildContext(producer: producer, scope: mockScope, inputs: [FileToBuild(absolutePath: Path.root.join("tmp/input"), fileType: mockFileType)], output: nil) // Check that task construction sets the correct env bindings. - await stdlibTool.constructSwiftStdLibraryToolTask(cbc, delegate, foldersToScan: nil, filterForSwiftOS: false, backDeploySwiftConcurrency: false) + await stdlibTool.constructSwiftStdLibraryToolTask(cbc, delegate, foldersToScan: nil, filterForSwiftOS: false, backDeploySwiftConcurrency: false, backDeploySwiftSpan: false) #expect(delegate.shellTasks.count == 1) let task = try #require(delegate.shellTasks[safe: 0]) #expect(task.environment.bindingsDictionary == ["CODESIGN_ALLOCATE": "/path/to/codesign_allocate"])