Skip to content

Commit f8fd35b

Browse files
committed
[PackageGraph] Detect and diagnose cycles in target graph
It cannot be done sooner because `Package` only knows about package names of products but doesn't attempt to resolve them and attempting to build resolved packages with cycles would result in an infinite recusion because builders are reference types.
1 parent 5855285 commit f8fd35b

File tree

3 files changed

+80
-9
lines changed

3 files changed

+80
-9
lines changed

Sources/PackageGraph/ModulesGraph+Loading.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PackageModel
1717

1818
import struct TSCBasic.KeyedPair
1919
import func TSCBasic.bestMatch
20+
import func TSCBasic.findCycle
2021

2122
extension ModulesGraph {
2223
/// Load the package graph for the given package path.
@@ -638,6 +639,31 @@ private func createResolvedPackages(
638639
}
639640
}
640641

642+
do {
643+
let targetBuilders = packageBuilders.flatMap {
644+
$0.targets.map {
645+
KeyedPair($0, key: $0.target)
646+
}
647+
}
648+
if let cycle = findCycle(targetBuilders, successors: {
649+
$0.item.dependencies.flatMap {
650+
switch $0 {
651+
case .product(let productBuilder, conditions: _):
652+
return productBuilder.targets.map { KeyedPair($0, key: $0.target) }
653+
case .target:
654+
return [] // local targets were checked by PackageBuilder.
655+
}
656+
}
657+
}) {
658+
observabilityScope.emit(
659+
ModuleError.cycleDetected(
660+
(cycle.path.map(\.key.name), cycle.cycle.map(\.key.name))
661+
)
662+
)
663+
return IdentifiableSet()
664+
}
665+
}
666+
641667
return IdentifiableSet(try packageBuilders.map { try $0.construct() })
642668
}
643669

Tests/PackageGraphTests/ModulesGraphTests.swift

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,32 +190,77 @@ final class ModulesGraphTests: XCTestCase {
190190
}
191191
}
192192

193-
func testCycle2() throws {
193+
func testLocalTargetCycle() throws {
194194
let fs = InMemoryFileSystem(emptyFiles:
195195
"/Foo/Sources/Foo/source.swift",
196-
"/Bar/Sources/Bar/source.swift",
197-
"/Baz/Sources/Baz/source.swift"
196+
"/Foo/Sources/Bar/source.swift"
198197
)
199198

200199
let observability = ObservabilitySystem.makeForTesting()
201200
_ = try loadModulesGraph(
201+
fileSystem: fs,
202+
manifests: [
203+
Manifest.createRootManifest(
204+
displayName: "Foo",
205+
path: "/Foo",
206+
targets: [
207+
TargetDescription(name: "Foo", dependencies: ["Bar"]),
208+
TargetDescription(name: "Bar", dependencies: ["Foo"])
209+
]),
210+
],
211+
observabilityScope: observability.topScope
212+
)
213+
214+
testDiagnostics(observability.diagnostics) { result in
215+
result.check(diagnostic: "cyclic dependency declaration found: Bar -> Foo -> Bar", severity: .error)
216+
}
217+
}
218+
219+
func testDependencyCycleWithoutTargetCycle() throws {
220+
let fs = InMemoryFileSystem(emptyFiles:
221+
"/Foo/Sources/Foo/source.swift",
222+
"/Bar/Sources/Bar/source.swift",
223+
"/Bar/Sources/Baz/source.swift"
224+
)
225+
226+
let observability = ObservabilitySystem.makeForTesting()
227+
let graph = try loadModulesGraph(
202228
fileSystem: fs,
203229
manifests: [
204230
Manifest.createRootManifest(
205231
displayName: "Foo",
206232
path: "/Foo",
207233
dependencies: [
208-
.localSourceControl(path: "/Foo", requirement: .upToNextMajor(from: "1.0.0"))
234+
.localSourceControl(path: "/Bar", requirement: .upToNextMajor(from: "1.0.0"))
235+
],
236+
products: [
237+
ProductDescription(name: "Foo", type: .library(.automatic), targets: ["Foo"])
209238
],
210239
targets: [
211-
TargetDescription(name: "Foo"),
240+
TargetDescription(name: "Foo", dependencies: ["Bar"]),
212241
]),
242+
Manifest.createFileSystemManifest(
243+
displayName: "Bar",
244+
path: "/Bar",
245+
dependencies: [
246+
.localSourceControl(path: "/Foo", requirement: .upToNextMajor(from: "1.0.0"))
247+
],
248+
products: [
249+
ProductDescription(name: "Bar", type: .library(.automatic), targets: ["Bar"]),
250+
ProductDescription(name: "Baz", type: .library(.automatic), targets: ["Baz"])
251+
],
252+
targets: [
253+
TargetDescription(name: "Bar"),
254+
TargetDescription(name: "Baz", dependencies: ["Foo"]),
255+
])
213256
],
214257
observabilityScope: observability.topScope
215258
)
216259

217-
testDiagnostics(observability.diagnostics) { result in
218-
result.check(diagnostic: "cyclic dependency declaration found: Foo -> Foo", severity: .error)
260+
XCTAssertNoDiagnostics(observability.diagnostics)
261+
PackageGraphTester(graph) { result in
262+
result.check(packages: "Foo", "Bar")
263+
result.check(targets: "Bar", "Baz", "Foo")
219264
}
220265
}
221266

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12019,7 +12019,7 @@ final class WorkspaceTests: XCTestCase {
1201912019
targets: [
1202012020
.init(name: "Root1Target", dependencies: [
1202112021
.product(name: "FooProduct", package: "foo"),
12022-
.product(name: "Root2Target", package: "Root2")
12022+
.product(name: "Root2Product", package: "Root2")
1202312023
]),
1202412024
],
1202512025
products: [
@@ -12115,7 +12115,7 @@ final class WorkspaceTests: XCTestCase {
1211512115
try workspace.checkPackageGraph(roots: ["Root1", "Root2"]) { _, diagnostics in
1211612116
testDiagnostics(diagnostics) { result in
1211712117
result.check(
12118-
diagnostic: .regex("cyclic dependency declaration found: Root[1|2] -> *"),
12118+
diagnostic: .regex("cyclic dependency declaration found: Root[1|2]Target -> *"),
1211912119
severity: .error
1212012120
)
1212112121
}

0 commit comments

Comments
 (0)