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.
This commit is contained in:
Todd Gamblin 2016-09-27 10:22:46 -04:00
parent 43ca805248
commit 0d3d74e5c2
3 changed files with 103 additions and 29 deletions

View file

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

View file

@ -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")
deps = []
# 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()
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
# 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.append((spec.name, dep_name))
# Add edges for each depends_on in the package.
for dep_name, dep in spec.package.dependencies.iteritems():
deps.add((spec.name, dep_name))
# 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))
# 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))
for s in spec.traverse(deptype=deptype):
skey, slabel = key_label(s)
out.write(' "%s" [label="%s"]\n' % (skey, slabel))
for d in s.dependencies(deptype=deptype):
dkey, _ = key_label(d)
deps.add((skey, dkey))
out.write('\n')

View file

@ -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."""