
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>
344 lines
8.8 KiB
Python
Executable File
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()
|