Factor graph code out into its own module, rework spack graph.
This commit is contained in:
parent
478af54cce
commit
0a0291678e
@ -23,17 +23,39 @@
|
|||||||
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from external import argparse
|
from external import argparse
|
||||||
|
|
||||||
import spack
|
import spack
|
||||||
import spack.cmd
|
import spack.cmd
|
||||||
|
from spack.graph import *
|
||||||
|
|
||||||
description = "Write out inter-package dependencies in dot graph format"
|
description = "Generate graphs of package dependency relationships."
|
||||||
|
|
||||||
def setup_parser(subparser):
|
def setup_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.")
|
||||||
|
|
||||||
|
method.add_argument(
|
||||||
|
'--concretize', action='store_true', help="Concretize specs before graphing.")
|
||||||
|
|
||||||
subparser.add_argument(
|
subparser.add_argument(
|
||||||
'specs', nargs=argparse.REMAINDER,
|
'specs', nargs=argparse.REMAINDER, help="specs of packages to graph.")
|
||||||
help="specs of packages to graph. Default is all packages.")
|
|
||||||
|
|
||||||
|
|
||||||
def graph(parser, args):
|
def graph(parser, args):
|
||||||
specs = spack.cmd.parse_specs(args.specs)
|
specs = spack.cmd.parse_specs(
|
||||||
spack.db.graph_dependencies(*specs)
|
args.specs, normalize=True, concretize=args.concretize)
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
||||||
|
for spec in specs[1:]:
|
||||||
|
print # extra line bt/w independent graphs
|
||||||
|
graph_ascii(spec)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."
|
||||||
|
|
||||||
@ -44,14 +44,9 @@ def spec(parser, args):
|
|||||||
print "Normalized"
|
print "Normalized"
|
||||||
print "------------------------------"
|
print "------------------------------"
|
||||||
spec.normalize()
|
spec.normalize()
|
||||||
print spec.tree(color=True, indent=2, cover='paths')
|
print spec.tree(color=True, indent=2)
|
||||||
|
|
||||||
print "Concretized"
|
print "Concretized"
|
||||||
print "------------------------------"
|
print "------------------------------"
|
||||||
spec.concretize()
|
spec.concretize()
|
||||||
print spec.tree(color=True, indent=2)
|
print spec.tree(color=True, indent=2)
|
||||||
|
|
||||||
print "Graph"
|
|
||||||
print "------------------------------"
|
|
||||||
spec.graph()
|
|
||||||
return
|
|
||||||
|
480
lib/spack/spack/graph.py
Normal file
480
lib/spack/spack/graph.py
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
##############################################################################
|
||||||
|
# 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,
|
||||||
|
knd of like the graph git shows with "git log --graph".
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class AsciiGraph(object):
|
||||||
|
def __init__(self):
|
||||||
|
# These can be set after initialization or after a call to
|
||||||
|
# graph() to change behavior.
|
||||||
|
self.node_character = 'o'
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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, collapse, label):
|
||||||
|
"""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:
|
||||||
|
|
||||||
|
collapse -- whether the frontier is collapsing or staying the
|
||||||
|
same size.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# connect to the left
|
||||||
|
if j < i:
|
||||||
|
if i-j > 1: # two lines if distance > 1
|
||||||
|
self._back_edge([], j, i, True, label)
|
||||||
|
self._back_edge([j], -1, -1, (i-j == 1), label)
|
||||||
|
|
||||||
|
# connect to the right
|
||||||
|
else:
|
||||||
|
if i < j:
|
||||||
|
self._frontier.pop(j)
|
||||||
|
self._frontier.insert(i, deps)
|
||||||
|
if j-i > 1:
|
||||||
|
self._back_edge([], i, j+1, collapse, label)
|
||||||
|
self._back_edge([i], -1, -1, not (j-i > 1) and collapse, label)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif deps:
|
||||||
|
self._frontier.insert(i, deps)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _add_deps_to_frontier(self, node, i):
|
||||||
|
"""Add dependencies to frontier.
|
||||||
|
|
||||||
|
Adds the dependencies of <node> to the frontier, and connects
|
||||||
|
them to other open edges if they match. Also deletes parent
|
||||||
|
pointers in the node to mark edges as covered.
|
||||||
|
|
||||||
|
"""
|
||||||
|
deps = sorted((d for d in node.dependencies), reverse=True)
|
||||||
|
self._connect_deps(i, deps, True, "add_deps")
|
||||||
|
for d in deps:
|
||||||
|
del self._nodes[d].dependents[node.name]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _back_edge(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)] )
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
self._out.write(" " * 10)
|
||||||
|
if label:
|
||||||
|
self._out.write(label)
|
||||||
|
self._out.write("%s" % self._frontier)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
# This array tracks the open edges at the frontier of the
|
||||||
|
# graph we're writing out.
|
||||||
|
self._frontier = []
|
||||||
|
|
||||||
|
self._add_deps_to_frontier(spec, 0)
|
||||||
|
self._indent()
|
||||||
|
self._out.write('%s %s\n' % (self.node_character, spec.name))
|
||||||
|
topo_order.pop()
|
||||||
|
|
||||||
|
while self._frontier:
|
||||||
|
# Find an unexpanded part of frontier
|
||||||
|
i = find(self._frontier, lambda f: len(f) > 1)
|
||||||
|
|
||||||
|
# Expand frontier until there are enough columns for all children.
|
||||||
|
if i >= 0:
|
||||||
|
# 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(prev_ends, b, i, False)
|
||||||
|
del prev_ends[:]
|
||||||
|
prev_ends.append(b)
|
||||||
|
self._back_edge(prev_ends, -1, -1, False)
|
||||||
|
|
||||||
|
if not self._frontier[i]:
|
||||||
|
self._frontier.pop(i)
|
||||||
|
|
||||||
|
elif len(self._frontier[i]) > 1:
|
||||||
|
# Expand forawrd after doing all back connections
|
||||||
|
self._indent()
|
||||||
|
for c in range(i):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
self._write_edge("|", i)
|
||||||
|
|
||||||
|
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 immediate expand/contract.
|
||||||
|
name = self._frontier[i+1][0]
|
||||||
|
self._frontier[i].remove(name)
|
||||||
|
|
||||||
|
self._write_edge("\\", i+1)
|
||||||
|
for c in range(i+1, len(self._frontier)):
|
||||||
|
self._write_edge("| ", c )
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Just allow the expansion here.
|
||||||
|
name = self._frontier[i].pop(0)
|
||||||
|
deps = [name]
|
||||||
|
self._write_edge("\\", i)
|
||||||
|
for c in range(i+1, len(self._frontier)):
|
||||||
|
self._write_edge(" \\", c)
|
||||||
|
self._out.write("\n")
|
||||||
|
self._connect_deps(i, deps, True, "expansion")
|
||||||
|
|
||||||
|
# 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, True, "rem_back"):
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
name = topo_order.pop()
|
||||||
|
node = self._nodes[name]
|
||||||
|
|
||||||
|
# Find the next node in topo order and remove it from
|
||||||
|
# the frontier. Since specs are single-rooted DAGs,
|
||||||
|
# the node is always there. If the graph had multiple
|
||||||
|
# roots, we'd need to handle that case case of a new root.
|
||||||
|
i = find(self._frontier, lambda f: name in f)
|
||||||
|
self._frontier.pop(i)
|
||||||
|
|
||||||
|
self._indent()
|
||||||
|
for c in range(i):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
self._out.write("%s " % self.node_character)
|
||||||
|
for c in range(i, len(self._frontier)):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
self._out.write(" %s\n" % name)
|
||||||
|
|
||||||
|
if node.dependencies:
|
||||||
|
self._add_deps_to_frontier(node, i)
|
||||||
|
elif self._frontier:
|
||||||
|
self._indent()
|
||||||
|
for c in range(i):
|
||||||
|
self._write_edge("| ", c)
|
||||||
|
for c in range(i, len(self._frontier)):
|
||||||
|
self._write_edge(" /", c)
|
||||||
|
self._out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
packages = spack.db.all_packages()
|
||||||
|
else:
|
||||||
|
packages = []
|
||||||
|
for spec in specs:
|
||||||
|
packages.extend(s.package for s in spec.normalized().traverse())
|
||||||
|
|
||||||
|
deps = []
|
||||||
|
for pkg in 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')
|
@ -214,48 +214,6 @@ def get_class_for_package_name(self, pkg_name):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
def graph_dependencies(self, *specs, **kwargs):
|
|
||||||
"""Print out a graph of all the dependencies between package.
|
|
||||||
Graph is in dot format."""
|
|
||||||
out = kwargs.pop('out', sys.stdout)
|
|
||||||
check_kwargs(kwargs, self.graph_dependencies)
|
|
||||||
|
|
||||||
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:
|
|
||||||
packages = self.all_packages()
|
|
||||||
else:
|
|
||||||
packages = []
|
|
||||||
for spec in specs:
|
|
||||||
packages.extend(s.package for s in spec.normalized().traverse())
|
|
||||||
|
|
||||||
deps = []
|
|
||||||
for pkg in 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):
|
||||||
|
@ -93,7 +93,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import itertools
|
import itertools
|
||||||
import hashlib
|
import hashlib
|
||||||
from heapq import *
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
@ -1326,267 +1325,6 @@ def tree(self, **kwargs):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def graph(self, **kwargs):
|
|
||||||
N = kwargs.get('node', 'o') # Node character
|
|
||||||
color = kwargs.get('color', True)
|
|
||||||
out = kwargs.get('out', ColorStream(sys.stdout, color=color))
|
|
||||||
debug = kwargs.get('debug', False)
|
|
||||||
indent = kwargs.get('indent', 0)
|
|
||||||
indent *= ' '
|
|
||||||
|
|
||||||
topo_order = self.topological_sort(reverse=True)
|
|
||||||
|
|
||||||
# Work on a clone so the spec is self contained (no incoming
|
|
||||||
# parent edges), and so we don't destroy this spec.
|
|
||||||
clone = self.copy()
|
|
||||||
|
|
||||||
# Fast access to nodes in the spec.
|
|
||||||
nodes = clone.index()
|
|
||||||
|
|
||||||
# Colors associated with each node in the DAG.
|
|
||||||
# Edges are colored by the node they point to.
|
|
||||||
all_colors = 'rgbmcyRGBMCY'
|
|
||||||
colors = dict((name, all_colors[i % len(all_colors)])
|
|
||||||
for i, name in enumerate(topo_order))
|
|
||||||
def write_edge(string, index, sub=0):
|
|
||||||
edge = "@%s{%s}" % (colors[frontier[index][sub]], string)
|
|
||||||
out.write(edge)
|
|
||||||
|
|
||||||
frontier = []
|
|
||||||
|
|
||||||
def ordered_deps(node):
|
|
||||||
deps = node.dependencies
|
|
||||||
return sorted((d for d in deps), reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
def back_edge(prev_ends, end, start, collapse, label=None):
|
|
||||||
# Use prev & next for pipelining -- pipelined edges have
|
|
||||||
# the same start, and they're in sorted order e.g.::
|
|
||||||
#
|
|
||||||
# start
|
|
||||||
# | |_|_|_|/|
|
|
||||||
# |/| | |_|/|
|
|
||||||
# | | |/| | | <-- when doing this line.
|
|
||||||
# prev end
|
|
||||||
#
|
|
||||||
out.write(indent)
|
|
||||||
|
|
||||||
f = len(frontier)
|
|
||||||
|
|
||||||
self._pos = 0
|
|
||||||
def advance(to, fun):
|
|
||||||
for i in range(self._pos, to):
|
|
||||||
fun()
|
|
||||||
self._pos += 1
|
|
||||||
|
|
||||||
for p in prev_ends:
|
|
||||||
advance(p, lambda: write_edge("| ", self._pos))
|
|
||||||
advance(p+1, lambda: write_edge("|/", self._pos))
|
|
||||||
if end >= 0:
|
|
||||||
advance(end + 1, lambda: write_edge("| ", self._pos))
|
|
||||||
advance(start - 1, lambda: (write_edge("|", self._pos) or
|
|
||||||
write_edge("_", end)))
|
|
||||||
else:
|
|
||||||
advance(start - 1, lambda: write_edge("| ", self._pos))
|
|
||||||
|
|
||||||
if start >= 0:
|
|
||||||
advance(start, lambda: (write_edge("|", self._pos) or
|
|
||||||
write_edge("/", end)))
|
|
||||||
|
|
||||||
if collapse:
|
|
||||||
advance(len(frontier), lambda: write_edge(" /", self._pos))
|
|
||||||
else:
|
|
||||||
advance(len(frontier), lambda: write_edge("| ", self._pos))
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
out.write(" " * 10)
|
|
||||||
if label: out.write(label)
|
|
||||||
out.write("%s" % frontier)
|
|
||||||
|
|
||||||
out.write("\n")
|
|
||||||
|
|
||||||
|
|
||||||
def connect_deps(i, deps, collapse, label):
|
|
||||||
"""Connect dependencies to frontier at position i."""
|
|
||||||
if len(deps) == 1 and deps in frontier:
|
|
||||||
j = frontier.index(deps)
|
|
||||||
|
|
||||||
# connect to the left
|
|
||||||
if j < i:
|
|
||||||
if i-j > 1: # two lines if distance > 1
|
|
||||||
back_edge([], j, i, True, label)
|
|
||||||
back_edge([j], -1, -1, (i-j == 1), label)
|
|
||||||
|
|
||||||
# connect to the right
|
|
||||||
else:
|
|
||||||
if i < j:
|
|
||||||
frontier.pop(j)
|
|
||||||
frontier.insert(i, deps)
|
|
||||||
if j-i > 1:
|
|
||||||
back_edge([], i, j+1, collapse, label)
|
|
||||||
back_edge([i], -1, -1, not (j-i > 1) and collapse, label)
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif deps:
|
|
||||||
frontier.insert(i, deps)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def add_deps_to_frontier(node, i):
|
|
||||||
"""Add dependencies to frontier, connecting them if they're fully
|
|
||||||
expanded, and deleting parent pointers."""
|
|
||||||
deps = ordered_deps(node)
|
|
||||||
connect_deps(i, deps, True, "add_deps")
|
|
||||||
for d in deps:
|
|
||||||
del nodes[d].dependents[node.name]
|
|
||||||
|
|
||||||
|
|
||||||
def find(seq, predicate):
|
|
||||||
for i, elt in enumerate(seq):
|
|
||||||
if predicate(elt):
|
|
||||||
return i
|
|
||||||
return -1
|
|
||||||
|
|
||||||
|
|
||||||
add_deps_to_frontier(self, 0)
|
|
||||||
out.write(indent)
|
|
||||||
out.write('%s %s\n' % (N, self.name))
|
|
||||||
topo_order.pop()
|
|
||||||
|
|
||||||
while frontier:
|
|
||||||
# Find an unexpanded part of frontier
|
|
||||||
i = find(frontier, lambda f: len(f) > 1)
|
|
||||||
|
|
||||||
# Expand frontier until there are enough columns for all children.
|
|
||||||
if i >= 0:
|
|
||||||
# Figure out how many back connections there are and
|
|
||||||
# sort them so we do them in order
|
|
||||||
back = []
|
|
||||||
for d in frontier[i]:
|
|
||||||
b = find(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):
|
|
||||||
frontier[i].remove(d)
|
|
||||||
if i-b > 1:
|
|
||||||
back_edge(prev_ends, b, i, False)
|
|
||||||
del prev_ends[:]
|
|
||||||
prev_ends.append(b)
|
|
||||||
back_edge(prev_ends, -1, -1, False)
|
|
||||||
|
|
||||||
if not frontier[i]:
|
|
||||||
frontier.pop(i)
|
|
||||||
|
|
||||||
elif len(frontier[i]) > 1:
|
|
||||||
# Expand forawrd after doing all back connections
|
|
||||||
out.write(indent)
|
|
||||||
for c in range(i):
|
|
||||||
write_edge("| ", c)
|
|
||||||
write_edge("|", i)
|
|
||||||
|
|
||||||
if (i+1 < len(frontier) and len(frontier[i+1]) == 1
|
|
||||||
and frontier[i+1][0] in frontier[i]):
|
|
||||||
# We need to connect to the element to the right.
|
|
||||||
# Keep lines straight by connecting directly and
|
|
||||||
# avoiding immediate expand/contract.
|
|
||||||
name = frontier[i+1][0]
|
|
||||||
frontier[i].remove(name)
|
|
||||||
|
|
||||||
write_edge("\\", i+1)
|
|
||||||
for c in range(i+1, len(frontier)):
|
|
||||||
write_edge("| ", c )
|
|
||||||
out.write("\n")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Just allow the expansion here.
|
|
||||||
name = frontier[i].pop(0)
|
|
||||||
deps = [name]
|
|
||||||
write_edge("\\", i)
|
|
||||||
for c in range(i+1, len(frontier)):
|
|
||||||
write_edge(" \\", c)
|
|
||||||
out.write("\n")
|
|
||||||
connect_deps(i, deps, True, "expansion")
|
|
||||||
|
|
||||||
# Handle any remaining back edges to the right
|
|
||||||
j = i+1
|
|
||||||
while j < len(frontier):
|
|
||||||
deps = frontier.pop(j)
|
|
||||||
# TODO: semantics of connect_deps are weird.
|
|
||||||
# TODO: false return means the popped item was put
|
|
||||||
# TODO: back & not connected.
|
|
||||||
if not connect_deps(j, deps, True, "rem_back"):
|
|
||||||
j += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
name = topo_order.pop()
|
|
||||||
node = nodes[name]
|
|
||||||
|
|
||||||
# Find the next node in topo order and remove it from
|
|
||||||
# the frontier. Since specs are single-rooted DAGs,
|
|
||||||
# the node is always there. If the graph had multiple
|
|
||||||
# roots, we'd need to handle that case case of a new root.
|
|
||||||
i = find(frontier, lambda f: name in f)
|
|
||||||
frontier.pop(i)
|
|
||||||
|
|
||||||
out.write(indent)
|
|
||||||
for c in range(i):
|
|
||||||
write_edge("| ", c)
|
|
||||||
out.write("%s " % N)
|
|
||||||
for c in range(i, len(frontier)):
|
|
||||||
write_edge("| ", c)
|
|
||||||
out.write(" %s\n" % name)
|
|
||||||
|
|
||||||
if node.dependencies:
|
|
||||||
add_deps_to_frontier(node, i)
|
|
||||||
elif frontier:
|
|
||||||
out.write(indent)
|
|
||||||
for c in range(i):
|
|
||||||
write_edge("| ", c)
|
|
||||||
for c in range(i, len(frontier)):
|
|
||||||
write_edge(" /", c)
|
|
||||||
out.write("\n")
|
|
||||||
|
|
||||||
|
|
||||||
def topological_sort(self, **kwargs):
|
|
||||||
"""Return a list of dependency specs sorted topologically.
|
|
||||||
This spec 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
|
|
||||||
|
|
||||||
spec = self.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 __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user