Compare commits

...

1 Commits

Author SHA1 Message Date
Wouter Deconinck
23197b78f9 feat: spack graph --mermaid 2024-05-04 16:13:12 -07:00
3 changed files with 60 additions and 20 deletions

View File

@@ -9,7 +9,7 @@
import spack.environment as ev import spack.environment as ev
import spack.store import spack.store
from spack.cmd.common import arguments from spack.cmd.common import arguments
from spack.graph import DAGWithDependencyTypes, SimpleDAG, graph_ascii, graph_dot, static_graph_dot from spack.graph import DotGraph, MermaidGraph, DAGWithDependencyTypes, SimpleDAG, graph_ascii, graph_dot, static_graph_dot
description = "generate graphs of package dependency relationships" description = "generate graphs of package dependency relationships"
section = "basic" section = "basic"
@@ -33,6 +33,9 @@ def setup_parser(subparser):
method.add_argument( method.add_argument(
"-d", "--dot", action="store_true", help="generate graph in dot format and print to stdout" "-d", "--dot", action="store_true", help="generate graph in dot format and print to stdout"
) )
method.add_argument(
"-m", "--mermaid", action="store_true", help="generate graph in mermaid format and print to stdout"
)
subparser.add_argument( subparser.add_argument(
"-s", "-s",
@@ -85,10 +88,14 @@ def graph(parser, args):
static_graph_dot(specs, depflag=args.deptype) static_graph_dot(specs, depflag=args.deptype)
return return
if args.dot: if args.dot or args.mermaid:
builder = SimpleDAG() if args.dot:
graph = DotGraph()
if args.mermaid:
graph = MermaidGraph()
builder = SimpleDAG(graph=graph)
if args.color: if args.color:
builder = DAGWithDependencyTypes() builder = DAGWithDependencyTypes(graph=graph)
graph_dot(specs, builder=builder, depflag=args.deptype) graph_dot(specs, builder=builder, depflag=args.deptype)
return return

View File

@@ -34,7 +34,7 @@
/ /
o boost o boost
graph_dot() will output a graph of a spec (or multiple specs) in dot format. graph_dot() will output a graph of a spec (or multiple specs) in dot or mermaid format.
""" """
import enum import enum
import sys import sys
@@ -446,10 +446,27 @@ def graph_ascii(
graph.write(spec, color=color, out=out) graph.write(spec, color=color, out=out)
class DotGraphBuilder: class DotGraph:
"""Visit edges of a graph a build DOT options for nodes and edges""" """Configuration for DOT graphs"""
def __init__(self): def __init__(self):
self.label = "label="
self.template = "misc/graph.dot"
class MermaidGraph:
"""Configuration for Mermaid graphs"""
def __init__(self):
self.label = ""
self.template = "misc/graph.md"
class GraphBuilder:
"""Visit edges of a graph a build options for nodes and edges"""
def __init__(self, graph = DotGraph()):
self.graph: Union[DotGraph, MermaidGraph] = graph
self.nodes: Set[Tuple[str, str]] = set() self.nodes: Set[Tuple[str, str]] = set()
self.edges: Set[Tuple[str, str, str]] = set() self.edges: Set[Tuple[str, str, str]] = set()
@@ -472,40 +489,40 @@ def edge_entry(self, edge: spack.spec.DependencySpec) -> Tuple[str, str, str]:
raise NotImplementedError("Need to be implemented by derived classes") raise NotImplementedError("Need to be implemented by derived classes")
def context(self): def context(self):
"""Return the context to be used to render the DOT graph template""" """Return the context to be used to render the graph template"""
result = {"nodes": self.nodes, "edges": self.edges} result = {"nodes": self.nodes, "edges": self.edges}
return result return result
def render(self) -> str: def render(self) -> str:
"""Return a string with the output in DOT format""" """Return a string with the output in format"""
environment = spack.tengine.make_environment() environment = spack.tengine.make_environment()
template = environment.get_template("misc/graph.dot") template = environment.get_template(self.graph.template)
return template.render(self.context()) return template.render(self.context())
class SimpleDAG(DotGraphBuilder): class SimpleDAG(GraphBuilder):
"""Simple DOT graph, with nodes colored uniformly and edges without properties""" """Simple graph, with nodes colored uniformly and edges without properties"""
def node_entry(self, node): def node_entry(self, node):
format_option = "{name}{@version}{%compiler}{/hash:7}" format_option = "{name}{@version}{%compiler}{/hash:7}"
return node.dag_hash(), f'[label="{node.format(format_option)}"]' return node.dag_hash(), f'[{self.graph.label}"{node.format(format_option)}"]'
def edge_entry(self, edge): def edge_entry(self, edge):
return edge.parent.dag_hash(), edge.spec.dag_hash(), None return edge.parent.dag_hash(), edge.spec.dag_hash(), None
class StaticDag(DotGraphBuilder): class StaticDag(GraphBuilder):
"""DOT graph for possible dependencies""" """Graph for possible dependencies"""
def node_entry(self, node): def node_entry(self, node):
return node.name, f'[label="{node.name}"]' return node.name, f'[{self.graph.label}"{node.name}"]'
def edge_entry(self, edge): def edge_entry(self, edge):
return edge.parent.name, edge.spec.name, None return edge.parent.name, edge.spec.name, None
class DAGWithDependencyTypes(DotGraphBuilder): class DAGWithDependencyTypes(GraphBuilder):
"""DOT graph with link,run nodes grouped together and edges colored according to """Graph with link,run nodes grouped together and edges colored according to
the dependency types. the dependency types.
""" """
@@ -521,7 +538,7 @@ def visit(self, edge):
def node_entry(self, node): def node_entry(self, node):
node_str = node.format("{name}{@version}{%compiler}{/hash:7}") node_str = node.format("{name}{@version}{%compiler}{/hash:7}")
options = f'[label="{node_str}", group="build_dependencies", fillcolor="coral"]' options = f'[{self.graph.label}"{node_str}", group="build_dependencies", fillcolor="coral"]'
if node.dag_hash() in self.main_unified_space: if node.dag_hash() in self.main_unified_space:
options = f'[label="{node_str}", group="main_psid"]' options = f'[label="{node_str}", group="main_psid"]'
return node.dag_hash(), options return node.dag_hash(), options
@@ -574,7 +591,7 @@ def static_graph_dot(
def graph_dot( def graph_dot(
specs: List[spack.spec.Spec], specs: List[spack.spec.Spec],
builder: Optional[DotGraphBuilder] = None, builder: Optional[GraphBuilder] = None,
depflag: dt.DepFlag = dt.ALL, depflag: dt.DepFlag = dt.ALL,
out: Optional[TextIO] = None, out: Optional[TextIO] = None,
): ):

View File

@@ -0,0 +1,16 @@
flowchart TD
{% for node, node_options in nodes %}
{% if node_options %}
{{ node }}{{ node_options }}
{% else %}
{{ node }}
{% endif %}
{% endfor %}
{% for edge_parent, edge_child, edge_options in edges %}
{% if edge_options %}
{{ edge_parent }} --> {{ edge_child }}{{ edge_options }}
{% else %}
{{ edge_parent }} --> {{ edge_child }}
{% endif %}
{% endfor %}