Skip to content

Commit 0e0ef0d

Browse files
authored
Merge pull request #1273 from ahoppen/background-preparation
2 parents d4a42ea + 740262c commit 0e0ef0d

19 files changed

+731
-75
lines changed

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,16 @@ extension BuildServerBuildSystem: BuildSystem {
279279
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
280280
}
281281

282+
public func generateBuildGraph() {}
283+
284+
public func topologicalSort(of targets: [ConfiguredTarget]) async -> [ConfiguredTarget]? {
285+
return nil
286+
}
287+
288+
public func prepare(targets: [ConfiguredTarget]) async throws {
289+
throw PrepareNotSupportedError()
290+
}
291+
282292
public func registerForChangeNotifications(for uri: DocumentURI) {
283293
let request = RegisterForChanges(uri: uri, action: .register)
284294
_ = self.buildServer?.send(request) { result in

Sources/SKCore/BuildSystem.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@ public struct SourceFileInfo: Sendable {
3131
/// The URI of the source file.
3232
public let uri: DocumentURI
3333

34+
/// `true` if this file belongs to the root project that the user is working on. It is false, if the file belongs
35+
/// to a dependency of the project.
36+
public let isPartOfRootProject: Bool
37+
3438
/// Whether the file might contain test cases. This property is an over-approximation. It might be true for files
3539
/// from non-test targets or files that don't actually contain any tests. Keeping this list of files with
3640
/// `mayContainTets` minimal as possible helps reduce the amount of work that the syntactic test indexer needs to
3741
/// perform.
3842
public let mayContainTests: Bool
3943

40-
public init(uri: DocumentURI, mayContainTests: Bool) {
44+
public init(uri: DocumentURI, isPartOfRootProject: Bool, mayContainTests: Bool) {
4145
self.uri = uri
46+
self.isPartOfRootProject = isPartOfRootProject
4247
self.mayContainTests = mayContainTests
4348
}
4449
}
@@ -64,6 +69,13 @@ public struct ConfiguredTarget: Hashable, Sendable {
6469
}
6570
}
6671

72+
/// An error build systems can throw from `prepare` if they don't support preparation of targets.
73+
public struct PrepareNotSupportedError: Error, CustomStringConvertible {
74+
public init() {}
75+
76+
public var description: String { "Preparation not supported" }
77+
}
78+
6779
/// Provider of FileBuildSettings and other build-related information.
6880
///
6981
/// The primary role of the build system is to answer queries for
@@ -114,6 +126,22 @@ public protocol BuildSystem: AnyObject, Sendable {
114126
/// Return the list of targets and run destinations that the given document can be built for.
115127
func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget]
116128

129+
/// Re-generate the build graph including all the tasks that are necessary for building the entire build graph, like
130+
/// resolving package versions.
131+
func generateBuildGraph() async throws
132+
133+
/// Sort the targets so that low-level targets occur before high-level targets.
134+
///
135+
/// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows
136+
/// index data to be available earlier.
137+
///
138+
/// `nil` if the build system doesn't support topological sorting of targets.
139+
func topologicalSort(of targets: [ConfiguredTarget]) async -> [ConfiguredTarget]?
140+
141+
/// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target
142+
/// dependencies.
143+
func prepare(targets: [ConfiguredTarget]) async throws
144+
117145
/// If the build system has knowledge about the language that this document should be compiled in, return it.
118146
///
119147
/// This is used to determine the language in which a source file should be background indexed.
@@ -146,5 +174,3 @@ public protocol BuildSystem: AnyObject, Sendable {
146174
/// The callback might also be called without an actual change to `sourceFiles`.
147175
func addSourceFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async
148176
}
149-
150-
public let buildTargetsNotSupported = ResponseError.methodNotFound(BuildTargets.method)

Sources/SKCore/BuildSystemManager.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ extension BuildSystemManager {
208208
return settings
209209
}
210210

211+
public func generateBuildGraph() async throws {
212+
try await self.buildSystem?.generateBuildGraph()
213+
}
214+
215+
public func topologicalSort(of targets: [ConfiguredTarget]) async throws -> [ConfiguredTarget]? {
216+
return await buildSystem?.topologicalSort(of: targets)
217+
}
218+
219+
public func prepare(targets: [ConfiguredTarget]) async throws {
220+
try await buildSystem?.prepare(targets: targets)
221+
}
222+
211223
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
212224
logger.debug("registerForChangeNotifications(\(uri.forLogging))")
213225
let mainFile = await mainFile(for: uri, language: language)
@@ -247,7 +259,7 @@ extension BuildSystemManager {
247259

248260
public func testFiles() async -> [DocumentURI] {
249261
return await sourceFiles().compactMap { (info: SourceFileInfo) -> DocumentURI? in
250-
guard info.mayContainTests else {
262+
guard info.isPartOfRootProject, info.mayContainTests else {
251263
return nil
252264
}
253265
return info.uri

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
125125
return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")]
126126
}
127127

128+
public func prepare(targets: [ConfiguredTarget]) async throws {
129+
throw PrepareNotSupportedError()
130+
}
131+
132+
public func generateBuildGraph() {}
133+
134+
public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
135+
return nil
136+
}
137+
128138
public func registerForChangeNotifications(for uri: DocumentURI) async {
129139
self.watchedFiles.insert(uri)
130140
}
@@ -208,7 +218,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
208218
return []
209219
}
210220
return compdb.allCommands.map {
211-
SourceFileInfo(uri: DocumentURI($0.url), mayContainTests: true)
221+
SourceFileInfo(uri: DocumentURI($0.url), isPartOfRootProject: true, mayContainTests: true)
212222
}
213223
}
214224

Sources/SKSupport/Collection+PartitionIntoBatches.swift

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

13-
public extension Collection {
13+
public extension Collection where Index == Int {
1414
/// Partition the elements of the collection into `numberOfBatches` roughly equally sized batches.
1515
///
1616
/// Elements are assigned to the batches round-robin. This ensures that elements that are close to each other in the
@@ -32,4 +32,18 @@ public extension Collection {
3232
}
3333
return batches.filter { !$0.isEmpty }
3434
}
35+
36+
/// Partition the collection into batches that have a maximum size of `batchSize`.
37+
///
38+
/// The last batch will contain the remainder elements.
39+
func partition(intoBatchesOfSize batchSize: Int) -> [[Element]] {
40+
var batches: [[Element]] = []
41+
batches.reserveCapacity(self.count / batchSize)
42+
var lastIndex = self.startIndex
43+
for index in stride(from: self.startIndex, to: self.endIndex, by: batchSize).dropFirst() + [self.endIndex] {
44+
batches.append(Array(self[lastIndex..<index]))
45+
lastIndex = index
46+
}
47+
return batches
48+
}
3549
}

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 110 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ public actor SwiftPMBuildSystem {
100100

101101
var fileToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
102102
var sourceDirToTarget: [AbsolutePath: SwiftBuildTarget] = [:]
103-
var targets: [SwiftBuildTarget] = []
103+
104+
/// Maps target ids (aka. `ConfiguredTarget.targetID`) to their SwiftPM build target as well as an index in their
105+
/// topological sorting. Targets with lower index are more low level, ie. targets with higher indices depend on
106+
/// targets with lower indices.
107+
var targets: [String: (index: Int, buildTarget: SwiftBuildTarget)] = [:]
104108

105109
/// The URIs for which the delegate has registered for change notifications,
106110
/// mapped to the language the delegate specified when registering for change notifications.
@@ -119,6 +123,18 @@ public actor SwiftPMBuildSystem {
119123
/// Force-unwrapped optional because initializing it requires access to `self`.
120124
var fileDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil
121125

126+
/// A `ObservabilitySystem` from `SwiftPM` that logs.
127+
private let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
128+
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
129+
})
130+
131+
/// Whether the SwiftPMBuildSystem may modify `Package.resolved` or not.
132+
///
133+
/// This is `false` if the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
134+
/// user's build. In this case `SwiftPMBuildSystem` is allowed to clone repositories even if no `Package.resolved`
135+
/// exists.
136+
private let forceResolvedVersions: Bool
137+
122138
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
123139
///
124140
/// - Parameters:
@@ -132,11 +148,13 @@ public actor SwiftPMBuildSystem {
132148
toolchainRegistry: ToolchainRegistry,
133149
fileSystem: FileSystem = localFileSystem,
134150
buildSetup: BuildSetup,
151+
forceResolvedVersions: Bool,
135152
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in }
136153
) async throws {
137154
self.workspacePath = workspacePath
138155
self.fileSystem = fileSystem
139156
self.toolchainRegistry = toolchainRegistry
157+
self.forceResolvedVersions = forceResolvedVersions
140158

141159
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
142160
throw Error.noManifest(workspacePath: workspacePath)
@@ -204,7 +222,6 @@ public actor SwiftPMBuildSystem {
204222
}
205223
await delegate.filesDependenciesUpdated(filesWithUpdatedDependencies)
206224
}
207-
208225
try await reloadPackage()
209226
}
210227

