| #!/usr/bin/env python3 |
| # Copyright 2020 The ChromiumOS Authors |
| # 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() |