#!/usr/bin/env python
#!/usr/bin/python2.4
#!/usr/bin/python
"""
==========================================================================
scons2dot.py
==========================================================================
This is a simple python program meant to vizualize build dependencies.
It grabs scons_ --tree=all output and converts it to a graphviz_ dot
graph. The current version of scons2dot.py is found
`here <http://www.cs.virginia.edu/~dww4s/tools/scons2dot/scons2dot.txt>`_
(HTML `scons2dot.html`_).
Example::
+-call
+-call.o
| +-call.s
| | +-call.c
| | +-/usr/bin/g++
| +-/usr/bin/g++
+-/usr/bin/g++
+-dbxml:call.o.xml
+-call.o.xml
| +-call.s
| | +-call.c
| | +-/usr/bin/g++
| +-/home/dww4s/research/metaman/mm_v2/bin/metaman_asm.py
+-scons.dbxml
+-/home/dww4s/research/metaman/mm_v2/bin/manage_container
Gets converted to the following using dot:
.. dot:: builddep.dot
digraph scons_graph {
graph [rankdir="LR", bgcolor="transparent"]
n0 [label="manage_container"]
n1 [label="call.o.xml"]
n2 [label="scons.dbxml"]
n3 [color="red", label="dbxml:call.o.xml"]
n4 [label="metaman_asm.py"]
n5 [label="call.s"]
n6 [label="call.o"]
n7 [color="red", label="call"]
n8 [label="g++"]
n9 [label="call.c"]
n4 -> n1
n5 -> n1
n0 -> n3
n1 -> n3
n2 -> n3
n8 -> n5
n9 -> n5
n8 -> n6
n5 -> n6
n6 -> n7
n8 -> n7
}
Using the command::
scons --tree=all -n | scons2dot.py
Note that scons2dot.py uses the basename of the entry as the label, though
the entire string is stored in the Node data structure. If the ``--save``
option is used, the dot output is saved to a temp file, dot is invoked, and
the resulting file is saved, as specified by ``--outfile``.
Invoke scons2dot.py -h for more info on command options.
TODO
==========================================================================
- Add support for the --tree=all,status option, coloring built nodes, etc.
----
:author: Dan Williams (dan_williams@cs.virginia.edu)
:Homepage: http://www.cs.virginia.edu/~dww4s
:Last doc rebuild: |date|
:copyright: University of Virginia, 2009
:license: BSD_
:version: 0.1
:python version: `2.4 <http://www.python.org/download/releases/2.4.6/>`_
.. _BSD: http://www.opensource.org/licenses/bsd-license.php
.. _scons: http://www.scons.org/
.. _graphviz: http://www.graphviz.org/
.. |date| date:: %m-%d-%y
"""
cmd_line_usage = \
"""
Usage: %prog [options]
%prog reads the output from scons with the --tree=all option, and produces
dot code that can be used to create a visualization of your build
dependencies. The easiest way to use %prog is to pipe the output from scons
to %prog, and use the --save and --outfile options to store the resulting
pdf.
Example:
scons --tree=all -n | %prog --save --outfile deps.pdf
"""[1:-1] #(removes first&last newline)
# Note: Command to build the __doc__ file (assuming rst2html, with a "dot"
# extension):
# python -c "import scons2dot; print scons2dot.__doc__" > scons2dot.rst
# rst2html scons2dot.rst scons2dot.html
##########################################################################
# Std. imports
##########################################################################
import re,sys,os.path
import sets,tempfile
##########################################################################
# Defaults
##########################################################################
GRAPH_DEFAULTS = {"bgcolor":"transparent",
"rankdir": "LR",
}
##########################################################################
# Classes
##########################################################################
class DotBuilder(object):
"""
Build a dot for output. This class assumes the trees argument
is a list of Node objects. It looks for a few other parameters
related to dot building.
"""
def __init__(self, trees=[], **kw):
self.trees = trees
self.nodes = []
self.graph_name = kw.get('graph_name', 'scons_graph')
self.graph_attrs = GRAPH_DEFAULTS
self.graph_attrs.update(kw.get('graph_attrs', {}))
self.node_attrs = kw.get('node_attrs', {})
self.edge_attrs = kw.get('edge_attrs', {})
self.fd = kw.get('fd', sys.stdout)
self.dot_fname = kw.get('dot_fname', 'scons-dep.pdf')
self.dot_format = kw.get('dot_format', 'pdf')
self.dot_cmd = "dot -T%(dot_format)s -o %(dot_fname)s"
self.ignore_list= kw.get('ignore_list', [])
self.verbose = bool(kw.get('verbose', True))
self.ignore_re = [ re.compile(x) for x in self.ignore_list ]
def dict2attrs(self, attrs):
"""
Converts a dictionary to a list k="value",...; the format
used by attributes in dot.
"""
return ", ".join(['%s="%s"'%x for
x in attrs.items()])
def _gather_nodes(self):
"""
A simple bfs worklist to gather all unique nodes from
the self.trees data structure.
"""
results = sets.Set(self.trees)
change = True
work_list = self.trees[:]
while len(work_list) > 0:
item = work_list.pop()
s = sets.Set(item.children)
s = s - results
work_list.extend(list(s))
results = results.union(s)
self.nodes = list(results)
self._set_ignores()
#print self.nodes
return results
def _set_ignores(self):
"""
Processes and sets the ignored nodes.
"""
for node in self.nodes:
for ire in self.ignore_re:
if ire.match(node.name):
node.ignore = True
def save_graph(self):
"""
Save the graph as temp file an invoke dot to
create a graph is the format self.dot_format,
and with the name self.dot_fname.
"""
self.fd, fname = tempfile.mkstemp('.dot')
self.fd = os.fdopen(self.fd, 'w')
self.print_graph()
self.fd.close()
#(the following is a cheap trick to do str replacement
# using this object's dictionary as the replacement.)
cmd = self.dot_cmd % self.__dict__
cmd += " %s" % fname
if self.verbose: print cmd
os.system(cmd)
def print_graph(self):
"""
Prints the graph to self.fd.
"""
self._gather_nodes()
self._begin_graph()
self._print_nodes()
self._print_edges()
self._end_graph()
def _begin_graph(self):
self.fd.write("digraph %s {\n" % self.graph_name)
attr_strn = self.dict2attrs(self.graph_attrs)
self.fd.write("graph [%s]\n\n" % attr_strn)
def _print_nodes(self):
if len(self.node_attrs):
attr_strn = self.dict2attrs(self.node_attrs)
self.fd.write("node [%s]\n" % attr_strn)
for ind,node in enumerate(self.nodes):
if node.ignore: continue
node.id = "n%d" % ind
attrs = {"label":os.path.basename(node.name)}
if node in self.trees:
attrs["color"] = "red"
self.fd.write('\t%s [%s]\n' %
(node.id, self.dict2attrs(attrs)))
self.fd.write('\n')
def _print_edges(self):
if len(self.edge_attrs):
attr_strn = self.dict2attrs(self.edge_attrs)
self.fd.write("edge [%s]\n" % attr_strn)
for node in self.nodes:
if node.ignore: continue
for child in node.children:
if child.ignore: continue
self.fd.write('\t%s -> %s\n' % (child.id, node.id))
def _end_graph(self):
self.fd.write("\n}\n")
class NullFD(object):
def write(self, strn): pass
def close(self): pass
class SConsForestParser(object):
"""
A simple class that takes a file-like object and does
basic regex scans for lines that appear to be
"""
L1RE = re.compile(r"^\+-(\S+)")
LNRE = re.compile(r"^(\s*(\| +)*)\+-(\S+)")
def __init__(self, inp=sys.stdin, altfd=NullFD()):
self.input = inp
self.top_level = []
self.altfd = altfd
def process(self):
current_stack = []
for line in self.input:
l1m = self.L1RE.search(line)
lNm = self.LNRE.search(line)
if l1m:
node = mk_node(l1m.group(1))
current_stack = [node]
self.top_level.append(node)
elif lNm:
level = len(lNm.group(1)) / 2
level+=1
node = mk_node(lNm.group(3))
if len(current_stack) > (level-1):
current_stack = current_stack[:(level-1)]
current_stack.append(node)
current_stack[level-2].children.add(node)
else:
self.altfd.write(line)
##########################################################################
# Data structure
##########################################################################
def mk_node(name):
global NODE_HASH
if name in NODE_HASH:
return NODE_HASH[name]
else:
node = Node(name)
NODE_HASH[name] = node
return node
NODE_HASH = {}
class Node(object):
"""
The Node data structure creates a simple ad hoc tree.
Using mk_node, above limits the Nodes to be unique by name.
(I think this could be done with some __new__ magic, but
I'd have to look it up).
"""
def __init__(self, name, children=None):
self.name = name
self.ignore = False
if children == None:
self.children = sets.Set()
else:
self.children = children
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
def __repr__(self):
return "%s:%d" % (self.name, len(self.children))
##########################################################################
# Script
##########################################################################
def run(args):
"""
Parse the command-line(-eque) args, and run the program.
"""
import optparse
parser = optparse.OptionParser(usage=cmd_line_usage)
parser.add_option('-o', '--outfile', default='-')
parser.add_option('-i', '--infile', default='-')
parser.add_option('--altfile', default='-',
help="The file to put things not matched as part of the tree, default is stdout.")
parser.add_option('--save', action="store_true",
help='Save the dot file and invoke the dot command, save the resulting file to OUTFILE')
parser.add_option('--format', default='pdf',
help="Format to output when using the --save option, default is pdf. For legal values, see the -T option in dot.")
parser.add_option('--ignore', default=[], action='append',
help='ignore a file pattern (python re)')
parser.add_option('--graph-opt', default=[], action='append',
help='Dot graph option, as a key=value pair.')
parser.add_option('--node-opt', default=[], action='append',
help='Dot node option, as a key=value pair.')
parser.add_option('--edge-opt', default=[], action='append',
help='Dot edge option, as a key=value pair.')
(opts, args) = parser.parse_args(args)
infd = None
if opts.infile == '-':
infd = sys.stdin
else:
infd = file(opts.infile, 'r')
altfd = None
if opts.altfile == '-':
altfd = sys.stdout
else:
altfd = file(opts.altfile, 'w')
# process dot options
graph_attrs = {}
for opt in opts.graph_opt:
k,v = opt.split('=')
graph_attrs[k] = v
node_attrs = {}
for opt in opts.node_opt:
k,v = opt.split('=')
node_attrs[k] = v
edge_attrs = {}
for opt in opts.edge_opt:
k,v = opt.split('=')
edge_attrs[k] = v
sfp = SConsForestParser(infd, altfd=altfd)
sfp.process()
db = DotBuilder(sfp.top_level,
dot_fname=opts.outfile,
dot_format=opts.format,
ignore_list=opts.ignore,
graph_attrs=graph_attrs,
node_attrs=node_attrs,
edge_attrs=edge_attrs,
)
if opts.save:
db.save_graph()
else:
outfd = None
if opts.outfile == '-':
outfd = sys.stdout
else:
outfd = file(opts.outfile, 'w')
db.fd = outfd
db.print_graph()
if __name__ == "__main__":
run(sys.argv[1:])