@@ -217,6 +234,7 @@ public actor SwiftPMBuildSystem {
217234
url: URL,
218235
toolchainRegistry: ToolchainRegistry,
219236
buildSetup: BuildSetup,
237+
forceResolvedVersions: Bool,
220238
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void
221239
) async {
222240
do {
@@ -225,6 +243,7 @@ public actor SwiftPMBuildSystem {
225243
toolchainRegistry: toolchainRegistry,
226244
fileSystem: localFileSystem,
227245
buildSetup: buildSetup,
246+
forceResolvedVersions: forceResolvedVersions,
228247
reloadPackageStatusCallback: reloadPackageStatusCallback
229248
)
230249
} catch Error.noManifest {
@@ -237,6 +256,9 @@ public actor SwiftPMBuildSystem {
237256
}
238257

239258
extension SwiftPMBuildSystem {
259+
public func generateBuildGraph() async throws {
260+
try await self.reloadPackage()
261+
}
240262

241263
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
242264
/// dependencies.
@@ -248,13 +270,9 @@ extension SwiftPMBuildSystem {
248270
}
249271
}
250272

251-
let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
252-
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
253-
})
254-
255273
let modulesGraph = try self.workspace.loadPackageGraph(
256274
rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]),
257-
forceResolvedVersions: true,
275+
forceResolvedVersions: forceResolvedVersions,
258276
observabilityScope: observabilitySystem.topScope
259277
)
260278

