From 0d3d74e5c2f0ad76c818449f21f91d6c9ed4e74a Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Tue, 27 Sep 2016 10:22:46 -0400
Subject: [PATCH] Improvements to the Spack graph command.

- Distinguish between static (package) and dynamic (spec) graphs.

  - static graphs ignore conditions and multiple instances (hashes) and
    plot raw dependencies among packages.

  - dynamic graphs include information from particular specs (instances of
    packages) and can have multiple instances with hashes.

- Allow graphing all packages in the install DB.

  - useful for debugging.
---
 lib/spack/spack/cmd/graph.py | 40 +++++++++++++++---
 lib/spack/spack/graph.py     | 78 +++++++++++++++++++++++++-----------
 lib/spack/spack/package.py   | 14 +++++++
 3 files changed, 103 insertions(+), 29 deletions(-)

diff --git a/lib/spack/spack/cmd/graph.py b/lib/spack/spack/cmd/graph.py
index 8faabfbb7b..6de1ed974b 100644
--- a/lib/spack/spack/cmd/graph.py
+++ b/lib/spack/spack/cmd/graph.py
@@ -24,8 +24,11 @@
 ##############################################################################
 import argparse
 
+import llnl.util.tty as tty
+
 import spack
 import spack.cmd
+from spack.spec import *
 from spack.graph import *
 
 description = "Generate graphs of package dependency relationships."
