Skip to content

Commit 64865b8

Browse files
committed
Support background preparation of targets
1 parent 80694a3 commit 64865b8

17 files changed

+651
-42
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: 77 additions & 4 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.
@@ -395,6 +405,72 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
395405
return []
396406
}
397407

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

490566
public func sourceFiles() -> [SourceFileInfo] {
491567
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-
}
496568
// We should only set mayContainTests to `true` for files from test targets
497569
// (https://github.com/apple/sourcekit-lsp/issues/1174).
498570
return SourceFileInfo(
499571
uri: DocumentURI(path.asURL),
572+
isPartOfRootProject: target.isPartOfRootPackage,
500573
mayContainTests: true
501574
)
502575
}

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)