Skip to content

Commit 041e066

Browse files
authored
Merge pull request #2149 from bnbarham/merge-main-20250512
Merge main into release/6.2
2 parents 9092459 + fde7d70 commit 041e066

File tree

11 files changed

+344
-76
lines changed

11 files changed

+344
-76
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

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/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+
}
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+
}

Sources/SourceKitD/SourceKitD.swift

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -193,52 +193,56 @@ package actor SourceKitD {
193193
let dlopenModes: DLOpenFlags = [.lazy, .local, .first]
194194
#endif
195195
let dlhandle = try dlopen(path.filePath, mode: dlopenModes)
196-
do {
197-
try self.init(
198-
dlhandle: dlhandle,
199-
path: path,
200-
pluginPaths: pluginPaths,
201-
initialize: initialize
202-
)
203-
} catch {
204-
try? dlhandle.close()
205-
throw error
206-
}
196+
try self.init(
197+
dlhandle: dlhandle,
198+
path: path,
199+
pluginPaths: pluginPaths,
200+
initialize: initialize
201+
)
207202
}
208203

204+
/// Create a `SourceKitD` instance from an existing `DLHandle`. `SourceKitD` takes over ownership of the `DLHandler`
205+
/// and will close it when the `SourceKitD` instance gets deinitialized or if the initializer throws.
209206
package init(dlhandle: DLHandle, path: URL, pluginPaths: PluginPaths?, initialize: Bool) throws {
210-
self.path = path
211-
self.dylib = dlhandle
212-
let api = try sourcekitd_api_functions_t(dlhandle)
213-
self.api = api
214-
215-
// We load the plugin-related functions eagerly so the members are initialized and we don't have data races on first
216-
// access to eg. `pluginApi`. But if one of the functions is missing, we will only emit that error when that family
217-
// of functions is being used. For example, it is expected that the plugin functions are not available in
218-
// SourceKit-LSP.
219-
self.ideApiResult = Result(catching: { try sourcekitd_ide_api_functions_t(dlhandle) })
220-
self.pluginApiResult = Result(catching: { try sourcekitd_plugin_api_functions_t(dlhandle) })
221-
self.servicePluginApiResult = Result(catching: { try sourcekitd_service_plugin_api_functions_t(dlhandle) })
222-
223-
if let pluginPaths {
224-
api.register_plugin_path?(pluginPaths.clientPlugin.path, pluginPaths.servicePlugin.path)
225-
}
226-
if initialize {
227-
self.api.initialize()
228-
}
207+
do {
208+
self.path = path
209+
self.dylib = dlhandle
210+
let api = try sourcekitd_api_functions_t(dlhandle)
211+
self.api = api
212+
213+
// We load the plugin-related functions eagerly so the members are initialized and we don't have data races on first
214+
// access to eg. `pluginApi`. But if one of the functions is missing, we will only emit that error when that family
215+
// of functions is being used. For example, it is expected that the plugin functions are not available in
216+
// SourceKit-LSP.
217+
self.ideApiResult = Result(catching: { try sourcekitd_ide_api_functions_t(dlhandle) })
218+
self.pluginApiResult = Result(catching: { try sourcekitd_plugin_api_functions_t(dlhandle) })
219+
self.servicePluginApiResult = Result(catching: { try sourcekitd_service_plugin_api_functions_t(dlhandle) })
229220

230-
if initialize {
231-
self.api.set_notification_handler { [weak self] rawResponse in
232-
guard let self, let rawResponse else { return }
233-
let response = SKDResponse(rawResponse, sourcekitd: self)
234-
self.notificationHandlingQueue.async {
235-
let handlers = await self.notificationHandlers.compactMap(\.value)
221+
if let pluginPaths {
222+
api.register_plugin_path?(pluginPaths.clientPlugin.path, pluginPaths.servicePlugin.path)
223+
}
224+
if initialize {
225+
self.api.initialize()
226+
}
227+
228+
if initialize {
229+
self.api.set_notification_handler { [weak self] rawResponse in
230+
guard let self, let rawResponse else { return }
231+
let response = SKDResponse(rawResponse, sourcekitd: self)
232+
self.notificationHandlingQueue.async {
233+
let handlers = await self.notificationHandlers.compactMap(\.value)
236234

237-
for handler in handlers {
238-
handler.notification(response)
235+
for handler in handlers {
236+
handler.notification(response)
237+
}
239238
}
240239
}
241240
}
241+
} catch {
242+
orLog("Closing dlhandle after opening sourcekitd failed") {
243+
try? dlhandle.close()
244+
}
245+
throw error
242246
}
243247
}
244248

Sources/SourceKitD/dlopen.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SKLogging
1314
import SwiftExtensions
1415

1516
#if os(Windows)
@@ -45,7 +46,9 @@ package final class DLHandle: Sendable {
4546
}
4647

4748
deinit {
48-
precondition(rawValue.value == nil, "DLHandle must be closed or explicitly leaked before destroying")
49+
if rawValue.value != nil {
50+
logger.fault("DLHandle must be closed or explicitly leaked before destroying")
51+
}
4952
}
5053

5154
/// The handle must not be used anymore after calling `close`.

0 commit comments

Comments
 (0)