Skip to content

Commit f5245bf

Browse files
authored
Merge pull request #1664 from ahoppen/buildgraph-generation-targets
Implicitly trigger build graph generation when creating a `SwiftPMBuildSystem` and migrate `targets(dependingOn:)` and `topologicalSort` to BSP
2 parents 367b19a + cc11fe1 commit f5245bf

File tree

11 files changed

+133
-202
lines changed

11 files changed

+133
-202
lines changed

Sources/BuildSystemIntegration/BuildServerBuildSystem.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -334,18 +334,8 @@ extension BuildServerBuildSystem: BuiltInBuildSystem {
334334
return nil
335335
}
336336

337-
package func scheduleBuildGraphGeneration() {}
338-
339337
package func waitForUpToDateBuildGraph() async {}
340338

341-
package func topologicalSort(of targets: [BuildTargetIdentifier]) async -> [BuildTargetIdentifier]? {
342-
return nil
343-
}
344-
345-
package func targets(dependingOn targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
346-
return nil
347-
}
348-
349339
package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) {
350340
// BuildServerBuildSystem does not support syntactic test discovery or background indexing.
351341
// (https://github.com/swiftlang/sourcekit-lsp/issues/1173).

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
118118

119119
private var cachedTargetSources = RequestCache<BuildTargetSourcesRequest>()
120120

121+
private var cachedTargetDepths: (buildTargets: [BuildTarget], depths: [BuildTargetIdentifier: Int])? = nil
122+
121123
/// The root of the project that this build system manages. For example, for SwiftPM packages, this is the folder
122124
/// containing Package.swift. For compilation databases it is the root folder based on which the compilation database
123125
/// was found.
@@ -481,20 +483,64 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
481483
return settings
482484
}
483485

484-
package func scheduleBuildGraphGeneration() async throws {
485-
try await self.buildSystem?.underlyingBuildSystem.scheduleBuildGraphGeneration()
486-
}
487-
488486
package func waitForUpToDateBuildGraph() async {
489487
await self.buildSystem?.underlyingBuildSystem.waitForUpToDateBuildGraph()
490488
}
491489

490+
/// The root targets of the project have depth of 0 and all target dependencies have a greater depth than the target
491+
// itself.
492+
private func targetDepths(for buildTargets: [BuildTarget]) -> [BuildTargetIdentifier: Int] {
493+
if let cachedTargetDepths, cachedTargetDepths.buildTargets == buildTargets {
494+
return cachedTargetDepths.depths
495+
}
496+
var nonRoots: Set<BuildTargetIdentifier> = []
497+
for buildTarget in buildTargets {
498+
nonRoots.formUnion(buildTarget.dependencies)
499+
}
500+
let targetsById = Dictionary(elements: buildTargets, keyedBy: \.id)
501+
var depths: [BuildTargetIdentifier: Int] = [:]
502+
let rootTargets = buildTargets.filter { !nonRoots.contains($0.id) }
503+
var worksList: [(target: BuildTargetIdentifier, depth: Int)] = rootTargets.map { ($0.id, 0) }
504+
while let (target, depth) = worksList.popLast() {
505+
depths[target] = max(depths[target, default: 0], depth)
506+
for dependency in targetsById[target]?.dependencies ?? [] {
507+
// Check if we have already recorded this target with a greater depth, in which case visiting it again will
508+
// not increase its depth or any of its children.
509+
if depths[target, default: 0] < depth + 1 {
510+
worksList.append((dependency, depth + 1))
511+
}
512+
}
513+
}
514+
cachedTargetDepths = (buildTargets, depths)
515+
return depths
516+
}
517+
518+
/// Sort the targets so that low-level targets occur before high-level targets.
519+
///
520+
/// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows
521+
/// index data to be available earlier.
522+
///
523+
/// `nil` if the build system doesn't support topological sorting of targets.
492524
package func topologicalSort(of targets: [BuildTargetIdentifier]) async throws -> [BuildTargetIdentifier]? {
493-
return await buildSystem?.underlyingBuildSystem.topologicalSort(of: targets)
525+
guard let workspaceTargets = await orLog("Getting build targets for topological sort", { try await buildTargets() })
526+
else {
527+
return nil
528+
}
529+
530+
let depths = targetDepths(for: workspaceTargets)
531+
return targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in
532+
return depths[lhs, default: 0] > depths[rhs, default: 0]
533+
}
494534
}
495535

