Skip to content

Commit a045c76

Browse files
authored
Merge pull request #1621 from ahoppen/traverse-build-graph
Use BuildDescription.traverseModules to get the target targets of a SwiftPM package
2 parents 0501d9f + 430fdef commit a045c76

File tree

3 files changed

+93
-160
lines changed

3 files changed

+93
-160
lines changed

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 84 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -110,57 +110,38 @@ package struct SwiftPMTestHooks: Sendable {
110110
/// Package Manager (SwiftPM) package. The settings are determined by loading the Package.swift
111111
/// manifest using `libSwiftPM` and constructing a build plan using the default (debug) parameters.
112112
package actor SwiftPMBuildSystem {
113-
114113
package enum Error: Swift.Error {
115-
116114
/// Could not find a manifest (Package.swift file). This is not a package.
117115
case noManifest(workspacePath: TSCAbsolutePath)
118116

119117
/// Could not determine an appropriate toolchain for swiftpm to use for manifest loading.
120118
case cannotDetermineHostToolchain
121119
}
122120

121+
// MARK: Integration with SourceKit-LSP
122+
123+
/// Options that allow the user to pass extra compiler flags.
124+
private let options: SourceKitLSPOptions
125+
126+
private let testHooks: SwiftPMTestHooks
127+
123128
/// Delegate to handle any build system events.
124129
package weak var delegate: BuildSystemIntegration.BuildSystemDelegate? = nil
125130

126131
package func setDelegate(_ delegate: BuildSystemIntegration.BuildSystemDelegate?) async {
127132
self.delegate = delegate
128133
}
129134

135+
/// This callback is informed when `reloadPackage` starts and ends executing.
136+
private var reloadPackageStatusCallback: (ReloadPackageStatus) async -> Void
137+
130138
/// Callbacks that should be called if the list of possible test files has changed.
131139
private var testFilesDidChangeCallbacks: [() async -> Void] = []
132140

133-
private let workspacePath: TSCAbsolutePath
134-
135-
/// Options that allow the user to pass extra compiler flags.
136-
private let options: SourceKitLSPOptions
137-
138-
/// The directory containing `Package.swift`.
139-
package var projectRoot: TSCAbsolutePath
140-
141-
private var buildDescription: SourceKitLSPAPI.BuildDescription?
142-
private var modulesGraph: ModulesGraph
143-
private let workspace: Workspace
144-
package let toolsBuildParameters: BuildParameters
145-
package let destinationBuildParameters: BuildParameters
146-
private let fileSystem: FileSystem
147-
private let toolchain: Toolchain
148-
149-
private var fileToTargets: [DocumentURI: [SwiftBuildTarget]] = [:]
150-
private var sourceDirToTargets: [DocumentURI: [SwiftBuildTarget]] = [:]
151-
152-
/// Maps configured targets ids to their SwiftPM build target as well as an index in their topological sorting.
153-
///
154-
/// Targets with lower index are more low level, ie. targets with higher indices depend on targets with lower indices.
155-
private var targets: [ConfiguredTarget: (index: Int, buildTarget: SwiftBuildTarget)] = [:]
156-
157141
/// The URIs for which the delegate has registered for change notifications,
158142
/// mapped to the language the delegate specified when registering for change notifications.
159143
private var watchedFiles: Set<DocumentURI> = []
160144

161-
/// This callback is informed when `reloadPackage` starts and ends executing.
162-
private var reloadPackageStatusCallback: (ReloadPackageStatus) async -> Void
163-
164145
/// Debounces calls to `delegate.filesDependenciesUpdated`.
165146
///
166147
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
@@ -171,16 +152,43 @@ package actor SwiftPMBuildSystem {
171152
/// Force-unwrapped optional because initializing it requires access to `self`.
172153
private var fileDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil
173154

155+
/// Whether the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
156+
/// user's build.
157+
private var isForIndexBuild: Bool { options.backgroundIndexingOrDefault }
158+
159+
// MARK: Build system options (set once and not modified)
160+
161+
/// The path at which the LSP workspace is opened. This might be a subdirectory of the path that contains the
162+
/// `Package.swift`.
163+
private let workspacePath: TSCAbsolutePath
164+
165+
/// The directory containing `Package.swift`.
166+
package let projectRoot: TSCAbsolutePath
167+
168+
package let toolsBuildParameters: BuildParameters
169+
package let destinationBuildParameters: BuildParameters
170+
171+
private let fileSystem: FileSystem
172+
private let toolchain: Toolchain
173+
private let workspace: Workspace
174+
174175
/// A `ObservabilitySystem` from `SwiftPM` that logs.
175176
private let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
176177
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
177178
})
178179

179-
/// Whether the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
180-
/// user's build.
181-
private var isForIndexBuild: Bool { options.backgroundIndexingOrDefault }
180+
// MARK: Build system state (modified on package reload)
182181

183-
private let testHooks: SwiftPMTestHooks
182+
/// The entry point via with we can access the `SourceKitLSPAPI` provided by SwiftPM.
183+
private var buildDescription: SourceKitLSPAPI.BuildDescription?
184+
185+
/// Maps source and header files to the target that include them.
186+
private var fileToTargets: [DocumentURI: Set<ConfiguredTarget>] = [:]
187+
188+
/// Maps configured targets ids to their SwiftPM build target as well as the depth at which they occur in the build
189+
/// graph. Top level targets on which no other target depends have a depth of `1`. Targets with dependencies have a
190+
/// greater depth.
191+
private var targets: [ConfiguredTarget: (buildTarget: SwiftBuildTarget, depth: Int)] = [:]
184192

185193
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
186194
///
@@ -306,12 +314,6 @@ package actor SwiftPMBuildSystem {
306314
flags: buildFlags
307315
)
308316

309-
self.modulesGraph = try ModulesGraph(
310-
rootPackages: [],
311-
packages: IdentifiableSet(),
312-
dependencies: [],
313-
binaryArtifacts: [:]
314-
)
315317
self.reloadPackageStatusCallback = reloadPackageStatusCallback
316318

317319
// The debounce duration of 500ms was chosen arbitrarily without scientific research.
@@ -394,45 +396,26 @@ extension SwiftPMBuildSystem {
394396
/// Make sure to execute any throwing statements before setting any
395397
/// properties because otherwise we might end up in an inconsistent state
396398
/// with only some properties modified.
397-
self.modulesGraph = modulesGraph
398-
399-
self.targets = Dictionary(
400-
try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph).enumerated().map { (index, target) in
401-
return (key: ConfiguredTarget(target), value: (index, target))
402-
},
403-
uniquingKeysWith: { first, second in
404-
logger.fault("Found two targets with the same name \(first.buildTarget.name)")
405-
return second
406-
}
407-
)
408399

409-
self.fileToTargets = [DocumentURI: [SwiftBuildTarget]](
410-
modulesGraph.allModules.flatMap { target in
411-
return target.sources.paths.compactMap { (filePath) -> (key: DocumentURI, value: [SwiftBuildTarget])? in
412-
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
413-
return nil
414-
}
415-
return (key: DocumentURI(filePath.asURL), value: [buildTarget])
400+
self.targets = [:]
401+
self.fileToTargets = [:]
402+
buildDescription.traverseModules { buildTarget, parent, depth in
403+
let configuredTarget = ConfiguredTarget(buildTarget)
404+
var depth = depth
405+
if let existingDepth = targets[configuredTarget]?.depth {
406+
depth = max(existingDepth, depth)
407+
} else {
408+
for source in buildTarget.sources + buildTarget.headers {
409+
fileToTargets[DocumentURI(source), default: []].insert(configuredTarget)
416410
}
417-
},
418-
uniquingKeysWith: { $0 + $1 }
419-
)
420-
421-
self.sourceDirToTargets = [DocumentURI: [SwiftBuildTarget]](
422-
modulesGraph.allModules.compactMap { (target) -> (DocumentURI, [SwiftBuildTarget])? in
423-
guard let buildTarget = buildDescription.getBuildTarget(for: target, in: modulesGraph) else {
424-
return nil
425-
}
426-
return (key: DocumentURI(target.sources.root.asURL), value: [buildTarget])
427-
},
428-
uniquingKeysWith: { $0 + $1 }
429-
)
411+
}
412+
targets[configuredTarget] = (buildTarget, depth)
413+
}
430414

431-
guard let delegate = self.delegate else {
432-
return
415+
if let delegate = self.delegate {
416+
await delegate.fileBuildSettingsChanged(self.watchedFiles)
417+
await delegate.fileHandlingCapabilityChanged()
433418
}
434-
await delegate.fileBuildSettingsChanged(self.watchedFiles)
435-
await delegate.fileHandlingCapabilityChanged()
436419
for testFilesDidChangeCallback in testFilesDidChangeCallbacks {
437420
await testFilesDidChangeCallback()
438421
}
@@ -546,7 +529,7 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
546529

547530
let targets = buildTargets(for: uri)
548531
if !targets.isEmpty {
549-
return targets.map(ConfiguredTarget.init)
532+
return targets.sorted { ($0.targetID, $0.runDestinationID) < ($1.targetID, $1.runDestinationID) }
550533
}
551534

552535
if path.basename == "Package.swift"
@@ -557,10 +540,6 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
557540
return [ConfiguredTarget.forPackageManifest]
558541
}
559542

560-
if let targets = try? inferredTargets(for: path) {
561-
return targets
562-
}
563-
564543
return []
565544
}
566545

