blob: 44e4995769f1d7ede986a923fd10078396975839 [file] [log] [blame]
#!/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()