Skip to content

Commit d38daf2

Browse files
xedinfurby-tm
authored andcommitted
[Package/ModuleGraph] Allow cyclic package dependencies if they don't introduce a cycle in a build graph (swiftlang#7530)
### Motivation: It should be possible for packages to depend on each other if such dependence doesn't introduce cycles in the build graph. ### Modifications: - Introduced a new DFS method to walk over graphs that breaks cycles. - Replaces use of `findCycle` + `topologicalSort` with `DFS` while building manifest and package graphs. This allows cycles in dependencies to be modeled correctly. - Removes some of the redundant data transformations from modules graph. - Modifies `ResolvedPackage` to carry identities of its dependencies instead of resolved dependencies themselves. This helps to simplify logic in `createResolvedPackages`. - Adds detection of target cycles across packages. ### Result: Makes it possible for package A to depend on package B and B to depend on A if their targets don't form a cycle.
1 parent a82b351 commit d38daf2

21 files changed

+275
-218
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
Note: This is in reverse chronological order, so newer entries are added to the top.
22

3+
* [#7530]
4+
5+
Makes it possible for packages to depend on each other if such dependency doesn't form any target-level cycles. For example,
6+
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
7+
targets from `B` and vice versa.
8+
39
Swift 6.0
410
-----------
511

Sources/Basics/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ add_library(Basics
3636
FileSystem/VFSOverlay.swift
3737
Graph/AdjacencyMatrix.swift
3838
Graph/DirectedGraph.swift
39+
Graph/GraphAlgorithms.swift
3940
Graph/UndirectedGraph.swift
4041
SourceControlURL.swift
4142
HTTPClient/HTTPClient.swift
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2015-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct OrderedCollections.OrderedSet
14+
15+
/// Implements a pre-order depth-first search.
16+
///
17+
/// The cycles are handled by skipping cycle points but it should be possible to
18+
/// to extend this in the future to provide a callback for every cycle.
19+
///
20+
/// - Parameters:
21+
/// - nodes: The list of input nodes to sort.
22+
/// - successors: A closure for fetching the successors of a particular node.
23+
/// - onUnique: A callback to indicate the the given node is being processed for the first time.
24+
/// - onDuplicate: A callback to indicate that the node was already processed at least once.
25+
///
26+
/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges
27+
/// reachable from the input nodes via the relation.
28+
public func depthFirstSearch<T: Hashable>(
29+
_ nodes: [T],
30+
successors: (T) throws -> [T],
31+
onUnique: (T) -> Void,
32+
onDuplicate: (T, T) -> Void
33+
) rethrows {
34+
var stack = OrderedSet<T>()
35+
var visited = Set<T>()
36+
37+
for node in nodes {
38+
precondition(stack.isEmpty)
39+
stack.append(node)
40+
41+
while !stack.isEmpty {
42+
let curr = stack.removeLast()
43+
44+
let visitResult = visited.insert(curr)
45+
if visitResult.inserted {
46+
onUnique(curr)
47+
} else {
48+
onDuplicate(visitResult.memberAfterInsert, curr)
49+
continue
50+
}
51+
52+
for succ in try successors(curr) {
53+
stack.append(succ)
54+
}
55+
}
56+
}
57+
}

Sources/Commands/PackageCommands/CompletionCommand.swift

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ extension SwiftPackageCommand {
6868
// command's result output goes on stdout
6969
// ie "swift package list-dependencies" should output to stdout
7070
ShowDependencies.dumpDependenciesOf(
71+
graph: graph,
7172
rootPackage: graph.rootPackages[graph.rootPackages.startIndex],
7273
mode: .flatlist,
7374
on: TSCBasic.stdoutStream

Sources/Commands/PackageCommands/PluginCommand.swift

+12-4
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ struct PluginCommand: SwiftCommand {
371371
pkgConfigDirectories: swiftCommandState.options.locations.pkgConfigDirectories,
372372
sdkRootPath: buildParameters.toolchain.sdkRootPath,
373373
fileSystem: swiftCommandState.fileSystem,
374+
modulesGraph: packageGraph,
374375
observabilityScope: swiftCommandState.observabilityScope,
375376
callbackQueue: delegateQueue,
376377
delegate: pluginDelegate,
@@ -382,10 +383,17 @@ struct PluginCommand: SwiftCommand {
382383

383384
static func availableCommandPlugins(in graph: ModulesGraph, limitedTo packageIdentity: String?) -> [PluginTarget] {
384385
// All targets from plugin products of direct dependencies are "available".
385-
let directDependencyPackages = graph.rootPackages.flatMap { $0.dependencies }.filter { $0.matching(identity: packageIdentity) }
386+
let directDependencyPackages = graph.rootPackages.flatMap {
387+
$0.dependencies
388+
}.filter {
389+
$0.matching(identity: packageIdentity)
390+
}.compactMap {
391+
graph.package(for: $0)
392+
}
393+
386394
let directDependencyPluginTargets = directDependencyPackages.flatMap { $0.products.filter { $0.type == .plugin } }.flatMap { $0.targets }
387395
// As well as any plugin targets in root packages.
388-
let rootPackageTargets = graph.rootPackages.filter { $0.matching(identity: packageIdentity) }.flatMap { $0.targets }
396+
let rootPackageTargets = graph.rootPackages.filter { $0.identity.matching(identity: packageIdentity) }.flatMap { $0.targets }
389397
return (directDependencyPluginTargets + rootPackageTargets).compactMap { $0.underlying as? PluginTarget }.filter {
390398
switch $0.capability {
391399
case .buildTool: return false
@@ -469,10 +477,10 @@ extension SandboxNetworkPermission {
469477
}
470478
}
471479

472-
extension ResolvedPackage {
480+
extension PackageIdentity {
473481
fileprivate func matching(identity: String?) -> Bool {
474482
if let identity {
475-
return self.identity == .plain(identity)
483+
return self == .plain(identity)
476484
} else {
477485
return true
478486
}

Sources/Commands/PackageCommands/ShowDependencies.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ extension SwiftPackageCommand {
4343
// ie "swift package show-dependencies" should output to stdout
4444
let stream: OutputByteStream = try outputPath.map { try LocalFileOutputByteStream($0) } ?? TSCBasic.stdoutStream
4545
Self.dumpDependenciesOf(
46+
graph: graph,
4647
rootPackage: graph.rootPackages[graph.rootPackages.startIndex],
4748
mode: format,
4849
on: stream
4950
)
5051
}
5152

52-
static func dumpDependenciesOf(rootPackage: ResolvedPackage, mode: ShowDependenciesMode, on stream: OutputByteStream) {
53+
static func dumpDependenciesOf(
54+
graph: ModulesGraph,
55+
rootPackage: ResolvedPackage,
56+
mode: ShowDependenciesMode,
57+
on stream: OutputByteStream
58+
) {
5359
let dumper: DependenciesDumper
5460
switch mode {
5561
case .text:
@@ -61,7 +67,7 @@ extension SwiftPackageCommand {
6167
case .flatlist:
6268
dumper = FlatListDumper()
6369
}
64-
dumper.dump(dependenciesOf: rootPackage, on: stream)
70+
dumper.dump(graph: graph, dependenciesOf: rootPackage, on: stream)
6571
stream.flush()
6672
}
6773

Sources/Commands/Utilities/DependenciesSerializer.swift

+11-11
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import enum TSCBasic.JSON
1717
import protocol TSCBasic.OutputByteStream
1818

1919
protocol DependenciesDumper {
20-
func dump(dependenciesOf: ResolvedPackage, on: OutputByteStream)
20+
func dump(graph: ModulesGraph, dependenciesOf: ResolvedPackage, on: OutputByteStream)
2121
}
2222

2323
final class PlainTextDumper: DependenciesDumper {
24-
func dump(dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
24+
func dump(graph: ModulesGraph, dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
2525
func recursiveWalk(packages: [ResolvedPackage], prefix: String = "") {
2626
var hanger = prefix + "├── "
2727

@@ -39,38 +39,38 @@ final class PlainTextDumper: DependenciesDumper {
3939
var childPrefix = hanger
4040
let startIndex = childPrefix.index(childPrefix.endIndex, offsetBy: -4)
4141
childPrefix.replaceSubrange(startIndex..<childPrefix.endIndex, with: replacement)
42-
recursiveWalk(packages: package.dependencies, prefix: childPrefix)
42+
recursiveWalk(packages: graph.directDependencies(for: package), prefix: childPrefix)
4343
}
4444
}
4545
}
4646

4747
if !rootpkg.dependencies.isEmpty {
4848
stream.send(".\n")
49-
recursiveWalk(packages: rootpkg.dependencies)
49+
recursiveWalk(packages: graph.directDependencies(for: rootpkg))
5050
} else {
5151
stream.send("No external dependencies found\n")
5252
}
5353
}
5454
}
5555

5656
final class FlatListDumper: DependenciesDumper {
57-
func dump(dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
57+
func dump(graph: ModulesGraph, dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
5858
func recursiveWalk(packages: [ResolvedPackage]) {
5959
for package in packages {
6060
stream.send(package.identity.description).send("\n")
6161
if !package.dependencies.isEmpty {
62-
recursiveWalk(packages: package.dependencies)
62+
recursiveWalk(packages: graph.directDependencies(for: package))
6363
}
6464
}
6565
}
6666
if !rootpkg.dependencies.isEmpty {
67-
recursiveWalk(packages: rootpkg.dependencies)
67+
recursiveWalk(packages: graph.directDependencies(for: rootpkg))
6868
}
6969
}
7070
}
7171

7272
final class DotDumper: DependenciesDumper {
73-
func dump(dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
73+
func dump(graph: ModulesGraph, dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
7474
var nodesAlreadyPrinted: Set<String> = []
7575
func printNode(_ package: ResolvedPackage) {
7676
let url = package.manifest.packageLocation
@@ -87,7 +87,7 @@ final class DotDumper: DependenciesDumper {
8787
var dependenciesAlreadyPrinted: Set<DependencyURLs> = []
8888
func recursiveWalk(rootpkg: ResolvedPackage) {
8989
printNode(rootpkg)
90-
for dependency in rootpkg.dependencies {
90+
for dependency in graph.directDependencies(for: rootpkg) {
9191
let rootURL = rootpkg.manifest.packageLocation
9292
let dependencyURL = dependency.manifest.packageLocation
9393
let urlPair = DependencyURLs(root: rootURL, dependency: dependencyURL)
@@ -120,15 +120,15 @@ final class DotDumper: DependenciesDumper {
120120
}
121121

122122
final class JSONDumper: DependenciesDumper {
123-
func dump(dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
123+
func dump(graph: ModulesGraph, dependenciesOf rootpkg: ResolvedPackage, on stream: OutputByteStream) {
124124
func convert(_ package: ResolvedPackage) -> JSON {
125125
return .orderedDictionary([
126126
"identity": .string(package.identity.description),
127127
"name": .string(package.manifest.displayName), // TODO: remove?
128128
"url": .string(package.manifest.packageLocation),
129129
"version": .string(package.manifest.version?.description ?? "unspecified"),
130130
"path": .string(package.path.pathString),
131-
"dependencies": .array(package.dependencies.map(convert)),
131+
"dependencies": .array(package.dependencies.compactMap { graph.packages[$0] }.map(convert)),
132132
])
133133
}
134134

0 commit comments

Comments
 (0)