496-
package func targets(dependingOn targets: [BuildTargetIdentifier]) async -> [BuildTargetIdentifier]? {
497-
return await buildSystem?.underlyingBuildSystem.targets(dependingOn: targets)
536+
/// Returns the list of targets that might depend on the given target and that need to be re-prepared when a file in
537+
/// `target` is modified.
538+
package func targets(dependingOn targetIds: Set<BuildTargetIdentifier>) async -> [BuildTargetIdentifier] {
539+
guard let buildTargets = await orLog("Getting build targets for dependencies", { try await self.buildTargets() })
540+
else {
541+
return []
542+
}
543+
return buildTargets.filter { $0.dependencies.contains(anyIn: targetIds) }.map { $0.id }
498544
}
499545

500546
package func prepare(

Sources/BuildSystemIntegration/BuiltInBuildSystem.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,9 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable {
102102
/// file or if it hasn't computed build settings for the file yet.
103103
func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse?
104104

105-
/// Schedule a task that re-generates the build graph. The function may return before the build graph has finished
106-
/// being generated. If clients need to wait for an up-to-date build graph, they should call
107-
/// `waitForUpToDateBuildGraph` afterwards.
108-
func scheduleBuildGraphGeneration() async throws
109-
110105
/// Wait until the build graph has been loaded.
111106
func waitForUpToDateBuildGraph() async
112107

113-
/// Sort the targets so that low-level targets occur before high-level targets.
114-
///
115-
/// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows
116-
/// index data to be available earlier.
117-
///
118-
/// `nil` if the build system doesn't support topological sorting of targets.
119-
func topologicalSort(of targets: [BuildTargetIdentifier]) async -> [BuildTargetIdentifier]?
120-
121-
/// Returns the list of targets that might depend on the given target and that need to be re-prepared when a file in
122-
/// `target` is modified.
123-
///
124-
/// The returned list can be an over-approximation, in which case the indexer will perform more work than strictly
125-
/// necessary by scheduling re-preparation of a target where it isn't necessary.
126-
///
127-
/// Returning `nil` indicates that all targets should be considered depending on the given target.
128-
func targets(dependingOn targets: [BuildTargetIdentifier]) async -> [BuildTargetIdentifier]?
129-
130108
/// The toolchain that should be used to open the given document.
131109
///
132110
/// If `nil` is returned, then the default toolchain for the given language is used.

Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,18 +157,8 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem {
157157
return nil
158158
}
159159

160-
package func scheduleBuildGraphGeneration() {}
161-
162160
package func waitForUpToDateBuildGraph() async {}
163161

164-
package func topologicalSort(of targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
165-
return nil
166-
}
167-
168-
package func targets(dependingOn targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
169-
return nil
170-
}
171-
172162
private func database(for uri: DocumentURI) -> CompilationDatabase? {
173163
if let url = uri.fileURL, let path = try? AbsolutePath(validating: url.path) {
174164
return database(for: path)

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 40 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@ package actor SwiftPMBuildSystem {
210210
/// Maps source and header files to the target that include them.
211211
private var fileToTargets: [DocumentURI: Set<BuildTargetIdentifier>] = [:]
212212

213-
/// Maps target ids to their SwiftPM build target as well as the depth at which they occur in the build
214-
/// graph. Top level targets on which no other target depends have a depth of `1`. Targets with dependencies have a
215-
/// greater depth.
216-
private var targets: [BuildTargetIdentifier: (buildTarget: SwiftBuildTarget, depth: Int)] = [:]
213+
/// Maps target ids to their SwiftPM build target.
214+
private var swiftPMTargets: [BuildTargetIdentifier: SwiftBuildTarget] = [:]
215+
216+
private var targetDependencies: [BuildTargetIdentifier: Set<BuildTargetIdentifier>] = [:]
217217

218218
static package func projectRoot(
219219
for path: TSCBasic.AbsolutePath,
@@ -361,6 +361,15 @@ package actor SwiftPMBuildSystem {
361361
)
362362

363363
self.reloadPackageStatusCallback = reloadPackageStatusCallback
364+
365+
packageLoadingQueue.async {
366+
await orLog("Initial package loading") {
367+
// Schedule an initial generation of the build graph. Once the build graph is loaded, the build system will send
368+
// call `fileHandlingCapabilityChanged`, which allows us to move documents to a workspace with this build
369+
// system.
370+
try await self.reloadPackageAssumingOnPackageLoadingQueue()
371+
}
372+
}
364373
}
365374

366375
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
@@ -398,15 +407,9 @@ package actor SwiftPMBuildSystem {
398407
extension SwiftPMBuildSystem {
399408
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
400409
/// dependencies.
401-
@discardableResult
402-
package func schedulePackageReload() -> Task<Void, Swift.Error> {
403-
return packageLoadingQueue.asyncThrowing {
404-
try await self.reloadPackageImpl()
405-
}
406-
}
407-
410+
///
408411
/// - Important: Must only be called on `packageLoadingQueue`.
409-
private func reloadPackageImpl() async throws {
412+
private func reloadPackageAssumingOnPackageLoadingQueue() async throws {
410413
await reloadPackageStatusCallback(.start)
411414
await testHooks.reloadPackageDidStart?()
412415
defer {
@@ -437,22 +440,26 @@ extension SwiftPMBuildSystem {
437440
/// properties because otherwise we might end up in an inconsistent state
438441
/// with only some properties modified.
439442

440-
self.targets = [:]
443+
self.swiftPMTargets = [:]
441444
self.fileToTargets = [:]
445+
self.targetDependencies = [:]
446+
442447
buildDescription.traverseModules { buildTarget, parent, depth in
443448
let targetIdentifier = orLog("Getting build target identifier") { try BuildTargetIdentifier(buildTarget) }
444449
guard let targetIdentifier else {
445450
return
446451
}
447-
var depth = depth
448-
if let existingDepth = targets[targetIdentifier]?.depth {
449-
depth = max(existingDepth, depth)
450-
} else {
452+
if swiftPMTargets[targetIdentifier] == nil {
451453
for source in buildTarget.sources + buildTarget.headers {
452454
fileToTargets[DocumentURI(source), default: []].insert(targetIdentifier)
453455
}
454456
}
455-
targets[targetIdentifier] = (buildTarget, depth)
457+
if let parent,
458+
let parentIdentifier = orLog("Getting parent build target identifier", { try BuildTargetIdentifier(parent) })
459+
{
460+
self.targetDependencies[parentIdentifier, default: []].insert(targetIdentifier)
461+
}
462+
swiftPMTargets[targetIdentifier] = buildTarget
456463
}
457464

458465
await messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil))
@@ -511,9 +518,9 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
511518
}
512519

513520
package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse {
514-
let targets = self.targets.map { (targetId, target) in
521+
let targets = self.swiftPMTargets.map { (targetId, target) in
515522
var tags: [BuildTargetTag] = [.test]
516-
if target.depth != 1 {
523+
if !target.isPartOfRootPackage {
517524
tags.append(.dependency)
518525
}
519526
return BuildTarget(
@@ -524,8 +531,7 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
524531
capabilities: BuildTargetCapabilities(),
525532
// Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property.
526533
languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift],
527-
// FIXME: (BSP migration) List the target's dependencies
528-
dependencies: []
534+
dependencies: self.targetDependencies[targetId, default: []].sorted { $0.uri.stringValue < $1.uri.stringValue }
529535
)
530536
}
531537
return BuildTargetsResponse(targets: targets)
@@ -536,10 +542,10 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
536542
// TODO: Query The SwiftPM build system for the document's language and add it to SourceItem.data
537543
// (https://github.com/swiftlang/sourcekit-lsp/issues/1267)
538544
for target in request.targets {
539-
guard let swiftPMTarget = self.targets[target] else {
545+
guard let swiftPMTarget = self.swiftPMTargets[target] else {
540546
continue
541547
}
542-
let sources = swiftPMTarget.buildTarget.sources.map {
548+
let sources = swiftPMTarget.sources.map {
543549
SourceItem(uri: DocumentURI($0), kind: .file, generated: false)
544550
}
545551
result.append(SourcesItem(target: target, sources: sources))
@@ -557,13 +563,13 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
557563
return try settings(forPackageManifest: path)
558564
}
559565

560-
guard let buildTarget = self.targets[request.target]?.buildTarget else {
566+
guard let swiftPMTarget = self.swiftPMTargets[request.target] else {
561567
logger.fault("Did not find target \(request.target.forLogging)")
562568
return nil
563569
}
564570

565-
if !buildTarget.sources.lazy.map(DocumentURI.init).contains(request.textDocument.uri),
566-
let substituteFile = buildTarget.sources.sorted(by: { $0.path < $1.path }).first
571+
if !swiftPMTarget.sources.lazy.map(DocumentURI.init).contains(request.textDocument.uri),
572+
let substituteFile = swiftPMTarget.sources.sorted(by: { $0.path < $1.path }).first
567573
{
568574
logger.info("Getting compiler arguments for \(url) using substitute file \(substituteFile)")
569575
// If `url` is not part of the target's source, it's most likely a header file. Fake compiler arguments for it
@@ -574,7 +580,7 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
574580
// getting its compiler arguments and then patching up the compiler arguments by replacing the substitute file
575581
// with the `.cpp` file.
576582
let buildSettings = FileBuildSettings(
577-
compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: buildTarget),
583+
compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget),
578584
workingDirectory: projectRoot.pathString
579585
).patching(newFile: try resolveSymlinks(path).pathString, originalFile: substituteFile.absoluteString)
580586
return SourceKitOptionsResponse(
@@ -584,7 +590,7 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
584590
}
585591

586592
return SourceKitOptionsResponse(
587-
compilerArguments: try await compilerArguments(for: request.textDocument.uri, in: buildTarget),
593+
compilerArguments: try await compilerArguments(for: request.textDocument.uri, in: swiftPMTarget),
588594
workingDirectory: projectRoot.pathString
589595
)
590596
}
@@ -620,43 +626,10 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
620626
return InverseSourcesResponse(targets: targets(for: request.textDocument.uri))
621627
}
622628

623-
package func scheduleBuildGraphGeneration() async throws {
624-
self.schedulePackageReload()
625-
}
626-
627629
package func waitForUpToDateBuildGraph() async {
628630
await self.packageLoadingQueue.async {}.valuePropagatingCancellation
629631
}
630632

631-
package func topologicalSort(of targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
632-
return targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in
633-
let lhsDepth = self.targets[lhs]?.depth ?? 0
634-
let rhsDepth = self.targets[rhs]?.depth ?? 0
635-
return lhsDepth > rhsDepth
636-
}
637-
}
638-
639-
package func targets(dependingOn targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
640-
let targetDepths = targets.compactMap { self.targets[$0]?.depth }
641-
let minimumTargetDepth: Int?
642-
if targetDepths.count == targets.count {
643-
minimumTargetDepth = targetDepths.max()
644-
} else {
645-
// One of the targets didn't have an entry in self.targets. We don't know what might depend on it.
646-
minimumTargetDepth = nil
647-
}
648-
649-
// Files that occur before the target in the topological sorting don't depend on it.
650-
// Ideally, we should consult the dependency graph here for more accurate dependency analysis instead of relying on
651-
// a flattened list (https://github.com/swiftlang/sourcekit-lsp/issues/1312).
652-
return self.targets.compactMap { (targets, value) -> BuildTargetIdentifier? in
653-
if let minimumTargetDepth, value.depth >= minimumTargetDepth {
654-
return nil
655-
}
656-
return targets
657-
}
658-
}
659-
660633
package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse {
661634
// TODO: Support preparation of multiple targets at once. (https://github.com/swiftlang/sourcekit-lsp/issues/1262)
662635
for target in request.targets {
@@ -810,25 +783,12 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
810783
package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async {
811784
if notification.changes.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) {
812785
logger.log("Reloading package because of file change")
813-
await orLog("Reloading package") {
814-
try await self.schedulePackageReload().value
815-
}
816-
}
817-
}
818-
819-
package func sourceFiles() -> [SourceFileInfo] {
820-
var sourceFiles: [DocumentURI: SourceFileInfo] = [:]
821-
for (buildTarget, depth) in self.targets.values {
822-
for sourceFile in buildTarget.sources {
823-
let uri = DocumentURI(sourceFile)
824-
sourceFiles[uri] = SourceFileInfo(
825-
uri: uri,
826-
isPartOfRootProject: depth == 1 || (sourceFiles[uri]?.isPartOfRootProject ?? false),
827-
mayContainTests: true
828-
)
829-
}
786+
await packageLoadingQueue.async {
787+
await orLog("Reloading package") {
788+
try await self.reloadPackageAssumingOnPackageLoadingQueue()
789+
}
790+
}.valuePropagatingCancellation
830791
}
831-
return sourceFiles.values.sorted { $0.uri.pseudoPath < $1.uri.pseudoPath }
832792
}
833793

834794
package func addSourceFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async {

Sources/BuildSystemIntegration/TestBuildSystem.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,11 @@ package actor TestBuildSystem: BuiltInBuildSystem {
7474
return nil
7575
}
7676

77-
package func scheduleBuildGraphGeneration() {}
78-
7977
package func waitForUpToDateBuildGraph() async {}
8078

8179
package func topologicalSort(of targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
8280
return nil
8381
}
8482

85-
package func targets(dependingOn targets: [BuildTargetIdentifier]) -> [BuildTargetIdentifier]? {
86-
return nil
87-
}
88-
8983
package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) async {}
9084
}

0 commit comments

Comments
 (0)