# 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)