198 lines
5.9 KiB
Nim
198 lines
5.9 KiB
Nim
# nimpak/dependency.nim
|
|
# Dependency graph resolution and management system
|
|
|
|
import std/[tables, sets, sequtils, algorithm, strformat]
|
|
import ./types
|
|
|
|
type
|
|
DependencyGraph* = object
|
|
nodes*: Table[PackageId, Fragment]
|
|
edges*: Table[PackageId, seq[PackageId]]
|
|
resolved*: seq[PackageId]
|
|
|
|
InstallOrder* = object
|
|
packages*: seq[PackageId]
|
|
totalSteps*: int
|
|
|
|
DependencyError* = object of NimPakError
|
|
conflictingPackages*: seq[PackageId]
|
|
missingDependencies*: seq[PackageId]
|
|
cyclicDependencies*: seq[PackageId]
|
|
|
|
# Public API
|
|
proc resolveDependencies*(root: PackageId, fragments: Table[PackageId, Fragment]): Result[InstallOrder, DependencyError] =
|
|
## Resolve dependencies for a root package and return installation order
|
|
var graph = DependencyGraph()
|
|
|
|
# Build the dependency graph
|
|
let buildResult = buildDependencyGraph(graph, root, fragments)
|
|
if buildResult.isErr:
|
|
return err(buildResult.error)
|
|
|
|
# Perform topological sort to get installation order
|
|
let sortResult = topologicalSort(graph)
|
|
if sortResult.isErr:
|
|
return err(sortResult.error)
|
|
|
|
ok(InstallOrder(
|
|
packages: sortResult.get(),
|
|
totalSteps: sortResult.get().len
|
|
))
|
|
|
|
proc buildDependencyGraph(graph: var DependencyGraph, root: PackageId, fragments: Table[PackageId, Fragment]): Result[void, DependencyError] =
|
|
## Build directed graph from package dependencies (6.1.1, 6.1.2)
|
|
var visited = initHashSet[PackageId]()
|
|
var visiting = initHashSet[PackageId]()
|
|
|
|
proc visitNode(pkgId: PackageId): Result[void, DependencyError] =
|
|
if pkgId in visiting:
|
|
# Cycle detected
|
|
return err(DependencyError(
|
|
code: DependencyConflict,
|
|
msg: "Circular dependency detected",
|
|
cyclicDependencies: @[pkgId]
|
|
))
|
|
|
|
if pkgId in visited:
|
|
return ok()
|
|
|
|
# Check if fragment exists
|
|
if pkgId notin fragments:
|
|
return err(DependencyError(
|
|
code: PackageNotFound,
|
|
msg: "Missing dependency: " & pkgId.name,
|
|
missingDependencies: @[pkgId]
|
|
))
|
|
|
|
visiting.incl(pkgId)
|
|
let fragment = fragments[pkgId]
|
|
|
|
# Add node to graph
|
|
graph.nodes[pkgId] = fragment
|
|
graph.edges[pkgId] = fragment.dependencies
|
|
|
|
# Visit dependencies recursively
|
|
for dep in fragment.dependencies:
|
|
let depResult = visitNode(dep)
|
|
if depResult.isErr:
|
|
return depResult
|
|
|
|
visiting.excl(pkgId)
|
|
visited.incl(pkgId)
|
|
ok()
|
|
|
|
visitNode(root)
|
|
|
|
proc topologicalSort(graph: DependencyGraph): Result[seq[PackageId], DependencyError] =
|
|
## Perform topological sort to determine installation order (6.1.3)
|
|
var inDegree = initTable[PackageId, int]()
|
|
var queue: seq[PackageId] = @[]
|
|
var result: seq[PackageId] = @[]
|
|
|
|
# Initialize in-degree count
|
|
for node in graph.nodes.keys:
|
|
inDegree[node] = 0
|
|
|
|
# Calculate in-degrees
|
|
for (node, deps) in graph.edges.pairs:
|
|
for dep in deps:
|
|
if dep in inDegree:
|
|
inDegree[dep] += 1
|
|
|
|
# Find nodes with no incoming edges
|
|
for (node, degree) in inDegree.pairs:
|
|
if degree == 0:
|
|
queue.add(node)
|
|
|
|
# Process queue
|
|
while queue.len > 0:
|
|
let current = queue.pop()
|
|
result.add(current)
|
|
|
|
# Reduce in-degree for dependencies
|
|
if current in graph.edges:
|
|
for dep in graph.edges[current]:
|
|
if dep in inDegree:
|
|
inDegree[dep] -= 1
|
|
if inDegree[dep] == 0:
|
|
queue.add(dep)
|
|
|
|
# Check for cycles
|
|
if result.len != graph.nodes.len:
|
|
let remaining = toSeq(graph.nodes.keys).filterIt(it notin result)
|
|
return err(DependencyError(
|
|
code: DependencyConflict,
|
|
msg: "Circular dependencies detected",
|
|
cyclicDependencies: remaining
|
|
))
|
|
|
|
# Reverse to get correct installation order (dependencies first)
|
|
result.reverse()
|
|
ok(result)
|
|
|
|
proc resolveVersionConstraint*(pkg: string, constraint: string): Result[PackageId, DependencyError] =
|
|
## Stub for version constraint resolution (6.1.4)
|
|
# TODO: Implement semantic version constraint resolution
|
|
# For now, return a basic PackageId
|
|
ok(PackageId(name: pkg, version: "latest", stream: Stable))
|
|
|
|
proc validateDependencies*(fragments: Table[PackageId, Fragment]): Result[void, DependencyError] =
|
|
## Validate all dependencies exist and are consistent
|
|
var missingDeps: seq[PackageId] = @[]
|
|
|
|
for (pkgId, fragment) in fragments.pairs:
|
|
for dep in fragment.dependencies:
|
|
if dep notin fragments:
|
|
missingDeps.add(dep)
|
|
|
|
if missingDeps.len > 0:
|
|
return err(DependencyError(
|
|
code: PackageNotFound,
|
|
msg: "Missing dependencies found",
|
|
missingDependencies: missingDeps
|
|
))
|
|
|
|
ok()
|
|
|
|
# Helper functions for diagnostics (6.1.5)
|
|
proc formatDependencyError*(err: DependencyError): string =
|
|
## Format dependency error with useful diagnostics
|
|
result = fmt"Dependency Error: {err.msg}\n"
|
|
|
|
if err.missingDependencies.len > 0:
|
|
result.add("Missing Dependencies:\n")
|
|
for dep in err.missingDependencies:
|
|
result.add(fmt" - {dep.name} {dep.version}\n")
|
|
|
|
if err.cyclicDependencies.len > 0:
|
|
result.add("Circular Dependencies:\n")
|
|
for dep in err.cyclicDependencies:
|
|
result.add(fmt" - {dep.name} {dep.version}\n")
|
|
|
|
if err.conflictingPackages.len > 0:
|
|
result.add("Conflicting Packages:\n")
|
|
for dep in err.conflictingPackages:
|
|
result.add(fmt" - {dep.name} {dep.version}\n")
|
|
|
|
proc getDependencyTree*(root: PackageId, fragments: Table[PackageId, Fragment]): Result[string, DependencyError] =
|
|
## Generate a visual dependency tree for debugging
|
|
var output = ""
|
|
var visited = initHashSet[PackageId]()
|
|
|
|
proc printTree(pkgId: PackageId, indent: int = 0) =
|
|
let prefix = " ".repeat(indent)
|
|
output.add(fmt"{prefix}- {pkgId.name} {pkgId.version}\n")
|
|
|
|
if pkgId in visited:
|
|
output.add(fmt"{prefix} (already processed)\n")
|
|
return
|
|
|
|
visited.incl(pkgId)
|
|
|
|
if pkgId in fragments:
|
|
let fragment = fragments[pkgId]
|
|
for dep in fragment.dependencies:
|
|
printTree(dep, indent + 1)
|
|
|
|
printTree(root)
|
|
ok(output) |