From 1cc43897b1138d58b97cb450c3f3b29d77f67a67 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Thu, 10 Jul 2025 05:37:24 +1000 Subject: [PATCH] Quick n' dirty SwiftGraph --- Package.swift | 4 + Sources/apk/Graph/ApkPackageGraph.swift | 141 ++++++++---------- Sources/apk/Graph/ApkPackageGraphNode.swift | 2 + .../apk/Index/ApkIndexRequirementRef.swift | 2 + .../dpk-cli/Subcommands/DpkGraphCommand.swift | 51 ++++--- 5 files changed, 99 insertions(+), 101 deletions(-) diff --git a/Package.swift b/Package.swift index 37b27a9..1713016 100644 --- a/Package.swift +++ b/Package.swift @@ -8,10 +8,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.6.1"), + .package(url: "https://github.com/davecom/SwiftGraph", from: "3.1.0"), ], targets: [ .target( name: "darwin-apk", + dependencies: [ + .product(name: "SwiftGraph", package: "SwiftGraph"), + ], path: "Sources/apk", ), .testTarget( diff --git a/Sources/apk/Graph/ApkPackageGraph.swift b/Sources/apk/Graph/ApkPackageGraph.swift index dd03ba6..ba33829 100644 --- a/Sources/apk/Graph/ApkPackageGraph.swift +++ b/Sources/apk/Graph/ApkPackageGraph.swift @@ -4,103 +4,71 @@ */ import Foundation +import SwiftGraph public class ApkPackageGraph { - public let pkgIndex: ApkIndex + let graph: UnweightedGraph - private var _nodes = [ApkPackageGraphNode]() - - public var nodes: [ApkPackageGraphNode] { self._nodes } - public var shallowIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.parents.isEmpty) } - public var deepIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.children.isEmpty) } - - public init(index: ApkIndex) { - self.pkgIndex = index + public var shallowIsolates: [ApkIndex.Index] { + self.graph.indices.lazy.filter { index in !self.graph.edges.contains { edge in edge.endIndex == index } } + .map { index in self.graph.vertexAtIndex(index) } + } + public var deepIsolates: [ApkIndex.Index] { + self.graph.indices.lazy.filter { index in self.graph.edgesForIndex(index).isEmpty } + .map { index in self.graph.vertexAtIndex(index) } } - public func buildGraphNode() { - var provides = [String: Int]() + public init(from pkgIndex: inout ApkIndex) throws(GraphError) { + self.graph = UnweightedGraph() - for (idx, package) in self.pkgIndex.packages.enumerated() { - provides[package.name] = idx - for provision in package.provides { - if !provides.keys.contains(provision.name) { - provides[provision.name] = idx - } + // Add each package to the graph + for pkgIdx in pkgIndex.packages.indices { + // Skip packages already added by requirements + guard !self.graph.vertexInGraph(vertex: pkgIdx) else { + continue } - } + // Add package ID as a vertex + let u = self.graph.addVertex(pkgIdx) - for (id, package) in pkgIndex.packages.enumerated() { - let children: [ApkIndexRequirementRef] = package.dependencies.compactMap { dependency in - guard !dependency.requirement.versionSpec.conflict, - let id = provides[dependency.requirement.name] else { - return nil + // Add dependent packages to the graphs and link them via edges + let pkg = pkgIndex.packages[pkgIdx] + for dep in pkg.dependencies { + // Resolve package dependency + guard let depIdx = pkgIndex.resolveIndex(requirement: dep.requirement) else { + // It's okay to skip missing conflicts + if dep.requirement.versionSpec.isConflict { + continue + } + // Didn't find a satisfactory dependency in the index + //throw .missingDependency(dep.requirement, pkg) + print("WARN: Couldn't satisfy \"\(dep.requirement)\" required by \"\(pkg.nameDescription)\"") + continue } - return .init(self, id: id, constraint: .dep(version: dependency.requirement.versionSpec)) - } + package.installIf.compactMap { installIf in - guard let id = provides[installIf.requirement.name] else { - return nil - } - return .init(self, id: id, constraint: .installIf(version: installIf.requirement.versionSpec )) + + // Get the graph vertex of dependency, or add it to the graph if it doesn't exist + let v = self.graph.indexOfVertex(depIdx) ?? self.graph.addVertex(depIdx) + + self.graph.addEdge(fromIndex: u, toIndex: v, directed: true) } - self._nodes.append(.init(self, - id: id, - children: children - )) - } - - var reverseDependencies = [ApkIndexRequirementRef: [ApkIndexRequirementRef]]() - - for (index, node) in self._nodes.enumerated() { - for child in node.children { - reverseDependencies[child, default: []].append( - .init(self, id: index, constraint: child.constraint) - ) - } - } - - for (ref, parents) in reverseDependencies { - self._nodes[ref.packageID].parents = parents } } } 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.packageID) { - return (node, depNode) - } - - if !visited.contains(depNode.packageID) { - resolving.insert(depNode.packageID) - if let cycle = findDependencyCycle(node: depNode, &resolving, &visited) { - return cycle - } - - resolving.remove(depNode.packageID) - visited.insert(depNode.packageID) + public func sorted(breakCycles: Bool = true) throws(SortError) -> [ApkIndex.Index] { + if !breakCycles { + guard let sorted = self.graph.topologicalSort() else { + throw .cyclicDependency(cycles: self.graph.detectCycles().description) } + return sorted.reversed() } + fatalError("Not yet implemented") - return nil - } - - public func parallelOrderSort(breakCycles: Bool = true) throws(SortError) -> [[ApkPackageGraphNode]] { - var results = [[ApkPackageGraphNode]]() + /* + var results = [[ApkIndex.Index]]() // Map all nodes to all of their children, remove any self dependencies + var working = self.graph.isDAG 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 { @@ -134,9 +102,7 @@ extension ApkPackageGraph { break } - let cycles = working.keys.compactMap { node in - self.findDependencyCycle(node: node) - } + let cycles = self.graph.detectCycles() // Error if cycle breaking is turned off if !breakCycles { @@ -159,14 +125,25 @@ extension ApkPackageGraph { d[node.key] = node.value.subtracting(set) } } + */ + } +} - return results +extension ApkPackageGraph { + public enum GraphError: Error, LocalizedError { + case missingDependency(ApkVersionRequirement, ApkIndexPackage) + + public var errorDescription: String? { + switch self { + case .missingDependency(let r, let p): "Couldn't satisfy \"\(r)\" required by \"\(p.nameDescription)\"" + } + } } public enum SortError: Error, LocalizedError { case cyclicDependency(cycles: String) - var errorDescription: String { + public 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 a91a521..a079cf0 100644 --- a/Sources/apk/Graph/ApkPackageGraphNode.swift +++ b/Sources/apk/Graph/ApkPackageGraphNode.swift @@ -5,6 +5,7 @@ import Foundation +/* public class ApkPackageGraphNode { private weak var _graph: ApkPackageGraph? let packageID: Int @@ -48,3 +49,4 @@ extension ApkPackageGraphNode: CustomStringConvertible { return result } } +*/ diff --git a/Sources/apk/Index/ApkIndexRequirementRef.swift b/Sources/apk/Index/ApkIndexRequirementRef.swift index b8e2144..29aff77 100644 --- a/Sources/apk/Index/ApkIndexRequirementRef.swift +++ b/Sources/apk/Index/ApkIndexRequirementRef.swift @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +/* struct ApkIndexRequirementRef { private weak var _graph: ApkPackageGraph? @@ -62,3 +63,4 @@ extension ApkIndexRequirementRef: CustomStringConvertible { } } } +*/ diff --git a/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift b/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift index 5d34652..d120872 100644 --- a/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift +++ b/Sources/dpk-cli/Subcommands/DpkGraphCommand.swift @@ -14,28 +14,41 @@ struct DpkGraphCommand: AsyncParsableCommand { let graph: ApkPackageGraph do { let localRepositories = try await ApkRepositoriesConfig() - graph = ApkPackageGraph(index: try await ApkIndexReader.resolve(localRepositories.repositories, fetch: .lazy)) - graph.buildGraphNode() - try graph.pkgIndex.description.write(to: URL(filePath: "packages.txt"), atomically: false, encoding: .utf8) + var timerStart = DispatchTime.now() + var pkgIndex = try await ApkIndexReader.resolve(localRepositories.repositories, fetch: .lazy) + print("Index build took \(timerStart.distance(to: .now()).seconds) seconds") + try pkgIndex.description.write(to: URL(filePath: "packages.txt"), atomically: false, encoding: .utf8) + + timerStart = DispatchTime.now() + try graph = ApkPackageGraph(from: &pkgIndex) + print("Graph build took \(timerStart.distance(to: .now()).seconds) seconds") + + try graph.shallowIsolates.map { pkgIndex.packages[$0].nameDescription }.joined(separator: "\n") + .write(to: URL(filePath: "shallowIsolates.txt"), atomically: false, encoding: .utf8) + try graph.deepIsolates.map { pkgIndex.packages[$0].nameDescription }.joined(separator: "\n") + .write(to: URL(filePath: "deepIsolates.txt"), atomically: false, encoding: .utf8) + + let sorted = try graph.sorted(breakCycles: false) + try sorted.map { pkgIndex.packages[$0].nameDescription }.joined(separator: "\n") + .write(to: URL(filePath: "sorted.txt"), atomically: false, encoding: .utf8) + } catch { 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 + } +} + +fileprivate extension DispatchTimeInterval { + var seconds: Double { + switch self { + case .seconds(let value): Double(value) + case .milliseconds(let value): Double(value) / 1_000 + case .microseconds(let value): Double(value) / 1_000_000 + case .nanoseconds(let value): Double(value) / 1_000_000_000 + case .never: .infinity + @unknown default: + fatalError("Unsupported") + } } }