@@ -43,8 +46,21 @@ def setup_parser(subparser):
         help="Generate graph in dot format and print to stdout.")
 
     subparser.add_argument(
-        '--concretize', action='store_true',
-        help="Concretize specs before graphing.")
+        '--normalize', action='store_true',
+        help="Skip concretization; only print normalized spec.")
+
+    subparser.add_argument(
+        '-s', '--static', action='store_true',
+        help="Use static information from packages, not dynamic spec info.")
+
+    subparser.add_argument(
+        '-i', '--installed', action='store_true',
+        help="Graph all installed specs in dot format (implies --dot).")
+
+    subparser.add_argument(
+        '-t', '--deptype', action='store',
+        help="Comma-separated list of deptypes to traverse. default=%s."
+        % ','.join(alldeps))
 
     subparser.add_argument(
         'specs', nargs=argparse.REMAINDER,
@@ -52,15 +68,29 @@ def setup_parser(subparser):
 
 
 def graph(parser, args):
-    specs = spack.cmd.parse_specs(
-        args.specs, normalize=True, concretize=args.concretize)
+    concretize = not args.normalize
+    if args.installed:
+        if args.specs:
+            tty.die("Can't specify specs with --installed")
+        args.dot = True
+        specs = spack.installed_db.query()
+
+    else:
+        specs = spack.cmd.parse_specs(
+            args.specs, normalize=True, concretize=concretize)
 
     if not specs:
         setup_parser.parser.print_help()
         return 1
 
+    deptype = alldeps
+    if args.deptype:
+        deptype = tuple(args.deptype.split(','))
+        validate_deptype(deptype)
+        deptype = canonical_deptype(deptype)
+
     if args.dot:  # Dot graph only if asked for.
-        graph_dot(*specs)
+        graph_dot(specs, static=args.static, deptype=deptype)
 
     elif specs:  # ascii is default: user doesn't need to provide it explicitly
         graph_ascii(specs[0], debug=spack.debug)
diff --git a/lib/spack/spack/graph.py b/lib/spack/spack/graph.py
index b875e9da99..330e08cc71 100644
--- a/lib/spack/spack/graph.py
+++ b/lib/spack/spack/graph.py
@@ -67,8 +67,7 @@
 from llnl.util.lang import *
 from llnl.util.tty.color import *
 
-import spack
-from spack.spec import Spec
+from spack.spec import *
 
 __all__ = ['topological_sort', 'graph_ascii', 'AsciiGraph', 'graph_dot']
 
@@ -501,7 +500,7 @@ def graph_ascii(spec, **kwargs):
     graph.write(spec, color=color, out=out)
 
 
-def graph_dot(*specs, **kwargs):
+def graph_dot(specs, deptype=None, static=False, out=None):
     """Generate a graph in dot format of all provided specs.
 
     Print out a dot formatted graph of all the dependencies between
@@ -510,42 +509,73 @@ def graph_dot(*specs, **kwargs):
         spack graph --dot qt | dot -Tpdf > spack-graph.pdf
 
     """
-    out = kwargs.pop('out', sys.stdout)
-    check_kwargs(kwargs, graph_dot)
+    if out is None:
+        out = sys.stdout
+
+    if deptype is None:
+        deptype = alldeps
 
     out.write('digraph G {\n')
-    out.write('  label = "Spack Dependencies"\n')
     out.write('  labelloc = "b"\n')
     out.write('  rankdir = "LR"\n')
     out.write('  ranksep = "5"\n')
+    out.write('node[\n')
+    out.write('     fontname=Monaco,\n')
+    out.write('     penwidth=2,\n')
+    out.write('     fontsize=12,\n')
+    out.write('     margin=.1,\n')
+    out.write('     shape=box,\n')
+    out.write('     fillcolor=lightblue,\n')
+    out.write('     style="rounded,filled"]\n')
+
     out.write('\n')
 
-    def quote(string):
+    def q(string):
         return '"%s"' % string
 
     if not specs:
-        specs = [p.name for p in spack.repo.all_packages()]
-    else:
-        roots = specs
-        specs = set()
-        for spec in roots:
-            specs.update(Spec(s.name) for s in spec.normalized().traverse())
+        raise ValueError("Must provide specs ot graph_dot")
+
+    # Static graph includes anything a package COULD depend on.
+    if static:
+        names = set.union(*[s.package.possible_dependencies() for s in specs])
+        specs = [Spec(name) for name in names]
+
+    labeled = set()
 
-    deps = []
+    def label(key, label):
+        if key not in labeled:
+            out.write('  "%s" [label="%s"]\n' % (key, label))
+            labeled.add(key)
+
+    deps = set()
     for spec in specs:
-        out.write('  %-30s [label="%s"]\n' % (quote(spec.name), spec.name))
+        if static:
+            out.write('  "%s" [label="%s"]\n' % (spec.name, spec.name))
+
+            # Skip virtual specs (we'll find out about them from concrete ones.
+            if spec.virtual:
+                continue
+
+            # Add edges for each depends_on in the package.
+            for dep_name, dep in spec.package.dependencies.iteritems():
+                deps.add((spec.name, dep_name))
 
-        # Skip virtual specs (we'll find out about them from concrete ones.
-        if spec.virtual:
-            continue
+            # If the package provides something, add an edge for that.
+            for provider in set(s.name for s in spec.package.provided):
+                deps.add((provider, spec.name))
+
+        else:
+            def key_label(s):
+                return s.dag_hash(), "%s-%s" % (s.name, s.dag_hash(7))
 
-        # Add edges for each depends_on in the package.
-        for dep_name, dep in spec.package.dependencies.iteritems():
-            deps.append((spec.name, dep_name))
+            for s in spec.traverse(deptype=deptype):
+                skey, slabel = key_label(s)
+                out.write('  "%s" [label="%s"]\n' % (skey, slabel))
 
-        # If the package provides something, add an edge for that.
-        for provider in set(s.name for s in spec.package.provided):
-            deps.append((provider, spec.name))
+                for d in s.dependencies(deptype=deptype):
+                    dkey, _ = key_label(d)
+                    deps.add((skey, dkey))
 
     out.write('\n')
 
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index fe19e5d400..cffc795586 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -411,6 +411,20 @@ def __init__(self, spec):
         if self.is_extension:
             spack.repo.get(self.extendee_spec)._check_extendable()
 
+    def possible_dependencies(self, visited=None):
+        """Return set of possible transitive dependencies of this package."""
+        if visited is None:
+            visited = set()
+
+        visited.add(self.name)
+        for name in self.dependencies:
+            if name not in visited and not spack.spec.Spec(name).virtual:
+                pkg = spack.repo.get(name)
+                for name in pkg.possible_dependencies(visited):
+                    visited.add(name)
+
+        return visited
+
     @property
     def package_dir(self):
         """Return the directory where the package.py file lives."""
-- 
GitLab