Skip to content

Commit 958a07a

Browse files
committed
Merge remote-tracking branch 'origin/main' into release/6.2
2 parents 9092459 + 3f1568c commit 958a07a

28 files changed

+721
-160
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ find_package(ArgumentParser CONFIG REQUIRED)
2525
find_package(SwiftCollections QUIET)
2626
find_package(SwiftSyntax CONFIG REQUIRED)
2727
find_package(SwiftCrypto CONFIG REQUIRED)
28+
find_package(SwiftASN1 CONFIG REQUIRED)
2829

2930
include(SwiftSupport)
3031

Documentation/Configuration File.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,4 @@ The structure of the file is currently not guaranteed to be stable. Options may
5858
- `swiftPublishDiagnosticsDebounceDuration: number`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`.
5959
- `workDoneProgressDebounceDuration: number`: When a task is started that should be displayed to the client as a work done progress, how many milliseconds to wait before actually starting the work done progress. This prevents flickering of the work done progress in the client for short-lived index tasks which end within this duration.
6060
- `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd.
61+
- `semanticServiceRestartTimeout: number`: If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.

Sources/BuildSystemIntegration/BuildTargetIdentifierExtensions.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ extension BuildTargetIdentifier {
9999
// MARK: BuildTargetIdentifier for CompileCommands
100100

101101
extension BuildTargetIdentifier {
102-
/// - Important: *For testing only*
103102
package static func createCompileCommands(compiler: String) throws -> BuildTargetIdentifier {
104103
var components = URLComponents()
105104
components.scheme = "compilecommands"

Sources/BuildSystemIntegration/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_library(BuildSystemIntegration STATIC
1919
LegacyBuildServerBuildSystem.swift
2020
MainFilesProvider.swift
2121
SplitShellCommand.swift
22+
SwiftlyResolver.swift
2223
SwiftPMBuildSystem.swift)
2324
set_target_properties(BuildSystemIntegration PROPERTIES
2425
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
@@ -35,7 +36,8 @@ target_link_libraries(BuildSystemIntegration PUBLIC
3536
PackageModel
3637
TSCBasic
3738
Build
38-
SourceKitLSPAPI)
39+
SourceKitLSPAPI
40+
SwiftASN1)
3941

4042
target_link_libraries(BuildSystemIntegration PRIVATE
4143
SKUtilities

Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ actor ExternalBuildSystemAdapter {
185185
protocol: bspRegistry,
186186
stderrLoggingCategory: "bsp-server-stderr",
187187
client: messagesToSourceKitLSPHandler,
188-
terminationHandler: { [weak self] terminationStatus in
188+
terminationHandler: { [weak self] terminationReason in
189189
guard let self else {
190190
return
191191
}
192-
if terminationStatus != 0 {
192+
if terminationReason != .exited(exitCode: 0) {
193193
Task {
194194
await orLog("Restarting BSP server") {
195195
try await self.handleBspServerCrash()

Sources/BuildSystemIntegration/JSONCompilationDatabaseBuildSystem.swift

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,23 @@ fileprivate extension CompilationDatabaseCompileCommand {
2525
/// without specifying a path.
2626
///
2727
/// The absence of a compiler means we have an empty command line, which should never happen.
28-
var compiler: String? {
29-
return commandLine.first
28+
///
29+
/// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a
30+
/// real toolchain and returns that executable.
31+
func compiler(swiftlyResolver: SwiftlyResolver) async -> String? {
32+
guard let compiler = commandLine.first else {
33+
return nil
34+
}
35+
let swiftlyResolved = await orLog("Resolving swiftly") {
36+
try await swiftlyResolver.resolve(
37+
compiler: URL(fileURLWithPath: compiler),
38+
workingDirectory: URL(fileURLWithPath: directory)
39+
)?.filePath
40+
}
41+
if let swiftlyResolved {
42+
return swiftlyResolved
43+
}
44+
return compiler
3045
}
3146
}
3247

@@ -49,14 +64,17 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
4964

5065
package let configPath: URL
5166

67+
private let swiftlyResolver = SwiftlyResolver()
68+
5269
// Watch for all all changes to `compile_commands.json` and `compile_flags.txt` instead of just the one at
5370
// `configPath` so that we cover the following semi-common scenario:
5471
// The user has a build that stores `compile_commands.json` in `mybuild`. In order to pick it up, they create a
5572
// symlink from `<project root>/compile_commands.json` to `mybuild/compile_commands.json`. We want to get notified
5673
// about the change to `mybuild/compile_commands.json` because it effectively changes the contents of
5774
// `<project root>/compile_commands.json`.
5875
package let fileWatchers: [FileSystemWatcher] = [
59-
FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete])
76+
FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]),
77+
FileSystemWatcher(globPattern: "**/.swift-version", kind: [.create, .change, .delete]),
6078
]
6179

6280
private var _indexStorePath: LazyValue<URL?> = .uninitialized
@@ -92,7 +110,11 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
92110
}
93111

94112
package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
95-
let compilers = Set(compdb.commands.compactMap(\.compiler)).sorted { $0 < $1 }
113+
let compilers = Set(
114+
await compdb.commands.asyncCompactMap { (command) -> String? in
115+
await command.compiler(swiftlyResolver: swiftlyResolver)
116+
}
117+
).sorted { $0 < $1 }
96118
let targets = try await compilers.asyncMap { compiler in
97119
let toolchainUri: URI? =
98120
if let toolchainPath = await toolchainRegistry.toolchain(withCompiler: URL(fileURLWithPath: compiler))?.path {
@@ -115,12 +137,12 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
115137
}
116138

117139
package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
118-
let items = request.targets.compactMap { (target) -> SourcesItem? in
140+
let items = await request.targets.asyncCompactMap { (target) -> SourcesItem? in
119141
guard let targetCompiler = orLog("Compiler for target", { try target.compileCommandsCompiler }) else {
120142
return nil
121143
}
122-
let commandsWithRequestedCompilers = compdb.commands.lazy.filter { command in
123-
return targetCompiler == command.compiler
144+
let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in
145+
return await targetCompiler == command.compiler(swiftlyResolver: swiftlyResolver)
124146
}
125147
let sources = commandsWithRequestedCompilers.map {
126148
SourceItem(uri: $0.uri, kind: .file, generated: false)
@@ -131,10 +153,14 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
131153
return BuildTargetSourcesResponse(items: items)
132154
}
133155

134-
package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) {
156+
package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async {
135157
if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == Self.dbName }) {
136158
self.reloadCompilationDatabase()
137159
}
160+
if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == ".swift-version" }) {
161+
await swiftlyResolver.clearCache()
162+
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
163+
}
138164
}
139165

140166
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
@@ -145,8 +171,8 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
145171
request: TextDocumentSourceKitOptionsRequest
146172
) async throws -> TextDocumentSourceKitOptionsResponse? {
147173
let targetCompiler = try request.target.compileCommandsCompiler
148-
let command = compdb[request.textDocument.uri].filter {
149-
$0.compiler == targetCompiler
174+
let command = await compdb[request.textDocument.uri].asyncFilter {
175+
return await $0.compiler(swiftlyResolver: swiftlyResolver) == targetCompiler
150176
}.first
151177
guard let command else {
152178
return nil
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SKUtilities
15+
import SwiftExtensions
16+
import TSCExtensions
17+
18+
import struct TSCBasic.AbsolutePath
19+
import class TSCBasic.Process
20+
21+
/// Given a path to a compiler, which might be a symlink to `swiftly`, this type determines the compiler executable in
22+
/// an actual toolchain. It also caches the results. The client needs to invalidate the cache if the path that swiftly
23+
/// might resolve to has changed, eg. because `.swift-version` has been updated.
24+
actor SwiftlyResolver {
25+
private struct CacheKey: Hashable {
26+
let compiler: URL
27+
let workingDirectory: URL?
28+
}
29+
30+
private var cache: LRUCache<CacheKey, Result<URL?, Error>> = LRUCache(capacity: 100)
31+
32+
/// Check if `compiler` is a symlink to `swiftly`. If so, find the executable in the toolchain that swiftly resolves
33+
/// to within the given working directory and return the URL of the corresponding compiler in that toolchain.
34+
/// If `compiler` does not resolve to `swiftly`, return `nil`.
35+
func resolve(compiler: URL, workingDirectory: URL?) async throws -> URL? {
36+
let cacheKey = CacheKey(compiler: compiler, workingDirectory: workingDirectory)
37+
if let cached = cache[cacheKey] {
38+
return try cached.get()
39+
}
40+
let computed: Result<URL?, Error>
41+
do {
42+
computed = .success(
43+
try await resolveSwiftlyTrampolineImpl(compiler: compiler, workingDirectory: workingDirectory)
44+
)
45+
} catch {
46+
computed = .failure(error)
47+
}
48+
cache[cacheKey] = computed
49+
return try computed.get()
50+
}
51+
52+
private func resolveSwiftlyTrampolineImpl(compiler: URL, workingDirectory: URL?) async throws -> URL? {
53+
let realpath = try compiler.realpath
54+
guard realpath.lastPathComponent == "swiftly" else {
55+
return nil
56+
}
57+
let swiftlyResult = try await Process.run(
58+
arguments: [realpath.filePath, "use", "-p"],
59+
workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath)
60+
)
61+
let swiftlyToolchain = URL(
62+
fileURLWithPath: try swiftlyResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines)
63+
)
64+
let resolvedCompiler = swiftlyToolchain.appending(components: "usr", "bin", compiler.lastPathComponent)
65+
if FileManager.default.fileExists(at: resolvedCompiler) {
66+
return resolvedCompiler
67+
}
68+
return nil
69+
}
70+
71+
func clearCache() {
72+
cache.removeAll()
73+
}
74+
}

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ import struct CDispatch.dispatch_fd_t
3131
/// For example, inside a language server, the `JSONRPCConnection` takes the language service implementation as its
3232
// `receiveHandler` and itself provides the client connection for sending notifications and callbacks.
3333
public final class JSONRPCConnection: Connection {
34+
public enum TerminationReason: Sendable, Equatable {
35+
/// The process on the other end of the `JSONRPCConnection` terminated with the given exit code.
36+
case exited(exitCode: Int32)
37+
38+
/// The process on the other end of the `JSONRPCConnection` terminated with a signal. The signal that it terminated
39+
/// with is not known.
40+
case uncaughtSignal
41+
}
3442

3543
/// A name of the endpoint for this connection, used for logging, e.g. `clangd`.
3644
private let name: String
@@ -198,7 +206,7 @@ public final class JSONRPCConnection: Connection {
198206
protocol messageRegistry: MessageRegistry,
199207
stderrLoggingCategory: String,
200208
client: MessageHandler,
201-
terminationHandler: @Sendable @escaping (_ terminationStatus: Int32) -> Void
209+
terminationHandler: @Sendable @escaping (_ terminationReason: TerminationReason) -> Void
202210
) throws -> (connection: JSONRPCConnection, process: Process) {
203211
let clientToServer = Pipe()
204212
let serverToClient = Pipe()
@@ -238,10 +246,22 @@ public final class JSONRPCConnection: Connection {
238246
process.terminationHandler = { process in
239247
logger.log(
240248
level: process.terminationReason == .exit ? .default : .error,
241-
"\(name) exited: \(String(reflecting: process.terminationReason)) \(process.terminationStatus)"
249+
"\(name) exited: \(process.terminationReason.rawValue) \(process.terminationStatus)"
242250
)
243251
connection.close()
244-
terminationHandler(process.terminationStatus)
252+
let terminationReason: TerminationReason
253+
switch process.terminationReason {
254+
case .exit:
255+
terminationReason = .exited(exitCode: process.terminationStatus)
256+
case .uncaughtSignal:
257+
terminationReason = .uncaughtSignal
258+
@unknown default:
259+
logger.fault(
260+
"Process terminated with unknown termination reason: \(process.terminationReason.rawValue, privacy: .public)"
261+
)
262+
terminationReason = .exited(exitCode: 0)
263+
}
264+
terminationHandler(terminationReason)
245265
}
246266
try process.run()
247267

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,17 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
422422
return .seconds(120)
423423
}
424424

425+
/// If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging
426+
/// for some reason and won't recover. To restore semantic functionality, we terminate and restart it.
427+
public var semanticServiceRestartTimeout: Double? = nil
428+
429+
public var semanticServiceRestartTimeoutOrDefault: Duration {
430+
if let semanticServiceRestartTimeout {
431+
return .seconds(semanticServiceRestartTimeout)
432+
}
433+
return .seconds(300)
434+
}
435+
425436
public init(
426437
swiftPM: SwiftPMOptions? = .init(),
427438
fallbackBuildSystem: FallbackBuildSystemOptions? = .init(),
@@ -439,7 +450,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
439450
experimentalFeatures: Set<ExperimentalFeature>? = nil,
440451
swiftPublishDiagnosticsDebounceDuration: Double? = nil,
441452
workDoneProgressDebounceDuration: Double? = nil,
442-
sourcekitdRequestTimeout: Double? = nil
453+
sourcekitdRequestTimeout: Double? = nil,
454+
semanticServiceRestartTimeout: Double? = nil
443455
) {
444456
self.swiftPM = swiftPM
445457
self.fallbackBuildSystem = fallbackBuildSystem
@@ -458,6 +470,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
458470
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
459471
self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration
460472
self.sourcekitdRequestTimeout = sourcekitdRequestTimeout
473+
self.semanticServiceRestartTimeout = semanticServiceRestartTimeout
461474
}
462475

463476
public init?(fromLSPAny lspAny: LSPAny?) throws {
@@ -517,7 +530,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
517530
?? base.swiftPublishDiagnosticsDebounceDuration,
518531
workDoneProgressDebounceDuration: override?.workDoneProgressDebounceDuration
519532
?? base.workDoneProgressDebounceDuration,
520-
sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout
533+
sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout,
534+
semanticServiceRestartTimeout: override?.semanticServiceRestartTimeout ?? base.semanticServiceRestartTimeout
521535
)
522536
}
523537

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
package import Foundation
14+
import SwiftExtensions
15+
import ToolchainRegistry
16+
import XCTest
17+
18+
import class TSCBasic.Process
19+
20+
/// Compiles the given Swift source code into a binary at `executablePath`.
21+
package func createBinary(_ sourceCode: String, at executablePath: URL) async throws {
22+
try await withTestScratchDir { scratchDir in
23+
let sourceFile = scratchDir.appending(component: "source.swift")
24+
try await sourceCode.writeWithRetry(to: sourceFile)
25+
26+
var compilerArguments = try [
27+
sourceFile.filePath,
28+
"-o",
29+
executablePath.filePath,
30+
]
31+
if let defaultSDKPath {
32+
compilerArguments += ["-sdk", defaultSDKPath]
33+
}
34+
try await Process.checkNonZeroExit(
35+
arguments: [unwrap(ToolchainRegistry.forTesting.default?.swiftc?.filePath)] + compilerArguments
36+
)
37+
}
38+
}

0 commit comments

Comments
 (0)