Merge branch 'features/dep-graph' into develop
This commit is contained in:
commit
f73abe6849
10
bin/spack
10
bin/spack
@ -103,7 +103,7 @@ if args.insecure:
|
|||||||
# Try to load the particular command asked for and run it
|
# Try to load the particular command asked for and run it
|
||||||
command = spack.cmd.get_command(args.command)
|
command = spack.cmd.get_command(args.command)
|
||||||
try:
|
try:
|
||||||
command(parser, args)
|
return_val = command(parser, args)
|
||||||
except SpackError, e:
|
except SpackError, e:
|
||||||
if spack.debug:
|
if spack.debug:
|
||||||
# In debug mode, raise with a full stack trace.
|
# In debug mode, raise with a full stack trace.
|
||||||
@ -116,3 +116,11 @@ except SpackError, e:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.stderr.write('\n')
|
sys.stderr.write('\n')
|
||||||
tty.die("Keyboard interrupt.")
|
tty.die("Keyboard interrupt.")
|
||||||
|
|
||||||
|
# Allow commands to return values if they want to exit with some ohter code.
|
||||||
|
if return_val is None:
|
||||||
|
sys.exit(0)
|
||||||
|
elif isinstance(return_val, int):
|
||||||
|
sys.exit(return_val)
|
||||||
|
else:
|
||||||
|
tty.die("Bad return value from command %s: %s" % (args.command, return_val))
|
||||||
|
@ -269,6 +269,28 @@ def in_function(function_name):
|
|||||||
del stack
|
del stack
|
||||||
|
|
||||||
|
|
||||||
|
def check_kwargs(kwargs, fun):
|
||||||
|
"""Helper for making functions with kwargs. Checks whether the kwargs
|
||||||
|
are empty after all of them have been popped off. If they're
|
||||||
|
not, raises an error describing which kwargs are invalid.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
def foo(self, **kwargs):
|
||||||
|
x = kwargs.pop('x', None)
|
||||||
|
y = kwargs.pop('y', None)
|
||||||
|
z = kwargs.pop('z', None)
|
||||||
|
check_kwargs(kwargs, self.foo)
|
||||||
|
|
||||||
|
# This raises a TypeError:
|
||||||
|
foo(w='bad kwarg')
|
||||||
|
"""
|
||||||
|
if kwargs:
|
||||||
|
raise TypeError(
|
||||||
|
"'%s' is an invalid keyword argument for function %s()."
|
||||||
|
% (next(kwargs.iterkeys()), fun.__name__))
|
||||||
|
|
||||||
|
|
||||||
class RequiredAttributeError(ValueError):
|
class RequiredAttributeError(ValueError):
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super(RequiredAttributeError, self).__init__(message)
|
super(RequiredAttributeError, self).__init__(message)
|
||||||
|
@ -177,17 +177,20 @@ def cescape(string):
|
|||||||
|
|
||||||
class ColorStream(object):
|
class ColorStream(object):
|
||||||
def __init__(self, stream, color=None):
|
def __init__(self, stream, color=None):
|
||||||
self.__class__ = type(stream.__class__.__name__,
|
self._stream = stream
|
||||||
(self.__class__, stream.__class__), {})
|
self._color = color
|
||||||
self.__dict__ = stream.__dict__
|
|
||||||
self.color = color
|
|
||||||
self.stream = stream
|
|
||||||
|
|
||||||
def write(self, string, **kwargs):
|
def write(self, string, **kwargs):
|
||||||
if kwargs.get('raw', False):
|
raw = kwargs.get('raw', False)
|
||||||
super(ColorStream, self).write(string)
|
raw_write = getattr(self._stream, 'write')
|
||||||
|
|
||||||
|
color = self._color
|
||||||
|
if self._color is None:
|
||||||
|
if raw:
|
||||||
|
color=True
|
||||||
else:
|
else:
|
||||||
cwrite(string, self.stream, self.color)
|
color = self._stream.isatty()
|
||||||
|
raw_write(colorize(string, color=color))
|
||||||
|
|
||||||
def writelines(self, sequence, **kwargs):
|
def writelines(self, sequence, **kwargs):
|
||||||
raw = kwargs.get('raw', False)
|
raw = kwargs.get('raw', False)
|
||||||
|
@ -22,9 +22,45 @@
|
|||||||
# along with this program; if not, write to the Free Software Foundation,
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
##############################################################################
|
##############################################################################
|
||||||
import spack
|
from external import argparse
|
||||||
|
|
||||||
|
import spack
|
||||||
|
import spack.cmd
|
||||||
|
from spack.graph import *
|
||||||
|
|
||||||
|
description = "Generate graphs of package dependency relationships."
|
||||||
|
|
||||||
|
def setup_parser(subparser):
|
||||||
|
setup_parser.parser = subparser
|
||||||
|
|
||||||
|
method = subparser.add_mutually_exclusive_group()
|
||||||
|
method.add_argument(
|
||||||
|
'--ascii', action='store_true',
|
||||||
|
help="Draw graph as ascii to stdout (default).")
|
||||||
|
method.add_argument(
|
||||||
|
'--dot', action='store_true',
|
||||||
|
help="Generate graph in dot format and print to stdout.")
|
||||||
|
|
||||||
|
subparser.add_argument(
|
||||||
|
'--concretize', action='store_true', help="Concretize specs before graphing.")
|
||||||
|
|
||||||
|
subparser.add_argument(
|
||||||
|
'specs', nargs=argparse.REMAINDER, help="specs of packages to graph.")
|
||||||
|
|
||||||
description = "Write out inter-package dependencies in dot graph format"
|
|
||||||
|
|
||||||
def graph(parser, args):
|
def graph(parser, args):
|
||||||
spack.db.graph_dependencies()
|
specs = spack.cmd.parse_specs(
|
||||||
|
args.specs, normalize=True, concretize=args.concretize)
|
||||||
|
|
||||||
|
if not specs:
|
||||||
|
setup_parser.parser.print_help()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.dot: # Dot graph only if asked for.
|
||||||
|
graph_dot(*specs)
|
||||||
|
|
||||||
|
elif specs: # ascii is default: user doesn't need to provide it explicitly
|
||||||
|
graph_ascii(specs[0], debug=spack.debug)
|
||||||
|
for spec in specs[1:]:
|
||||||
|
print # extra line bt/w independent graphs
|
||||||
|
graph_ascii(spec, debug=spack.debug)
|
||||||
|
@ -111,4 +111,3 @@ def location(parser, args):
|
|||||||
tty.die("Build directory does not exist yet. Run this to create it:",
|
tty.die("Build directory does not exist yet. Run this to create it:",
|
||||||
"spack stage " + " ".join(args.spec))
|
"spack stage " + " ".join(args.spec))
|
||||||
print pkg.stage.source_path
|
print pkg.stage.source_path
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ def setup_parser(subparser):
|
|||||||
def md5(parser, args):
|
def md5(parser, args):
|
||||||
if not args.files:
|
if not args.files:
|
||||||
setup_parser.parser.print_help()
|
setup_parser.parser.print_help()
|
||||||
|
return 1
|
||||||
|
|
||||||
for f in args.files:
|
for f in args.files:
|
||||||
if not os.path.isfile(f):
|
if not os.path.isfile(f):
|
||||||
|
@ -27,8 +27,8 @@
|
|||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
|
||||||
import spack.url as url
|
|
||||||
import spack
|
import spack
|
||||||
|
import spack.url as url
|
||||||
|
|
||||||
description = "print out abstract and concrete versions of a spec."
|
description = "print out abstract and concrete versions of a spec."
|
||||||
|
|
||||||
|
553
lib/spack/spack/graph.py
Normal file
553
lib/spack/spack/graph.py
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
##############################################################################
|
||||||
|
# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
|
||||||
|
# Produced at the Lawrence Livermore National Laboratory.
|
||||||
|
#
|
||||||
|
# This file is part of Spack.
|
||||||
|
# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
|
||||||
|
# LLNL-CODE-647188
|
||||||
|
#
|
||||||
|
# For details, see https://scalability-llnl.github.io/spack
|
||||||
|
# Please also see the LICENSE file for our notice and the LGPL.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License (as published by
|
||||||
|
# the Free Software Foundation) version 2.1 dated February 1999.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
|
||||||
|
# conditions of the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
##############################################################################
|
||||||
|
"""Functions for graphing DAGs of dependencies.
|
||||||
|
|
||||||
|
This file contains code for graphing DAGs of software packages
|
||||||
|
(i.e. Spack specs). There are two main functions you probably care
|
||||||
|
about:
|
||||||
|
|
||||||
|
graph_ascii() will output a colored graph of a spec in ascii format,
|
||||||
|
kind of like the graph git shows with "git log --graph", e.g.::
|
||||||
|
|
||||||
|
o mpileaks
|
||||||
|
|\
|
||||||
|
| |\
|
||||||
|
| o | callpath
|
||||||
|
|/| |
|
||||||
|
| |\|
|
||||||
|
| |\ \
|
||||||
|
| | |\ \
|
||||||
|
| | | | o adept-utils
|
||||||
|
| |_|_|/|
|
||||||
|
|/| | | |
|
||||||
|
o | | | | mpi
|
||||||
|
/ / / /
|
||||||
|
| | o | dyninst
|
||||||
|
| |/| |
|
||||||
|
|/|/| |
|
||||||
|
| | |/
|
||||||
|
| o | libdwarf
|
||||||
|
|/ /
|
||||||
|
o | libelf
|
||||||
|
/
|
||||||
|
o boost
|
||||||
|
|
||||||
|
graph_dot() will output a graph of a spec (or multiple specs) in dot
|
||||||
|
format.
|
||||||
|
|
||||||
|
Note that ``graph_ascii`` assumes a single spec while ``graph_dot``
|
||||||
|
can take a number of specs as input.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__all__ = ['topological_sort', 'graph_ascii', 'AsciiGraph', 'graph_dot']
|
||||||
|
|
||||||
|
from heapq import *
|
||||||
|
|
||||||
|
from llnl.util.lang import *
|
||||||
|
from llnl.util.tty.color import *
|
||||||
|
|
||||||
|
import spack
|
||||||
|
from spack.spec import Spec
|
||||||
|
|
||||||
|
|
||||||
|
def topological_sort(spec, **kwargs):
|
||||||
|
"""Topological sort for specs.
|
||||||
|
|
||||||
|
Return a list of dependency specs sorted topologically. The spec
|
||||||
|
argument is not modified in the process.
|
||||||
|
|
||||||
|
"""
|
||||||
|
reverse = kwargs.get('reverse', False)
|
||||||
|
if not reverse:
|
||||||
|
parents = lambda s: s.dependents
|
||||||
|
children = lambda s: s.dependencies
|
||||||
|
else:
|
||||||
|
parents = lambda s: s.dependencies
|
||||||
|
children = lambda s: s.dependents
|
||||||
|
|
||||||
|
# Work on a copy so this is nondestructive.
|
||||||
|
spec = spec.copy()
|
||||||
|
nodes = spec.index()
|
||||||
|
|
||||||
|
topo_order = []
|
||||||
|
remaining = [name for name in nodes.keys() if not parents(nodes[name])]
|
||||||
|
heapify(remaining)
|
||||||
|
|
||||||
|
while remaining:
|
||||||
|
name = heappop(remaining)
|
||||||
|
topo_order.append(name)
|
||||||
|
|
||||||
|
node = nodes[name]
|
||||||
|
for dep in children(node).values():
|
||||||
|
del parents(dep)[node.name]
|
||||||
|
if not parents(dep):
|
||||||
|
heappush(remaining, dep.name)
|
||||||
|
|
||||||
|
if any(parents(s) for s in spec.traverse()):
|
||||||
|
raise ValueError("Spec has cycles!")
|
||||||
|
else:
|
||||||
|
return topo_order
|
||||||
|
|
||||||
|
|
||||||
|
def find(seq, predicate):
|
||||||
|
"""Find index in seq for which predicate is True.
|
||||||
|
|
||||||
|
Searches the sequence and returns the index of the element for
|
||||||
|
which the predicate evaluates to True. Returns -1 if the
|
||||||
|
predicate does not evaluate to True for any element in seq.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for i, elt in enumerate(seq):
|
||||||
|
if predicate(elt):
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
# Names of different graph line states. We Record previous line
|
||||||
|
# states so that we can easily determine what to do when connecting.
|
||||||
|
states = ('node', 'collapse', 'merge-right', 'expand-right', 'back-edge')
|
||||||
|
NODE, COLLAPSE, MERGE_RIGHT, EXPAND_RIGHT, BACK_EDGE = states
|
||||||
|
|
||||||
|
class AsciiGraph(object):
|
||||||
|
def __init__(self):
|
||||||
|
# These can be set after initialization or after a call to
|
||||||
|
# graph() to change behavior.
|
||||||
|
self.node_character = '*'
|
||||||
|
self.debug = False
|
||||||
|
self.indent = 0
|
||||||
|
|
||||||
|
# These are colors in the order they'll be used for edges.
|
||||||
|
# See llnl.util.tty.color for details on color characters.
|
||||||
|
self.colors = 'rgbmcyRGBMCY'
|
||||||
|
|
||||||
|
# Internal vars are used in the graph() function and are
|
||||||
|
# properly initialized there.
|
||||||
|
self._name_to_color = None # Node name to color
|
||||||
|
self._out = None # Output stream
|
||||||
|
self._frontier = None # frontier
|
||||||
|
self._nodes = None # dict from name -> node
|
||||||
|
self._prev_state = None # State of previous line
|
||||||
|
self._prev_index = None # Index of expansion point of prev line
|
||||||
|
|
||||||
|
|
||||||
|
def _indent(self):
|
||||||
|
self._out.write(self.indent * ' ')
|
||||||
|
|
||||||
|
|
||||||
|
def _write_edge(self, string, index, sub=0):
|
||||||
|
"""Write a colored edge to the output stream."""
|
||||||
|
name = self._frontier[index][sub]
|
||||||
|
edge = "@%s{%s}" % (self._name_to_color[name], string)
|
||||||
|
self._out.write(edge)
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_deps(self, i, deps, label=None):
|
||||||
|
"""Connect dependencies to existing edges in the frontier.
|
||||||
|
|
||||||
|
``deps`` are to be inserted at position i in the
|
||||||
|
frontier. This routine determines whether other open edges
|
||||||
|
should be merged with <deps> (if there are other open edges
|
||||||
|
pointing to the same place) or whether they should just be
|
||||||
|
inserted as a completely new open edge.
|
||||||
|
|
||||||
|
Open edges that are not fully expanded (i.e. those that point
|
||||||
|
at multiple places) are left intact.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
label -- optional debug label for the connection.
|
||||||
|
|
||||||
|
Returns: True if the deps were connected to another edge
|
||||||
|
(i.e. the frontier did not grow) and False if the deps were
|
||||||
|
NOT already in the frontier (i.e. they were inserted and the
|
||||||
|
frontier grew).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(deps) == 1 and deps in self._frontier:
|
||||||
|
j = self._frontier.index(deps)
|
||||||
|
|
||||||
|
# convert a right connection into a left connection
|
||||||
|
if i < j:
|
||||||
|
self._frontier.pop(j)
|
||||||
|
self._frontier.insert(i, deps)
|
||||||
|
return self._connect_deps(j, deps, label)
|
||||||
|
|
||||||
|
collapse = True
|
||||||
|
if self._prev_state == EXPAND_RIGHT:
|
||||||
|
# Special case where previous line expanded and i is off by 1.
|
||||||
|
self._back_edge_line([], j, i+1, True, label + "-1.5 " + str((i+1,j)))
|
||||||
|
collapse = False
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Previous node also expanded here, so i is off by one.
|
||||||
|
if self._prev_state == NODE and self._prev_index < i:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if i-j > 1:
|
||||||
|
# We need two lines to connect if distance > 1
|
||||||
|
self._back_edge_line([], j, i, True, label + "-1 " + str((i,j)))
|
||||||
|
collapse = False
|
||||||
|
|
||||||
|
self._back_edge_line([j], -1, -1, collapse, label + "-2 " + str((i,j)))
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif deps:
|
||||||
|
self._frontier.insert(i, deps)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _set_state(self, state, index, label=None):
|
||||||
|
if state not in states:
|
||||||
|
raise ValueError("Invalid graph state!")
|
||||||
|
self._prev_state = state
|
||||||
|
self._prev_index = index
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
self._out.write(" " * 20)
|
||||||
|
self._out.write("%-20s" % (
|
||||||
|
str(self._prev_state) if self._prev_state else ''))
|
||||||
|
self._out.write("%-20s" % (str(label) if label else ''))
|
||||||
|
self._out.write("%s" % self._frontier)
|
||||||
|
|
||||||
|
|
||||||
|
def _back_edge_line(self, prev_ends, end, start, collapse, label=None):
|
||||||
|
"""Write part of a backwards edge in the graph.
|
||||||
|
|
||||||
|
Writes single- or multi-line backward edges in an ascii graph.
|
||||||
|
For example, a single line edge::
|
||||||
|
|
||||||
|
| | | | o |
|
||||||
|
| | | |/ / <-- single-line edge connects two nodes.
|
||||||
|
| | | o |
|
||||||
|
|
||||||
|
Or a multi-line edge (requires two calls to back_edge)::
|
||||||
|
|
||||||
|
| | | | o |
|
||||||
|
| |_|_|/ / <-- multi-line edge crosses vertical edges.
|
||||||
|
|/| | | |
|
||||||
|
o | | | |
|
||||||
|
|
||||||
|
Also handles "pipelined" edges, where the same line contains
|
||||||
|
parts of multiple edges::
|
||||||
|
|
||||||
|
o start
|
||||||
|
| |_|_|_|/|
|
||||||
|
|/| | |_|/| <-- this line has parts of 2 edges.
|
||||||
|
| | |/| | |
|
||||||
|
o o
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
prev_ends -- indices in frontier of previous edges that need
|
||||||
|
to be finished on this line.
|
||||||
|
|
||||||
|
end -- end of the current edge on this line.
|
||||||
|
|
||||||
|
start -- start index of the current edge.
|
||||||
|
|
||||||
|
collapse -- whether the graph will be collapsing (i.e. whether
|
||||||
|
to slant the end of the line or keep it straight)
|
||||||
|
|
||||||
|
label -- optional debug label to print after the line.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def advance(to_pos, edges):
|
||||||
|
"""Write edges up to <to_pos>."""
|
||||||
|
for i in range(self._pos, to_pos):
|
||||||
|
for e in edges():
|
||||||
|
self._write_edge(*e)
|
||||||
|
self._pos += 1
|
||||||
|
|
||||||
|
flen = len(self._frontier)
|
||||||
|
self._pos = 0
|
||||||
|
self._indent()
|
||||||
|
|
||||||
|
for p in prev_ends:
|
||||||
|
advance(p, lambda: [("| ", self._pos)] )
|
||||||
|
advance(p+1, lambda: [("|/", self._pos)] )
|
||||||
|
|
||||||
|
if end >= 0:
|
||||||
|
advance(end + 1, lambda: [("| ", self._pos)] )
|
||||||
|
advance(start - 1, lambda: [("|", self._pos), ("_", end)] )
|
||||||
|
else:
|
||||||
|
advance(start - 1, lambda: [("| ", self._pos)] )
|
||||||
|
|
||||||
|
if start >= 0:
|
||||||
|
advance(start, lambda: [("|", self._pos), ("/", end)] )
|
||||||
|
|
||||||
|
if collapse:
|
||||||
|
advance(flen, lambda: [(" /", self._pos)] )
|
||||||
|
else:
|
||||||
|
advance(flen, lambda: [("| ", self._pos)] )
|
||||||
|
|
||||||
|
self._set_state(BACK_EDGE, end, label)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _node_line(self, index, name):
|
||||||
|
"""Writes a line with a node at index."""
|
||||||
|
self._indent()
|
||||||
|
for c in range(index):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
|
||||||
|
self._out.write("%s " % self.node_character)
|
||||||
|
|
||||||
|
for c in range(index+1, len(self._frontier)):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
|
||||||
|
self._out.write(" %s" % name)
|
||||||
|
self._set_state(NODE, index)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _collapse_line(self, index):
|
||||||
|
"""Write a collapsing line after a node was added at index."""
|
||||||
|
self._indent()
|
||||||
|
for c in range(index):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
for c in range(index, len(self._frontier)):
|
||||||
|
self._write_edge(" /", c)
|
||||||
|
|
||||||
|
self._set_state(COLLAPSE, index)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_right_line(self, index):
|
||||||
|
"""Edge at index is same as edge to right. Merge directly with '\'"""
|
||||||
|
self._indent()
|
||||||
|
for c in range(index):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
self._write_edge("|", index)
|
||||||
|
self._write_edge("\\", index+1)
|
||||||
|
for c in range(index+1, len(self._frontier)):
|
||||||
|
self._write_edge("| ", c )
|
||||||
|
|
||||||
|
self._set_state(MERGE_RIGHT, index)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_right_line(self, index):
|
||||||
|
self._indent()
|
||||||
|
for c in range(index):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
|
||||||
|
self._write_edge("|", index)
|
||||||
|
self._write_edge("\\", index+1)
|
||||||
|
|
||||||
|
for c in range(index+2, len(self._frontier)):
|
||||||
|
self._write_edge(" \\", c)
|
||||||
|
|
||||||
|
self._set_state(EXPAND_RIGHT, index)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def write(self, spec, **kwargs):
|
||||||
|
"""Write out an ascii graph of the provided spec.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
spec -- spec to graph. This only handles one spec at a time.
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
|
||||||
|
out -- file object to write out to (default is sys.stdout)
|
||||||
|
|
||||||
|
color -- whether to write in color. Default is to autodetect
|
||||||
|
based on output file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
out = kwargs.get('out', None)
|
||||||
|
if not out:
|
||||||
|
out = sys.stdout
|
||||||
|
|
||||||
|
color = kwargs.get('color', None)
|
||||||
|
if not color:
|
||||||
|
color = out.isatty()
|
||||||
|
self._out = ColorStream(sys.stdout, color=color)
|
||||||
|
|
||||||
|
# We'll traverse the spec in topo order as we graph it.
|
||||||
|
topo_order = topological_sort(spec, reverse=True)
|
||||||
|
|
||||||
|
# Work on a copy to be nondestructive
|
||||||
|
spec = spec.copy()
|
||||||
|
self._nodes = spec.index()
|
||||||
|
|
||||||
|
# Colors associated with each node in the DAG.
|
||||||
|
# Edges are colored by the node they point to.
|
||||||
|
self._name_to_color = dict((name, self.colors[i % len(self.colors)])
|
||||||
|
for i, name in enumerate(topo_order))
|
||||||
|
|
||||||
|
# Frontier tracks open edges of the graph as it's written out.
|
||||||
|
self._frontier = [[spec.name]]
|
||||||
|
while self._frontier:
|
||||||
|
# Find an unexpanded part of frontier
|
||||||
|
i = find(self._frontier, lambda f: len(f) > 1)
|
||||||
|
|
||||||
|
if i >= 0:
|
||||||
|
# Expand frontier until there are enough columns for all children.
|
||||||
|
|
||||||
|
# Figure out how many back connections there are and
|
||||||
|
# sort them so we do them in order
|
||||||
|
back = []
|
||||||
|
for d in self._frontier[i]:
|
||||||
|
b = find(self._frontier[:i], lambda f: f == [d])
|
||||||
|
if b != -1:
|
||||||
|
back.append((b, d))
|
||||||
|
|
||||||
|
# Do all back connections in sorted order so we can
|
||||||
|
# pipeline them and save space.
|
||||||
|
if back:
|
||||||
|
back.sort()
|
||||||
|
prev_ends = []
|
||||||
|
for j, (b, d) in enumerate(back):
|
||||||
|
self._frontier[i].remove(d)
|
||||||
|
if i-b > 1:
|
||||||
|
self._back_edge_line(prev_ends, b, i, False, 'left-1')
|
||||||
|
del prev_ends[:]
|
||||||
|
prev_ends.append(b)
|
||||||
|
|
||||||
|
# Check whether we did ALL the deps as back edges,
|
||||||
|
# in which case we're done.
|
||||||
|
collapse = not self._frontier[i]
|
||||||
|
if collapse:
|
||||||
|
self._frontier.pop(i)
|
||||||
|
self._back_edge_line(prev_ends, -1, -1, collapse, 'left-2')
|
||||||
|
|
||||||
|
elif len(self._frontier[i]) > 1:
|
||||||
|
# Expand forward after doing all back connections
|
||||||
|
|
||||||
|
if (i+1 < len(self._frontier) and len(self._frontier[i+1]) == 1
|
||||||
|
and self._frontier[i+1][0] in self._frontier[i]):
|
||||||
|
# We need to connect to the element to the right.
|
||||||
|
# Keep lines straight by connecting directly and
|
||||||
|
# avoiding unnecessary expand/contract.
|
||||||
|
name = self._frontier[i+1][0]
|
||||||
|
self._frontier[i].remove(name)
|
||||||
|
self._merge_right_line(i)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Just allow the expansion here.
|
||||||
|
name = self._frontier[i].pop(0)
|
||||||
|
deps = [name]
|
||||||
|
self._frontier.insert(i, deps)
|
||||||
|
self._expand_right_line(i)
|
||||||
|
|
||||||
|
self._frontier.pop(i)
|
||||||
|
self._connect_deps(i, deps, "post-expand")
|
||||||
|
|
||||||
|
|
||||||
|
# Handle any remaining back edges to the right
|
||||||
|
j = i+1
|
||||||
|
while j < len(self._frontier):
|
||||||
|
deps = self._frontier.pop(j)
|
||||||
|
if not self._connect_deps(j, deps, "back-from-right"):
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Nothing to expand; add dependencies for a node.
|
||||||
|
name = topo_order.pop()
|
||||||
|
node = self._nodes[name]
|
||||||
|
|
||||||
|
# Find the named node in the frontier and draw it.
|
||||||
|
i = find(self._frontier, lambda f: name in f)
|
||||||
|
self._node_line(i, name)
|
||||||
|
|
||||||
|
# Replace node with its dependencies
|
||||||
|
self._frontier.pop(i)
|
||||||
|
if node.dependencies:
|
||||||
|
deps = sorted((d for d in node.dependencies), reverse=True)
|
||||||
|
self._connect_deps(i, deps, "new-deps") # anywhere.
|
||||||
|
|
||||||
|
elif self._frontier:
|
||||||
|
self._collapse_line(i)
|
||||||
|
|
||||||
|
|
||||||
|
def graph_ascii(spec, **kwargs):
|
||||||
|
node_character = kwargs.get('node', 'o')
|
||||||
|
out = kwargs.pop('out', None)
|
||||||
|
debug = kwargs.pop('debug', False)
|
||||||
|
indent = kwargs.pop('indent', 0)
|
||||||
|
color = kwargs.pop('color', None)
|
||||||
|
check_kwargs(kwargs, graph_ascii)
|
||||||
|
|
||||||
|
graph = AsciiGraph()
|
||||||
|
graph.debug = debug
|
||||||
|
graph.indent = indent
|
||||||
|
graph.node_character = node_character
|
||||||
|
|
||||||
|
graph.write(spec, color=color, out=out)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def graph_dot(*specs, **kwargs):
|
||||||
|
"""Generate a graph in dot format of all provided specs.
|
||||||
|
|
||||||
|
Print out a dot formatted graph of all the dependencies between
|
||||||
|
package. Output can be passed to graphviz, e.g.:
|
||||||
|
|
||||||
|
spack graph --dot qt | dot -Tpdf > spack-graph.pdf
|
||||||
|
|
||||||
|
"""
|
||||||
|
out = kwargs.pop('out', sys.stdout)
|
||||||
|
check_kwargs(kwargs, graph_dot)
|
||||||
|
|
||||||
|
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('\n')
|
||||||
|
|
||||||
|
def quote(string):
|
||||||
|
return '"%s"' % string
|
||||||
|
|
||||||
|
if not specs:
|
||||||
|
specs = [p.name for p in spack.db.all_packages()]
|
||||||
|
else:
|
||||||
|
roots = specs
|
||||||
|
specs = set()
|
||||||
|
for spec in roots:
|
||||||
|
specs.update(Spec(s.name) for s in spec.normalized().traverse())
|
||||||
|
|
||||||
|
deps = []
|
||||||
|
for spec in specs:
|
||||||
|
out.write(' %-30s [label="%s"]\n' % (quote(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.append((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))
|
||||||
|
|
||||||
|
out.write('\n')
|
||||||
|
|
||||||
|
for pair in deps:
|
||||||
|
out.write(' "%s" -> "%s"\n' % pair)
|
||||||
|
out.write('}\n')
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import join_path
|
from llnl.util.filesystem import join_path
|
||||||
from llnl.util.lang import memoized
|
from llnl.util.lang import *
|
||||||
|
|
||||||
import spack.error
|
import spack.error
|
||||||
import spack.spec
|
import spack.spec
|
||||||
@ -214,38 +214,6 @@ def get_class_for_package_name(self, pkg_name):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
def graph_dependencies(self, out=sys.stdout):
|
|
||||||
"""Print out a graph of all the dependencies between package.
|
|
||||||
Graph is in dot format."""
|
|
||||||
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('\n')
|
|
||||||
|
|
||||||
def quote(string):
|
|
||||||
return '"%s"' % string
|
|
||||||
|
|
||||||
deps = []
|
|
||||||
for pkg in self.all_packages():
|
|
||||||
out.write(' %-30s [label="%s"]\n' % (quote(pkg.name), pkg.name))
|
|
||||||
|
|
||||||
# Add edges for each depends_on in the package.
|
|
||||||
for dep_name, dep in pkg.dependencies.iteritems():
|
|
||||||
deps.append((pkg.name, dep_name))
|
|
||||||
|
|
||||||
# If the package provides something, add an edge for that.
|
|
||||||
for provider in set(p.name for p in pkg.provided):
|
|
||||||
deps.append((provider, pkg.name))
|
|
||||||
|
|
||||||
out.write('\n')
|
|
||||||
|
|
||||||
for pair in deps:
|
|
||||||
out.write(' "%s" -> "%s"\n' % pair)
|
|
||||||
out.write('}\n')
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownPackageError(spack.error.SpackError):
|
class UnknownPackageError(spack.error.SpackError):
|
||||||
"""Raised when we encounter a package spack doesn't have."""
|
"""Raised when we encounter a package spack doesn't have."""
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
@ -712,6 +712,15 @@ def flat_dependencies(self, **kwargs):
|
|||||||
raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message)
|
raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message)
|
||||||
|
|
||||||
|
|
||||||
|
def index(self):
|
||||||
|
"""Return DependencyMap that points to all the dependencies in this
|
||||||
|
spec."""
|
||||||
|
dm = DependencyMap()
|
||||||
|
for spec in self.traverse():
|
||||||
|
dm[spec.name] = spec
|
||||||
|
return dm
|
||||||
|
|
||||||
|
|
||||||
def flatten(self):
|
def flatten(self):
|
||||||
"""Pull all dependencies up to the root (this spec).
|
"""Pull all dependencies up to the root (this spec).
|
||||||
Merge constraints for dependencies with the same name, and if they
|
Merge constraints for dependencies with the same name, and if they
|
||||||
@ -858,7 +867,7 @@ def normalize(self, **kwargs):
|
|||||||
def normalized(self):
|
def normalized(self):
|
||||||
"""Return a normalized copy of this spec without modifying this spec."""
|
"""Return a normalized copy of this spec without modifying this spec."""
|
||||||
clone = self.copy()
|
clone = self.copy()
|
||||||
clone.normalized()
|
clone.normalize()
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
|
||||||
@ -1289,12 +1298,13 @@ def __str__(self):
|
|||||||
def tree(self, **kwargs):
|
def tree(self, **kwargs):
|
||||||
"""Prints out this spec and its dependencies, tree-formatted
|
"""Prints out this spec and its dependencies, tree-formatted
|
||||||
with indentation."""
|
with indentation."""
|
||||||
color = kwargs.get('color', False)
|
color = kwargs.pop('color', False)
|
||||||
depth = kwargs.get('depth', False)
|
depth = kwargs.pop('depth', False)
|
||||||
showid = kwargs.get('ids', False)
|
showid = kwargs.pop('ids', False)
|
||||||
cover = kwargs.get('cover', 'nodes')
|
cover = kwargs.pop('cover', 'nodes')
|
||||||
indent = kwargs.get('indent', 0)
|
indent = kwargs.pop('indent', 0)
|
||||||
format = kwargs.get('format', '$_$@$%@$+$=')
|
fmt = kwargs.pop('format', '$_$@$%@$+$=')
|
||||||
|
check_kwargs(kwargs, self.tree)
|
||||||
|
|
||||||
out = ""
|
out = ""
|
||||||
cur_id = 0
|
cur_id = 0
|
||||||
@ -1311,7 +1321,7 @@ def tree(self, **kwargs):
|
|||||||
out += (" " * d)
|
out += (" " * d)
|
||||||
if d > 0:
|
if d > 0:
|
||||||
out += "^"
|
out += "^"
|
||||||
out += node.format(format, color=color) + "\n"
|
out += node.format(fmt, color=color) + "\n"
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user