#!/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 `_ (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 `_ .. _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:])