Quick n' dirty SwiftGraph

This commit is contained in:
2025-07-10 05:37:24 +10:00
parent 6ea612f9fc
commit 1cc43897b1
5 changed files with 99 additions and 101 deletions

View File

@ -8,10 +8,14 @@ let package = Package(
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.6.1"), .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: [ targets: [
.target( .target(
name: "darwin-apk", name: "darwin-apk",
dependencies: [
.product(name: "SwiftGraph", package: "SwiftGraph"),
],
path: "Sources/apk", path: "Sources/apk",
), ),
.testTarget( .testTarget(

View File

@ -4,103 +4,71 @@
*/ */
import Foundation import Foundation
import SwiftGraph
public class ApkPackageGraph { public class ApkPackageGraph {
public let pkgIndex: ApkIndex let graph: UnweightedGraph<ApkIndex.Index>
private var _nodes = [ApkPackageGraphNode]() public var shallowIsolates: [ApkIndex.Index] {
self.graph.indices.lazy.filter { index in !self.graph.edges.contains { edge in edge.endIndex == index } }
public var nodes: [ApkPackageGraphNode] { self._nodes } .map { index in self.graph.vertexAtIndex(index) }
public var shallowIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.parents.isEmpty) } }
public var deepIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.children.isEmpty) } public var deepIsolates: [ApkIndex.Index] {
self.graph.indices.lazy.filter { index in self.graph.edgesForIndex(index).isEmpty }
public init(index: ApkIndex) { .map { index in self.graph.vertexAtIndex(index) }
self.pkgIndex = index
} }
public func buildGraphNode() { public init(from pkgIndex: inout ApkIndex) throws(GraphError) {
var provides = [String: Int]() self.graph = UnweightedGraph<ApkIndex.Index>()
for (idx, package) in self.pkgIndex.packages.enumerated() { // Add each package to the graph
provides[package.name] = idx for pkgIdx in pkgIndex.packages.indices {
for provision in package.provides { // Skip packages already added by requirements
if !provides.keys.contains(provision.name) { guard !self.graph.vertexInGraph(vertex: pkgIdx) else {
provides[provision.name] = idx continue
}
} }
} // Add package ID as a vertex
let u = self.graph.addVertex(pkgIdx)
for (id, package) in pkgIndex.packages.enumerated() { // Add dependent packages to the graphs and link them via edges
let children: [ApkIndexRequirementRef] = package.dependencies.compactMap { dependency in let pkg = pkgIndex.packages[pkgIdx]
guard !dependency.requirement.versionSpec.conflict, for dep in pkg.dependencies {
let id = provides[dependency.requirement.name] else { // Resolve package dependency
return nil 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 // Get the graph vertex of dependency, or add it to the graph if it doesn't exist
guard let id = provides[installIf.requirement.name] else { let v = self.graph.indexOfVertex(depIdx) ?? self.graph.addVertex(depIdx)
return nil
} self.graph.addEdge(fromIndex: u, toIndex: v, directed: true)
return .init(self, id: id, constraint: .installIf(version: installIf.requirement.versionSpec ))
} }
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 { extension ApkPackageGraph {
func findDependencyCycle(node: ApkPackageGraphNode) -> (ApkPackageGraphNode, ApkPackageGraphNode)? { public func sorted(breakCycles: Bool = true) throws(SortError) -> [ApkIndex.Index] {
var resolving = Set<Int>() if !breakCycles {
var visited = Set<Int>() guard let sorted = self.graph.topologicalSort() else {
return self.findDependencyCycle(node: node, &resolving, &visited) throw .cyclicDependency(cycles: self.graph.detectCycles().description)
}
func findDependencyCycle(
node: ApkPackageGraphNode,
_ resolving: inout Set<Int>,
_ visited: inout Set<Int>
) -> (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)
} }
return sorted.reversed()
} }
fatalError("Not yet implemented")
return nil /*
} var results = [[ApkIndex.Index]]()
public func parallelOrderSort(breakCycles: Bool = true) throws(SortError) -> [[ApkPackageGraphNode]] {
var results = [[ApkPackageGraphNode]]()
// Map all nodes to all of their children, remove any self dependencies // 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<ApkPackageGraphNode>]()) { d, node in var working = self._nodes.reduce(into: [ApkPackageGraphNode: Set<ApkPackageGraphNode>]()) { d, node in
d[node] = Set(node.children.filter { child in d[node] = Set(node.children.filter { child in
if case .dep(let version) = child.constraint { if case .dep(let version) = child.constraint {
@ -134,9 +102,7 @@ extension ApkPackageGraph {
break break
} }
let cycles = working.keys.compactMap { node in let cycles = self.graph.detectCycles()
self.findDependencyCycle(node: node)
}
// Error if cycle breaking is turned off // Error if cycle breaking is turned off
if !breakCycles { if !breakCycles {
@ -159,14 +125,25 @@ extension ApkPackageGraph {
d[node.key] = node.value.subtracting(set) 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 { public enum SortError: Error, LocalizedError {
case cyclicDependency(cycles: String) case cyclicDependency(cycles: String)
var errorDescription: String { public var errorDescription: String? {
switch self { switch self {
case .cyclicDependency(let cycles): "Dependency cycles found:\n\(cycles)" case .cyclicDependency(let cycles): "Dependency cycles found:\n\(cycles)"
} }

View File

@ -5,6 +5,7 @@
import Foundation import Foundation
/*
public class ApkPackageGraphNode { public class ApkPackageGraphNode {
private weak var _graph: ApkPackageGraph? private weak var _graph: ApkPackageGraph?
let packageID: Int let packageID: Int
@ -48,3 +49,4 @@ extension ApkPackageGraphNode: CustomStringConvertible {
return result return result
} }
} }
*/

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/*
struct ApkIndexRequirementRef { struct ApkIndexRequirementRef {
private weak var _graph: ApkPackageGraph? private weak var _graph: ApkPackageGraph?
@ -62,3 +63,4 @@ extension ApkIndexRequirementRef: CustomStringConvertible {
} }
} }
} }
*/

View File

@ -14,28 +14,41 @@ struct DpkGraphCommand: AsyncParsableCommand {
let graph: ApkPackageGraph let graph: ApkPackageGraph
do { do {
let localRepositories = try await ApkRepositoriesConfig() 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 { } catch {
fatalError(error.localizedDescription) fatalError(error.localizedDescription)
} }
}
#if false }
if var out = TextFileWriter(URL(filePath: "shallowIsolates.txt")) {
for node in graph.shallowIsolates { print(node, to: &out) } fileprivate extension DispatchTimeInterval {
} var seconds: Double {
if var out = TextFileWriter(URL(filePath: "deepIsolates.txt")) { switch self {
for node in graph.deepIsolates { print(node, to: &out) } case .seconds(let value): Double(value)
} case .milliseconds(let value): Double(value) / 1_000
#else case .microseconds(let value): Double(value) / 1_000_000
do { case .nanoseconds(let value): Double(value) / 1_000_000_000
let sorted = try graph.parallelOrderSort() case .never: .infinity
print(sorted) @unknown default:
} catch { fatalError("Unsupported")
fatalError(error.localizedDescription) }
}
#endif
} }
} }