diff --git a/Sources/apk/Graph/ApkPackageGraph.swift b/Sources/apk/Graph/ApkPackageGraph.swift index cad2494..99a0721 100644 --- a/Sources/apk/Graph/ApkPackageGraph.swift +++ b/Sources/apk/Graph/ApkPackageGraph.swift @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import Foundation + public class ApkPackageGraph { public let pkgIndex: ApkIndex @@ -26,9 +28,9 @@ public class ApkPackageGraph { } } - for package in pkgIndex.packages { - self._nodes.append(.init( - package: package, + for (id, package) in pkgIndex.packages.enumerated() { + self._nodes.append(.init(self, + id: id, children: package.dependencies.compactMap { dependency in guard let id = provides[dependency.requirement.name] else { return nil @@ -62,3 +64,114 @@ public class ApkPackageGraph { } } } + +extension ApkPackageGraph { + func findDependencyCycle(node: ApkPackageGraphNode) -> (ApkPackageGraphNode, ApkPackageGraphNode)? { + var resolving = Set() + var visited = Set() + return self.findDependencyCycle(node: node, &resolving, &visited) + } + + func findDependencyCycle( + node: ApkPackageGraphNode, + _ resolving: inout Set, + _ visited: inout Set + ) -> (ApkPackageGraphNode, ApkPackageGraphNode)? { + for dependency in node.children { + let depNode = self._nodes[dependency.packageID] + if resolving.contains(depNode) { + return (node, depNode) + } + + if !visited.contains(depNode) { + resolving.insert(depNode) + if let cycle = findDependencyCycle(node: depNode, &resolving, &visited) { + return cycle + } + + resolving.remove(depNode) + visited.insert(depNode) + } + } + + return nil + } + + public func parallelOrderSort(breakCycles: Bool = true) throws(SortError) -> [[ApkPackageGraphNode]] { + var results = [[ApkPackageGraphNode]]() + + // Map all nodes to all of their children, remove any self dependencies + var working = self._nodes.reduce(into: [ApkPackageGraphNode: Set]()) { d, node in + d[node] = Set(node.children.filter { child in + if case .dep(let version) = child.constraint { + version != .conflict && child.packageID != node.packageID + } else { false } + }.map { self._nodes[$0.packageID] }) + } + + // Collect all child nodes that aren't already in the map + // This should be empty every time + let extras = working.values.reduce(Set()) { a, b in + a.union(b) + }.subtracting(working.keys) + assert(extras.isEmpty, "Dangling nodes in the graph") + + // Put all extra nodes into the working map, with an empty set + extras.forEach { + working[$0] = .init() + } + + while true { + // Set of all nodes now with satisfied dependencies + var set = working + .filter { _, children in children.isEmpty } + .map(\.key) + + // If nothing was satisfied in this loop, check for cycles + // If no cycles exist and the working set is empty, resolve is complete + if set.isEmpty { + if working.isEmpty { + break + } + + let cycles = working.keys.compactMap { node in + self.findDependencyCycle(node: node) + } + + // Error if cycle breaking is turned off + if !breakCycles { + throw .cyclicDependency(cycles: cycles.map { node, dependency in + "\(node) -> \(dependency)" + }.joined(separator: "\n")) + } + + // Bread cycles by setting the new resolution set to dependencies that cycled + set = cycles.map(\.1) + } + + // Add installation set to list of installation sets + results.append(set) + + // Filter the working set for anything that wasn't dealt with this iteration + working = working.filter { node, _ in + !set.contains(node) + }.reduce(into: [ApkPackageGraphNode: Set]()) { d, node in + d[node.key] = node.value.subtracting(set) + } + } + + print(working) + + return results + } + + public enum SortError: Error, LocalizedError { + case cyclicDependency(cycles: String) + + var errorDescription: String { + switch self { + case .cyclicDependency(let cycles): "Dependency cycles found:\n\(cycles)" + } + } + } +} diff --git a/Sources/apk/Graph/ApkPackageGraphNode.swift b/Sources/apk/Graph/ApkPackageGraphNode.swift index 6471d44..a91a521 100644 --- a/Sources/apk/Graph/ApkPackageGraphNode.swift +++ b/Sources/apk/Graph/ApkPackageGraphNode.swift @@ -6,20 +6,35 @@ import Foundation public class ApkPackageGraphNode { - private weak var graph: ApkPackageGraph! - let package: ApkIndexPackage + private weak var _graph: ApkPackageGraph? + let packageID: Int //private var _parents = NSHashTable.weakObjects() //private var _children = NSHashTable.weakObjects() var parents = [ApkIndexRequirementRef]() var children: [ApkIndexRequirementRef] - internal init(package: ApkIndexPackage, children: [ApkIndexRequirementRef]) { - self.package = package + var package: ApkIndexPackage { + self._graph!.pkgIndex.packages[self.packageID] + } + + internal init(_ graph: ApkPackageGraph, id: Int, children: [ApkIndexRequirementRef]) { + self._graph = graph + self.packageID = id self.children = children } } +extension ApkPackageGraphNode: Equatable, Hashable { + public static func == (lhs: ApkPackageGraphNode, rhs: ApkPackageGraphNode) -> Bool { + lhs.packageID == rhs.packageID + } + + public func hash(into hasher: inout Hasher) { + self.packageID.hash(into: &hasher) + } +} + extension ApkPackageGraphNode: CustomStringConvertible { public var description: String { var result = "node[\(self.package.name)]" diff --git a/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift b/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift index 561b0d9..5d34652 100644 --- a/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift +++ b/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift @@ -22,11 +22,20 @@ struct DpkGraphCommand: AsyncParsableCommand { fatalError(error.localizedDescription) } +#if false if var out = TextFileWriter(URL(filePath: "shallowIsolates.txt")) { for node in graph.shallowIsolates { print(node, to: &out) } } if var out = TextFileWriter(URL(filePath: "deepIsolates.txt")) { for node in graph.deepIsolates { print(node, to: &out) } } +#else + do { + let sorted = try graph.parallelOrderSort() + print(sorted) + } catch { + fatalError(error.localizedDescription) + } +#endif } }