blob: 44e4995769f1d7ede986a923fd10078396975839 [file] [log] [blame]
Jack Rosenthal1b7b3982020-10-02 09:15:01 -06001#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Tool to visualize the inheritance of portage overlays using graphviz.
7
8Usage example (write graph to output_file.dot, and trim the visualization to
9just show what samus-private:base and auron_yuna:base requires):
10 graphviz_overlays.py -o output_file.dot -r samus-private:base auron_yuna:base
11
12This is contrib-quality code. Don't make something that really depends on it ;)
13"""
14
15import argparse
16import collections
17import os
18import pathlib
19import sys
20
21
22def dot_repr_str(str_to_repr):
23 """Represent a string compatible with dot syntax.
24
25 Args:
26 str_to_repr: The string to represent.
27
28 Returns:
29 The string, in dot lanugage compatible syntax.
30 """
31 out = repr(str_to_repr)
32 if out.startswith("'"):
33 out = '"{}"'.format(out[1:-1].replace("'", "\\'"))
34 return out
35
36
37class Digraph:
38 """Class representing a directed graph structure."""
39 def __init__(self, stylesheet=None):
40 self.nodes = {}
41 self.edges = []
42 self.subgraphs = {}
43 self.subgraph_items = collections.defaultdict(set)
44 self.stylesheet = stylesheet
45
46 def cut_to_roots(self, roots):
47 """Reduce a graph to only the specified nodes and their children.
48
49 Args:
50 roots: A list of the nodes desired.
51
52 Returns:
53 A new Digraph.
54 """
55 g = Digraph(stylesheet=self.stylesheet)
56 id_to_name = {v: k for k, v in self.nodes.items()}
57
58 def add_node(node):
59 if node in g.nodes:
60 return
61 g.add_node(node)
62 for from_e, to_e in self.edges:
63 if from_e == self.nodes[node]:
64 to_node = id_to_name[to_e]
65 add_node(to_node)
66 g.add_edge(node, to_node)
67
68 for node in roots:
69 add_node(node)
70
71 for subgraph_name, subgraph_id in self.subgraphs.items():
72 for node in self.subgraph_items[subgraph_id]:
73 if id_to_name[node] in g.nodes:
74 g.subgraph_set(subgraph_name, id_to_name[node])
75
76 return g
77
78
79 def add_node(self, name, subgraph=None):
80 """Add a node to the graph, or do nothing if it already exists.
81
82 Args:
83 name: The node label.
84 subgraph: Optionally, the subgraph to appear in.
85 """
86 if name in self.nodes:
87 # Node already added
88 return
89 nid = 'N{}'.format(len(self.nodes) + 1)
90 self.nodes[name] = nid
91 if subgraph:
92 self.subgraph_set(subgraph, name)
93
94 def subgraph_set(self, name, node_name):
95 """Set the subgraph of a node.
96
97 Args:
98 name: The subgraph.
99 node_name: The node.
100 """
101 if name not in self.subgraphs:
102 cid = 'cluster_{}'.format(len(self.subgraphs) + 1)
103 self.subgraphs[name] = cid
104 else:
105 cid = self.subgraphs[name]
106 self.subgraph_items[cid].add(self.nodes[node_name])
107
108 def add_edge(self, from_node, to_node):
109 """Add an edge to the graph.
110
111 Args:
112 from_node: The starting node.
113 to_node: The ending node.
114 """
115 self.edges.append((self.nodes[from_node], self.nodes[to_node]))
116
117 def to_dot(self, output_file=sys.stdout):
118 """Generate a dot-format representation of the graph.
119
120 Args:
121 output_file: The file to write to.
122 """
123 output_file.write('digraph {\n')
124 if self.stylesheet:
125 output_file.write(
126 'graph [stylesheet={}]\n'.format(dot_repr_str(self.stylesheet)))
127 output_file.write('node [shape=box, style=rounded]\n')
128 for node_label, node_id in self.nodes.items():
129 output_file.write('{} [label={}]\n'.format(
130 node_id, dot_repr_str(node_label)))
131 for subgraph_label, subgraph_id in self.subgraphs.items():
132 output_file.write('subgraph {}'.format(subgraph_id))
133 output_file.write(' {\n')
134 output_file.write('label = {}\n'.format(
135 dot_repr_str(subgraph_label)))
136 output_file.write('{}\n'.format(
137 '; '.join(self.subgraph_items[subgraph_id])))
138 output_file.write('}\n')
139 for from_nid, to_nid in self.edges:
140 output_file.write('{} -> {}\n'.format(from_nid, to_nid))
141 output_file.write('}\n')
142
143
144def add_profiles(graph, repo_name, path, basedir=None):
145 """Add profiles from a portage overlay to the graph.
146
147 Args:
148 graph: The graph to add to.
149 repo_name: The Portage "repo-name".
150 path: The path to the "profiles" directory in the overlay.
151 basedir: Used for recursive invocation by this function.
152
153 Yields:
154 Each of the profiles added to this graph in this overlay only.
155 """
156
157 if not basedir:
158 basedir = path
159 for ent in path.iterdir():
160 if ent.is_dir():
161 yield from add_profiles(graph, repo_name, ent, basedir=basedir)
162 elif ent.name == 'parent':
163 pname = '{}:{}'.format(repo_name, path.relative_to(basedir))
164 graph.add_node(pname)
165 yield pname
166 with open(ent, 'r') as f:
167 for line in f:
168 line, _, _ = line.partition('#')
169 line = line.strip()
170 if not line:
171 continue
172 if ':' in line:
173 cname = line
174 else:
175 cname = '{}:{}'.format(
176 repo_name,
177 (path / line).resolve().relative_to(basedir))
178 graph.add_node(cname)
179 graph.add_edge(pname, cname)
180 if cname.startswith('{}:'.format(repo_name)):
181 yield cname
182 elif ent.name in ('package.use', 'make.defaults'):
183 pname = '{}:{}'.format(repo_name, path.relative_to(basedir))
184 graph.add_node(pname)
185 yield pname
186
187
188def add_overlay(path, graph):
189 """Add an overlay to the graph.
190
191 Args:
192 path: The path to the overlay.
193 graph: The graph to add to.
194 """
195 with open(path / 'metadata' / 'layout.conf') as f:
196 for line in f:
197 k, part, v = line.partition('=')
198 if not part:
199 continue
200 if k.strip() == 'repo-name':
201 repo_name = v.strip()
202 break
203 else:
204 repo_name = path.name
205 subgraph = repo_name
206 if path.parent.name == 'private-overlays':
207 subgraph = 'Private Overlays'
208 elif path.parent.name == 'overlays':
209 subgraph = 'Public Overlays'
210 for profile in add_profiles(graph, repo_name, path / 'profiles'):
211 graph.subgraph_set(subgraph, profile)
212
213
214def find_overlays(path, max_depth=10, skip_dirs=()):
215 """Generator to find all portage overlays in a directory.
216
217 Args:
218 path: Path to begin search.
219 max_depth: Maximum recursion depth.
220 skip_dirs: Optional set of paths to skip.
221 """
222 if path.name == '.git':
223 return
224 if max_depth == 0:
225 return
226 for d in path.iterdir():
227 if d in skip_dirs:
228 continue
229 if d.is_dir():
230 if (d / 'metadata' / 'layout.conf').is_file():
231 yield d
232 else:
233 yield from find_overlays(d, max_depth=max_depth - 1)
234
235
236def get_default_src_dir():
237 """Find the path to ~/trunk/src."""
238 home = pathlib.Path(os.getenv('HOME'))
239 for path in (home / 'trunk' / 'src',
240 home / 'chromiumos' / 'src',
241 pathlib.Path('mnt') / 'host' / 'source' / 'src'):
242 if path.is_dir():
243 return path
244 raise OSError(
245 'Cannot find path to ~/trunk/src. '
246 'You may need to manually specify --src-dir.')
247
248
249def main():
250 """The main function."""
251 parser = argparse.ArgumentParser(description=__doc__)
252 parser.add_argument('--src-dir', type=pathlib.Path)
253 parser.add_argument('-o', '--output',
254 type=argparse.FileType('w'), default=sys.stdout)
255 parser.add_argument('-r', '--roots', nargs='*')
256 parser.add_argument(
257 '--stylesheet',
258 # pylint: disable=line-too-long
259 default='https://g3doc.corp.google.com/frameworks/g3doc/includes/graphviz-style.css',
260 # pylint: enable=line-too-long
261 )
262 args = parser.parse_args()
263
264 src_dir = args.src_dir
265 if not src_dir:
266 src_dir = get_default_src_dir()
267 src_dir = src_dir.resolve()
268
269 g = Digraph(stylesheet=args.stylesheet)
270 for d in find_overlays(src_dir, skip_dirs=(src_dir / 'platform',
271 src_dir / 'platform2')):
272 if not (d / 'profiles').is_dir():
273 print('WARNING: skipping {} due to missing profiles dir'.format(d),
274 file=sys.stderr)
275 continue
276 add_overlay(d, g)
277
278 if args.roots:
279 g = g.cut_to_roots(args.roots)
280 g.to_dot(args.output)
281
282
283if __name__ == '__main__':
284 main()