mirror of
https://github.com/mudler/luet.git
synced 2025-09-17 23:58:48 +00:00
233 lines
5.8 KiB
Go
233 lines
5.8 KiB
Go
package depgraph
|
|
|
|
import (
|
|
"errors"
|
|
)
|
|
|
|
// A node in this graph is just a string, so a nodeset is a map whose
|
|
// keys are the nodes that are present.
|
|
type nodeset map[string]struct{}
|
|
|
|
// depmap tracks the nodes that have some dependency relationship to
|
|
// some other node, represented by the key of the map.
|
|
type depmap map[string]nodeset
|
|
|
|
type Graph struct {
|
|
nodes nodeset
|
|
|
|
// Maintain dependency relationships in both directions. These
|
|
// data structures are the edges of the graph.
|
|
|
|
// `dependencies` tracks child -> parents.
|
|
dependencies depmap
|
|
// `dependents` tracks parent -> children.
|
|
dependents depmap
|
|
// Keep track of the nodes of the graph themselves.
|
|
}
|
|
|
|
func New() *Graph {
|
|
return &Graph{
|
|
dependencies: make(depmap),
|
|
dependents: make(depmap),
|
|
nodes: make(nodeset),
|
|
}
|
|
}
|
|
|
|
func (g *Graph) DependOn(child, parent string) error {
|
|
if child == parent {
|
|
return errors.New("self-referential dependencies not allowed")
|
|
}
|
|
|
|
if g.DependsOn(parent, child) {
|
|
return errors.New("circular dependencies not allowed")
|
|
}
|
|
|
|
// Add nodes.
|
|
g.nodes[parent] = struct{}{}
|
|
g.nodes[child] = struct{}{}
|
|
|
|
// Add edges.
|
|
addNodeToNodeset(g.dependents, parent, child)
|
|
addNodeToNodeset(g.dependencies, child, parent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Graph) DependsOn(child, parent string) bool {
|
|
deps := g.Dependencies(child)
|
|
_, ok := deps[parent]
|
|
return ok
|
|
}
|
|
|
|
func (g *Graph) HasDependent(parent, child string) bool {
|
|
deps := g.Dependents(parent)
|
|
_, ok := deps[child]
|
|
return ok
|
|
}
|
|
|
|
func (g *Graph) Leaves() []string {
|
|
leaves := make([]string, 0)
|
|
|
|
for node := range g.nodes {
|
|
if _, ok := g.dependencies[node]; !ok {
|
|
leaves = append(leaves, node)
|
|
}
|
|
}
|
|
|
|
return leaves
|
|
}
|
|
|
|
// TopoSortedLayers returns a slice of all of the graph nodes in topological sort order. That is,
|
|
// if `B` depends on `A`, then `A` is guaranteed to come before `B` in the sorted output.
|
|
// The graph is guaranteed to be cycle-free because cycles are detected while building the
|
|
// graph. Additionally, the output is grouped into "layers", which are guaranteed to not have
|
|
// any dependencies within each layer. This is useful, e.g. when building an execution plan for
|
|
// some DAG, in which case each element within each layer could be executed in parallel. If you
|
|
// do not need this layered property, use `Graph.TopoSorted()`, which flattens all elements.
|
|
func (g *Graph) TopoSortedLayers() [][]string {
|
|
layers := [][]string{}
|
|
|
|
// Copy the graph
|
|
shrinkingGraph := g.clone()
|
|
for {
|
|
leaves := shrinkingGraph.Leaves()
|
|
if len(leaves) == 0 {
|
|
break
|
|
}
|
|
|
|
layers = append(layers, leaves)
|
|
for _, leafNode := range leaves {
|
|
shrinkingGraph.remove(leafNode)
|
|
}
|
|
}
|
|
|
|
return layers
|
|
}
|
|
|
|
func removeFromDepmap(dm depmap, key, node string) {
|
|
nodes := dm[key]
|
|
if len(nodes) == 1 {
|
|
// The only element in the nodeset must be `node`, so we
|
|
// can delete the entry entirely.
|
|
delete(dm, key)
|
|
} else {
|
|
// Otherwise, remove the single node from the nodeset.
|
|
delete(nodes, node)
|
|
}
|
|
}
|
|
|
|
func (g *Graph) remove(node string) {
|
|
// Remove edges from things that depend on `node`.
|
|
for dependent := range g.dependents[node] {
|
|
removeFromDepmap(g.dependencies, dependent, node)
|
|
}
|
|
delete(g.dependents, node)
|
|
|
|
// Remove all edges from node to the things it depends on.
|
|
for dependency := range g.dependencies[node] {
|
|
removeFromDepmap(g.dependents, dependency, node)
|
|
}
|
|
delete(g.dependencies, node)
|
|
|
|
// Finally, remove the node itself.
|
|
delete(g.nodes, node)
|
|
}
|
|
|
|
// TopoSorted returns all the nodes in the graph is topological sort order.
|
|
// See also `Graph.TopoSortedLayers()`.
|
|
func (g *Graph) TopoSorted() []string {
|
|
nodeCount := 0
|
|
layers := g.TopoSortedLayers()
|
|
for _, layer := range layers {
|
|
nodeCount += len(layer)
|
|
}
|
|
|
|
allNodes := make([]string, 0, nodeCount)
|
|
for _, layer := range layers {
|
|
for _, node := range layer {
|
|
allNodes = append(allNodes, node)
|
|
}
|
|
}
|
|
|
|
return allNodes
|
|
}
|
|
|
|
func (g *Graph) Dependencies(child string) nodeset {
|
|
return g.buildTransitive(child, g.immediateDependencies)
|
|
}
|
|
|
|
func (g *Graph) immediateDependencies(node string) nodeset {
|
|
return g.dependencies[node]
|
|
}
|
|
|
|
func (g *Graph) Dependents(parent string) nodeset {
|
|
return g.buildTransitive(parent, g.immediateDependents)
|
|
}
|
|
|
|
func (g *Graph) immediateDependents(node string) nodeset {
|
|
return g.dependents[node]
|
|
}
|
|
|
|
func (g *Graph) clone() *Graph {
|
|
return &Graph{
|
|
dependencies: copyDepmap(g.dependencies),
|
|
dependents: copyDepmap(g.dependents),
|
|
nodes: copyNodeset(g.nodes),
|
|
}
|
|
}
|
|
|
|
// buildTransitive starts at `root` and continues calling `nextFn` to keep discovering more nodes until
|
|
// the graph cannot produce any more. It returns the set of all discovered nodes.
|
|
func (g *Graph) buildTransitive(root string, nextFn func(string) nodeset) nodeset {
|
|
if _, ok := g.nodes[root]; !ok {
|
|
return nil
|
|
}
|
|
|
|
out := make(nodeset)
|
|
searchNext := []string{root}
|
|
for len(searchNext) > 0 {
|
|
// List of new nodes from this layer of the dependency graph. This is
|
|
// assigned to `searchNext` at the end of the outer "discovery" loop.
|
|
discovered := []string{}
|
|
for _, node := range searchNext {
|
|
// For each node to discover, find the next nodes.
|
|
for nextNode := range nextFn(node) {
|
|
// If we have not seen the node before, add it to the output as well
|
|
// as the list of nodes to traverse in the next iteration.
|
|
if _, ok := out[nextNode]; !ok {
|
|
out[nextNode] = struct{}{}
|
|
discovered = append(discovered, nextNode)
|
|
}
|
|
}
|
|
}
|
|
searchNext = discovered
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func copyNodeset(s nodeset) nodeset {
|
|
out := make(nodeset, len(s))
|
|
for k, v := range s {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func copyDepmap(m depmap) depmap {
|
|
out := make(depmap, len(m))
|
|
for k, v := range m {
|
|
out[k] = copyNodeset(v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addNodeToNodeset(dm depmap, key, node string) {
|
|
nodes, ok := dm[key]
|
|
if !ok {
|
|
nodes = make(nodeset)
|
|
dm[key] = nodes
|
|
}
|
|
nodes[node] = struct{}{}
|
|
}
|