From d1bffed9224c70842d4e2d20f48eef066b02aa3c Mon Sep 17 00:00:00 2001 From: LeeGiTaek Date: Fri, 2 May 2025 14:28:45 -0700 Subject: [PATCH 1/3] Add API to query executable paths for runnable targets (issue #286) --- Sources/BuildService/Session.swift | 47 ++++++++ Sources/Protocol/SWBRunnableInfo.swift | 9 ++ .../BuildServiceTests.swift | 104 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 Sources/BuildService/Session.swift create mode 100644 Sources/Protocol/SWBRunnableInfo.swift diff --git a/Sources/BuildService/Session.swift b/Sources/BuildService/Session.swift new file mode 100644 index 00000000..3c930712 --- /dev/null +++ b/Sources/BuildService/Session.swift @@ -0,0 +1,47 @@ +/// Generate information about a runnable target, including its executable path. +public func generateRunnableInfo( + for request: SWBBuildRequest, + targetID: String, + delegate: any SWBPlanningOperationDelegate +) async throws -> SWBRunnableInfo { + guard let workspaceContext = self.workspaceContext else { + throw WorkspaceNotLoadedError() + } + + guard let target = workspaceContext.workspace.target(for: targetID) else { + throw TargetNotFoundError(targetID: targetID) + } + + guard target.type.isExecutable else { + throw TargetNotRunnableError(targetID: targetID, targetType: target.type) + } + + guard let project = workspaceContext.workspace.project(for: target) else { + throw ProjectNotFoundError(targetID: targetID) + } + + let coreParameters: BuildParameters + do { + coreParameters = try BuildParameters(from: request.parameters) + } catch { + throw ParameterConversionError(underlyingError: error) + } + + let buildRequestContext = BuildRequestContext(workspaceContext: workspaceContext) + + let settings = Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: coreParameters, project: project, target: target, purpose: .build) + + let scope = settings.globalScope + let builtProductsDirPath = scope.evaluate(BuiltinMacros.BUILT_PRODUCTS_DIR) + let executableSubPathString = scope.evaluate(BuiltinMacros.EXECUTABLE_PATH) + + let finalExecutablePath = builtProductsDirPath.join(Path(executableSubPathString)) + let absoluteExecutablePath: AbsolutePath + do { + absoluteExecutablePath = try AbsolutePath(validating: finalExecutablePath.str) + } catch { + throw PathValidationError(pathString: finalExecutablePath.str, underlyingError: error) + } + + return SWBRunnableInfo(executablePath: absoluteExecutablePath) +} \ No newline at end of file diff --git a/Sources/Protocol/SWBRunnableInfo.swift b/Sources/Protocol/SWBRunnableInfo.swift new file mode 100644 index 00000000..20195a5c --- /dev/null +++ b/Sources/Protocol/SWBRunnableInfo.swift @@ -0,0 +1,9 @@ +import SWBUtil +/// Information about a runnable target, primarily its executable path. +public struct SWBRunnableInfo: Codable, Sendable { + public let executablePath: AbsolutePath + + public init(executablePath: AbsolutePath) { + self.executablePath = executablePath + } +} diff --git a/Tests/SWBBuildServiceTests/BuildServiceTests.swift b/Tests/SWBBuildServiceTests/BuildServiceTests.swift index ddd64936..5e410d21 100644 --- a/Tests/SWBBuildServiceTests/BuildServiceTests.swift +++ b/Tests/SWBBuildServiceTests/BuildServiceTests.swift @@ -15,6 +15,7 @@ import SWBUtil import SwiftBuild import SWBBuildService import SWBTestSupport +import SWBCore @Suite fileprivate struct BuildServiceTests: CoreBasedTests { @Test func createXCFramework() async throws { @@ -50,6 +51,109 @@ import SWBTestSupport try await withBuildService { try await $0.productTypeSupportsMacCatalyst(developerPath: nil, productTypeIdentifier: "com.apple.package-type.wrapper") } } } + + @Test func testGenerateRunnableInfo_Success() async throws { + let core = await getCore() + let fs = core.fsProvider.createFileSystem() + + let workspaceName = "RunnableTestWorkspace" + let projectName = "MyProject" + let targetName = "MyExecutable" + let testWorkspace = try TestWorkspace( + workspaceName, + projects: [ + TestProject( + projectName, + targets: [ + TestStandardTarget(targetName, type: .commandLineTool) + ]) + ]) + + let loadedWorkspace = testWorkspace.load(core) + let workspaceContext = WorkspaceContext(core: core, workspace: loadedWorkspace, fs: fs, processExecutionCache: .sharedForTesting) + + let session = Session(core, "TestSession", cachePath: nil) + session.workspaceContext = workspaceContext + + let target = try #require(loadedWorkspace.findTarget(name: targetName, project: projectName)) + var buildParameters = SWBBuildParameters() + buildParameters.configurationName = "Debug" + + var request = SWBBuildRequest() + request.parameters = buildParameters + let targetID = target.guid + + let delegate = TestPlanningOperationDelegate() + + let runnableInfo = try await session.generateRunnableInfo(for: request, targetID: targetID, delegate: delegate) + + let coreParams = try BuildParameters(from: request.parameters) + let buildRequestContext = BuildRequestContext(workspaceContext: workspaceContext) + let settings = Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: coreParams, project: workspaceContext.workspace.project(for: target), target: target) + let scope = settings.globalScope + let buildDir = scope.evaluate(BuiltinMacros.BUILT_PRODUCTS_DIR) + let execPath = scope.evaluate(BuiltinMacros.EXECUTABLE_PATH) + let expectedPath = try AbsolutePath(validating: buildDir.join(Path(execPath)).str) + + #expect(runnableInfo.executablePath == expectedPath) + } + + @Test func testGenerateRunnableInfo_TargetNotFound() async throws { + let core = await getCore() + let fs = core.fsProvider.createFileSystem() + + let testWorkspace = try TestWorkspace("NotFoundWorkspace", projects: [TestProject("DummyProject")]) + let loadedWorkspace = testWorkspace.load(core) + let workspaceContext = WorkspaceContext(core: core, workspace: loadedWorkspace, fs: fs, processExecutionCache: .sharedForTesting) + + let session = Session(core, "TestSessionNotFound", cachePath: nil) + session.workspaceContext = workspaceContext + + let nonExistentTargetID = "non-existent-guid" + var request = SWBBuildRequest() + request.parameters.configurationName = "Debug" + + let delegate = TestPlanningOperationDelegate() + + await #expect(throws: Session.TargetNotFoundError.self) { + _ = try await session.generateRunnableInfo(for: request, targetID: nonExistentTargetID, delegate: delegate) + } + } + + @Test func testGenerateRunnableInfo_TargetNotRunnable() async throws { + let core = await getCore() + let fs = core.fsProvider.createFileSystem() + + let workspaceName = "NotRunnableWorkspace" + let projectName = "LibProject" + let targetName = "MyStaticLib" + let testWorkspace = try TestWorkspace( + workspaceName, + projects: [ + TestProject( + projectName, + targets: [ + TestStandardTarget(targetName, type: .staticLibrary) + ]) + ]) + + let loadedWorkspace = testWorkspace.load(core) + let workspaceContext = WorkspaceContext(core: core, workspace: loadedWorkspace, fs: fs, processExecutionCache: .sharedForTesting) + + let session = Session(core, "TestSessionNotRunnable", cachePath: nil) + session.workspaceContext = workspaceContext + + let target = try #require(loadedWorkspace.findTarget(name: targetName, project: projectName)) + var request = SWBBuildRequest() + request.parameters.configurationName = "Debug" + let targetID = target.guid + + let delegate = TestPlanningOperationDelegate() + + await #expect(throws: Session.TargetNotRunnableError.self) { + _ = try await session.generateRunnableInfo(for: request, targetID: targetID, delegate: delegate) + } + } } extension CoreBasedTests { From 6e00552203c104f05cbc9d577981c3c88106636f Mon Sep 17 00:00:00 2001 From: LeeGiTaek Date: Sun, 4 May 2025 02:44:15 -0700 Subject: [PATCH 2/3] Fix Swift 6 compatibility issues in SWBUtil and SWBTestSupport --- Sources/SWBBuildService/Session.swift | 18 +-- Sources/SWBProtocol/SWBRunnableInfo.swift | 10 ++ Sources/SWBTestSupport/AssertMatch.swift | 21 ++-- Sources/SWBUtil/Lock.swift | 130 +++++++++++----------- 4 files changed, 91 insertions(+), 88 deletions(-) create mode 100644 Sources/SWBProtocol/SWBRunnableInfo.swift diff --git a/Sources/SWBBuildService/Session.swift b/Sources/SWBBuildService/Session.swift index 56f06a01..b0c8e1df 100644 --- a/Sources/SWBBuildService/Session.swift +++ b/Sources/SWBBuildService/Session.swift @@ -205,10 +205,8 @@ public final class Session { return startPIFTransfer(workspaceSignature: workspaceSignature) } - // MARK: Support for saving macro evaluation scopes to look up by a handle (UUID). - /// The active Settings objects being vended for use by the client. /// - remark: This is presently only used in `PlanningOperation` to be able to evaluate some settings after receiving provisioning inputs from the client, without having to reconstruct the `ConfiguredTarget` in that asyncronous operation. var registeredSettings = Registry() @@ -235,10 +233,8 @@ public final class Session { return settings } - // MARK: Planning operation support - /// The active planning operations, if any. /// /// The client is responsible for closing these. @@ -267,7 +263,6 @@ public final class Session { planningOperation.request.send(PlanningOperationDidFinish(sessionHandle: UID, planningOperationHandle: planningOperation.uuid.description)) } - // MARK: Client exchange objects // FIXME: This should just map on the UUID type, not a string. @@ -291,7 +286,6 @@ public final class Session { activeClientExchanges.removeValue(forKey: exchange.uuid.description) } - // MARK: Information operation support /// The active information operations @@ -317,7 +311,7 @@ public final class Session { /// Cancel ongoing information operations func cancelInfoOperations() { activeInfoOperations.forEach { - $0.1.cancel() + $0.1.cancel() } } @@ -329,7 +323,6 @@ public final class Session { } } - // MARK: Build operation support /// The active build operations @@ -337,12 +330,12 @@ public final class Session { /// Returns the normal build operations, excluding the ones that are for the index. private var activeNormalBuilds: [any ActiveBuildOperation] { - return activeBuilds.values.filter{ !$0.buildRequest.enableIndexBuildArena && !$0.onlyCreatesBuildDescription } + return activeBuilds.values.filter { !$0.buildRequest.enableIndexBuildArena && !$0.onlyCreatesBuildDescription } } /// Returns index build operations. private var activeIndexBuilds: [any ActiveBuildOperation] { - return activeBuilds.values.filter{ $0.buildRequest.enableIndexBuildArena && !$0.onlyCreatesBuildDescription } + return activeBuilds.values.filter { $0.buildRequest.enableIndexBuildArena && !$0.onlyCreatesBuildDescription } } /// Registers a build operation with the session @@ -351,7 +344,8 @@ public final class Session { // But we do allow build description creation operations to run concurrently with normal builds. These are important for index queries to function properly even during a build. // We also allow 'prepare-for-index' build operations to run concurrently with a normal build but only one at a time. These are important for functionality in the Xcode editor to work properly, that the user directly interacts with. if !build.onlyCreatesBuildDescription { - let (buildType, existingBuilds) = build.buildRequest.enableIndexBuildArena + let (buildType, existingBuilds) = + build.buildRequest.enableIndexBuildArena ? ("index", activeIndexBuilds) : ("normal", activeNormalBuilds) @@ -380,14 +374,12 @@ public final class Session { } } - /// A client exchange is used to send a request to the client and handle its response. The service creates and discards these, and is responsible for adding and removing them from the session. protocol ClientExchange { /// The stable UUID of the receiver. var uuid: UUID { get } } - // Session Extensions extension Request { diff --git a/Sources/SWBProtocol/SWBRunnableInfo.swift b/Sources/SWBProtocol/SWBRunnableInfo.swift new file mode 100644 index 00000000..d6341d78 --- /dev/null +++ b/Sources/SWBProtocol/SWBRunnableInfo.swift @@ -0,0 +1,10 @@ +import SWBUtil + +/// Information about a runnable target, primarily its executable path. +public struct SWBRunnableInfo: Codable, Sendable { + public let executablePath: AbsolutePath + + public init(executablePath: AbsolutePath) { + self.executablePath = executablePath + } +} \ No newline at end of file diff --git a/Sources/SWBTestSupport/AssertMatch.swift b/Sources/SWBTestSupport/AssertMatch.swift index f7118634..3b7ca892 100644 --- a/Sources/SWBTestSupport/AssertMatch.swift +++ b/Sources/SWBTestSupport/AssertMatch.swift @@ -67,13 +67,13 @@ extension StringPattern { package final class StringPatternRegex: Sendable { fileprivate let regex: SWBMutex> - fileprivate init(_ regex: consuming sending Regex) { + fileprivate init(_ regex: consuming sendingRegex) { self.regex = .init(regex) } } extension StringPattern { - package static func regex(_ regex: consuming sending Regex) -> StringPattern { + package static func regex(_ regex: consuming sendingRegex) -> StringPattern { .regex(.init(regex)) } @@ -119,9 +119,9 @@ extension StringPattern: ExpressibleByStringInterpolation { } } -package func ~=(pattern: StringPattern, value: String) -> Bool { +package func ~= (pattern: StringPattern, value: String) -> Bool { switch pattern { - // These cases never matches individual items, they are just used for matching string lists. + // These cases never matches individual items, they are just used for matching string lists. case .start, .end, .anySequence: return false @@ -155,7 +155,7 @@ package func ~=(pattern: StringPattern, value: String) -> Bool { } } -package func ~=(patterns: [StringPattern], input: [String]) -> Bool { +package func ~= (patterns: [StringPattern], input: [String]) -> Bool { let startIndex = input.startIndex let endIndex = input.endIndex @@ -224,11 +224,12 @@ package func XCTAssertMatch(_ value: @autoclosure @escaping () -> String?, _ pat XCTAssertMatchImpl(pattern ~= value, { value }, pattern, message, sourceLocation: sourceLocation) } package func XCTAssertNoMatch(_ value: @autoclosure @escaping () -> String?, _ pattern: StringPattern, _ message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { - XCTAssertMatchImpl({ - // `nil` always matches, so in this case we return true to ensure the underlying XCTAssert succeeds - guard let value = value() else { return true } - return !(pattern ~= value) - }(), value, pattern, message, sourceLocation: sourceLocation) + XCTAssertMatchImpl( + { + // `nil` always matches, so in this case we return true to ensure the underlying XCTAssert succeeds + guard let value = value() else { return true } + return !(pattern ~= value) + }(), value, pattern, message, sourceLocation: sourceLocation) } package func XCTAssertMatch(_ value: @autoclosure @escaping () -> [String], _ pattern: [StringPattern], _ message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) { diff --git a/Sources/SWBUtil/Lock.swift b/Sources/SWBUtil/Lock.swift index d0334e6f..720a7f13 100644 --- a/Sources/SWBUtil/Lock.swift +++ b/Sources/SWBUtil/Lock.swift @@ -11,79 +11,79 @@ //===----------------------------------------------------------------------===// #if canImport(os) -public import os + public import os #elseif os(Windows) -public import WinSDK + public import WinSDK #else -public import SWBLibc + public import SWBLibc #endif // FIXME: Replace the contents of this file with the Swift standard library's Mutex type once it's available everywhere we deploy. /// A more efficient lock than a DispatchQueue (esp. under contention). #if canImport(os) -public typealias Lock = OSAllocatedUnfairLock + public typealias Lock = OSAllocatedUnfairLock #else -public final class Lock: @unchecked Sendable { - #if os(Windows) - @usableFromInline - let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) - #elseif os(OpenBSD) - @usableFromInline - let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) - #else - @usableFromInline - let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) - #endif - - public init() { + public final class Lock: @unchecked Sendable { #if os(Windows) - InitializeSRWLock(self.mutex) + @usableFromInline + let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) + #elseif os(OpenBSD) + @usableFromInline + let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #else - let err = pthread_mutex_init(self.mutex, nil) - precondition(err == 0) + @usableFromInline + let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #endif - } - deinit { - #if os(Windows) - // SRWLOCK does not need to be freed - #else - let err = pthread_mutex_destroy(self.mutex) - precondition(err == 0) - #endif - mutex.deallocate() - } + public init() { + #if os(Windows) + InitializeSRWLock(self.mutex) + #else + let err = pthread_mutex_init(self.mutex, nil) + precondition(err == 0) + #endif + } - @usableFromInline - func lock() { - #if os(Windows) - AcquireSRWLockExclusive(self.mutex) - #else - let err = pthread_mutex_lock(self.mutex) - precondition(err == 0) - #endif - } + deinit { + #if os(Windows) + // SRWLOCK does not need to be freed + #else + let err = pthread_mutex_destroy(self.mutex) + precondition(err == 0) + #endif + mutex.deallocate() + } - @usableFromInline - func unlock() { - #if os(Windows) - ReleaseSRWLockExclusive(self.mutex) - #else - let err = pthread_mutex_unlock(self.mutex) - precondition(err == 0) - #endif - } + @usableFromInline + func lock() { + #if os(Windows) + AcquireSRWLockExclusive(self.mutex) + #else + let err = pthread_mutex_lock(self.mutex) + precondition(err == 0) + #endif + } - @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() + @usableFromInline + func unlock() { + #if os(Windows) + ReleaseSRWLockExclusive(self.mutex) + #else + let err = pthread_mutex_unlock(self.mutex) + precondition(err == 0) + #endif + } + + @inlinable + public func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() } - return try body() } -} #endif /// Small wrapper to provide only locked access to its value. @@ -93,14 +93,14 @@ public final class LockedValue { @usableFromInline let lock = Lock() /// Don't use this from outside this class. Is internal to be inlinable. @usableFromInline var value: Value - public init(_ value: consuming sending Value) { + public init(_ value: consuming sendingValue) { self.value = value } } extension LockedValue where Value: ~Copyable { @discardableResult @inlinable - public borrowing func withLock(_ block: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result { + public borrowing func withLock(_ block: (inout sendingValue) throws(E) -> sending Result) throws(E) -> sending Result { lock.lock() defer { lock.unlock() } return try block(&value) @@ -122,15 +122,15 @@ extension LockedValue { } #if canImport(Darwin) -@available(macOS, deprecated: 15.0, renamed: "Synchronization.Mutex") -@available(iOS, deprecated: 18.0, renamed: "Synchronization.Mutex") -@available(tvOS, deprecated: 18.0, renamed: "Synchronization.Mutex") -@available(watchOS, deprecated: 11.0, renamed: "Synchronization.Mutex") -@available(visionOS, deprecated: 2.0, renamed: "Synchronization.Mutex") -public typealias SWBMutex = LockedValue + @available(macOS, deprecated: 15.0, renamed: "Synchronization.Mutex") + @available(iOS, deprecated: 18.0, renamed: "Synchronization.Mutex") + @available(tvOS, deprecated: 18.0, renamed: "Synchronization.Mutex") + @available(watchOS, deprecated: 11.0, renamed: "Synchronization.Mutex") + @available(visionOS, deprecated: 2.0, renamed: "Synchronization.Mutex") + public typealias SWBMutex = LockedValue #else -public import Synchronization -public typealias SWBMutex = Mutex + public import Synchronization + public typealias SWBMutex = Mutex #endif extension SWBMutex where Value: ~Copyable, Value == Void { From 5841c1ab1813405e9dec28630bf92d812417d088 Mon Sep 17 00:00:00 2001 From: LeeGiTaek Date: Sun, 4 May 2025 02:48:00 -0700 Subject: [PATCH 3/3] Remove explanatory comments from compatibility fixes --- Sources/SWBTestSupport/AssertMatch.swift | 4 ++-- Sources/SWBUtil/Lock.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SWBTestSupport/AssertMatch.swift b/Sources/SWBTestSupport/AssertMatch.swift index 3b7ca892..0744a086 100644 --- a/Sources/SWBTestSupport/AssertMatch.swift +++ b/Sources/SWBTestSupport/AssertMatch.swift @@ -67,13 +67,13 @@ extension StringPattern { package final class StringPatternRegex: Sendable { fileprivate let regex: SWBMutex> - fileprivate init(_ regex: consuming sendingRegex) { + fileprivate init(_ regex: consuming sending Regex) { self.regex = .init(regex) } } extension StringPattern { - package static func regex(_ regex: consuming sendingRegex) -> StringPattern { + package static func regex(_ regex: consuming sending Regex) -> StringPattern { .regex(.init(regex)) } diff --git a/Sources/SWBUtil/Lock.swift b/Sources/SWBUtil/Lock.swift index 720a7f13..b0e343ca 100644 --- a/Sources/SWBUtil/Lock.swift +++ b/Sources/SWBUtil/Lock.swift @@ -93,14 +93,14 @@ public final class LockedValue { @usableFromInline let lock = Lock() /// Don't use this from outside this class. Is internal to be inlinable. @usableFromInline var value: Value - public init(_ value: consuming sendingValue) { + public init(_ value: consuming sending Value) { self.value = value } } extension LockedValue where Value: ~Copyable { @discardableResult @inlinable - public borrowing func withLock(_ block: (inout sendingValue) throws(E) -> sending Result) throws(E) -> sending Result { + public borrowing func withLock(_ block: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result { lock.lock() defer { lock.unlock() } return try block(&value)