contrib: add graphviz_overlays.py script Just uploading this in case it helps other folks in the future. It's not particularly high-quality code, but hey, this is contrib, right? BUG=none TEST=example invocation from docstring Change-Id: Ic961c978c379e0b3e0f891458597ebff5e05457c Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2445799 Commit-Queue: Jack Rosenthal <jrosenth@chromium.org> Tested-by: Jack Rosenthal <jrosenth@chromium.org> Reviewed-by: Chris McDonald <cjmcdonald@chromium.org>
diff --git a/contrib/graphviz_overlays.py b/contrib/graphviz_overlays.py new file mode 100755 index 0000000..44e4995 --- /dev/null +++ b/contrib/graphviz_overlays.py
@@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# Copyright 2020 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tool to visualize the inheritance of portage overlays using graphviz. + +Usage example (write graph to output_file.dot, and trim the visualization to +just show what samus-private:base and auron_yuna:base requires): + graphviz_overlays.py -o output_file.dot -r samus-private:base auron_yuna:base + +This is contrib-quality code. Don't make something that really depends on it ;) +""" + +import argparse +import collections +import os +import pathlib +import sys + + +def dot_repr_str(str_to_repr): + """Represent a string compatible with dot syntax. + + Args: + str_to_repr: The string to represent. + + Returns: + The string, in dot lanugage compatible syntax. + """ + out = repr(str_to_repr) + if out.startswith("'"): + out = '"{}"'.format(out[1:-1].replace("'", "\\'")) + return out + + +class Digraph: + """Class representing a directed graph structure.""" + def __init__(self, stylesheet=None): + self.nodes = {} + self.edges = [] + self.subgraphs = {} + self.subgraph_items = collections.defaultdict(set) + self.stylesheet = stylesheet + + def cut_to_roots(self, roots): + """Reduce a graph to only the specified nodes and their children. + + Args: + roots: A list of the nodes desired. + + Returns: + A new Digraph. + """ + g = Digraph(stylesheet=self.stylesheet) + id_to_name = {v: k for k, v in self.nodes.items()} + + def add_node(node): + if node in g.nodes: + return + g.add_node(node) + for from_e, to_e in self.edges: + if from_e == self.nodes[node]: + to_node = id_to_name[to_e] + add_node(to_node) + g.add_edge(node, to_node) + + for node in roots: + add_node(node) + + for subgraph_name, subgraph_id in self.subgraphs.items(): + for node in self.subgraph_items[subgraph_id]: + if id_to_name[node] in g.nodes: + g.subgraph_set(subgraph_name, id_to_name[node]) + + return g + + + def add_node(self, name, subgraph=None): + """Add a node to the graph, or do nothing if it already exists. + + Args: + name: The node label. + subgraph: Optionally, the subgraph to appear in. + """ + if name in self.nodes: + # Node already added + return + nid = 'N{}'.format(len(self.nodes) + 1) + self.nodes[name] = nid + if subgraph: + self.subgraph_set(subgraph, name) + + def subgraph_set(self, name, node_name): + """Set the subgraph of a node. + + Args: + name: The subgraph. + node_name: The node. + """ + if name not in self.subgraphs: + cid = 'cluster_{}'.format(len(self.subgraphs) + 1) + self.subgraphs[name] = cid + else: + cid = self.subgraphs[name] + self.subgraph_items[cid].add(self.nodes[node_name]) + + def add_edge(self, from_node, to_node): + """Add an edge to the graph. + + Args: + from_node: The starting node. + to_node: The ending node. + """ + self.edges.append((self.nodes[from_node], self.nodes[to_node])) + + def to_dot(self, output_file=sys.stdout): + """Generate a dot-format representation of the graph. + + Args: + output_file: The file to write to. + """ + output_file.write('digraph {\n') + if self.stylesheet: + output_file.write( + 'graph [stylesheet={}]\n'.format(dot_repr_str(self.stylesheet))) + output_file.write('node [shape=box, style=rounded]\n') + for node_label, node_id in self.nodes.items(): + output_file.write('{} [label={}]\n'.format( + node_id, dot_repr_str(node_label))) + for subgraph_label, subgraph_id in self.subgraphs.items(): + output_file.write('subgraph {}'.format(subgraph_id)) + output_file.write(' {\n') + output_file.write('label = {}\n'.format( + dot_repr_str(subgraph_label))) + output_file.write('{}\n'.format( + '; '.join(self.subgraph_items[subgraph_id]))) + output_file.write('}\n') + for from_nid, to_nid in self.edges: + output_file.write('{} -> {}\n'.format(from_nid, to_nid)) + output_file.write('}\n') + + +def add_profiles(graph, repo_name, path, basedir=None): + """Add profiles from a portage overlay to the graph. + + Args: + graph: The graph to add to. + repo_name: The Portage "repo-name". + path: The path to the "profiles" directory in the overlay. + basedir: Used for recursive invocation by this function. + + Yields: + Each of the profiles added to this graph in this overlay only. + """ + + if not basedir: + basedir = path + for ent in path.iterdir(): + if ent.is_dir(): + yield from add_profiles(graph, repo_name, ent, basedir=basedir) + elif ent.name == 'parent': + pname = '{}:{}'.format(repo_name, path.relative_to(basedir)) + graph.add_node(pname) + yield pname + with open(ent, 'r') as f: + for line in f: + line, _, _ = line.partition('#') + line = line.strip() + if not line: + continue + if ':' in line: + cname = line + else: + cname = '{}:{}'.format( + repo_name, + (path / line).resolve().relative_to(basedir)) + graph.add_node(cname) + graph.add_edge(pname, cname) + if cname.startswith('{}:'.format(repo_name)): + yield cname + elif ent.name in ('package.use', 'make.defaults'): + pname = '{}:{}'.format(repo_name, path.relative_to(basedir)) + graph.add_node(pname) + yield pname + + +def add_overlay(path, graph): + """Add an overlay to the graph. + + Args: + path: The path to the overlay. + graph: The graph to add to. + """ + with open(path / 'metadata' / 'layout.conf') as f: + for line in f: + k, part, v = line.partition('=') + if not part: + continue + if k.strip() == 'repo-name': + repo_name = v.strip() + break + else: + repo_name = path.name + subgraph = repo_name + if path.parent.name == 'private-overlays': + subgraph = 'Private Overlays' + elif path.parent.name == 'overlays': + subgraph = 'Public Overlays' + for profile in add_profiles(graph, repo_name, path / 'profiles'): + graph.subgraph_set(subgraph, profile) + + +def find_overlays(path, max_depth=10, skip_dirs=()): + """Generator to find all portage overlays in a directory. + + Args: + path: Path to begin search. + max_depth: Maximum recursion depth. + skip_dirs: Optional set of paths to skip. + """ + if path.name == '.git': + return + if max_depth == 0: + return + for d in path.iterdir(): + if d in skip_dirs: + continue + if d.is_dir(): + if (d / 'metadata' / 'layout.conf').is_file(): + yield d + else: + yield from find_overlays(d, max_depth=max_depth - 1) + + +def get_default_src_dir(): + """Find the path to ~/trunk/src.""" + home = pathlib.Path(os.getenv('HOME')) + for path in (home / 'trunk' / 'src', + home / 'chromiumos' / 'src', + pathlib.Path('mnt') / 'host' / 'source' / 'src'): + if path.is_dir(): + return path + raise OSError( + 'Cannot find path to ~/trunk/src. ' + 'You may need to manually specify --src-dir.') + + +def main(): + """The main function.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--src-dir', type=pathlib.Path) + parser.add_argument('-o', '--output', + type=argparse.FileType('w'), default=sys.stdout) + parser.add_argument('-r', '--roots', nargs='*') + parser.add_argument( + '--stylesheet', + # pylint: disable=line-too-long + default='https://g3doc.corp.google.com/frameworks/g3doc/includes/graphviz-style.css', + # pylint: enable=line-too-long + ) + args = parser.parse_args() + + src_dir = args.src_dir + if not src_dir: + src_dir = get_default_src_dir() + src_dir = src_dir.resolve() + + g = Digraph(stylesheet=args.stylesheet) + for d in find_overlays(src_dir, skip_dirs=(src_dir / 'platform', + src_dir / 'platform2')): + if not (d / 'profiles').is_dir(): + print('WARNING: skipping {} due to missing profiles dir'.format(d), + file=sys.stderr) + continue + add_overlay(d, g) + + if args.roots: + g = g.cut_to_roots(args.roots) + g.to_dot(args.output) + + +if __name__ == '__main__': + main()