nova-nix-0.1.4.0: src/Nix/DependencyGraph.hs
-- | Dependency graph construction and topological sorting.
--
-- Given a root derivation, builds a graph of all transitive
-- dependencies by reading .drv files from the store. The graph
-- is then topologically sorted so dependencies are built before
-- their dependents.
module Nix.DependencyGraph
( -- * Types
DepNode (..),
DepGraph (..),
TopoResult (..),
-- * Graph construction
buildDepGraph,
-- * Topological sort
topoSort,
-- * Queries
transitiveDeps,
directDeps,
)
where
import Data.List (foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Sequence (Seq, (|>))
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import Nix.Derivation (Derivation (..))
import Nix.Store.Path (StorePath)
-- | A node in the dependency graph.
data DepNode = DepNode
{ -- | The .drv store path for this derivation.
dnDrvPath :: !StorePath,
-- | The parsed derivation.
dnDerivation :: !Derivation,
-- | Direct dependency .drv paths (from drvInputDrvs keys).
dnDeps :: ![StorePath]
}
deriving (Eq, Show)
-- | A complete dependency graph: maps .drv paths to their nodes.
newtype DepGraph = DepGraph {unDepGraph :: Map StorePath DepNode}
deriving (Eq, Show)
-- | Result of topological sorting.
data TopoResult
= -- | Successfully sorted: build order with leaves (no deps) first.
TopoSorted ![StorePath]
| -- | Cycle detected: the paths involved in the cycle.
TopoCycle ![StorePath]
deriving (Eq, Show)
-- ---------------------------------------------------------------------------
-- Graph construction
-- ---------------------------------------------------------------------------
-- | Build the full dependency graph starting from a root derivation.
--
-- The @readDrv@ function reads a .drv file from the store and parses it.
-- Returns @Left@ if any .drv file cannot be read.
--
-- Uses a 'Seq' work-queue for O(1) enqueue\/dequeue instead of list append.
buildDepGraph ::
(StorePath -> Either Text Derivation) ->
Derivation ->
StorePath ->
Either Text DepGraph
buildDepGraph readDrv rootDrv rootPath =
go Map.empty (Seq.singleton (rootPath, rootDrv))
where
go visited queue = case Seq.viewl queue of
Seq.EmptyL -> Right (DepGraph visited)
(sp, drv) Seq.:< rest
| Map.member sp visited -> go visited rest
| otherwise ->
let deps = Map.keys (drvInputDrvs drv)
node =
DepNode
{ dnDrvPath = sp,
dnDerivation = drv,
dnDeps = deps
}
visitedWithNode = Map.insert sp node visited
in case resolveNewDeps readDrv visitedWithNode deps of
Left err -> Left err
Right newItems -> go visitedWithNode (foldl' (|>) rest newItems)
-- | Resolve unvisited dependencies by reading their .drv files.
resolveNewDeps ::
(StorePath -> Either Text Derivation) ->
Map StorePath DepNode ->
[StorePath] ->
Either Text [(StorePath, Derivation)]
resolveNewDeps readDrv visited = traverse resolve . filter unvisited
where
unvisited dep = not (Map.member dep visited)
resolve dep = case readDrv dep of
Left err -> Left err
Right drv -> Right (dep, drv)
-- ---------------------------------------------------------------------------
-- Topological sort (Kahn's algorithm)
-- ---------------------------------------------------------------------------
-- | Topologically sort the dependency graph using Kahn's algorithm.
-- Returns leaves first (build order), or reports a cycle.
topoSort :: DepGraph -> TopoResult
topoSort (DepGraph graph) =
let allNodes = Map.keysSet graph
-- depCount: for each node, how many of its deps are in the graph
depCount = Map.map (countGraphDeps allNodes) graph
-- reverseAdj: for each dep, which nodes depend on it
reverseAdj = buildReverseAdj graph allNodes
-- Start with nodes that have zero deps (leaves)
zeroQueue = Seq.fromList [sp | (sp, 0) <- Map.toList depCount]
totalNodes = Map.size graph
in kahnLoop reverseAdj depCount zeroQueue [] 0 totalNodes
-- | Count how many of a node's dependencies exist in the graph.
countGraphDeps :: Set StorePath -> DepNode -> Int
countGraphDeps allNodes node =
length (filter (`Set.member` allNodes) (dnDeps node))
-- | Build reverse adjacency: maps each dep to the list of nodes that depend on it.
buildReverseAdj :: Map StorePath DepNode -> Set StorePath -> Map StorePath [StorePath]
buildReverseAdj graph allNodes =
Map.foldlWithKey' addReverse (Map.fromSet (const []) allNodes) graph
where
addReverse acc sp node =
let depsInGraph = filter (`Set.member` allNodes) (dnDeps node)
in foldl' (flip (Map.adjust (sp :))) acc depsInGraph
-- | Kahn's algorithm main loop.
--
-- Tracks @sortedCount@ instead of calling @length@ on the accumulator
-- each iteration, keeping the loop O(V + E) total.
kahnLoop ::
Map StorePath [StorePath] ->
Map StorePath Int ->
Seq StorePath ->
[StorePath] ->
Int ->
Int ->
TopoResult
kahnLoop _ _ queue sorted sortedCount totalNodes
| Seq.null queue =
if sortedCount == totalNodes
then TopoSorted (reverse sorted)
else TopoCycle (reverse sorted)
kahnLoop reverseAdj depCount queue sorted sortedCount totalNodes =
case Seq.viewl queue of
Seq.EmptyL -> TopoSorted (reverse sorted) -- unreachable, guarded above
sp Seq.:< rest ->
let dependents = fromMaybe [] (Map.lookup sp reverseAdj)
(updatedDepCount, newZero) = decrementDependents depCount dependents
extendedQueue = foldl' (|>) rest newZero
in kahnLoop reverseAdj updatedDepCount extendedQueue (sp : sorted) (sortedCount + 1) totalNodes
-- | Decrement in-degree for each dependent; collect any that reach zero.
decrementDependents :: Map StorePath Int -> [StorePath] -> (Map StorePath Int, [StorePath])
decrementDependents dc = foldl' step (dc, [])
where
step (counts, zeros) dep =
let newDeg = maybe 0 (subtract 1) (Map.lookup dep counts)
updatedCounts = Map.insert dep newDeg counts
in if newDeg == 0
then (updatedCounts, dep : zeros)
else (updatedCounts, zeros)
-- ---------------------------------------------------------------------------
-- Queries
-- ---------------------------------------------------------------------------
-- | All transitive dependencies of a store path (not including itself).
transitiveDeps :: DepGraph -> StorePath -> Set StorePath
transitiveDeps (DepGraph graph) root = go Set.empty (Seq.singleton root)
where
go visited queue = case Seq.viewl queue of
Seq.EmptyL -> visited
sp Seq.:< rest
| Set.member sp visited -> go visited rest
| otherwise ->
let deps = maybe [] dnDeps (Map.lookup sp graph)
visitedWithDep = if sp == root then visited else Set.insert sp visited
in go visitedWithDep (foldl' (|>) rest deps)
-- | Direct dependencies of a store path.
directDeps :: DepGraph -> StorePath -> [StorePath]
directDeps (DepGraph graph) sp =
maybe [] dnDeps (Map.lookup sp graph)