diff --git a/Sources/SWBBuildService/Messages.swift b/Sources/SWBBuildService/Messages.swift index ded6b880..5e0100bb 100644 --- a/Sources/SWBBuildService/Messages.swift +++ b/Sources/SWBBuildService/Messages.swift @@ -1528,6 +1528,31 @@ private struct ClearAllCaches: MessageHandler { } } +private struct GetPlatformPreferredRunDestinationMsg: MessageHandler { + struct PlatformNotFoundError: Error, CustomStringConvertible { + let platform: String + var description: String { "Unknown platform '\(platform)'" } + } + struct UnknownPreferredArchError: Error, CustomStringConvertible { + let platform: String + var description: String { "Platform '\(platform)' does not specify a preferred architecture" } + } + + func handle(request: Request, message: GetPlatformPreferredRunDestinationRequest) async throws -> GetPlatformPreferredRunDestinationResponse { + let session = try request.session(for: message) + guard let platform = session.core.platformRegistry.lookup(name: message.platform) else { + throw PlatformNotFoundError(platform: message.platform) + } + // All relevant platforms define a preferredArch, so the undefined_arch fallback case should never happen + // in practice, and indicates a serious issue occurred during plugin loading. + guard let targetArchitecture = platform.preferredArch else { + throw UnknownPreferredArchError(platform: message.platform) + } + let runDestination = RunDestinationInfo(platform: platform.name, sdk: platform.name, sdkVariant: nil, targetArchitecture: targetArchitecture, supportedArchitectures: [targetArchitecture], disableOnlyActiveArch: false, hostTargetedPlatform: nil) + return GetPlatformPreferredRunDestinationResponse(info: runDestination) + } +} + // MARK: ServiceExtension Support public struct ServiceSessionMessageHandlers: ServiceExtension { @@ -1593,6 +1618,7 @@ package struct ServiceMessageHandlers: ServiceExtension { service.registerMessageHandler(GetStatisticsDumpMsg.self) service.registerMessageHandler(GetBuildSettingsDescriptionDumpMsg.self) service.registerMessageHandler(ExecuteCommandLineToolMsg.self) + service.registerMessageHandler(GetPlatformPreferredRunDestinationMsg.self) service.registerMessageHandler(CreateXCFrameworkHandler.self) @@ -1613,7 +1639,7 @@ package struct ServiceMessageHandlers: ServiceExtension { service.registerMessageHandler(ComputeDependencyClosureMsg.self) service.registerMessageHandler(ComputeDependencyGraphMsg.self) service.registerMessageHandler(DumpBuildDependencyInfoMsg.self) - + service.registerMessageHandler(MacroEvaluationMsg.self) service.registerMessageHandler(AllExportedMacrosAndValuesMsg.self) service.registerMessageHandler(BuildSettingsEditorInfoMsg.self) diff --git a/Sources/SWBProtocol/Message.swift b/Sources/SWBProtocol/Message.swift index c32a01ad..4417fffd 100644 --- a/Sources/SWBProtocol/Message.swift +++ b/Sources/SWBProtocol/Message.swift @@ -65,7 +65,7 @@ public struct PingRequest: RequestMessage, Equatable { public struct SetConfigItemRequest: RequestMessage, Equatable { public typealias ResponseMessage = VoidResponse - + public static let name = "SET_CONFIG_ITEM" public let key: String @@ -286,6 +286,33 @@ public struct GetBuildSettingsDescriptionRequest: SessionMessage, RequestMessage } } +/// Get the preferred run destination for a given platform +public struct GetPlatformPreferredRunDestinationRequest: SessionMessage, RequestMessage, Equatable, SerializableCodable { + public typealias ResponseMessage = GetPlatformPreferredRunDestinationResponse + + public static let name = "GET_PLATFORM_PREFERRED_RUN_DESTINATION_REQUEST" + + public let sessionHandle: String + + /// The name of the platform to get the run destination for, eg. `macosx`, `iphoneos`, `iphonesimulator` + public let platform: String + + public init(sessionHandle: String, platform: String) { + self.sessionHandle = sessionHandle + self.platform = platform + } +} + +public struct GetPlatformPreferredRunDestinationResponse: Message, Equatable, SerializableCodable { + public static let name = "GET_PLATFORM_PREFERRED_RUN_DESTINATION_RESPONSE" + + public let info: RunDestinationInfo + + public init(info: RunDestinationInfo) { + self.info = info + } +} + public struct CreateXCFrameworkRequest: RequestMessage, Equatable, SerializableCodable { public typealias ResponseMessage = StringResponse @@ -1171,6 +1198,9 @@ public struct IPCMessage: Serializable, Sendable { GetBuildSettingsDescriptionRequest.self, ExecuteCommandLineToolRequest.self, + GetPlatformPreferredRunDestinationRequest.self, + GetPlatformPreferredRunDestinationResponse.self, + CreateSessionRequest.self, CreateSessionResponse.self, SetSessionSystemInfoRequest.self, diff --git a/Sources/SwiftBuild/SWBBuildServiceSession.swift b/Sources/SwiftBuild/SWBBuildServiceSession.swift index 07b29c65..22a79445 100644 --- a/Sources/SwiftBuild/SWBBuildServiceSession.swift +++ b/Sources/SwiftBuild/SWBBuildServiceSession.swift @@ -12,7 +12,7 @@ import Foundation -import SWBProtocol +public import SWBProtocol import SWBUtil public enum SwiftBuildServicePIFObjectType: Sendable { @@ -582,6 +582,19 @@ public final class SWBBuildServiceSession: Sendable { try await service.send(request: DeveloperPathRequest(sessionHandle: uid)).value } + public func preferredRunDestination(forPlatform platformName: String) async throws -> SWBRunDestinationInfo { + let runDestination = try await service.send(request: GetPlatformPreferredRunDestinationRequest(sessionHandle: uid, platform: platformName)).info + return SWBRunDestinationInfo( + platform: runDestination.platform, + sdk: runDestination.sdk, + sdkVariant: runDestination.sdkVariant, + targetArchitecture: runDestination.targetArchitecture, + supportedArchitectures: runDestination.supportedArchitectures.elements, + disableOnlyActiveArch: runDestination.disableOnlyActiveArch, + hostTargetedPlatform: runDestination.hostTargetedPlatform + ) + } + /// Set the session system information. public func setSystemInfo(_ systemInfo: SWBSystemInfo) async throws { _ = try await service.send(request: SetSessionSystemInfoRequest(sessionHandle: uid, operatingSystemVersion: Version(systemInfo.operatingSystemVersion), productBuildVersion: systemInfo.productBuildVersion, nativeArchitecture: systemInfo.nativeArchitecture)) diff --git a/Tests/SwiftBuildTests/PreferredRunDestinationTests.swift b/Tests/SwiftBuildTests/PreferredRunDestinationTests.swift new file mode 100644 index 00000000..0780f1ee --- /dev/null +++ b/Tests/SwiftBuildTests/PreferredRunDestinationTests.swift @@ -0,0 +1,135 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +import SwiftBuild +import SwiftBuildTestSupport +import SWBTestSupport +@_spi(Testing) import SWBUtil + +@Suite +fileprivate struct PreferredRunDestinationTests { + @Test(.requireSDKs(.macOS), .temporaryDirectory, .asyncDeferrable) + func platformExists() async throws { + let testSession = try await TestSWBSession(temporaryDirectory: TemporaryDirectoryTrait.temporaryDirectory) + try await AsyncDeferrableTrait.defer { + try await testSession.close() + } + let runDestination = try await testSession.session.preferredRunDestination(forPlatform: "macosx") + #expect(runDestination.sdk == "macosx") + } + + @Test(.temporaryDirectory, .asyncDeferrable) + func unknownPlatform() async throws { + let testSession = try await TestSWBSession(temporaryDirectory: TemporaryDirectoryTrait.temporaryDirectory) + try await AsyncDeferrableTrait.defer { + try await testSession.close() + } + await #expect(throws: (any Error).self) { + try await testSession.session.preferredRunDestination(forPlatform: "unknown_platform") + } + } +} + +struct TemporaryDirectoryTrait: TestTrait { + @TaskLocal private static var _temporaryDirectory: NamedTemporaryDirectory? + + static var temporaryDirectory: NamedTemporaryDirectory { + get throws { + guard let _temporaryDirectory else { + throw StubError.error("Accessing temporary directory in test that doesn't specify .temporaryDirectory trait") + } + return _temporaryDirectory + } + } + + package init() {} + + struct TestScopeProvider: TestScoping { + func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: @Sendable () async throws -> Void + ) async throws { + try await withTemporaryDirectory { temporaryDirectory in + try await $_temporaryDirectory.withValue(temporaryDirectory) { + try await function() + } + } + } + } + + func scopeProvider(for test: Test, testCase: Test.Case?) -> TestScopeProvider? { + guard testCase != nil else { + // We only need to set up a new temporary directory for the execution of a parameterized version of the test. + return nil + } + return TestScopeProvider() + } +} + +extension Trait where Self == TemporaryDirectoryTrait { + static var temporaryDirectory: TemporaryDirectoryTrait { + return TemporaryDirectoryTrait() + } +} + +struct AsyncDeferrableTrait: TestTrait { + @TaskLocal private static var _deferrable: Deferrable? + + static var deferrable: Deferrable { + get throws { + guard let _deferrable else { + throw StubError.error("Accessing temporary directory in test that doesn't specify .asyncDeferrable trait") + } + return _deferrable + } + } + + static func `defer`(sourceLocation: SourceLocation = #_sourceLocation, _ body: @escaping @Sendable () async throws -> Void) async throws { + try await deferrable.addBlock { + await #expect(throws: Never.self, sourceLocation: sourceLocation) { + try await body() + } + } + } + + package init() {} + + struct TestScopeProvider: TestScoping { + func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: @Sendable () async throws -> Void + ) async throws { + try await withAsyncDeferrable { deferrable in + try await $_deferrable.withValue(deferrable) { + try await function() + } + } + } + } + + func scopeProvider(for test: Test, testCase: Test.Case?) -> TestScopeProvider? { + guard testCase != nil else { + // We only need to set up a deferrable for the execution of a parameterized version of the test. + return nil + } + return TestScopeProvider() + } +} + +extension Trait where Self == AsyncDeferrableTrait { + static var asyncDeferrable: AsyncDeferrableTrait { + return AsyncDeferrableTrait() + } +}