Files
darwin-apk/Sources/apk/Graph/ApkPackageGraph.swift

217 lines
6.7 KiB
Swift
Raw Normal View History

2024-11-11 21:06:37 +11:00
/*
2025-07-05 18:38:14 +10:00
* darwin-apk © 2024, 2025 Gay Pizza Specifications
2024-11-11 21:06:37 +11:00
* SPDX-License-Identifier: Apache-2.0
*/
2024-11-11 23:08:01 +11:00
import Foundation
public class ApkPackageGraph {
2025-07-10 21:51:30 +10:00
public var pkgIndex: ApkIndex
2024-11-11 21:06:37 +11:00
private var _nodes = [ApkPackageGraphNode]()
public var nodes: [ApkPackageGraphNode] { self._nodes }
2025-07-10 21:51:30 +10:00
public var shallowIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.isShallow) }
public var deepIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.isDeep) }
2024-11-11 21:06:37 +11:00
public init(index: ApkIndex) {
2024-11-11 21:06:37 +11:00
self.pkgIndex = index
}
public func buildGraphNode() {
2025-07-10 21:51:30 +10:00
for (packageID, package) in pkgIndex.packages.enumerated() {
let children: [ApkPackageGraphNode.ChildRef] = package.dependencies.compactMap { dependency in
guard let providerID = pkgIndex.resolveIndex(requirement: dependency.requirement) else {
2025-02-16 17:48:04 -08:00
return nil
}
2025-07-10 21:51:30 +10:00
return .init(constraint: .dependency, packageID: providerID, versionSpec: dependency.requirement.versionSpec)
2025-07-05 18:38:14 +10:00
} + package.installIf.compactMap { installIf in
2025-07-10 21:51:30 +10:00
guard let prvID = pkgIndex.resolveIndex(requirement: installIf.requirement) else {
2025-07-05 18:38:14 +10:00
return nil
}
2025-07-10 21:51:30 +10:00
return .init(constraint: .installIf, packageID: prvID, versionSpec: installIf.requirement.versionSpec)
2025-02-16 17:48:04 -08:00
}
2024-11-11 23:08:01 +11:00
self._nodes.append(.init(self,
2025-07-10 21:51:30 +10:00
id: packageID,
2025-02-16 17:48:04 -08:00
children: children
2024-11-11 21:06:37 +11:00
))
}
2025-07-10 21:51:30 +10:00
var reverseDependencies = [ApkIndex.Index: [ApkIndex.Index]]()
2024-11-11 21:06:37 +11:00
for (index, node) in self._nodes.enumerated() {
for child in node.children {
2025-07-10 21:51:30 +10:00
reverseDependencies[child.packageID, default: []].append(index)
2024-11-11 21:06:37 +11:00
}
}
for (ref, parents) in reverseDependencies {
2025-07-10 21:51:30 +10:00
self._nodes[ref].parentIDs = parents
2024-11-11 21:06:37 +11:00
}
}
}
2024-11-11 23:08:01 +11:00
2025-07-10 22:39:56 +10:00
extension ApkPackageGraph {
public func orderSort(breakCycles: Bool = true) throws(SortError) -> [ApkPackageGraphNode] {
var stack = [ApkPackageGraphNode]()
var resolving = Set<ApkIndex.Index>()
var visited = Set<ApkIndex.Index>()
var ignoring = Set<ApkIndex.Index>()
for node in self.shallowIsolates {
try orderSort(node, &stack, &resolving, &visited, &ignoring, breakCycles)
}
return stack
}
internal func orderSort(
_ node: ApkPackageGraphNode,
_ stack: inout [ApkPackageGraphNode],
_ resolving: inout Set<ApkIndex.Index>,
_ visited: inout Set<ApkIndex.Index>,
_ ignoring: inout Set<ApkIndex.Index>,
_ breakCycles: Bool
) throws(SortError) {
for dep in node.children {
let depID = dep.packageID
guard !ignoring.contains(depID) else {
continue
}
guard !resolving.contains(depID) else {
throw .cyclicDependency(cycles: "\(node) -> \(dep)")
}
if !visited.contains(depID) {
resolving.insert(depID)
let depNode = self._nodes[depID]
do {
try orderSort(depNode, &stack, &resolving, &visited, &ignoring, breakCycles)
} catch {
guard breakCycles else {
throw error
}
stack.append(depNode)
ignoring.insert(depID)
try orderSort(depNode, &stack, &resolving, &visited, &ignoring, breakCycles)
ignoring.remove(depID)
}
resolving.remove(depID)
visited.insert(depID)
}
}
if !stack.contains(node) {
stack.append(node)
}
}
}
2024-11-11 23:08:01 +11:00
extension ApkPackageGraph {
func findDependencyCycle(node: ApkPackageGraphNode) -> (ApkPackageGraphNode, ApkPackageGraphNode)? {
2025-02-16 17:48:04 -08:00
var resolving = Set<Int>()
var visited = Set<Int>()
2024-11-11 23:08:01 +11:00
return self.findDependencyCycle(node: node, &resolving, &visited)
}
func findDependencyCycle(
node: ApkPackageGraphNode,
2025-07-10 21:51:30 +10:00
_ resolving: inout Set<ApkIndex.Index>,
_ visited: inout Set<ApkIndex.Index>
2024-11-11 23:08:01 +11:00
) -> (ApkPackageGraphNode, ApkPackageGraphNode)? {
for dependency in node.children {
let depNode = self._nodes[dependency.packageID]
2025-02-16 17:48:04 -08:00
if resolving.contains(depNode.packageID) {
2024-11-11 23:08:01 +11:00
return (node, depNode)
}
2025-02-16 17:48:04 -08:00
if !visited.contains(depNode.packageID) {
resolving.insert(depNode.packageID)
2024-11-11 23:08:01 +11:00
if let cycle = findDependencyCycle(node: depNode, &resolving, &visited) {
return cycle
}
2025-02-16 17:48:04 -08:00
resolving.remove(depNode.packageID)
visited.insert(depNode.packageID)
2024-11-11 23:08:01 +11:00
}
}
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<ApkPackageGraphNode>]()) { d, node in
d[node] = Set(node.children.filter { child in
2025-07-10 21:51:30 +10:00
if case .dependency = child.constraint {
!child.versionSpec.isConflict && child.packageID != node.packageID
2024-11-11 23:08:01 +11:00
} 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<ApkPackageGraphNode>()) { 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 {
2025-07-10 21:51:30 +10:00
working[$0] = []
2024-11-11 23:08:01 +11:00
}
2025-02-16 17:48:04 -08:00
while !working.isEmpty {
2024-11-11 23:08:01 +11:00
// Set of all nodes now with satisfied dependencies
2025-07-05 18:38:14 +10:00
var set = Set(working
2024-11-11 23:08:01 +11:00
.filter { _, children in children.isEmpty }
2025-07-05 18:38:14 +10:00
.map(\.key))
2024-11-11 23:08:01 +11:00
// 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)
}
2025-07-05 18:38:14 +10:00
2024-11-11 23:08:01 +11:00
// Error if cycle breaking is turned off
if !breakCycles {
throw .cyclicDependency(cycles: cycles.map { node, dependency in
"\(node) -> \(dependency)"
}.joined(separator: "\n"))
}
2025-07-05 18:38:14 +10:00
2025-02-16 17:48:04 -08:00
// Break cycles by setting the new resolution set to dependencies that cycled
2025-07-05 18:38:14 +10:00
set = Set(cycles.flatMap { [$0.0, $0.1] })
2024-11-11 23:08:01 +11:00
}
2025-07-05 18:38:14 +10:00
2024-11-11 23:08:01 +11:00
// Add installation set to list of installation sets
2025-07-05 18:38:14 +10:00
results.append(Array(set))
2024-11-11 23:08:01 +11:00
// 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<ApkPackageGraphNode>]()) { d, node in
d[node.key] = node.value.subtracting(set)
}
}
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)"
}
}
}
}