Skip to content

Commit d31440c

Browse files
committed
Don’t cause any file system file effects when trying to find an implicit workspace for a file
When looking for a workspace that can handle a file, we were creating full-fledged workspaces along the way, which we would then discard if they couldn’t handle the file being opened. This had multiple problems: 1. When background indexing is enabled, it caused semantic indexing of the workspace, which wrote files to a `.index-build` directory and was a waste of work 2. When background indexing is enabled, it caused package resolution, which also created a `.index-build` folder to be created 3. It caused a syntactic test index of the workspace, which was a waste of work. To fix this, do multiple things: 1. When creating a workspace, add a check right after build system creation. This allows us to early exit if the build system can’t handle the file and prevents us from generating the `Workspace`, fixing (1) and (3) 2. Don’t call `reloadPackage` when creating a `SwiftPMWorkspace`. Instead, explicitly call `generateBuildGraph` once we committed to creating the workspace.
1 parent ea6a585 commit d31440c

14 files changed

+216
-123
lines changed

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ extension BuildServerBuildSystem: BuildSystem {
279279
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
280280
}
281281

282-
public func generateBuildGraph() {}
282+
public func generateBuildGraph(allowFileSystemWrites: Bool) {}
283283

284284
public func topologicalSort(of targets: [ConfiguredTarget]) async -> [ConfiguredTarget]? {
285285
return nil

Sources/SKCore/BuildSystem.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ public protocol BuildSystem: AnyObject, Sendable {
135135
/// Return the list of targets and run destinations that the given document can be built for.
136136
func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget]
137137

138-
/// Re-generate the build graph including all the tasks that are necessary for building the entire build graph, like
139-
/// resolving package versions.
140-
func generateBuildGraph() async throws
138+
/// Re-generate the build graph.
139+
///
140+
/// If `allowFileSystemWrites` is `true`, this should include all the tasks that are necessary for building the entire
141+
/// build graph, like resolving package versions.
142+
///
143+
/// If `allowFileSystemWrites` is `false`, no files must be written to disk. This mode is used to determine whether
144+
/// the build system can handle a source file, and decide whether a workspace should be opened with this build system
145+
func generateBuildGraph(allowFileSystemWrites: Bool) async throws
141146

142147
/// Sort the targets so that low-level targets occur before high-level targets.
143148
///

Sources/SKCore/BuildSystemManager.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ extension BuildSystemManager {
219219
return settings
220220
}
221221

222-
public func generateBuildGraph() async throws {
223-
try await self.buildSystem?.generateBuildGraph()
222+
public func generateBuildGraph(allowFileSystemWrites: Bool) async throws {
223+
try await self.buildSystem?.generateBuildGraph(allowFileSystemWrites: allowFileSystemWrites)
224224
}
225225

226226
public func topologicalSort(of targets: [ConfiguredTarget]) async throws -> [ConfiguredTarget]? {

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
132132
throw PrepareNotSupportedError()
133133
}
134134

135-
public func generateBuildGraph() {}
135+
public func generateBuildGraph(allowFileSystemWrites: Bool) {}
136136

137137
public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
138138
return nil

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ public actor SwiftPMBuildSystem {
178178
forRootPackage: AbsolutePath(packageRoot),
179179
fileSystem: fileSystem
180180
)
181-
if let scratchDirectory = buildSetup.path {
181+
if isForIndexBuild {
182+
location.scratchDirectory = AbsolutePath(packageRoot.appending(component: ".index-build"))
183+
} else if let scratchDirectory = buildSetup.path {
182184
location.scratchDirectory = AbsolutePath(scratchDirectory)
183185
}
184186

@@ -227,7 +229,6 @@ public actor SwiftPMBuildSystem {
227229
}
228230
await delegate.filesDependenciesUpdated(filesWithUpdatedDependencies)
229231
}
230-
try await reloadPackage()
231232
}
232233

233234
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
@@ -261,13 +262,9 @@ public actor SwiftPMBuildSystem {
261262
}
262263

263264
extension SwiftPMBuildSystem {
264-
public func generateBuildGraph() async throws {
265-
try await self.reloadPackage()
266-
}
267-
268265
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
269266
/// dependencies.
270-
func reloadPackage() async throws {
267+
public func reloadPackage(forceResolvedVersions: Bool) async throws {
271268
await reloadPackageStatusCallback(.start)
272269
defer {
273270
Task {
@@ -277,7 +274,7 @@ extension SwiftPMBuildSystem {
277274

278275
let modulesGraph = try self.workspace.loadPackageGraph(
279276
rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]),
280-
forceResolvedVersions: !isForIndexBuild,
277+
forceResolvedVersions: forceResolvedVersions,
281278
observabilityScope: observabilitySystem.topScope
282279
)
283280

@@ -430,6 +427,10 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
430427
return []
431428
}
432429

430+
public func generateBuildGraph(allowFileSystemWrites: Bool) async throws {
431+
try await self.reloadPackage(forceResolvedVersions: !isForIndexBuild || !allowFileSystemWrites)
432+
}
433+
433434
public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
434435
return targets.sorted { (lhs: ConfiguredTarget, rhs: ConfiguredTarget) -> Bool in
435436
let lhsIndex = self.targets[lhs]?.index ?? self.targets.count
@@ -590,7 +591,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
590591
logger.log("Reloading package because of file change")
591592
await orLog("Reloading package") {
592593
// TODO: It should not be necessary to reload the entire package just to get build settings for one file.
593-
try await self.reloadPackage()
594+
try await self.reloadPackage(forceResolvedVersions: !isForIndexBuild)
594595
}
595596
}
596597

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ public final actor SemanticIndexManager {
192192
defer {
193193
signposter.endInterval("Preparing", state)
194194
}
195-
await orLog("Generating build graph") { try await self.buildSystemManager.generateBuildGraph() }
195+
await orLog("Generating build graph") {
196+
try await self.buildSystemManager.generateBuildGraph(allowFileSystemWrites: true)
197+
}
196198
// Ensure that we have an up-to-date indexstore-db. Waiting for the indexstore-db to be updated is cheaper than
197199
// potentially not knowing about unit files, which causes the corresponding source files to be re-indexed.
198200
index.pollForUnitChangesAndWait()

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
add_library(SourceKitLSP STATIC
33
CapabilityRegistry.swift
4+
CreateBuildSystem.swift
45
DocumentManager.swift
56
DocumentSnapshot+FromFileContents.swift
67
IndexProgressManager.swift
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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 LSPLogging
14+
import LanguageServerProtocol
15+
import SKCore
16+
import SKSwiftPMWorkspace
17+
18+
import struct TSCBasic.AbsolutePath
19+
import struct TSCBasic.RelativePath
20+
21+
/// Tries to create a build system for a workspace at the given location, with the given parameters.
22+
func createBuildSystem(
23+
rootUri: DocumentURI,
24+
options: SourceKitLSPServer.Options,
25+
toolchainRegistry: ToolchainRegistry,
26+
reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void
27+
) async -> BuildSystem? {
28+
guard let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) else {
29+
// We assume that workspaces are directories. This is only true for URLs not for URIs in general.
30+
// Simply skip setting up the build integration in this case.
31+
logger.error(
32+
"cannot setup build integration at URI \(rootUri.forLogging) because the URI it is not a valid file URL"
33+
)
34+
return nil
35+
}
36+
func createSwiftPMBuildSystem(rootUrl: URL) async -> SwiftPMBuildSystem? {
37+
return await SwiftPMBuildSystem(
38+
url: rootUrl,
39+
toolchainRegistry: toolchainRegistry,
40+
buildSetup: options.buildSetup,
41+
isForIndexBuild: options.indexOptions.enableBackgroundIndexing,
42+
reloadPackageStatusCallback: reloadPackageStatusCallback
43+
)
44+
}
45+
46+
func createCompilationDatabaseBuildSystem(rootPath: AbsolutePath) -> CompilationDatabaseBuildSystem? {
47+
return CompilationDatabaseBuildSystem(
48+
projectRoot: rootPath,
49+
searchPaths: options.compilationDatabaseSearchPaths
50+
)
51+
}
52+
53+
func createBuildServerBuildSystem(rootPath: AbsolutePath) async -> BuildServerBuildSystem? {
54+
return await BuildServerBuildSystem(projectRoot: rootPath, buildSetup: options.buildSetup)
55+
}
56+
57+
let defaultBuildSystem: BuildSystem? =
58+
switch options.buildSetup.defaultWorkspaceType {
59+
case .buildServer: await createBuildServerBuildSystem(rootPath: rootPath)
60+
case .compilationDatabase: createCompilationDatabaseBuildSystem(rootPath: rootPath)
61+
case .swiftPM: await createSwiftPMBuildSystem(rootUrl: rootUrl)
62+
case nil: nil
63+
}
64+
let buildSystem: BuildSystem
65+
if let defaultBuildSystem {
66+
buildSystem = defaultBuildSystem
67+
} else if let buildServer = await createBuildServerBuildSystem(rootPath: rootPath) {
68+
buildSystem = buildServer
69+
} else if let swiftpm = await createSwiftPMBuildSystem(rootUrl: rootUrl) {
70+
buildSystem = swiftpm
71+
} else if let compdb = createCompilationDatabaseBuildSystem(rootPath: rootPath) {
72+
buildSystem = compdb
73+
} else {
74+
logger.error("Could not set up a build system at '\(rootUri.forLogging)'")
75+
return nil
76+
}
77+
return buildSystem
78+
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -591,11 +591,19 @@ public actor SourceKitLSPServer {
591591
// The latter might happen if there is an existing SwiftPM workspace that hasn't been reloaded after a new file
592592
// was added to it and thus currently doesn't know that it can handle that file. In that case, we shouldn't open
593593
// a new workspace for the same root. Instead, the existing workspace's build system needs to be reloaded.
594-
if let workspace = await self.createWorkspace(WorkspaceFolder(uri: DocumentURI(url))),
595-
await workspace.buildSystemManager.fileHandlingCapability(for: uri) == .handled,
596-
let projectRoot = await workspace.buildSystemManager.projectRoot,
597-
!projectRoots.contains(projectRoot)
598-
{
594+
let workspace = await self.createWorkspace(WorkspaceFolder(uri: DocumentURI(url))) { buildSystem in
595+
guard let buildSystem else {
596+
return false
597+
}
598+
do {
599+
try await buildSystem.generateBuildGraph(allowFileSystemWrites: false)
600+
} catch {
601+
return false
602+
}
603+
let projectRoot = await buildSystem.projectRoot
604+
return await buildSystem.fileHandlingCapability(for: uri) == .handled && !projectRoots.contains(projectRoot)
605+
}
606+
if let workspace {
599607
return workspace
600608
}
601609
url.deleteLastPathComponent()
@@ -1223,25 +1231,22 @@ extension SourceKitLSPServer {
12231231
}
12241232

12251233
/// Creates a workspace at the given `uri`.
1226-
private func createWorkspace(_ workspaceFolder: WorkspaceFolder) async -> Workspace? {
1234+
///
1235+
/// If the build system that was determined for the workspace does not satisfy `condition`, `nil` is returned.
1236+
private func createWorkspace(
1237+
_ workspaceFolder: WorkspaceFolder,
1238+
condition: (BuildSystem?) async -> Bool = { _ in true }
1239+
) async -> Workspace? {
12271240
guard let capabilityRegistry = capabilityRegistry else {
12281241
logger.log("Cannot open workspace before server is initialized")
12291242
return nil
12301243
}
12311244
var options = self.options
12321245
options.buildSetup = self.options.buildSetup.merging(buildSetup(for: workspaceFolder))
1233-
return try? await Workspace(
1234-
documentManager: self.documentManager,
1246+
let buildSystem = await createBuildSystem(
12351247
rootUri: workspaceFolder.uri,
1236-
capabilityRegistry: capabilityRegistry,
1237-
toolchainRegistry: self.toolchainRegistry,
12381248
options: options,
1239-
compilationDatabaseSearchPaths: self.options.compilationDatabaseSearchPaths,
1240-
indexOptions: self.options.indexOptions,
1241-
indexTaskScheduler: indexTaskScheduler,
1242-
indexProcessDidProduceResult: { [weak self] in
1243-
self?.indexTaskDidProduceResult($0)
1244-
},
1249+
toolchainRegistry: toolchainRegistry,
12451250
reloadPackageStatusCallback: { [weak self] status in
12461251
guard let self else { return }
12471252
switch status {
@@ -1250,6 +1255,34 @@ extension SourceKitLSPServer {
12501255
case .end:
12511256
await self.packageLoadingWorkDoneProgress.endProgress(server: self)
12521257
}
1258+
}
1259+
)
1260+
guard await condition(buildSystem) else {
1261+
return nil
1262+
}
1263+
do {
1264+
try await buildSystem?.generateBuildGraph(allowFileSystemWrites: true)
1265+
} catch {
1266+
logger.error("failed to generate build graph at \(workspaceFolder.uri.forLogging): \(error.forLogging)")
1267+
return nil
1268+
}
1269+
1270+
let projectRoot = await buildSystem?.projectRoot.pathString
1271+
logger.log(
1272+
"Created workspace at \(workspaceFolder.uri.forLogging) as \(type(of: buildSystem)) with project root \(projectRoot ?? "<nil>")"
1273+
)
1274+
1275+
return try? await Workspace(
1276+
documentManager: self.documentManager,
1277+
rootUri: workspaceFolder.uri,
1278+
capabilityRegistry: capabilityRegistry,
1279+
buildSystem: buildSystem,
1280+
toolchainRegistry: self.toolchainRegistry,
1281+
options: options,
1282+
indexOptions: self.options.indexOptions,
1283+
indexTaskScheduler: indexTaskScheduler,
1284+
indexProcessDidProduceResult: { [weak self] in
1285+
self?.indexTaskDidProduceResult($0)
12531286
},
12541287
indexTasksWereScheduled: { [weak self] count in
12551288
self?.indexProgressManager.indexTasksWereScheduled(count: count)

Sources/SourceKitLSP/Workspace.swift

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import LSPLogging
1515
import LanguageServerProtocol
1616
import SKCore
1717
import SKSupport
18-
import SKSwiftPMWorkspace
1918
import SemanticIndex
2019

2120
import struct TSCBasic.AbsolutePath
@@ -148,82 +147,15 @@ public final class Workspace: Sendable {
148147
documentManager: DocumentManager,
149148
rootUri: DocumentURI,
150149
capabilityRegistry: CapabilityRegistry,
150+
buildSystem: BuildSystem?,
151151
toolchainRegistry: ToolchainRegistry,
152152
options: SourceKitLSPServer.Options,
153-
compilationDatabaseSearchPaths: [RelativePath],
154153
indexOptions: IndexOptions = IndexOptions(),
155154
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
156155
indexProcessDidProduceResult: @escaping @Sendable (IndexProcessResult) -> Void,
157-
reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void,
158156
indexTasksWereScheduled: @Sendable @escaping (Int) -> Void,
159157
indexStatusDidChange: @Sendable @escaping () -> Void
160158
) async throws {
161-
var buildSystem: BuildSystem? = nil
162-
163-
if let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) {
164-
var options = options
165-
var isForIndexBuild = false
166-
if options.indexOptions.enableBackgroundIndexing, options.buildSetup.path == nil {
167-
options.buildSetup.path = rootPath.appending(component: ".index-build")
168-
isForIndexBuild = true
169-
}
170-
func createSwiftPMBuildSystem(rootUrl: URL) async -> SwiftPMBuildSystem? {
171-
return await SwiftPMBuildSystem(
172-
url: rootUrl,
173-
toolchainRegistry: toolchainRegistry,
174-
buildSetup: options.buildSetup,
175-
isForIndexBuild: isForIndexBuild,
176-
reloadPackageStatusCallback: reloadPackageStatusCallback
177-
)
178-
}
179-
180-
func createCompilationDatabaseBuildSystem(rootPath: AbsolutePath) -> CompilationDatabaseBuildSystem? {
181-
return CompilationDatabaseBuildSystem(
182-
projectRoot: rootPath,
183-
searchPaths: compilationDatabaseSearchPaths
184-
)
185-
}
186-
187-
func createBuildServerBuildSystem(rootPath: AbsolutePath) async -> BuildServerBuildSystem? {
188-
return await BuildServerBuildSystem(projectRoot: rootPath, buildSetup: options.buildSetup)
189-
}
190-
191-
let defaultBuildSystem: BuildSystem? =
192-
switch options.buildSetup.defaultWorkspaceType {
193-
case .buildServer: await createBuildServerBuildSystem(rootPath: rootPath)
194-
case .compilationDatabase: createCompilationDatabaseBuildSystem(rootPath: rootPath)
195-
case .swiftPM: await createSwiftPMBuildSystem(rootUrl: rootUrl)
196-
case nil: nil
197-
}
198-
if let defaultBuildSystem {
199-
buildSystem = defaultBuildSystem
200-
} else if let buildServer = await createBuildServerBuildSystem(rootPath: rootPath) {
201-
buildSystem = buildServer
202-
} else if let swiftpm = await createSwiftPMBuildSystem(rootUrl: rootUrl) {
203-
buildSystem = swiftpm
204-
} else if let compdb = createCompilationDatabaseBuildSystem(rootPath: rootPath) {
205-
buildSystem = compdb
206-
} else {
207-
buildSystem = nil
208-
}
209-
if let buildSystem {
210-
let projectRoot = await buildSystem.projectRoot
211-
logger.log(
212-
"Opening workspace at \(rootUrl) as \(type(of: buildSystem)) with project root \(projectRoot.pathString)"
213-
)
214-
} else {
215-
logger.error(
216-
"Could not set up a build system for workspace at '\(rootUri.forLogging)'"
217-
)
218-
}
219-
} else {
220-
// We assume that workspaces are directories. This is only true for URLs not for URIs in general.
221-
// Simply skip setting up the build integration in this case.
222-
logger.error(
223-
"cannot setup build integration for workspace at URI \(rootUri.forLogging) because the URI it is not a valid file URL"
224-
)
225-
}
226-
227159
var index: IndexStoreDB? = nil
228160
var indexDelegate: SourceKitIndexDelegate? = nil
229161

0 commit comments

Comments
 (0)