Skip to content

Commit 41f3a4f

Browse files
committed
[PackageGraph] Allow package-level cyclic dependency only for >= 6.0 manifests
Follow-up to swiftlang#7530 Otherwise it might be suprising for package authors to discover that their packages cannot be used with older tools because they inadvertently introduced a cyclic dependency in a new version. (cherry picked from commit 3098b2d)
1 parent aaeecec commit 41f3a4f

File tree

5 files changed

+168
-22
lines changed

5 files changed

+168
-22
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ Swift 6.0
55

66
* [#7530]
77

8-
Makes it possible for packages to depend on each other if such dependency doesn't form any target-level cycles. For example,
9-
package `A` can depend on `B` and `B` on `A` unless targets in `B` depend on products of `A` that depend on some of the same
8+
Starting from tools-version 6.0 makes it possible for packages to depend on each other if such dependency doesn't form any target-level cycles.
9+
For example, package `A` can depend on `B` and `B` on `A` unless targets in `B` depend on products of `A` that depend on some of the same
1010
targets from `B` and vice versa.
1111

1212
* [#7507]

Sources/PackageGraph/ModulesGraph+Loading.swift

+31-6
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,16 @@ extension ModulesGraph {
6464
)
6565
}
6666
}
67-
let inputManifests = rootManifestNodes + rootDependencyNodes
67+
let inputManifests = (rootManifestNodes + rootDependencyNodes).map {
68+
KeyedPair($0, key: $0.id)
69+
}
6870

6971
// Collect the manifests for which we are going to build packages.
7072
var allNodes = [GraphLoadingNode]()
7173