@@ -272,7 +290,15 @@ extension SwiftPMBuildSystem {
272290
/// with only some properties modified.
273291
self.modulesGraph = modulesGraph
274292

275-
self.targets = try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph)
293+
self.targets = Dictionary(
294+
try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph).enumerated().map { (index, target) in
295+
return (key: target.name, (index, target))
296+
},
297+
uniquingKeysWith: { first, second in
298+
logger.fault("Found two targets with the same name \(first.buildTarget.name)")
299+
return second
300+
}
301+
)
276302

277303
self.fileToTarget = [AbsolutePath: SwiftBuildTarget](
278304
modulesGraph.allTargets.flatMap { target in
@@ -343,14 +369,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
343369
return try settings(forPackageManifest: path)
344370
}
345371

346-
let buildTargets = self.targets.filter({ $0.name == configuredTarget.targetID })
347-
if buildTargets.count > 1 {
348-
logger.error("Found multiple targets with name \(configuredTarget.targetID). Picking the first one")
349-
}
350-
guard let buildTarget = buildTargets.first else {
351-
if buildTargets.isEmpty {
352-
logger.error("Did not find target with name \(configuredTarget.targetID)")
353-
}
372+
guard let buildTarget = self.targets[configuredTarget.targetID]?.buildTarget else {
373+
logger.error("Did not find target with name \(configuredTarget.targetID)")
354374
return nil
355375
}
356376

@@ -368,7 +388,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
368388
}
369389

370390
public func defaultLanguage(for document: DocumentURI) async -> Language? {
371-
// TODO (indexing): Query The SwiftPM build system for the document's language
391+
// TODO (indexing): Query The SwiftPM build system for the document's language.
392+
// https://github.com/apple/sourcekit-lsp/issues/1267
372393
return nil
373394
}
374395

@@ -395,6 +416,77 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
395416
return []
396417
}
397418

