nip/src/nimpak/dependency.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)