72-
// Cycles in dependencies don't matter as long as there are no target cycles between packages.
73-
depthFirstSearch(inputManifests.map { KeyedPair($0, key: $0.id) }) {
74-
$0.item.requiredDependencies.compactMap { dependency in
75-
manifestMap[dependency.identity].map { (manifest, fileSystem) in
74+
let nodeSuccessorProvider = { (node: KeyedPair<GraphLoadingNode, PackageIdentity>) in
75+
node.item.requiredDependencies.compactMap { dependency in
76+
manifestMap[dependency.identity].map { manifest, _ in
7677
KeyedPair(
7778
GraphLoadingNode(
7879
identity: dependency.identity,
@@ -83,7 +84,31 @@ extension ModulesGraph {
8384
)
8485
}
8586
}
86-
} onUnique: {
87+
}
88+
89+
// Package dependency cycles feature is gated on tools version 6.0.
90+
if !root.manifests.allSatisfy({ $1.toolsVersion >= .v6_0 }) {
91+
if let cycle = findCycle(inputManifests, successors: nodeSuccessorProvider) {
92+
let path = (cycle.path + cycle.cycle).map(\.item.manifest)
93+
observabilityScope.emit(PackageGraphError.dependencyCycleDetected(
94+
path: path, cycle: cycle.cycle[0].item.manifest
95+
))
96+
97+
return try ModulesGraph(
98+
rootPackages: [],
99+
rootDependencies: [],
100+
packages: IdentifiableSet(),
101+
dependencies: requiredDependencies,
102+
binaryArtifacts: binaryArtifacts
103+
)
104+
}
105+
}
106+
107+
// Cycles in dependencies don't matter as long as there are no target cycles between packages.
108+
depthFirstSearch(
109+
inputManifests,
110+
successors: nodeSuccessorProvider
111+
) {
87112
allNodes.append($0.item)
88113
} onDuplicate: { _,_ in
89114
// no de-duplication is required.

Sources/PackageGraph/ModulesGraph.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum PackageGraphError: Swift.Error {
2020
case noModules(Package)
2121

2222
/// The package dependency declaration has cycle in it.
23-
case cycleDetected((path: [Manifest], cycle: [Manifest]))
23+
case dependencyCycleDetected(path: [Manifest], cycle: Manifest)
2424

2525
/// The product dependency not found.
2626
case productDependencyNotFound(package: String, targetName: String, dependencyProductName: String, dependencyPackageName: String?, dependencyProductInDecl: Bool, similarProductName: String?, packageContainingSimilarProduct: String?)
@@ -226,10 +226,10 @@ extension PackageGraphError: CustomStringConvertible {
226226
case .noModules(let package):
227227
return "package '\(package)' contains no products"
228228

229-
case .cycleDetected(let cycle):
230-
return "cyclic dependency declaration found: " +
231-
(cycle.path + cycle.cycle).map({ $0.displayName }).joined(separator: " -> ") +
232-
" -> " + cycle.cycle[0].displayName
229+
case .dependencyCycleDetected(let path, let package):
230+
return "cyclic dependency between packages " +
231+
(path.map({ $0.displayName }).joined(separator: " -> ")) +
232+
" -> \(package.displayName) requires tools-version 6.0 or later"
233233

234234
case .productDependencyNotFound(let package, let targetName, let dependencyProductName, let dependencyPackageName, let dependencyProductInDecl, let similarProductName, let packageContainingSimilarProduct):
235235
if dependencyProductInDecl {

Tests/PackageGraphTests/ModulesGraphTests.swift

+127-2
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ final class ModulesGraphTests: XCTestCase {
183183
)
184184

185185
testDiagnostics(observability.diagnostics) { result in
186-
result.check(diagnostic: "cyclic dependency declaration found: Bar -> Baz -> Bar", severity: .error)
186+
result.check(
187+
diagnostic: "cyclic dependency between packages Foo -> Bar -> Baz -> Bar requires tools-version 6.0 or later",
188+
severity: .error
189+
)
187190
}
188191
}
189192

@@ -209,11 +212,132 @@ final class ModulesGraphTests: XCTestCase {
209212
)
210213

211214
testDiagnostics(observability.diagnostics) { result in
212-
result.check(diagnostic: "cyclic dependency declaration found: Bar -> Foo -> Bar", severity: .error)
215+
result.check(
216+
diagnostic: "cyclic dependency declaration found: Bar -> Foo -> Bar",
217+
severity: .error
218+
)
219+
}
220+
}
221+
222+
func testDependencyCycleWithoutTargetCycleV5() throws {
223+
let fs = InMemoryFileSystem(emptyFiles:
224+
"/Foo/Sources/Foo/source.swift",
225+
"/Bar/Sources/Bar/source.swift",
226+
"/Bar/Sources/Baz/source.swift"
227+
)
228+
229+
let observability = ObservabilitySystem.makeForTesting()
230+
let _ = try loadModulesGraph(
231+
fileSystem: fs,
232+
manifests: [
233+
Manifest.createRootManifest(
234+
displayName: "Foo",
235+
path: "/Foo",
236+
toolsVersion: .v5_10,
237+
dependencies: [
238+
.localSourceControl(path: "/Bar", requirement: .upToNextMajor(from: "1.0.0"))
239+
],
240+
products: [
241+
ProductDescription(name: "Foo", type: .library(.automatic), targets: ["Foo"])
242+
],
243+
targets: [
244+
TargetDescription(name: "Foo", dependencies: ["Bar"]),
245+
]),
246+
Manifest.createFileSystemManifest(
247+
displayName: "Bar",
248+
path: "/Bar",
249+
dependencies: [
250+
.localSourceControl(path: "/Foo", requirement: .upToNextMajor(from: "1.0.0"))
251+
],
252+
products: [
253+
ProductDescription(name: "Bar", type: .library(.automatic), targets: ["Bar"]),
254+
ProductDescription(name: "Baz", type: .library(.automatic), targets: ["Baz"])
255+
],
256+
targets: [
257+
TargetDescription(name: "Bar"),
258+
TargetDescription(name: "Baz", dependencies: ["Foo"]),
259+
])
260+
],
261+
observabilityScope: observability.topScope
262+
)
263+
264+
testDiagnostics(observability.diagnostics) { result in
265+
result.check(
266+
diagnostic: "cyclic dependency between packages Foo -> Bar -> Foo requires tools-version 6.0 or later",
267+
severity: .error
268+
)
213269
}
214270
}
215271

216272
func testDependencyCycleWithoutTargetCycle() throws {
273+
let fs = InMemoryFileSystem(emptyFiles:
274+
"/A/Sources/A/source.swift",
275+
"/B/Sources/B/source.swift",
276+
"/C/Sources/C/source.swift"
277+
)
278+
279+
func testDependencyCycleDetection(rootToolsVersion: ToolsVersion) throws -> [Diagnostic] {
280+
let observability = ObservabilitySystem.makeForTesting()
281+
let _ = try loadModulesGraph(
282+
fileSystem: fs,
283+
manifests: [
284+
Manifest.createRootManifest(
285+
displayName: "A",
286+
path: "/A",
287+
toolsVersion: rootToolsVersion,
288+
dependencies: [
289+
.localSourceControl(path: "/B", requirement: .upToNextMajor(from: "1.0.0"))
290+
],
291+
products: [
292+
ProductDescription(name: "A", type: .library(.automatic), targets: ["A"])
293+
],
294+
targets: [
295+
TargetDescription(name: "A", dependencies: ["B"]),
296+
]
297+
),
298+
Manifest.createFileSystemManifest(
299+
displayName: "B",
300+
path: "/B",
301+
dependencies: [
302+
.localSourceControl(path: "/C", requirement: .upToNextMajor(from: "1.0.0"))
303+
],
304+
products: [
305+
ProductDescription(name: "B", type: .library(.automatic), targets: ["B"]),
306+
],
307+
targets: [
308+
TargetDescription(name: "B"),
309+
]
310+
),
311+
Manifest.createFileSystemManifest(
312+
displayName: "C",
313+
path: "/C",
314+
dependencies: [
315+
.localSourceControl(path: "/A", requirement: .upToNextMajor(from: "1.0.0"))
316+
],
317+
products: [
318+
ProductDescription(name: "C", type: .library(.automatic), targets: ["C"]),
319+
],
320+
targets: [
321+
TargetDescription(name: "C"),
322+
]
323+
)
324+
],
325+
observabilityScope: observability.topScope
326+
)
327+
return observability.diagnostics
328+
}
329+
330+
try testDiagnostics(testDependencyCycleDetection(rootToolsVersion: .v5)) { result in
331+
result.check(
332+
diagnostic: "cyclic dependency between packages A -> B -> C -> A requires tools-version 6.0 or later",
333+
severity: .error
334+
)
335+
}
336+
337+
try XCTAssertNoDiagnostics(testDependencyCycleDetection(rootToolsVersion: .v6_0))
338+
}
339+
340+
func testDependencyCycleWithoutTargetCycleV6() throws {
217341
let fs = InMemoryFileSystem(emptyFiles:
218342
"/Foo/Sources/Foo/source.swift",
219343
"/Bar/Sources/Bar/source.swift",
@@ -227,6 +351,7 @@ final class ModulesGraphTests: XCTestCase {
227351
Manifest.createRootManifest(
228352
displayName: "Foo",
229353
path: "/Foo",
354+
toolsVersion: .v6_0,
230355
dependencies: [
231356
.localSourceControl(path: "/Bar", requirement: .upToNextMajor(from: "1.0.0"))
232357
],

Tests/WorkspaceTests/WorkspaceTests.swift

+3-7
Original file line numberDiff line numberDiff line change
@@ -11019,7 +11019,7 @@ final class WorkspaceTests: XCTestCase {
1101911019
requirement: .upToNextMajor(from: "1.0.0")
1102011020
),
1102111021
],
11022-
toolsVersion: .v5
11022+
toolsVersion: .v6_0
1102311023
),
1102411024
],
1102511025
packages: [
@@ -11167,11 +11167,7 @@ final class WorkspaceTests: XCTestCase {
1116711167
// FIXME: rdar://72940946
1116811168
// we need to improve this situation or diagnostics when working on identity
1116911169
result.check(
11170-
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.",
11171-
severity: .warning
11172-
)
11173-
result.check(
11174-
diagnostic: "product 'OtherUtilityProduct' required by package 'bar' target 'BarTarget' not found in package 'OtherUtilityPackage'.",
11170+
diagnostic: "cyclic dependency between packages Root -> FooUtilityPackage -> BarPackage -> FooUtilityPackage requires tools-version 6.0 or later",
1117511171
severity: .error
1117611172
)
1117711173
}
@@ -11202,7 +11198,7 @@ final class WorkspaceTests: XCTestCase {
1120211198
requirement: .upToNextMajor(from: "1.0.0")
1120311199
),
1120411200
],
11205-
toolsVersion: .v5
11201+
toolsVersion: .v6_0
1120611202
),
1120711203
],
1120811204
packages: [

0 commit comments

Comments
 (0)