419+
public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
420+
return targets.sorted { (lhs: ConfiguredTarget, rhs: ConfiguredTarget) -> Bool in
421+
let lhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count
422+
let rhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count
423+
return lhsIndex < rhsIndex
424+
}
425+
}
426+
427+
public func prepare(targets: [ConfiguredTarget]) async throws {
428+
// TODO (indexing): Support preparation of multiple targets at once.
429+
// https://github.com/apple/sourcekit-lsp/issues/1262
430+
for target in targets {
431+
try await prepare(singleTarget: target)
432+
}
433+
}
434+
435+
private func prepare(singleTarget target: ConfiguredTarget) async throws {
436+
// TODO (indexing): Add a proper 'prepare' job in SwiftPM instead of building the target.
437+
// https://github.com/apple/sourcekit-lsp/issues/1254
438+
guard let toolchain = await toolchainRegistry.default else {
439+
logger.error("Not preparing because not toolchain exists")
440+
return
441+
}
442+
guard let swift = toolchain.swift else {
443+
logger.error(
444+
"Not preparing because toolchain at \(toolchain.identifier) does not contain a Swift compiler"
445+
)
446+
return
447+
}
448+
let arguments = [
449+
swift.pathString, "build",
450+
"--scratch-path", self.workspace.location.scratchDirectory.pathString,
451+
"--disable-index-store",
452+
"--target", target.targetID,
453+
]
454+
let process = Process(
455+
arguments: arguments,
456+
workingDirectory: workspacePath
457+
)
458+
try process.launch()
459+
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
460+
switch result.exitStatus.exhaustivelySwitchable {
461+
case .terminated(code: 0):
462+
break
463+
case .terminated(code: let code):
464+
// This most likely happens if there are compilation errors in the source file. This is nothing to worry about.
465+
let stdout = (try? String(bytes: result.output.get(), encoding: .utf8)) ?? "<no stderr>"
466+
let stderr = (try? String(bytes: result.stderrOutput.get(), encoding: .utf8)) ?? "<no stderr>"
467+
logger.debug(
468+
"""
469+
Preparation of target \(target.targetID) terminated with non-zero exit code \(code)
470+
Stderr:
471+
\(stderr)
472+
Stdout:
473+
\(stdout)
474+
"""
475+
)
476+
case .signalled(signal: let signal):
477+
if !Task.isCancelled {
478+
// The indexing job finished with a signal. Could be because the compiler crashed.
479+
// Ignore signal exit codes if this task has been cancelled because the compiler exits with SIGINT if it gets
480+
// interrupted.
481+
logger.error("Preparation of target \(target.targetID) signaled \(signal)")
482+
}
483+
case .abnormal(exception: let exception):
484+
if !Task.isCancelled {
485+
logger.error("Preparation of target \(target.targetID) exited abnormally \(exception)")
486+
}
487+
}
488+
}
489+
398490
public func registerForChangeNotifications(for uri: DocumentURI) async {
399491
self.watchedFiles.insert(uri)
400492
}
@@ -489,14 +581,11 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
489581

490582
public func sourceFiles() -> [SourceFileInfo] {
491583
return fileToTarget.compactMap { (path, target) -> SourceFileInfo? in
492-
guard target.isPartOfRootPackage else {
493-
// Don't consider files from package dependencies as possible test files.
494-
return nil
495-
}
496584
// We should only set mayContainTests to `true` for files from test targets
497585
// (https://github.com/apple/sourcekit-lsp/issues/1174).
498586
return SourceFileInfo(
499587
uri: DocumentURI(path.asURL),
588+
isPartOfRootProject: target.isPartOfRootPackage,
500589
mayContainTests: true
501590
)
502591
}

0 commit comments

Comments
 (0)