Matt Torok 14e3c77f60 Updated brew graph
Changed 'graph' command to show only installed by default. Prettified
resulting graph.

By default, the `brew graph` command would output a dependency graph for every
formula it knew about. Now it only outputs a dependency graph the for formulas
installed. If you want to see the graph for all formulas, use `brew graph
--all`.

Additionally, the old version of the graph command would filter out any
formulas without depdency connections. The updated version now only does this
if calculating dependencies for all formulas via the `--all` flag.

Finally, the resulting graph has been redesigned to be simpler to read. All
formulas which have no other formulas depending on them (i.e., root nodes) are
aligned to the left. They are also outlined in a light grey box, which is
labelled "Safe to Remove". As implied, all of the formulas in this box can be
safely removed without breaking other installed formulas; all formulas outside
this box have at least one installed formula depending on them. This new graph
style is surpressed if the `--all` flag is used.

Closes Homebrew/homebrew#18282.

Signed-off-by: Adam Vandenberg <flangy@gmail.com>
2013-06-09 08:21:44 -07:00

344 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python
"""
$ brew install graphviz
$ brew graph | dot -Tsvg -ohomebrew.html
$ open homebrew.html
"""
from __future__ import with_statement
from contextlib import contextmanager
import re
from subprocess import Popen, PIPE
import sys
def run(command, print_command=False):
"Run a command, returning the exit code and output."
if print_command: print command
p = Popen(command, stdout=PIPE)
output, errput = p.communicate()
return p.returncode, output
def _quote_id(id):
return '"' + id.replace('"', '\"') + '"'
def format_attribs(attrib):
if len(attrib) == 0:
return ''
values = ['%s="%s"' % (k, attrib[k]) for k in attrib]
return '[' + ','.join(values) + ']'
class Output(object):
def __init__(self, fd=sys.stdout, tabstyle=" "):
self.fd = fd
self.tabstyle = tabstyle
self.tablevel = 0
def close(self):
self.fd = None
def out(self, s):
self.tabout()
self.fd.write(s)
def outln(self, s=None):
if s is not None:
self.tabout()
self.fd.write(s)
self.fd.write('\n')
@contextmanager
def indented(self):
self.indent()
yield self
self.dedent()
def indent(self):
self.tablevel += 1
def dedent(self):
if self.tablevel == 0:
raise Exception('No existing indent level.')
self.tablevel -= 1
def tabout(self):
if self.tablevel:
self.fd.write(self.tabstyle * self.tablevel)
class NodeContainer(object):
def __init__(self):
self.nodes = list()
self.node_defaults = dict()
# Stack of node attribs
self._node_styles = list()
def _node_style(self):
if (len(self._node_styles) > 0):
return self._node_styles[-1]
else:
return dict()
def _push_node_style(self, attrib):
self._node_styles.append(attrib)
def _pop_node_style(self):
return self._node_styles.pop()
@contextmanager
def node_styles(self, attrib):
self._push_node_style(attrib)
yield
self._pop_node_style()
def node(self, nodeid, label, attrib=None):
_attrib = dict(self._node_style())
if attrib is not None:
_attrib.update(attrib)
n = Node(nodeid, label, _attrib)
self.nodes.append(n)
return n
def nodes_to_dot(self, out):
if len(self.node_defaults) > 0:
out.outln("node " + format_attribs(self.node_defaults) + ";")
if len(self.nodes) == 0:
return
id_width = max([len(_quote_id(n.id)) for n in self.nodes])
for node in self.nodes:
node.to_dot(out, id_width)
class Node(object):
def __init__(self, nodeid, label, attrib=None):
self.id = nodeid
self.label = label
self.attrib = attrib if attrib is not None else dict()
def as_dot(self, id_width=1):
_attribs = dict(self.attrib)
_attribs['label'] = self.label
return '%-*s %s' % (id_width, _quote_id(self.id), format_attribs(_attribs))
def to_dot(self, out, id_width=1):
out.outln(self.as_dot(id_width))
class ClusterContainer(object):
def __init__(self):
self.clusters = list()
def cluster(self, clusterid, label, attrib=None):
c = Cluster(clusterid, label, self, attrib)
self.clusters.append(c)
return c
class Cluster(NodeContainer, ClusterContainer):
def __init__(self, clusterid, label, parentcluster=None, attrib=None):
NodeContainer.__init__(self)
ClusterContainer.__init__(self)
self.id = clusterid
self.label = label
self.attrib = attrib if attrib is not None else dict()
self.parentcluster = parentcluster
def cluster_id(self):
return _quote_id("cluster_" + self.id)
def to_dot(self, out):
out.outln("subgraph %s {" % self.cluster_id())
with out.indented():
out.outln('label = "%s"' % self.label)
for k in self.attrib:
out.outln('%s = "%s"' % (k, self.attrib[k]))
for cluster in self.clusters:
cluster.to_dot(out)
self.nodes_to_dot(out)
out.outln("}")
class Edge(object):
def __init__(self, source, target, attrib=None):
if attrib is None:
attrib = dict()
self.source = source
self.target = target
self.attrib = attrib
def to_dot(self, out):
out.outln(self.as_dot())
def as_dot(self):
return " ".join((_quote_id(self.source), "->", _quote_id(self.target), format_attribs(self.attrib)))
class EdgeContainer(object):
def __init__(self):
self.edges = list()
self.edge_defaults = dict()
# Stack of edge attribs
self._edge_styles = list()
def _edge_style(self):
if (len(self._edge_styles) > 0):
return self._edge_styles[-1]
else:
return dict()
def _push_edge_style(self, attrib):
self._edge_styles.append(attrib)
def _pop_edge_style(self):
return self._edge_styles.pop()
@contextmanager
def edge_styles(self, attrib):
self._push_edge_style(attrib)
yield
self._pop_edge_style()
def link(self, source, target, attrib=None):
_attrib = dict(self._edge_style())
if attrib is not None:
_attrib.update(attrib)
e = Edge(source, target, _attrib)
self.edges.append(e)
return e
def edges_to_dot(self, out):
if len(self.edge_defaults) > 0:
out.outln("edge " + format_attribs(self.edge_defaults) + ";")
if len(self.edges) == 0:
return
for edge in self.edges:
edge.to_dot(out)
class Graph(NodeContainer, EdgeContainer, ClusterContainer):
"""
Contains the nodes, edges, and subgraph definitions for a graph to be
turned into a Graphviz DOT file.
"""
def __init__(self, label=None, attrib=None):
NodeContainer.__init__(self)
EdgeContainer.__init__(self)
ClusterContainer.__init__(self)
self.label = label if label is not None else "Default Label"
self.attrib = attrib if attrib is not None else dict()
def dot(self, fd=sys.stdout):
try:
self.o = Output(fd)
self._dot()
finally:
self.o.close()
def _dot(self):
self.o.outln("digraph G {")
with self.o.indented():
self.o.outln('label = "%s"' % self.label)
for k in self.attrib:
self.o.outln('%s = "%s"' % (k, self.attrib[k]))
self.nodes_to_dot(self.o)
for cluster in self.clusters:
self.o.outln()
cluster.to_dot(self.o)
self.o.outln()
self.edges_to_dot(self.o)
self.o.outln("}")
def main():
cmd = ["brew", "deps"]
if sys.argv[1:]:
if '--all' in sys.argv[1:]:
show = 'all'
cmd.extend(['--all'])
else:
show = 'one'
hideOrphaned = False
cmd.extend(sys.argv[1:])
else:
show = 'installed'
cmd.extend(['--installed'])
code, output = run(cmd)
output = output.strip()
depgraph = list()
for f in output.split("\n"):
stuff = f.split(":",2)
if len(stuff) < 2:
continue
name = stuff[0]
deps = stuff[1].strip()
if not deps:
deps = list()
else:
deps = deps.split(" ")
depgraph.append((name, deps))
hb = Graph("Homebrew Dependencies", attrib={'labelloc':'t', 'rankdir':'LR', 'ranksep':'5'})
# Independent formulas (those that are not dependended on by any other formula) get placed in
# their own subgraph so we can align them together on the left.
if show == 'installed':
sub = hb.cluster("independent", "Safe to Remove", attrib={'rank': 'min', 'style': 'filled', 'fillcolor': '#F0F0F0', 'color': 'invis'})
else:
sub = hb
seen = set()
def addNode(graph, name):
if name not in seen:
graph.node(name, name, attrib={'shape': 'box'})
seen.add(name)
return True
return False
independent = set()
for f in depgraph:
# Filter out orphan formulas when showing all, to cut down on noise
if show == 'all' and len(f[1]) == 0:
continue
independent.add(f[0])
for d in f[1]:
independent.discard(d)
hb.link(f[0], d)
# Children we can add right away because we don't care where they go
addNode(hb, d)
# For all installed formulas, place them in the 'indep' subgraph iff they
# are not depended on by other formulas, i.e. are root nodes.
for d in independent:
addNode(sub, d)
hb.dot()
if __name__ == "__main__":
main()