diff --git a/Documentation/Background Indexing.md b/Documentation/Enable Experimental Background Indexing.md similarity index 87% rename from Documentation/Background Indexing.md rename to Documentation/Enable Experimental Background Indexing.md index c5bc53ff5..209cd006d 100644 --- a/Documentation/Background Indexing.md +++ b/Documentation/Enable Experimental Background Indexing.md @@ -22,9 +22,10 @@ Next, point your editor to use the just-built copy of SourceKit-LSP and enable b "swift.sourcekit-lsp.serverArguments": [ "--experimental-feature", "background-indexing" ], ``` +Background indexing requires a Swift 6 toolchain. You can download Swift 6 nightly toolchains from https://www.swift.org/download/#swift-60-development. + ## Known issues -- The only supported toolchain for background indexing are currently [Swift 6.0 nightly toolchain snapshots](https://www.swift.org/download/#swift-60-development). Older toolchains are not supported and the nightly toolchains from `main` are having issues because building a target non-deterministically builds for tools or the destination [#1288](https://github.com/apple/sourcekit-lsp/pull/1288#issuecomment-2111400459) [rdar://128100158](rdar://128100158) - Not really a background indexing related issue but Swift nightly toolchain snapshots are crashing on macOS 14.4 and 14.5 (swift#73327)[https://github.com/apple/swift/issues/73327] - Workaround: Run the toolchains on an older version of macOS, if possible - Background Indexing is only supported for SwiftPM projects [#1269](https://github.com/apple/sourcekit-lsp/issues/1269), [#1271](https://github.com/apple/sourcekit-lsp/issues/1271) diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift index 74a30f1d1..8eaacd929 100644 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ b/Sources/Diagnose/DiagnoseCommand.swift @@ -407,8 +407,7 @@ public struct DiagnoseCommand: AsyncParsableCommand { // is responsible for showing the diagnose bundle location to the user if self.bundleOutputPath == nil { do { - let process = try Process.launch(arguments: ["open", "-R", bundlePath.path], workingDirectory: nil) - try await process.waitUntilExitSendingSigIntOnTaskCancellation() + _ = try await Process.run(arguments: ["open", "-R", bundlePath.path], workingDirectory: nil) } catch { // If revealing the bundle in Finder should fail, we don't care. We still printed the bundle path to stdout. } diff --git a/Sources/Diagnose/IndexCommand.swift b/Sources/Diagnose/IndexCommand.swift index cc0fc5cf7..bd3414372 100644 --- a/Sources/Diagnose/IndexCommand.swift +++ b/Sources/Diagnose/IndexCommand.swift @@ -68,6 +68,15 @@ public struct IndexCommand: AsyncParsableCommand { ) var toolchainOverride: String? + @Option( + name: .customLong("experimental-index-feature"), + help: """ + Enable an experimental sourcekit-lsp feature. + Available features are: \(ExperimentalFeature.allCases.map(\.rawValue).joined(separator: ", ")) + """ + ) + var experimentalFeatures: [ExperimentalFeature] = [] + @Option(help: "The path to the project that should be indexed") var project: String @@ -75,6 +84,7 @@ public struct IndexCommand: AsyncParsableCommand { public func run() async throws { var serverOptions = SourceKitLSPServer.Options() + serverOptions.experimentalFeatures = Set(experimentalFeatures) serverOptions.experimentalFeatures.insert(.backgroundIndexing) let installPath = @@ -109,3 +119,9 @@ fileprivate extension SourceKitLSPServer { } } } + +#if compiler(>=6) +extension ExperimentalFeature: @retroactive ExpressibleByArgument {} +#else +extension ExperimentalFeature: ExpressibleByArgument {} +#endif diff --git a/Sources/Diagnose/MergeSwiftFiles.swift b/Sources/Diagnose/MergeSwiftFiles.swift index ee0c30000..6eaad768d 100644 --- a/Sources/Diagnose/MergeSwiftFiles.swift +++ b/Sources/Diagnose/MergeSwiftFiles.swift @@ -27,10 +27,11 @@ extension RequestInfo { progressUpdate(0, "Merging all .swift files into a single file") + let compilerArgs = compilerArgs.filter { $0 != "-primary-file" && !$0.hasSuffix(".swift") } + ["$FILE"] let mergedRequestInfo = RequestInfo( requestTemplate: requestTemplate, offset: offset, - compilerArgs: compilerArgs.filter { !$0.hasSuffix(".swift") } + ["$FILE"], + compilerArgs: compilerArgs, fileContents: mergedFile ) diff --git a/Sources/Diagnose/ReduceFrontendCommand.swift b/Sources/Diagnose/ReduceFrontendCommand.swift index 9906e839c..cdd9903df 100644 --- a/Sources/Diagnose/ReduceFrontendCommand.swift +++ b/Sources/Diagnose/ReduceFrontendCommand.swift @@ -96,6 +96,9 @@ public struct ReduceFrontendCommand: AsyncParsableCommand { reproducerPredicate: nsPredicate ) + defer { + progressBar.complete(success: true) + } let reducedRequestInfo = try await reduceFrontendIssue( frontendArgs: frontendArgs, using: executor @@ -103,10 +106,8 @@ public struct ReduceFrontendCommand: AsyncParsableCommand { progressBar.update(step: Int(progress * 100), total: 100, text: message) } - progressBar.complete(success: true) - print("Reduced compiler arguments:") - print(reducedRequestInfo.compilerArgs) + print(reducedRequestInfo.compilerArgs.joined(separator: " ")) print("") print("Reduced file contents:") diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift index b10fc6562..4701d5e52 100644 --- a/Sources/Diagnose/RequestInfo.swift +++ b/Sources/Diagnose/RequestInfo.swift @@ -121,10 +121,12 @@ public struct RequestInfo: Sendable { // Inline the file list so we can reduce the compiler arguments by removing individual source files. // A couple `output-filelist`-related compiler arguments don't work with the file list inlined. Remove them as they - // are unlikely to be responsible for the swift-frontend cache + // are unlikely to be responsible for the swift-frontend cache. + // `-index-system-modules` is invalid when no output file lists are specified. while let frontendArg = iterator.next() { switch frontendArg { - case "-supplementary-output-file-map", "-output-filelist", "-index-unit-output-path-filelist": + case "-supplementary-output-file-map", "-output-filelist", "-index-unit-output-path-filelist", + "-index-system-modules": _ = iterator.next() case "-filelist": guard let fileList = iterator.next() else { diff --git a/Sources/SKCore/BuildServerBuildSystem.swift b/Sources/SKCore/BuildServerBuildSystem.swift index a52fd5301..6da6e1929 100644 --- a/Sources/SKCore/BuildServerBuildSystem.swift +++ b/Sources/SKCore/BuildServerBuildSystem.swift @@ -278,6 +278,10 @@ extension BuildServerBuildSystem: BuildSystem { return nil } + public func toolchain(for uri: DocumentURI, _ language: Language) async -> SKCore.Toolchain? { + return nil + } + public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } diff --git a/Sources/SKCore/BuildSystem.swift b/Sources/SKCore/BuildSystem.swift index f409c1ab5..7ea32e4c0 100644 --- a/Sources/SKCore/BuildSystem.swift +++ b/Sources/SKCore/BuildSystem.swift @@ -179,6 +179,11 @@ public protocol BuildSystem: AnyObject, Sendable { /// If `nil` is returned, the language based on the file's extension. func defaultLanguage(for document: DocumentURI) async -> Language? + /// The toolchain that should be used to open the given document. + /// + /// If `nil` is returned, then the default toolchain for the given language is used. + func toolchain(for uri: DocumentURI, _ language: Language) async -> Toolchain? + /// Register the given file for build-system level change notifications, such /// as command line flag changes, dependency changes, etc. /// diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index 415e98fc6..51a84faa8 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -104,9 +104,18 @@ extension BuildSystemManager { /// Returns the toolchain that should be used to process the given document. public func toolchain(for uri: DocumentURI, _ language: Language) async -> Toolchain? { - // To support multiple toolchains within a single workspace, we need to ask the build system which toolchain to use - // for this document. - return await toolchainRegistry.defaultToolchain(for: language) + if let toolchain = await buildSystem?.toolchain(for: uri, language) { + return toolchain + } + + switch language { + case .swift: + return await toolchainRegistry.preferredToolchain(containing: [\.sourcekitd, \.swift, \.swiftc]) + case .c, .cpp, .objective_c, .objective_cpp: + return await toolchainRegistry.preferredToolchain(containing: [\.clang, \.clangd]) + default: + return nil + } } /// - Note: Needed so we can set the delegate from a different isolation context. diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index f8b541803..fe3ae73f8 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -119,6 +119,10 @@ extension CompilationDatabaseBuildSystem: BuildSystem { return nil } + public func toolchain(for uri: DocumentURI, _ language: Language) async -> SKCore.Toolchain? { + return nil + } + public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } diff --git a/Sources/SKCore/IndexTaskID.swift b/Sources/SKCore/IndexTaskID.swift index 4ca4564b3..169115196 100644 --- a/Sources/SKCore/IndexTaskID.swift +++ b/Sources/SKCore/IndexTaskID.swift @@ -19,7 +19,7 @@ public enum IndexTaskID: Sendable { case updateIndexStore(id: UInt32) private static func numberToEmojis(_ number: Int, numEmojis: Int) -> String { - let emojis = ["🟥", "🟩", "🟦", "🟧", "⬜️", "🟪", "⬛️", "🟨", "🟫"] + let emojis = ["🟥", "🟩", "🟦", "⬜️", "🟪", "⬛️", "🟨", "🟫"] var number = abs(number) var result = "" for _ in 0.. { return result } } + +/// Version of the `withTaskPriorityChangedHandler` where the body doesn't throw. +fileprivate func withTaskPriorityChangedHandler( + initialPriority: TaskPriority = Task.currentPriority, + pollingInterval: Duration = .seconds(0.1), + @_inheritActorContext operation: @escaping @Sendable () async -> Void, + taskPriorityChanged: @escaping @Sendable () -> Void +) async { + do { + try await withTaskPriorityChangedHandler( + initialPriority: initialPriority, + pollingInterval: pollingInterval, + operation: operation as @Sendable () async throws -> Void, + taskPriorityChanged: taskPriorityChanged + ) + } catch is CancellationError { + } catch { + // Since `operation` does not throw, the only error we expect `withTaskPriorityChangedHandler` to throw is a + // `CancellationError`, in which case we can just return. + logger.fault("Unexpected error thrown from withTaskPriorityChangedHandler: \(error.forLogging)") + } +} diff --git a/Sources/SKCore/ToolchainRegistry.swift b/Sources/SKCore/ToolchainRegistry.swift index ee0010015..11ae357ab 100644 --- a/Sources/SKCore/ToolchainRegistry.swift +++ b/Sources/SKCore/ToolchainRegistry.swift @@ -246,26 +246,14 @@ public final actor ToolchainRegistry { return darwinToolchainOverride ?? ToolchainRegistry.darwinDefaultToolchainIdentifier } - /// The toolchain to use for a document in the given language if the build system doesn't override it. - func defaultToolchain(for language: Language) -> Toolchain? { - let supportsLang = { (toolchain: Toolchain) -> Bool in - // FIXME: the fact that we're looking at clangd/sourcekitd instead of the compiler indicates this method needs a parameter stating what kind of tool we're looking for. - switch language { - case .swift: - return toolchain.sourcekitd != nil - case .c, .cpp, .objective_c, .objective_cpp: - return toolchain.clangd != nil - default: - return false - } - } - - if let toolchain = self.default, supportsLang(toolchain) { + /// Returns the preferred toolchain that contains all the tools at the given key paths. + public func preferredToolchain(containing requiredTools: [KeyPath]) -> Toolchain? { + if let toolchain = self.default, requiredTools.allSatisfy({ toolchain[keyPath: $0] != nil }) { return toolchain } for toolchain in toolchains { - if supportsLang(toolchain) { + if requiredTools.allSatisfy({ toolchain[keyPath: $0] != nil }) { return toolchain } } diff --git a/Sources/SKSupport/CMakeLists.txt b/Sources/SKSupport/CMakeLists.txt index 6474d84bb..8aa8900b8 100644 --- a/Sources/SKSupport/CMakeLists.txt +++ b/Sources/SKSupport/CMakeLists.txt @@ -9,8 +9,7 @@ add_library(SKSupport STATIC FileSystem.swift LineTable.swift PipeAsStringHandler.swift - Process+LaunchWithWorkingDirectoryIfPossible.swift - Process+WaitUntilExitWithCancellation.swift + Process+Run.swift Random.swift Result.swift SwitchableProcessResultExitStatus.swift diff --git a/Sources/SKSupport/Process+LaunchWithWorkingDirectoryIfPossible.swift b/Sources/SKSupport/Process+LaunchWithWorkingDirectoryIfPossible.swift deleted file mode 100644 index 45e513058..000000000 --- a/Sources/SKSupport/Process+LaunchWithWorkingDirectoryIfPossible.swift +++ /dev/null @@ -1,70 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LSPLogging - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import enum TSCBasic.ProcessEnv -import struct TSCBasic.ProcessEnvironmentBlock - -extension Process { - /// Launches a new process with the given parameters. - /// - /// - Important: If `workingDirectory` is not supported on this platform, this logs an error and falls back to launching the - /// process without the working directory set. - public static func launch( - arguments: [String], - environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, - workingDirectory: AbsolutePath?, - outputRedirection: OutputRedirection = .collect, - startNewProcessGroup: Bool = true, - loggingHandler: LoggingHandler? = .none - ) throws -> Process { - let process = - if let workingDirectory { - Process( - arguments: arguments, - environmentBlock: environmentBlock, - workingDirectory: workingDirectory, - outputRedirection: outputRedirection, - startNewProcessGroup: startNewProcessGroup, - loggingHandler: loggingHandler - ) - } else { - Process( - arguments: arguments, - environmentBlock: environmentBlock, - outputRedirection: outputRedirection, - startNewProcessGroup: startNewProcessGroup, - loggingHandler: loggingHandler - ) - } - do { - try process.launch() - } catch Process.Error.workingDirectoryNotSupported where workingDirectory != nil { - // TODO (indexing): We need to figure out how to set the working directory on all platforms. - logger.error( - "Working directory not supported on the platform. Launching process without working directory \(workingDirectory!.pathString)" - ) - return try Process.launch( - arguments: arguments, - environmentBlock: environmentBlock, - workingDirectory: nil, - outputRedirection: outputRedirection, - startNewProcessGroup: startNewProcessGroup, - loggingHandler: loggingHandler - ) - } - return process - } -} diff --git a/Sources/SKSupport/Process+Run.swift b/Sources/SKSupport/Process+Run.swift new file mode 100644 index 000000000..d59d10efd --- /dev/null +++ b/Sources/SKSupport/Process+Run.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import LSPLogging +import SwiftExtensions + +import struct TSCBasic.AbsolutePath +import class TSCBasic.Process +import enum TSCBasic.ProcessEnv +import struct TSCBasic.ProcessEnvironmentBlock +import struct TSCBasic.ProcessResult + +#if os(Windows) +import WinSDK +#endif + +extension Process { + /// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process. + @discardableResult + public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult { + return try await withTaskCancellationHandler { + try await waitUntilExit() + } onCancel: { + signal(SIGINT) + } + } + + /// Launches a new process with the given parameters. + /// + /// - Important: If `workingDirectory` is not supported on this platform, this logs an error and falls back to launching the + /// process without the working directory set. + private static func launch( + arguments: [String], + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, + workingDirectory: AbsolutePath?, + outputRedirection: OutputRedirection = .collect, + startNewProcessGroup: Bool = true, + loggingHandler: LoggingHandler? = .none + ) throws -> Process { + let process = + if let workingDirectory { + Process( + arguments: arguments, + environmentBlock: environmentBlock, + workingDirectory: workingDirectory, + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) + } else { + Process( + arguments: arguments, + environmentBlock: environmentBlock, + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) + } + do { + try process.launch() + } catch Process.Error.workingDirectoryNotSupported where workingDirectory != nil { + // TODO (indexing): We need to figure out how to set the working directory on all platforms. + logger.error( + "Working directory not supported on the platform. Launching process without working directory \(workingDirectory!.pathString)" + ) + return try Process.launch( + arguments: arguments, + environmentBlock: environmentBlock, + workingDirectory: nil, + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) + } + return process + } + + /// Runs a new process with the given parameters and waits for it to exit, sending SIGINT if this task is cancelled. + /// + /// The process's priority tracks the priority of the current task. + @discardableResult + public static func run( + arguments: [String], + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, + workingDirectory: AbsolutePath?, + outputRedirection: OutputRedirection = .collect, + startNewProcessGroup: Bool = true, + loggingHandler: LoggingHandler? = .none + ) async throws -> ProcessResult { + let process = try Self.launch( + arguments: arguments, + environmentBlock: environmentBlock, + workingDirectory: workingDirectory, + outputRedirection: outputRedirection, + startNewProcessGroup: startNewProcessGroup, + loggingHandler: loggingHandler + ) + return try await withTaskPriorityChangedHandler(initialPriority: Task.currentPriority) { @Sendable in + setProcessPriority(pid: process.processID, newPriority: Task.currentPriority) + return try await process.waitUntilExitSendingSigIntOnTaskCancellation() + } taskPriorityChanged: { + setProcessPriority(pid: process.processID, newPriority: Task.currentPriority) + } + } +} + +/// Set the priority of the given process to a value that's equivalent to `newPriority` on the current OS. +private func setProcessPriority(pid: Process.ProcessID, newPriority: TaskPriority) { + #if os(Windows) + guard let handle = OpenProcess(UInt32(PROCESS_SET_INFORMATION), /*bInheritHandle*/ false, UInt32(pid)) else { + logger.error("Failed to get process handle for \(pid) to change its priority: \(GetLastError())") + return + } + defer { + CloseHandle(handle) + } + if !SetPriorityClass(handle, UInt32(newPriority.windowsProcessPriority)) { + logger.error("Failed to set process priority of \(pid) to \(newPriority.rawValue): \(GetLastError())") + } + #elseif canImport(Darwin) + // `setpriority` is only able to decrease a process's priority and cannot elevate it. Since Swift task’s priorities + // can only be elevated, this means that we can effectively only change a process's priority once, when it is created. + // All subsequent calls to `setpriority` will fail. Because of this, don't log an error. + setpriority(PRIO_PROCESS, UInt32(pid), newPriority.posixProcessPriority) + #else + setpriority(__priority_which_t(PRIO_PROCESS.rawValue), UInt32(pid), newPriority.posixProcessPriority) + #endif +} + +fileprivate extension TaskPriority { + #if os(Windows) + var windowsProcessPriority: Int32 { + if self >= .high { + // SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the normal priority class. + return NORMAL_PRIORITY_CLASS + } + if self >= .medium { + return BELOW_NORMAL_PRIORITY_CLASS + } + return IDLE_PRIORITY_CLASS + } + #else + var posixProcessPriority: Int32 { + if self >= .high { + // SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the base 0 niceness value. + return 0 + } + if self >= .medium { + return 5 + } + if self >= .low { + return 10 + } + return 15 + } + #endif +} diff --git a/Sources/SKSupport/Process+WaitUntilExitWithCancellation.swift b/Sources/SKSupport/Process+WaitUntilExitWithCancellation.swift deleted file mode 100644 index b265161e6..000000000 --- a/Sources/SKSupport/Process+WaitUntilExitWithCancellation.swift +++ /dev/null @@ -1,28 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -import class TSCBasic.Process -import struct TSCBasic.ProcessResult - -extension Process { - /// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process. - @discardableResult - public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult { - return try await withTaskCancellationHandler { - try await waitUntilExit() - } onCancel: { - signal(SIGINT) - } - } -} diff --git a/Sources/SKSupport/SwitchableProcessResultExitStatus.swift b/Sources/SKSupport/SwitchableProcessResultExitStatus.swift index 3b0637897..43e25e1b3 100644 --- a/Sources/SKSupport/SwitchableProcessResultExitStatus.swift +++ b/Sources/SKSupport/SwitchableProcessResultExitStatus.swift @@ -21,6 +21,18 @@ public enum SwitchableProcessResultExitStatus { case abnormal(exception: UInt32) /// The process was terminated due to a signal. case signalled(signal: Int32) + + /// A description of the exit status that can be used in sentences like `Finished with `. + public var description: String { + switch self { + case .terminated(code: let code): + "exit code \(code)" + case .abnormal(exception: let exception): + "exception \(exception)" + case .signalled(signal: let signal): + "signal \(signal)" + } + } } extension ProcessResult.ExitStatus { diff --git a/Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift b/Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift index 153f60fe3..23ea99c76 100644 --- a/Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift +++ b/Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift @@ -59,15 +59,29 @@ public typealias BuildServerTarget = BuildServerProtocol.BuildTarget /// Same as `toolchainRegistry.default`. /// -/// Needed to work around a compiler crash that prevents us from accessing `toolchainRegistry.default` in +/// Needed to work around a compiler crash that prevents us from accessing `toolchainRegistry.preferredToolchain` in /// `SwiftPMWorkspace.init`. -private func getDefaultToolchain(_ toolchainRegistry: ToolchainRegistry) async -> SKCore.Toolchain? { - return await toolchainRegistry.default +private func preferredToolchain(_ toolchainRegistry: ToolchainRegistry) async -> SKCore.Toolchain? { + return await toolchainRegistry.preferredToolchain(containing: [ + \.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc, + ]) +} + +fileprivate extension BuildTriple { + /// A string that can be used to identify the build triple in `ConfiguredTarget.runDestinationID`. + var id: String { + switch self { + case .tools: + return "tools" + case .destination: + return "destination" + } + } } fileprivate extension ConfiguredTarget { init(_ buildTarget: any SwiftBuildTarget) { - self.init(targetID: buildTarget.name, runDestinationID: "dummy") + self.init(targetID: buildTarget.name, runDestinationID: buildTarget.buildTriple.id) } static let forPackageManifest = ConfiguredTarget(targetID: "", runDestinationID: "") @@ -103,21 +117,22 @@ public actor SwiftPMBuildSystem { private var testFilesDidChangeCallbacks: [() async -> Void] = [] private let workspacePath: TSCAbsolutePath + + /// The build setup that allows the user to pass extra compiler flags. + private let buildSetup: BuildSetup + /// The directory containing `Package.swift`. @_spi(Testing) public var projectRoot: TSCAbsolutePath + private var modulesGraph: ModulesGraph private let workspace: Workspace @_spi(Testing) public let buildParameters: BuildParameters private let fileSystem: FileSystem - private let toolchainRegistry: ToolchainRegistry - - private let swiftBuildSupportsPrepareForIndexingTask = SwiftExtensions.ThreadSafeBox?>( - initialValue: nil - ) + private let toolchain: SKCore.Toolchain - private var fileToTarget: [DocumentURI: SwiftBuildTarget] = [:] - private var sourceDirToTarget: [DocumentURI: SwiftBuildTarget] = [:] + private var fileToTargets: [DocumentURI: [SwiftBuildTarget]] = [:] + private var sourceDirToTargets: [DocumentURI: [SwiftBuildTarget]] = [:] /// Maps configured targets ids to their SwiftPM build target as well as an index in their topological sorting. /// @@ -170,8 +185,13 @@ public actor SwiftPMBuildSystem { reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in } ) async throws { self.workspacePath = workspacePath + self.buildSetup = buildSetup self.fileSystem = fileSystem - self.toolchainRegistry = toolchainRegistry + guard let toolchain = await preferredToolchain(toolchainRegistry) else { + throw Error.cannotDetermineHostToolchain + } + + self.toolchain = toolchain self.experimentalFeatures = experimentalFeatures guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else { @@ -180,12 +200,12 @@ public actor SwiftPMBuildSystem { self.projectRoot = try resolveSymlinks(packageRoot) - guard let destinationToolchainBinDir = await getDefaultToolchain(toolchainRegistry)?.swiftc?.parentDirectory else { + guard let destinationToolchainBinDir = toolchain.swiftc?.parentDirectory else { throw Error.cannotDetermineHostToolchain } let swiftSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(destinationToolchainBinDir)) - let toolchain = try UserToolchain(swiftSDK: swiftSDK) + let swiftPMToolchain = try UserToolchain(swiftSDK: swiftSDK) var location = try Workspace.Location( forRootPackage: AbsolutePath(packageRoot), @@ -204,7 +224,7 @@ public actor SwiftPMBuildSystem { fileSystem: fileSystem, location: location, configuration: configuration, - customHostToolchain: toolchain + customHostToolchain: swiftPMToolchain ) let buildConfiguration: PackageModel.BuildConfiguration @@ -216,9 +236,11 @@ public actor SwiftPMBuildSystem { } self.buildParameters = try BuildParameters( - dataPath: location.scratchDirectory.appending(component: toolchain.targetTriple.platformBuildPathComponent), + dataPath: location.scratchDirectory.appending( + component: swiftPMToolchain.targetTriple.platformBuildPathComponent + ), configuration: buildConfiguration, - toolchain: toolchain, + toolchain: swiftPMToolchain, flags: buildSetup.flags ) @@ -310,7 +332,7 @@ extension SwiftPMBuildSystem { self.targets = Dictionary( try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph).enumerated().map { (index, target) in - return (key: ConfiguredTarget(target), (index, target)) + return (key: ConfiguredTarget(target), value: (index, target)) }, uniquingKeysWith: { first, second in logger.fault("Found two targets with the same name \(first.buildTarget.name)") @@ -318,32 +340,26 @@ extension SwiftPMBuildSystem { } ) - self.fileToTarget = [DocumentURI: SwiftBuildTarget]( + self.fileToTargets = [DocumentURI: [SwiftBuildTarget]]( modulesGraph.allTargets.flatMap { target in - return target.sources.paths.compactMap { + return target.sources.paths.compactMap { (filePath) -> (key: DocumentURI, value: [SwiftBuildTarget])? in guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else { return nil } - return (key: DocumentURI($0.asURL), value: buildTarget) + return (key: DocumentURI(filePath.asURL), value: [buildTarget]) } }, - uniquingKeysWith: { td, _ in - // FIXME: is there a preferred target? - return td - } + uniquingKeysWith: { $0 + $1 } ) - self.sourceDirToTarget = [DocumentURI: SwiftBuildTarget]( - modulesGraph.allTargets.compactMap { (target) -> (DocumentURI, SwiftBuildTarget)? in + self.sourceDirToTargets = [DocumentURI: [SwiftBuildTarget]]( + modulesGraph.allTargets.compactMap { (target) -> (DocumentURI, [SwiftBuildTarget])? in guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else { return nil } - return (key: DocumentURI(target.sources.root.asURL), value: buildTarget) + return (key: DocumentURI(target.sources.root.asURL), value: [buildTarget]) }, - uniquingKeysWith: { td, _ in - // FIXME: is there a preferred target? - return td - } + uniquingKeysWith: { $0 + $1 } ) guard let delegate = self.delegate else { @@ -395,7 +411,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { #endif // Fix up compiler arguments that point to a `/Modules` subdirectory if the Swift version in the toolchain is less // than 6.0 because it places the modules one level higher up. - let toolchainVersion = await orLog("Getting Swift version") { try await toolchainRegistry.default?.swiftVersion } + let toolchainVersion = await orLog("Getting Swift version") { try await toolchain.swiftVersion } guard let toolchainVersion, toolchainVersion < SwiftVersion(6, 0) else { return compileArguments } @@ -455,14 +471,19 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { return nil } + public func toolchain(for uri: DocumentURI, _ language: Language) async -> SKCore.Toolchain? { + return toolchain + } + public func configuredTargets(for uri: DocumentURI) -> [ConfiguredTarget] { guard let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) else { // We can't determine targets for non-file URIs. return [] } - if let target = buildTarget(for: uri) { - return [ConfiguredTarget(target)] + let targets = buildTargets(for: uri) + if !targets.isEmpty { + return targets.map(ConfiguredTarget.init) } if path.basename == "Package.swift" { @@ -471,8 +492,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { return [ConfiguredTarget.forPackageManifest] } - if let target = try? inferredTarget(for: path) { - return [target] + if let targets = try? inferredTargets(for: path) { + return targets } return [] @@ -518,7 +539,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { // TODO (indexing): Support preparation of multiple targets at once. // https://github.com/apple/sourcekit-lsp/issues/1262 for target in targets { - try await prepare(singleTarget: target, logMessageToIndexLog: logMessageToIndexLog) + await orLog("Preparing") { try await prepare(singleTarget: target, logMessageToIndexLog: logMessageToIndexLog) } } let filesInPreparedTargets = targets.flatMap { self.targets[$0]?.buildTarget.sources ?? [] } await fileDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets.map(DocumentURI.init))) @@ -535,16 +556,13 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { // TODO (indexing): Add a proper 'prepare' job in SwiftPM instead of building the target. // https://github.com/apple/sourcekit-lsp/issues/1254 - guard let toolchain = await toolchainRegistry.default else { - logger.error("Not preparing because not toolchain exists") - return - } guard let swift = toolchain.swift else { logger.error( - "Not preparing because toolchain at \(toolchain.identifier) does not contain a Swift compiler" + "Not preparing because toolchain at \(self.toolchain.identifier) does not contain a Swift compiler" ) return } + logger.debug("Preparing '\(target.targetID)' using \(self.toolchain.identifier)") var arguments = [ swift.pathString, "build", "--package-path", workspacePath.pathString, @@ -552,12 +570,14 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { "--disable-index-store", "--target", target.targetID, ] - arguments += ["-c", self.buildParameters.configuration.rawValue] - arguments += self.buildParameters.flags.cCompilerFlags.flatMap { ["-Xcc", $0] } - arguments += self.buildParameters.flags.cxxCompilerFlags.flatMap { ["-Xcxx", $0] } - arguments += self.buildParameters.flags.swiftCompilerFlags.flatMap { ["-Xswiftc", $0] } - arguments += self.buildParameters.flags.linkerFlags.flatMap { ["-Xlinker", $0] } - arguments += self.buildParameters.flags.xcbuildFlags?.flatMap { ["-Xxcbuild", $0] } ?? [] + if let configuration = buildSetup.configuration { + arguments += ["-c", configuration.rawValue] + } + arguments += buildSetup.flags.cCompilerFlags.flatMap { ["-Xcc", $0] } + arguments += buildSetup.flags.cxxCompilerFlags.flatMap { ["-Xcxx", $0] } + arguments += buildSetup.flags.swiftCompilerFlags.flatMap { ["-Xswiftc", $0] } + arguments += buildSetup.flags.linkerFlags.flatMap { ["-Xlinker", $0] } + arguments += buildSetup.flags.xcbuildFlags?.flatMap { ["-Xxcbuild", $0] } ?? [] if experimentalFeatures.contains(.swiftpmPrepareForIndexing) { arguments.append("--experimental-prepare-for-indexing") } @@ -577,7 +597,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) } let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) } - let process = try Process.launch( + let result = try await Process.run( arguments: arguments, workingDirectory: nil, outputRedirection: .stream( @@ -585,9 +605,9 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { stderr: { stderrHandler.handleDataFromPipe(Data($0)) } ) ) - let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation() - logMessageToIndexLog(logID, "Finished in \(start.duration(to: .now))") - switch result.exitStatus.exhaustivelySwitchable { + let exitStatus = result.exitStatus.exhaustivelySwitchable + logMessageToIndexLog(logID, "Finished with \(exitStatus.description) in \(start.duration(to: .now))") + switch exitStatus { case .terminated(code: 0): break case .terminated(code: let code): @@ -627,21 +647,21 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { self.watchedFiles.remove(uri) } - /// Returns the resolved target description for the given file, if one is known. - private func buildTarget(for file: DocumentURI) -> SwiftBuildTarget? { - if let td = fileToTarget[file] { - return td + /// Returns the resolved target descriptions for the given file, if one is known. + private func buildTargets(for file: DocumentURI) -> [SwiftBuildTarget] { + if let targets = fileToTargets[file] { + return targets } if let fileURL = file.fileURL, let realpath = try? resolveSymlinks(AbsolutePath(validating: fileURL.path)), - let td = fileToTarget[DocumentURI(realpath.asURL)] + let targets = fileToTargets[DocumentURI(realpath.asURL)] { - fileToTarget[file] = td - return td + fileToTargets[file] = targets + return targets } - return nil + return [] } /// An event is relevant if it modifies a file that matches one of the file rules used by the SwiftPM workspace. @@ -679,10 +699,10 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { // If a Swift file within a target is updated, reload all the other files within the target since they might be // referring to a function in the updated file. for event in events { - guard event.uri.fileURL?.pathExtension == "swift", let target = fileToTarget[event.uri] else { + guard event.uri.fileURL?.pathExtension == "swift", let targets = fileToTargets[event.uri] else { continue } - filesWithUpdatedDependencies.formUnion(target.sources.map { DocumentURI($0) }) + filesWithUpdatedDependencies.formUnion(targets.flatMap(\.sources).map(DocumentURI.init)) } // If a `.swiftmodule` file is updated, this means that we have performed a build / are @@ -695,7 +715,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { // If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when // preparation of a target finishes. if !isForIndexBuild, events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) { - filesWithUpdatedDependencies.formUnion(self.fileToTarget.keys) + filesWithUpdatedDependencies.formUnion(self.fileToTargets.keys) } await self.fileDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies) } @@ -708,12 +728,12 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { } public func sourceFiles() -> [SourceFileInfo] { - return fileToTarget.compactMap { (uri, target) -> SourceFileInfo? in + return fileToTargets.compactMap { (uri, targets) -> SourceFileInfo? in // We should only set mayContainTests to `true` for files from test targets // (https://github.com/apple/sourcekit-lsp/issues/1174). return SourceFileInfo( uri: uri, - isPartOfRootProject: target.isPartOfRootPackage, + isPartOfRootProject: targets.contains(where: \.isPartOfRootPackage), mayContainTests: true ) } @@ -749,24 +769,25 @@ extension SwiftPMBuildSystem { /// This finds the target a file belongs to based on its location in the file system. /// /// This is primarily intended to find the target a header belongs to. - private func inferredTarget(for path: AbsolutePath) throws -> ConfiguredTarget? { - func impl(_ path: AbsolutePath) throws -> ConfiguredTarget? { + private func inferredTargets(for path: AbsolutePath) throws -> [ConfiguredTarget] { + func impl(_ path: AbsolutePath) throws -> [ConfiguredTarget] { var dir = path.parentDirectory while !dir.isRoot { - if let buildTarget = sourceDirToTarget[DocumentURI(dir.asURL)] { - return ConfiguredTarget(buildTarget) + if let buildTargets = sourceDirToTargets[DocumentURI(dir.asURL)] { + return buildTargets.map(ConfiguredTarget.init) } dir = dir.parentDirectory } - return nil + return [] } - if let result = try impl(path) { + let result = try impl(path) + if !result.isEmpty { return result } let canonicalPath = try resolveSymlinks(path) - return try canonicalPath == path ? nil : impl(canonicalPath) + return try canonicalPath == path ? [] : impl(canonicalPath) } } diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift index 18de4f72f..898f195e6 100644 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ b/Sources/SKTestSupport/SwiftPMTestProject.swift @@ -163,7 +163,7 @@ public class SwiftPMTestProject: MultiFileTestProject { for (fileLocation, contents) in files { let directories = switch fileLocation.directories.first { - case "Sources", "Tests": + case "Sources", "Tests", "Plugins": fileLocation.directories case nil: ["Sources", "MyLibrary"] diff --git a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift index d15a0b5c3..fda726eff 100644 --- a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift +++ b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift @@ -356,25 +356,23 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription { let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) } let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) } - let process = try Process.launch( - arguments: processArguments, - workingDirectory: workingDirectory, - outputRedirection: .stream( - stdout: { stdoutHandler.handleDataFromPipe(Data($0)) }, - stderr: { stderrHandler.handleDataFromPipe(Data($0)) } - ) - ) // Time out updating of the index store after 2 minutes. We don't expect any single file compilation to take longer // than 2 minutes in practice, so this indicates that the compiler has entered a loop and we probably won't make any // progress here. We will try indexing the file again when it is edited or when the project is re-opened. // 2 minutes have been chosen arbitrarily. let result = try await withTimeout(.seconds(120)) { - try await process.waitUntilExitSendingSigIntOnTaskCancellation() + try await Process.run( + arguments: processArguments, + workingDirectory: workingDirectory, + outputRedirection: .stream( + stdout: { stdoutHandler.handleDataFromPipe(Data($0)) }, + stderr: { stderrHandler.handleDataFromPipe(Data($0)) } + ) + ) } - - logMessageToIndexLog(logID, "Finished in \(start.duration(to: .now))") - - switch result.exitStatus.exhaustivelySwitchable { + let exitStatus = result.exitStatus.exhaustivelySwitchable + logMessageToIndexLog(logID, "Finished with \(exitStatus.description) in \(start.duration(to: .now))") + switch exitStatus { case .terminated(code: 0): break case .terminated(code: let code): diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 65016b27c..ae723fa83 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -521,7 +521,7 @@ public actor SourceKitLSPServer { return nil } - logger.log("Using toolchain \(toolchain.displayName) (\(toolchain.identifier)) for \(uri.forLogging)") + logger.log("Using toolchain \(toolchain.identifier) (\(toolchain.identifier)) for \(uri.forLogging)") return workspace.documentService.withLock { documentService in if let concurrentlySetService = documentService[uri] { diff --git a/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift b/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift index 58367d1e0..8d3c1c8ee 100644 --- a/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift +++ b/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift @@ -16,18 +16,18 @@ /// `pollingInterval`. /// The function assumes that the original priority of the task is `initialPriority`. If the task priority changed /// compared to `initialPriority`, the `taskPriorityChanged` will be called. -public func withTaskPriorityChangedHandler( +public func withTaskPriorityChangedHandler( initialPriority: TaskPriority = Task.currentPriority, pollingInterval: Duration = .seconds(0.1), - @_inheritActorContext operation: @escaping @Sendable () async -> Void, + @_inheritActorContext operation: @escaping @Sendable () async throws -> T, taskPriorityChanged: @escaping @Sendable () -> Void -) async { +) async throws -> T { let lastPriority = ThreadSafeBox(initialValue: initialPriority) - await withTaskGroup(of: Void.self) { taskGroup in + let result: T? = try await withThrowingTaskGroup(of: Optional.self) { taskGroup in taskGroup.addTask { while true { if Task.isCancelled { - return + break } let newPriority = Task.currentPriority let didChange = lastPriority.withLock { lastPriority in @@ -46,15 +46,22 @@ public func withTaskPriorityChangedHandler( break } } + return nil } taskGroup.addTask { - await operation() + try await operation() } // The first task that watches the priority never finishes, so we are effectively await the `operation` task here // and cancelling the priority observation task once the operation task is done. // We do need to await the observation task as well so that priority escalation also affects the observation task. - for await _ in taskGroup { + for try await case let value? in taskGroup { taskGroup.cancelAll() + return value } + return nil + } + guard let result else { + throw CancellationError() } + return result } diff --git a/Sources/sourcekit-lsp/SourceKitLSP.swift b/Sources/sourcekit-lsp/SourceKitLSP.swift index 74b187ee0..9e54a4148 100644 --- a/Sources/sourcekit-lsp/SourceKitLSP.swift +++ b/Sources/sourcekit-lsp/SourceKitLSP.swift @@ -289,9 +289,3 @@ struct SourceKitLSP: AsyncParsableCommand { } } } - -#if compiler(>=6) -extension ExperimentalFeature: @retroactive ExpressibleByArgument {} -#else -extension ExperimentalFeature: ExpressibleByArgument {} -#endif diff --git a/Tests/SKCoreTests/BuildSystemManagerTests.swift b/Tests/SKCoreTests/BuildSystemManagerTests.swift index ab894d5ea..862df9735 100644 --- a/Tests/SKCoreTests/BuildSystemManagerTests.swift +++ b/Tests/SKCoreTests/BuildSystemManagerTests.swift @@ -465,6 +465,10 @@ class ManualBuildSystem: BuildSystem { return nil } + public func toolchain(for uri: DocumentURI, _ language: Language) async -> SKCore.Toolchain? { + return nil + } + public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 4cc2857bb..a4268dde8 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -276,7 +276,7 @@ final class BackgroundIndexingTests: XCTestCase { func testIndexCFile() async throws { let project = try await SwiftPMTestProject( files: [ - "MyLibrary/include/dummy.h": "", + "MyLibrary/include/destination.h": "", "MyFile.c": """ void 1️⃣someFunc() {} @@ -532,11 +532,11 @@ final class BackgroundIndexingTests: XCTestCase { var serverOptions = SourceKitLSPServer.Options.testDefault let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [ [ - ExpectedPreparation(targetID: "LibA", runDestinationID: "dummy"), - ExpectedPreparation(targetID: "LibB", runDestinationID: "dummy"), + ExpectedPreparation(targetID: "LibA", runDestinationID: "destination"), + ExpectedPreparation(targetID: "LibB", runDestinationID: "destination"), ], [ - ExpectedPreparation(targetID: "LibB", runDestinationID: "dummy") + ExpectedPreparation(targetID: "LibB", runDestinationID: "destination") ], ]) serverOptions.indexTestHooks = expectedPreparationTracker.testHooks @@ -639,16 +639,16 @@ final class BackgroundIndexingTests: XCTestCase { let expectedPreparationTracker = ExpectedIndexTaskTracker(expectedPreparations: [ // Preparation of targets during the initial of the target [ - ExpectedPreparation(targetID: "LibA", runDestinationID: "dummy"), - ExpectedPreparation(targetID: "LibB", runDestinationID: "dummy"), - ExpectedPreparation(targetID: "LibC", runDestinationID: "dummy"), - ExpectedPreparation(targetID: "LibD", runDestinationID: "dummy"), + ExpectedPreparation(targetID: "LibA", runDestinationID: "destination"), + ExpectedPreparation(targetID: "LibB", runDestinationID: "destination"), + ExpectedPreparation(targetID: "LibC", runDestinationID: "destination"), + ExpectedPreparation(targetID: "LibD", runDestinationID: "destination"), ], // LibB's preparation has already started by the time we browse through the other files, so we finish its preparation [ ExpectedPreparation( targetID: "LibB", - runDestinationID: "dummy", + runDestinationID: "destination", didStart: { libBStartedPreparation.fulfill() }, didFinish: { self.wait(for: [allDocumentsOpened], timeout: defaultTimeout) } ) @@ -657,7 +657,7 @@ final class BackgroundIndexingTests: XCTestCase { [ ExpectedPreparation( targetID: "LibD", - runDestinationID: "dummy", + runDestinationID: "destination", didFinish: { libDPreparedForEditing.fulfill() } ) ], @@ -999,4 +999,54 @@ final class BackgroundIndexingTests: XCTestCase { "Did not get expected diagnostic: \(diagnostics)" ) } + + func testLibraryUsedByExecutableTargetAndPackagePlugin() async throws { + try await SkipUnless.swiftpmStoresModulesInSubdirectory() + let project = try await SwiftPMTestProject( + files: [ + "Lib/MyFile.swift": """ + public func 1️⃣foo() {} + """, + "MyExec/MyExec.swift": """ + import Lib + func bar() { + 2️⃣foo() + } + """, + "Plugins/MyPlugin/MyPlugin.swift": """ + import PackagePlugin + @main + struct MyPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws {} + } + """, + ], + manifest: """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyLibrary", + targets: [ + .target(name: "Lib"), + .executableTarget(name: "MyExec", dependencies: ["Lib"]), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .sourceCodeFormatting(), + permissions: [] + ), + dependencies: ["MyExec"] + ) + ] + ) + """, + enableBackgroundIndexing: true + ) + + let (uri, positions) = try project.openDocument("MyExec.swift") + let definition = try await project.testClient.send( + DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"]) + ) + XCTAssertEqual(definition, .locations([try project.location(from: "1️⃣", to: "1️⃣", in: "MyFile.swift")])) + } } diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index c5439ca67..ab365e6a1 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -57,6 +57,10 @@ actor TestBuildSystem: BuildSystem { return nil } + public func toolchain(for uri: DocumentURI, _ language: Language) async -> SKCore.Toolchain? { + return nil + } + public func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] }