@@ -570,27 +549,27 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
570549

571550
package func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
572551
return targets.sorted { (lhs: ConfiguredTarget, rhs: ConfiguredTarget) -> Bool in
573-
let lhsIndex = self.targets[lhs]?.index ?? self.targets.count
574-
let rhsIndex = self.targets[rhs]?.index ?? self.targets.count
575-
return lhsIndex < rhsIndex
552+
let lhsDepth = self.targets[lhs]?.depth ?? 0
553+
let rhsDepth = self.targets[rhs]?.depth ?? 0
554+
return lhsDepth > rhsDepth
576555
}
577556
}
578557

579558
package func targets(dependingOn targets: [ConfiguredTarget]) -> [ConfiguredTarget]? {
580-
let targetIndices = targets.compactMap { self.targets[$0]?.index }
581-
let minimumTargetIndex: Int?
582-
if targetIndices.count == targets.count {
583-
minimumTargetIndex = targetIndices.min()
559+
let targetDepths = targets.compactMap { self.targets[$0]?.depth }
560+
let minimumTargetDepth: Int?
561+
if targetDepths.count == targets.count {
562+
minimumTargetDepth = targetDepths.max()
584563
} else {
585564
// One of the targets didn't have an entry in self.targets. We don't know what might depend on it.
586-
minimumTargetIndex = nil
565+
minimumTargetDepth = nil
587566
}
588567

589568
// Files that occur before the target in the topological sorting don't depend on it.
590569
// Ideally, we should consult the dependency graph here for more accurate dependency analysis instead of relying on
591570
// a flattened list (https://github.com/swiftlang/sourcekit-lsp/issues/1312).
592571
return self.targets.compactMap { (configuredTarget, value) -> ConfiguredTarget? in
593-
if let minimumTargetIndex, value.index <= minimumTargetIndex {
572+
if let minimumTargetDepth, value.depth >= minimumTargetDepth {
594573
return nil
595574
}
596575
return configuredTarget
@@ -715,7 +694,7 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
715694
}
716695

717696
/// Returns the resolved target descriptions for the given file, if one is known.
718-
private func buildTargets(for file: DocumentURI) -> [SwiftBuildTarget] {
697+
private func buildTargets(for file: DocumentURI) -> Set<ConfiguredTarget> {
719698
if let targets = fileToTargets[file] {
720699
return targets
721700
}
@@ -765,7 +744,9 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
765744
guard event.uri.fileURL?.pathExtension == "swift", let targets = fileToTargets[event.uri] else {
766745
continue
767746
}
768-
filesWithUpdatedDependencies.formUnion(targets.flatMap(\.sources).map(DocumentURI.init))
747+
for target in targets {
748+
filesWithUpdatedDependencies.formUnion(self.targets[target]?.buildTarget.sources.map(DocumentURI.init) ?? [])
749+
}
769750
}
770751

771752
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
@@ -791,66 +772,28 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuildSystem {
791772
}
792773

793774
package func sourceFiles() -> [SourceFileInfo] {
794-
return fileToTargets.compactMap { (uri, targets) -> SourceFileInfo? in
795-
// We should only set mayContainTests to `true` for files from test targets
796-
// (https://github.com/swiftlang/sourcekit-lsp/issues/1174).
797-
return SourceFileInfo(
798-
uri: uri,
799-
isPartOfRootProject: targets.contains(where: \.isPartOfRootPackage),
800-
mayContainTests: true
801-
)
775+
var sourceFiles: [DocumentURI: SourceFileInfo] = [:]
776+
for (buildTarget, depth) in self.targets.values {
777+
for sourceFile in buildTarget.sources {
778+
let uri = DocumentURI(sourceFile)
779+
sourceFiles[uri] = SourceFileInfo(
780+
uri: uri,
781+
isPartOfRootProject: depth == 1 || (sourceFiles[uri]?.isPartOfRootProject ?? false),
782+
mayContainTests: true
783+
)
784+
}
802785
}
786+
return sourceFiles.values.sorted { $0.uri.pseudoPath < $1.uri.pseudoPath }
803787
}
804788

805789
package func addSourceFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async {
806790
testFilesDidChangeCallbacks.append(callback)
807791
}
808-
}
809-
810-
extension SwiftPMBuildSystem {
811-
812-
// MARK: Implementation details
813792

814793
/// Retrieve settings for a package manifest (Package.swift).
815794
private func settings(forPackageManifest path: AbsolutePath) throws -> FileBuildSettings? {
816-
func impl(_ path: AbsolutePath) -> FileBuildSettings? {
817-
for package in modulesGraph.packages where path == package.manifest.path {
818-
let compilerArgs = workspace.interpreterFlags(for: package.path) + [path.pathString]
819-
return FileBuildSettings(compilerArguments: compilerArgs)
820-
}
821-
return nil
822-
}
823-
824-
if let result = impl(path) {
825-
return result
826-
}
827-
828-
let canonicalPath = try resolveSymlinks(path)
829-
return canonicalPath == path ? nil : impl(canonicalPath)
830-
}
831-
832-
/// This finds the target a file belongs to based on its location in the file system.
833-
///
834-
/// This is primarily intended to find the target a header belongs to.
835-
private func inferredTargets(for path: AbsolutePath) throws -> [ConfiguredTarget] {
836-
func impl(_ path: AbsolutePath) throws -> [ConfiguredTarget] {
837-
var dir = path.parentDirectory
838-
while !dir.isRoot {
839-
if let buildTargets = sourceDirToTargets[DocumentURI(dir.asURL)] {
840-
return buildTargets.map(ConfiguredTarget.init)
841-
}
842-
dir = dir.parentDirectory
843-
}
844-
return []
845-
}
846-
847-
let result = try impl(path)
848-
if !result.isEmpty {
849-
return result
850-
}
851-
852-
let canonicalPath = try resolveSymlinks(path)
853-
return try canonicalPath == path ? [] : impl(canonicalPath)
795+
let compilerArgs = workspace.interpreterFlags(for: path.parentDirectory) + [path.pathString]
796+
return FileBuildSettings(compilerArguments: compilerArgs)
854797
}
855798
}
856799

Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -408,23 +408,13 @@ final class SwiftPMBuildSystemTests: XCTestCase {
408408
.compilerArguments
409409
assertArgumentsContain(aswift.pathString, arguments: arguments)
410410
assertArgumentsDoNotContain(bswift.pathString, arguments: arguments)
411-
// Temporary conditional to work around revlock between SourceKit-LSP and SwiftPM
412-
// as a result of fix for SR-12050. Can be removed when that fix has been merged.
413-
if arguments.joined(separator: " ").contains("-Xcc -I -Xcc") {
414-
assertArgumentsContain(
415-
"-Xcc",
416-
"-I",
417-
"-Xcc",
418-
packageRoot.appending(components: "Sources", "libC", "include").pathString,
419-
arguments: arguments
420-
)
421-
} else {
422-
assertArgumentsContain(
423-
"-I",
424-
packageRoot.appending(components: "Sources", "libC", "include").pathString,
425-
arguments: arguments
426-
)
427-
}
411+
assertArgumentsContain(
412+
"-Xcc",
413+
"-I",
414+
"-Xcc",
415+
packageRoot.appending(components: "Sources", "libC", "include").pathString,
416+
arguments: arguments
417+
)
428418

429419
let argumentsB = try await unwrap(swiftpmBuildSystem.buildSettings(for: bswift.asURI, language: .swift))
430420
.compilerArguments
@@ -666,8 +656,8 @@ final class SwiftPMBuildSystemTests: XCTestCase {
666656
.compilerArguments
667657
XCTAssertNotNil(argsManifest)
668658

669-
assertArgumentsDoNotContain(manifest.pathString, arguments: argsManifest ?? [])
670-
assertArgumentsContain(try resolveSymlinks(manifest).pathString, arguments: argsManifest ?? [])
659+
assertArgumentsContain(manifest.pathString, arguments: argsManifest ?? [])
660+
assertArgumentsDoNotContain(try resolveSymlinks(manifest).pathString, arguments: argsManifest ?? [])
671661
}
672662
}
673663

0 commit comments

Comments
 (0)