mirror of
https://github.com/thead-yocto-mirror/skia
synced 2026-06-21 08:52:36 +02:00
407 lines
16 KiB
Python
Executable File
407 lines
16 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright 2013 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Dumps a graph of allowed and disallowed inter-module dependencies described
|
|
by the DEPS files in the source tree. Supports DOT and PNG as the output format.
|
|
|
|
Enables filtering and differential highlighting of parts of the graph based on
|
|
the specified criteria. This allows for a much easier visual analysis of the
|
|
dependencies, including answering questions such as "if a new source must
|
|
depend on modules A, B, and C, what valid options among the existing modules
|
|
are there to put it in."
|
|
|
|
See README.md for a detailed description of the DEPS format.
|
|
"""
|
|
|
|
import os
|
|
import optparse
|
|
import pipes
|
|
import re
|
|
import sys
|
|
|
|
from builddeps import DepsBuilder
|
|
from rules import Rule
|
|
|
|
|
|
class DepsGrapher(DepsBuilder):
|
|
"""Parses include_rules from DEPS files and outputs a DOT graph of the
|
|
allowed and disallowed dependencies between directories and specific file
|
|
regexps. Can generate only a subgraph of the whole dependency graph
|
|
corresponding to the provided inclusion and exclusion regexp filters.
|
|
Also can highlight fanins and/or fanouts of certain nodes matching the
|
|
provided regexp patterns.
|
|
"""
|
|
|
|
def __init__(self,
|
|
base_directory,
|
|
extra_repos,
|
|
verbose,
|
|
being_tested,
|
|
ignore_temp_rules,
|
|
ignore_specific_rules,
|
|
hide_disallowed_deps,
|
|
out_file,
|
|
out_format,
|
|
layout_engine,
|
|
unflatten_graph,
|
|
incl,
|
|
excl,
|
|
hilite_fanins,
|
|
hilite_fanouts):
|
|
"""Creates a new DepsGrapher.
|
|
|
|
Args:
|
|
base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
|
|
verbose: Set to true for debug output.
|
|
being_tested: Set to true to ignore the DEPS file at tools/graphdeps/DEPS.
|
|
ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
|
|
ignore_specific_rules: Ignore rules from specific_include_rules sections.
|
|
hide_disallowed_deps: Hide disallowed dependencies from the output graph.
|
|
out_file: Output file name.
|
|
out_format: Output format (anything GraphViz dot's -T option supports).
|
|
layout_engine: Layout engine for formats other than 'dot'
|
|
(anything that GraphViz dot's -K option supports).
|
|
unflatten_graph: Try to reformat the output graph so it is narrower and
|
|
taller. Helps fight overly flat and wide graphs, but
|
|
sometimes produces a worse result.
|
|
incl: Include only nodes matching this regexp; such nodes' fanin/fanout
|
|
is also included.
|
|
excl: Exclude nodes matching this regexp; such nodes' fanin/fanout is
|
|
processed independently.
|
|
hilite_fanins: Highlight fanins of nodes matching this regexp with a
|
|
different edge and node color.
|
|
hilite_fanouts: Highlight fanouts of nodes matching this regexp with a
|
|
different edge and node color.
|
|
"""
|
|
DepsBuilder.__init__(
|
|
self,
|
|
base_directory,
|
|
extra_repos,
|
|
verbose,
|
|
being_tested,
|
|
ignore_temp_rules,
|
|
ignore_specific_rules)
|
|
|
|
self.ignore_temp_rules = ignore_temp_rules
|
|
self.ignore_specific_rules = ignore_specific_rules
|
|
self.hide_disallowed_deps = hide_disallowed_deps
|
|
self.out_file = out_file
|
|
self.out_format = out_format
|
|
self.layout_engine = layout_engine
|
|
self.unflatten_graph = unflatten_graph
|
|
self.incl = incl
|
|
self.excl = excl
|
|
self.hilite_fanins = hilite_fanins
|
|
self.hilite_fanouts = hilite_fanouts
|
|
|
|
self.deps = set()
|
|
|
|
def DumpDependencies(self):
|
|
""" Builds a dependency rule table and dumps the corresponding dependency
|
|
graph to all requested formats."""
|
|
self._BuildDepsGraph()
|
|
self._DumpDependencies()
|
|
|
|
def _BuildDepsGraph(self):
|
|
"""Recursively traverses the source tree starting at the specified directory
|
|
and builds a dependency graph representation in self.deps."""
|
|
for (rules, _) in self.GetAllRulesAndFiles():
|
|
deps = rules.AsDependencyTuples(
|
|
include_general_rules=True,
|
|
include_specific_rules=not self.ignore_specific_rules)
|
|
self.deps.update(deps)
|
|
|
|
def _DumpDependencies(self):
|
|
"""Dumps the built dependency graph to the specified file with specified
|
|
format."""
|
|
if self.out_format == 'dot' and not self.layout_engine:
|
|
if self.unflatten_graph:
|
|
pipe = pipes.Template()
|
|
pipe.append('unflatten -l 2 -c 3', '--')
|
|
out = pipe.open(self.out_file, 'w')
|
|
else:
|
|
out = open(self.out_file, 'w')
|
|
else:
|
|
pipe = pipes.Template()
|
|
if self.unflatten_graph:
|
|
pipe.append('unflatten -l 2 -c 3', '--')
|
|
dot_cmd = 'dot -T' + self.out_format
|
|
if self.layout_engine:
|
|
dot_cmd += ' -K' + self.layout_engine
|
|
pipe.append(dot_cmd, '--')
|
|
out = pipe.open(self.out_file, 'w')
|
|
|
|
self._DumpDependenciesImpl(self.deps, out)
|
|
out.close()
|
|
|
|
def _DumpDependenciesImpl(self, deps, out):
|
|
"""Computes nodes' and edges' properties for the dependency graph |deps| and
|
|
carries out the actual dumping to a file/pipe |out|."""
|
|
deps_graph = dict()
|
|
deps_srcs = set()
|
|
|
|
# Pre-initialize the graph with src->(dst, allow) pairs.
|
|
for (allow, src, dst) in deps:
|
|
if allow == Rule.TEMP_ALLOW and self.ignore_temp_rules:
|
|
continue
|
|
|
|
deps_srcs.add(src)
|
|
if src not in deps_graph:
|
|
deps_graph[src] = []
|
|
deps_graph[src].append((dst, allow))
|
|
|
|
# Add all hierarchical parents too, in case some of them don't have their
|
|
# own DEPS, and therefore are missing from the list of rules. Those will
|
|
# be recursively populated with their parents' rules in the next block.
|
|
parent_src = os.path.dirname(src)
|
|
while parent_src:
|
|
if parent_src not in deps_graph:
|
|
deps_graph[parent_src] = []
|
|
parent_src = os.path.dirname(parent_src)
|
|
|
|
# For every node, propagate its rules down to all its children.
|
|
deps_srcs = list(deps_srcs)
|
|
deps_srcs.sort()
|
|
for src in deps_srcs:
|
|
parent_src = os.path.dirname(src)
|
|
if parent_src:
|
|
# We presort the list, so parents are guaranteed to precede children.
|
|
assert parent_src in deps_graph,\
|
|
"src: %s, parent_src: %s" % (src, parent_src)
|
|
for (dst, allow) in deps_graph[parent_src]:
|
|
# Check that this node does not explicitly override a rule from the
|
|
# parent that we're about to add.
|
|
if ((dst, Rule.ALLOW) not in deps_graph[src]) and \
|
|
((dst, Rule.TEMP_ALLOW) not in deps_graph[src]) and \
|
|
((dst, Rule.DISALLOW) not in deps_graph[src]):
|
|
deps_graph[src].append((dst, allow))
|
|
|
|
node_props = {}
|
|
edges = []
|
|
|
|
# 1) Populate a list of edge specifications in DOT format;
|
|
# 2) Populate a list of computed raw node attributes to be output as node
|
|
# specifications in DOT format later on.
|
|
# Edges and nodes are emphasized with color and line/border weight depending
|
|
# on how many of incl/excl/hilite_fanins/hilite_fanouts filters they hit,
|
|
# and in what way.
|
|
for src in deps_graph.keys():
|
|
for (dst, allow) in deps_graph[src]:
|
|
if allow == Rule.DISALLOW and self.hide_disallowed_deps:
|
|
continue
|
|
|
|
if allow == Rule.ALLOW and src == dst:
|
|
continue
|
|
|
|
edge_spec = "%s->%s" % (src, dst)
|
|
if not re.search(self.incl, edge_spec) or \
|
|
re.search(self.excl, edge_spec):
|
|
continue
|
|
|
|
if src not in node_props:
|
|
node_props[src] = {'hilite': None, 'degree': 0}
|
|
if dst not in node_props:
|
|
node_props[dst] = {'hilite': None, 'degree': 0}
|
|
|
|
edge_weight = 1
|
|
|
|
if self.hilite_fanouts and re.search(self.hilite_fanouts, src):
|
|
node_props[src]['hilite'] = 'lightgreen'
|
|
node_props[dst]['hilite'] = 'lightblue'
|
|
node_props[dst]['degree'] += 1
|
|
edge_weight += 1
|
|
|
|
if self.hilite_fanins and re.search(self.hilite_fanins, dst):
|
|
node_props[src]['hilite'] = 'lightblue'
|
|
node_props[dst]['hilite'] = 'lightgreen'
|
|
node_props[src]['degree'] += 1
|
|
edge_weight += 1
|
|
|
|
if allow == Rule.ALLOW:
|
|
edge_color = (edge_weight > 1) and 'blue' or 'green'
|
|
edge_style = 'solid'
|
|
elif allow == Rule.TEMP_ALLOW:
|
|
edge_color = (edge_weight > 1) and 'blue' or 'green'
|
|
edge_style = 'dashed'
|
|
else:
|
|
edge_color = 'red'
|
|
edge_style = 'dashed'
|
|
edges.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \
|
|
(src, dst, edge_style, edge_color, edge_weight))
|
|
|
|
# Reformat the computed raw node attributes into a final DOT representation.
|
|
nodes = []
|
|
for (node, attrs) in node_props.iteritems():
|
|
attr_strs = []
|
|
if attrs['hilite']:
|
|
attr_strs.append('style=filled,fillcolor=%s' % attrs['hilite'])
|
|
attr_strs.append('penwidth=%d' % (attrs['degree'] or 1))
|
|
nodes.append(' "%s" [%s];' % (node, ','.join(attr_strs)))
|
|
|
|
# Output nodes and edges to |out| (can be a file or a pipe).
|
|
edges.sort()
|
|
nodes.sort()
|
|
out.write('digraph DEPS {\n'
|
|
' fontsize=8;\n')
|
|
out.write('\n'.join(nodes))
|
|
out.write('\n\n')
|
|
out.write('\n'.join(edges))
|
|
out.write('\n}\n')
|
|
out.close()
|
|
|
|
|
|
def PrintUsage():
|
|
print """Usage: python graphdeps.py [--root <root>]
|
|
|
|
--root ROOT Specifies the repository root. This defaults to "../../.."
|
|
relative to the script file. This will be correct given the
|
|
normal location of the script in "<root>/tools/graphdeps".
|
|
|
|
--(others) There are a few lesser-used options; run with --help to show them.
|
|
|
|
Examples:
|
|
Dump the whole dependency graph:
|
|
graphdeps.py
|
|
Find a suitable place for a new source that must depend on /apps and
|
|
/content/browser/renderer_host. Limit potential candidates to /apps,
|
|
/chrome/browser and content/browser, and descendants of those three.
|
|
Generate both DOT and PNG output. The output will highlight the fanins
|
|
of /apps and /content/browser/renderer_host. Overlapping nodes in both fanins
|
|
will be emphasized by a thicker border. Those nodes are the ones that are
|
|
allowed to depend on both targets, therefore they are all legal candidates
|
|
to place the new source in:
|
|
graphdeps.py \
|
|
--root=./src \
|
|
--out=./DEPS.svg \
|
|
--format=svg \
|
|
--incl='^(apps|chrome/browser|content/browser)->.*' \
|
|
--excl='.*->third_party' \
|
|
--fanin='^(apps|content/browser/renderer_host)$' \
|
|
--ignore-specific-rules \
|
|
--ignore-temp-rules"""
|
|
|
|
|
|
def main():
|
|
option_parser = optparse.OptionParser()
|
|
option_parser.add_option(
|
|
"", "--root",
|
|
default="", dest="base_directory",
|
|
help="Specifies the repository root. This defaults "
|
|
"to '../../..' relative to the script file, which "
|
|
"will normally be the repository root.")
|
|
option_parser.add_option(
|
|
'', '--extra-repos',
|
|
action='append', dest='extra_repos', default=[],
|
|
help='Specifies extra repositories relative to root repository.')
|
|
option_parser.add_option(
|
|
"-f", "--format",
|
|
dest="out_format", default="dot",
|
|
help="Output file format. "
|
|
"Can be anything that GraphViz dot's -T option supports. "
|
|
"The most useful ones are: dot (text), svg (image), pdf (image)."
|
|
"NOTES: dotty has a known problem with fonts when displaying DOT "
|
|
"files on Ubuntu - if labels are unreadable, try other formats.")
|
|
option_parser.add_option(
|
|
"-o", "--out",
|
|
dest="out_file", default="DEPS",
|
|
help="Output file name. If the name does not end in an extension "
|
|
"matching the output format, that extension is automatically "
|
|
"appended.")
|
|
option_parser.add_option(
|
|
"-l", "--layout-engine",
|
|
dest="layout_engine", default="",
|
|
help="Layout rendering engine. "
|
|
"Can be anything that GraphViz dot's -K option supports. "
|
|
"The most useful are in decreasing order: dot, fdp, circo, osage. "
|
|
"NOTE: '-f dot' and '-f dot -l dot' are different: the former "
|
|
"will dump a raw DOT graph and stop; the latter will further "
|
|
"filter it through 'dot -Tdot -Kdot' layout engine.")
|
|
option_parser.add_option(
|
|
"-i", "--incl",
|
|
default="^.*$", dest="incl",
|
|
help="Include only edges of the graph that match the specified regexp. "
|
|
"The regexp is applied to edges of the graph formatted as "
|
|
"'source_node->target_node', where the '->' part is vebatim. "
|
|
"Therefore, a reliable regexp should look like "
|
|
"'^(chrome|chrome/browser|chrome/common)->content/public/browser$' "
|
|
"or similar, with both source and target node regexps present, "
|
|
"explicit ^ and $, and otherwise being as specific as possible.")
|
|
option_parser.add_option(
|
|
"-e", "--excl",
|
|
default="^$", dest="excl",
|
|
help="Exclude dependent nodes that match the specified regexp. "
|
|
"See --incl for details on the format.")
|
|
option_parser.add_option(
|
|
"", "--fanin",
|
|
default="", dest="hilite_fanins",
|
|
help="Highlight fanins of nodes matching the specified regexp.")
|
|
option_parser.add_option(
|
|
"", "--fanout",
|
|
default="", dest="hilite_fanouts",
|
|
help="Highlight fanouts of nodes matching the specified regexp.")
|
|
option_parser.add_option(
|
|
"", "--ignore-temp-rules",
|
|
action="store_true", dest="ignore_temp_rules", default=False,
|
|
help="Ignore !-prefixed (temporary) rules in DEPS files.")
|
|
option_parser.add_option(
|
|
"", "--ignore-specific-rules",
|
|
action="store_true", dest="ignore_specific_rules", default=False,
|
|
help="Ignore specific_include_rules section of DEPS files.")
|
|
option_parser.add_option(
|
|
"", "--hide-disallowed-deps",
|
|
action="store_true", dest="hide_disallowed_deps", default=False,
|
|
help="Hide disallowed dependencies in the output graph.")
|
|
option_parser.add_option(
|
|
"", "--unflatten",
|
|
action="store_true", dest="unflatten_graph", default=False,
|
|
help="Try to reformat the output graph so it is narrower and taller. "
|
|
"Helps fight overly flat and wide graphs, but sometimes produces "
|
|
"inferior results.")
|
|
option_parser.add_option(
|
|
"-v", "--verbose",
|
|
action="store_true", default=False,
|
|
help="Print debug logging")
|
|
options, args = option_parser.parse_args()
|
|
|
|
if not options.out_file.endswith(options.out_format):
|
|
options.out_file += '.' + options.out_format
|
|
|
|
deps_grapher = DepsGrapher(
|
|
base_directory=options.base_directory,
|
|
extra_repos=options.extra_repos,
|
|
verbose=options.verbose,
|
|
being_tested=False,
|
|
|
|
ignore_temp_rules=options.ignore_temp_rules,
|
|
ignore_specific_rules=options.ignore_specific_rules,
|
|
hide_disallowed_deps=options.hide_disallowed_deps,
|
|
|
|
out_file=options.out_file,
|
|
out_format=options.out_format,
|
|
layout_engine=options.layout_engine,
|
|
unflatten_graph=options.unflatten_graph,
|
|
|
|
incl=options.incl,
|
|
excl=options.excl,
|
|
hilite_fanins=options.hilite_fanins,
|
|
hilite_fanouts=options.hilite_fanouts)
|
|
|
|
if len(args) > 0:
|
|
PrintUsage()
|
|
return 1
|
|
|
|
print 'Using base directory: ', deps_grapher.base_directory
|
|
print 'include nodes : ', options.incl
|
|
print 'exclude nodes : ', options.excl
|
|
print 'highlight fanins of : ', options.hilite_fanins
|
|
print 'highlight fanouts of: ', options.hilite_fanouts
|
|
|
|
deps_grapher.DumpDependencies()
|
|
return 0
|
|
|
|
|
|
if '__main__' == __name__:
|
|
sys.exit(main())
|