From 0d3d74e5c2f0ad76c818449f21f91d6c9ed4e74a Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Tue, 27 Sep 2016 10:22:46 -0400 Subject: 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 heapq import * 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 @@ class Package(object): 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.""" -- cgit v1.2.3-70-g09d2