Skip to content

Commit 5e83d7d

Browse files
committed
Support background preparation of targets
1 parent d4a42ea commit 5e83d7d

18 files changed

+661
-43
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) ?? 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: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ public actor SwiftPMBuildSystem {
237237
}
238238

239239
extension SwiftPMBuildSystem {
240+
public func generateBuildGraph() async throws {
241+
let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
242+
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
243+
})
244+
try self.workspace.resolve(
245+
root: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]),
246+
observabilityScope: observabilitySystem.topScope
247+
)
248+
try await self.reloadPackage()
249+
}
240250

241251
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
242252
/// dependencies.
@@ -368,7 +378,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
368378
}
369379

370380
public func defaultLanguage(for document: DocumentURI) async -> Language? {
371-
// TODO (indexing): Query The SwiftPM build system for the document's language
381+
// TODO (indexing): Query The SwiftPM build system for the document's language.
382+
// https://github.com/apple/sourcekit-lsp/issues/1267
372383
return nil
373384
}
374385

@@ -395,6 +406,77 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
395406
return []
396407
}
397408

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

490572
public func sourceFiles() -> [SourceFileInfo] {
491573
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-
}
496574
// We should only set mayContainTests to `true` for files from test targets
497575
// (https://github.com/apple/sourcekit-lsp/issues/1174).
498576
return SourceFileInfo(
499577
uri: DocumentURI(path.asURL),
578+
isPartOfRootProject: target.isPartOfRootPackage,
500579
mayContainTests: true
501580
)
502581
}

Sources/SKTestSupport/FileManager+findFiles.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,16 @@ extension FileManager {
2424
}
2525
return result
2626
}
27+
28+
/// Returns the URLs of all files with the given file extension in the given directory (recursively).
29+
public func findFiles(named name: String, in directory: URL) -> [URL] {
30+
var result: [URL] = []
31+
let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil)
32+
while let url = enumerator?.nextObject() as? URL {
33+
if url.lastPathComponent == name {
34+
result.append(url)
35+
}
36+
}
37+
return result
38+
}
2739
}

Sources/SKTestSupport/SwiftPMDependencyProject.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ public class SwiftPMDependencyProject {
7171
var files = files
7272
files["Package.swift"] = manifest
7373

74-
for (fileLocation, contents) in files {
74+
for (fileLocation, markedContents) in files {
7575
let fileURL = fileLocation.url(relativeTo: packageDirectory)
7676
try FileManager.default.createDirectory(
7777
at: fileURL.deletingLastPathComponent(),
7878
withIntermediateDirectories: true
7979
)
80-
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
80+
try extractMarkers(markedContents).textWithoutMarkers.write(to: fileURL, atomically: true, encoding: .utf8)
8181
}
8282

8383
try await runGitCommand(["init"], workingDirectory: packageDirectory)

Sources/SemanticIndex/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
add_library(SemanticIndex STATIC
33
CheckedIndex.swift
4+
IndexTaskDescription.swift
5+
PreparationTaskDescription.swift
46
SemanticIndexManager.swift
57
UpdateIndexStoreTaskDescription.swift
68
)

0 commit comments

Comments
 (0)