Skip to content

Commit 5855285

Browse files
committed
[PackageGraph] Use DFS to build a set of package graph nodes to load
Avoids duplicating the work and allows dependencies to have cyclic entries because cyclic product and/or target references are the only problematic cases for the build since they cannot be planned properly.
1 parent 72522a0 commit 5855285

File tree

3 files changed

+29
-91
lines changed

3 files changed

+29
-91
lines changed

Sources/Basics/Graph/GraphAlgorithms.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ public func DFS<T: Hashable>(
4949
continue
5050
}
5151

52-
for succ in try successors(curr).reversed() {
52+
for succ in try successors(curr) {
5353
stack.append(succ)
5454
}
5555
}
5656
}
57-
}
57+
}

Sources/PackageGraph/ModulesGraph+Loading.swift

Lines changed: 19 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import OrderedCollections
1515
import PackageLoading
1616
import PackageModel
1717

18+
import struct TSCBasic.KeyedPair
1819
import func TSCBasic.bestMatch
1920

2021
extension ModulesGraph {
@@ -44,17 +45,6 @@ extension ModulesGraph {
4445
root.manifests.forEach {
4546
manifestMap[$0.key] = ($0.value, fileSystem)
4647
}
47-
func nodeSuccessorsProvider(node: GraphLoadingNode) -> [GraphLoadingNode] {
48-
node.requiredDependencies.compactMap { dependency in
49-
manifestMap[dependency.identity].map { (manifest, fileSystem) in
50-
GraphLoadingNode(
51-
identity: dependency.identity,
52-
manifest: manifest,
53-
productFilter: dependency.productFilter
54-
)
55-
}
56-
}
57-
}
5848

5949
// Construct the root root dependencies set.
6050
let rootDependencies = Set(root.dependencies.compactMap{
@@ -75,30 +65,26 @@ extension ModulesGraph {
7565
let inputManifests = rootManifestNodes + rootDependencyNodes
7666

7767
// Collect the manifests for which we are going to build packages.
78-
var allNodes: [GraphLoadingNode]
79-
80-
// Detect cycles in manifest dependencies.
81-
if let cycle = findCycle(inputManifests, successors: nodeSuccessorsProvider) {
82-
observabilityScope.emit(PackageGraphError.cycleDetected(cycle))
83-
// Break the cycle so we can build a partial package graph.
84-
allNodes = inputManifests.filter({ $0.manifest != cycle.cycle[0] })
85-
} else {
86-
// Sort all manifests topologically.
87-
allNodes = try topologicalSort(inputManifests, successors: nodeSuccessorsProvider)
88-
}
68+
var allNodes = [GraphLoadingNode]()
8969

90-
var flattenedManifests: [PackageIdentity: GraphLoadingNode] = [:]
91-
for node in allNodes {
92-
if let existing = flattenedManifests[node.identity] {
93-
let merged = GraphLoadingNode(
94-
identity: node.identity,
95-
manifest: node.manifest,
96-
productFilter: existing.productFilter.union(node.productFilter)
97-
)
98-
flattenedManifests[node.identity] = merged
99-
} else {
100-
flattenedManifests[node.identity] = node
70+
// Cycles in dependencies don't matter as long as there are no target cycles between packages.
71+
DFS(inputManifests.map { KeyedPair($0, key: $0.id) }) {
72+
$0.item.requiredDependencies.compactMap { dependency in
73+
manifestMap[dependency.identity].map { (manifest, fileSystem) in
74+
KeyedPair(
75+
GraphLoadingNode(
76+
identity: dependency.identity,
77+
manifest: manifest,
78+
productFilter: dependency.productFilter
79+
),
80+
key: dependency.identity
81+
)
82+
}
10183
}
84+
} onUnique: {
85+
allNodes.append($0.item)
86+
} onDuplicate: { _,_ in
87+
// no de-duplication is required.
10288
}
10389

10490
// Create the packages.
@@ -1065,55 +1051,3 @@ private final class ResolvedPackageBuilder: ResolvedBuilder<ResolvedPackage> {
10651051
)
10661052
}
10671053
}
1068-
1069-
/// Finds the first cycle encountered in a graph.
1070-
///
1071-
/// This is different from the one in tools support core, in that it handles equality separately from node traversal. Nodes traverse product filters, but only the manifests must be equal for there to be a cycle.
1072-
fileprivate func findCycle(
1073-
_ nodes: [GraphLoadingNode],
1074-
successors: (GraphLoadingNode) throws -> [GraphLoadingNode]
1075-
) rethrows -> (path: [Manifest], cycle: [Manifest])? {
1076-
// Ordered set to hold the current traversed path.
1077-
var path = OrderedCollections.OrderedSet<Manifest>()
1078-
1079-
var fullyVisitedManifests = Set<Manifest>()
1080-
1081-
// Function to visit nodes recursively.
1082-
// FIXME: Convert to stack.
1083-
func visit(
1084-
_ node: GraphLoadingNode,
1085-
_ successors: (GraphLoadingNode) throws -> [GraphLoadingNode]
1086-
) rethrows -> (path: [Manifest], cycle: [Manifest])? {
1087-
// Once all successors have been visited, this node cannot participate
1088-
// in a cycle.
1089-
if fullyVisitedManifests.contains(node.manifest) {
1090-
return nil
1091-
}
1092-
1093-
// If this node is already in the current path then we have found a cycle.
1094-
if !path.append(node.manifest).inserted {
1095-
let index = path.firstIndex(of: node.manifest)! // forced unwrap safe
1096-
return (Array(path[path.startIndex..<index]), Array(path[index..<path.endIndex]))
1097-
}
1098-
1099-
for succ in try successors(node) {
1100-
if let cycle = try visit(succ, successors) {
1101-
return cycle
1102-
}
1103-
}
1104-
// No cycle found for this node, remove it from the path.
1105-
let item = path.removeLast()
1106-
assert(item == node.manifest)
1107-
// Track fully visited nodes
1108-
fullyVisitedManifests.insert(node.manifest)
1109-
return nil
1110-
}
1111-
1112-
for node in nodes {
1113-
if let cycle = try visit(node, successors) {
1114-
return cycle
1115-
}
1116-
}
1117-
// Couldn't find any cycle in the graph.
1118-
return nil
1119-
}

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,7 +1971,7 @@ final class WorkspaceTests: XCTestCase {
19711971
// Ensure that the order of the manifests is stable.
19721972
XCTAssertEqual(
19731973
manifests.allDependencyManifests.map(\.value.manifest.displayName),
1974-
["Foo", "Bar", "Baz", "Bam"]
1974+
["Bam", "Baz", "Bar", "Foo"]
19751975
)
19761976
XCTAssertNoDiagnostics(diagnostics)
19771977
}
@@ -11123,7 +11123,7 @@ final class WorkspaceTests: XCTestCase {
1112311123
// FIXME: rdar://72940946
1112411124
// we need to improve this situation or diagnostics when working on identity
1112511125
result.check(
11126-
diagnostic: "cyclic dependency declaration found: Root -> FooUtilityPackage -> BarPackage -> FooUtilityPackage",
11126+
diagnostic: "'bar' dependency on '/tmp/ws/pkgs/other/utility' conflicts with dependency on '/tmp/ws/pkgs/foo/utility' which has the same identity 'utility'",
1112711127
severity: .error
1112811128
)
1112911129
}
@@ -11207,7 +11207,11 @@ final class WorkspaceTests: XCTestCase {
1120711207
// FIXME: rdar://72940946
1120811208
// we need to improve this situation or diagnostics when working on identity
1120911209
result.check(
11210-
diagnostic: "cyclic dependency declaration found: Root -> FooUtilityPackage -> BarPackage -> FooUtilityPackage",
11210+
diagnostic: "'bar' dependency on '/tmp/ws/pkgs/other/utility' conflicts with dependency on '/tmp/ws/pkgs/foo/utility' which has the same identity 'utility'. this will be escalated to an error in future versions of SwiftPM.",
11211+
severity: .warning
11212+
)
11213+
result.check(
11214+
diagnostic: "product 'OtherUtilityProduct' required by package 'bar' target 'BarTarget' not found in package 'OtherUtilityPackage'.",
1121111215
severity: .error
1121211216
)
1121311217
}
@@ -11282,7 +11286,7 @@ final class WorkspaceTests: XCTestCase {
1128211286
// FIXME: rdar://72940946
1128311287
// we need to improve this situation or diagnostics when working on identity
1128411288
result.check(
11285-
diagnostic: "cyclic dependency declaration found: Root -> BarPackage -> Root",
11289+
diagnostic: "product 'FooProduct' required by package 'bar' target 'BarTarget' not found in package 'FooPackage'.",
1128611290
severity: .error
1128711291
)
1128811292
}

0 commit comments

Comments
 (0)