ATLAS Offline Software
Loading...
Searching...
No Matches
cmake_depends.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
3#
4# Created: June 2020, Frank Winklmeier
5#
6"""
7Print target/package dependencies of ATLAS releases. For a given target/package
8name, the dependencies are printed as a plain list or DOT graph. The recursion
9depth is configurable.
10"""
11
12import sys
13import os
14import re
15from collections import deque
16import PyUtils.acmdlib as acmdlib
17import argparse
18import pygraphviz
19
20# Style for python dependencies:
21py_style = 'dashed'
22
23def read_package_list(package_file):
24 """Read packages.txt as a source for the full package path"""
25
26 with open(package_file) as f:
27 packages = [line.rstrip() for line in f if not line.startswith('#')]
28
29 return dict([(p.split('/')[-1],p) for p in packages])
30
31
33 """Return a short name for an external library"""
34 if '/LCG_' in lib:
35 dirs = lib.split('/')
36 lcg = next(d for d in dirs if d.startswith('LCG_'))
37 return '%s::%s' % (dirs[dirs.index(lcg)+1], dirs[-1])
38 elif lib.startswith('Gaudi'):
39 return 'Gaudi::%s' % lib
40 else:
41 return os.path.basename(lib)
42
43
44def lrstrip(s, prefix, postfix):
45 """Strip `prefix` and `postfix` from string `s`"""
46 if s.startswith(prefix): s = s[len(prefix):]
47 if s.endswith(postfix): s = s[:-len(postfix)]
48 return s
49
50
51def traverse(graph, root, reverse=False, maxdepth=None, nodegetter=lambda n:n):
52 """Depth-limited BFS edge traversal of graph starting at root.
53
54 @param graph graph
55 @param root start node for traversal
56 @param reverse traverse graph in reverse
57 @param maxdepth maximum traversal depth (1 = only direct neighbors)
58 @param nodegetter functor returning node names
59 @return edge tuple (parent,node)
60
61 Inspired by https://github.com/networkx/networkx/tree/master/networkx/algorithms/traversal
62 """
63 visited_nodes = set()
64 visited_edges = set()
65 queue = deque([(root,root,0)])
66 neighbors = graph.iterpred if reverse else graph.itersucc
67 while queue:
68 parent,node,level = queue.popleft()
69
70 if node not in visited_nodes:
71 visited_nodes.add(node)
72
73 # Add edges to neighbors into queue:
74 if maxdepth is None or level < maxdepth:
75 queue.extend((node,n,level+1) for n in neighbors(node))
76 # For the last level only edges to already visited nodes:
77 elif level==maxdepth:
78 queue.extend((node,n,level+1) for n in neighbors(node) if n in visited_nodes)
79
80 if (parent,node) not in visited_edges:
81 visited_edges.add((parent,node))
82 yield nodegetter(parent), nodegetter(node)
83
84
85def subgraph(graph, sources, reverse=False, maxdepth=None, nodegetter=lambda n : n.attr.get('label')):
86 """Extract subgraph created by traversing from one or more sources.
87 Parameters are the same as in `traverse`. Return list of edge tuples.
88 """
89 edges = set()
90 for root in sources:
91 for a,b in traverse(graph, root, reverse=reverse, maxdepth=maxdepth, nodegetter=nodegetter):
92 if a and b and a!=b:
93 if reverse: edges.add((b,a))
94 else: edges.add((a,b))
95
96 return edges
97
98
99def add_legend(graph):
100 """Add legend to graph"""
101 graph.add_subgraph(name='clusterLegend', label='Legend')
102 l = graph.subgraphs()[-1]
103 for n in 'abcd':
104 l.add_node(n, shape='point', style='invis')
105 l.add_edge('a','b', label='C++', constraint=False)
106 l.add_edge('c','d', label='Python', style=py_style, constraint=False)
107
108
109def copy_graph(source, dest):
110 """Copy graph nodes and edges from source to dest including attributes"""
111 for e in source.edges_iter():
112 dest.add_edge(e, **e.attr)
113 for n in source.nodes_iter():
114 dest.add_node(n, **n.attr)
115
116
118 """Class to hold dependency information for release"""
119
120 def __init__(self, dotfile):
121 """Read dotfile and and optionally transform package names to full paths"""
122
123 # Read dot file:
124 self.graph = pygraphviz.AGraph(dotfile)
125
126 # Build dictionary for node types:
127 legend = self.graph.get_subgraph('clusterLegend')
128 self.types = { n.attr['label'] : n.attr['shape'] for n in legend.nodes_iter() }
129
130 # Build dictionary for node names:
131 self.node = { n.attr['label'] : n.get_name() for n in self.graph.nodes_iter() }
132
133 def decorate_package(n0, n1=None):
134 """Assign package name to n0 -> n1 if n0 is a package target"""
135 p = n0.attr['label']
136 # Decorate target with package name:
137 if p.startswith('Package_'):
138 n0.attr['package'] = lrstrip(p, 'Package_', '_tests')
139 if n1 is not None:
140 n1.attr['package'] = n0.attr['package']
141
142 # Extract package dependencies:
143 for e in self.graph.edges_iter():
144 decorate_package(e[0], e[1])
145
146 # Another pass on nodes to cover packages without dependendencies:
147 for n in self.graph.nodes_iter():
148 decorate_package(n)
149
150 # Assign "package" names to externals if possible:
151 external_nodes = filter(lambda n : 'package' not in n.attr.keys(),
152 self.graph.nodes_iter())
153 for n in external_nodes:
154 name = externals_name(n.attr['label'])
155 n.attr['package'] = name.split('::')[0]
156 n.attr['label'] = name
157 n.attr['external'] = 'yes'
158
159 def get_node(self, label):
160 """Return graph node for label/target"""
161 return self.graph.get_node(self.node[label])
162
163 def ignore_target(self, node):
164 """Check if target should be ignored"""
165 label = node.attr['label']
166 return True if (label.startswith('__') or # internal targets
167 label.startswith('-') or # compiler flags (e.g. -pthread)
168 node.attr['shape']==self.types['Custom Target']) else False
169
170
171def create_dep_graph(target, deps, pydeps, args):
172 """Create and return dependency graph.
173
174 @param target name of target
175 @param deps AthGraph cmake dependencies
176 @param pydeps python dependencies
177 @param args command line arguments
178 """
179 # Helper for graph traversal below:
180 def getnode(node):
181 if not args.all and deps.ignore_target(node): return None
182 if args.externals or not node.attr['external']:
183 a = 'label' if args.target else 'package'
184 return node.attr[a]
185
186 target = target.split('/')[-1] # in case of full package path
187
188 # In package mode we have one extra level due to the Package_ target:
189 depth = args.recursive
190 if not args.target and not args.clients and args.recursive is not None:
191 depth += 1
192
193 # With regex, find all matching targets:
194 if args.regex:
195 r = re.compile(target)
196 targets = [getnode(n) for n in deps.graph.nodes_iter() if r.match(n.attr['label'])]
197 if args.py:
198 targets += [n for n in pydeps.nodes_iter() if r.match(n)]
199 targets = sorted(set(filter(lambda t : t is not None, targets)))
200 else:
201 targets = [target]
202
203 # Find the nodes from which graph traversal starts:
204 sources = []
205 for l in targets:
206 if not args.target:
207 l = 'Package_'+l
208 try:
209 if deps.get_node(l).attr['external'] and not args.externals:
210 raise RuntimeError(f"{l} is an external target. Run with -e/--externals.")
211
212 # To find clients of a package means finding clients of the targets
213 # within that package. First find all targets within the package:
214 if args.clients and not args.target:
215 sources.extend([b for a,b in traverse(deps.graph, deps.get_node(l), maxdepth=1)])
216 else:
217 sources.extend([deps.get_node(l)])
218 except KeyError:
219 raise RuntimeError(f"Target with name {l} does not exist.")
220
221 # Extract the dependency subgraph:
222 g = subgraph(deps.graph, sources, reverse=args.clients,
223 maxdepth=depth, nodegetter=getnode)
224
225 graph = pygraphviz.AGraph(name=target, directed=True, strict=False)
226 graph.add_edges_from(g)
227
228 # Add python dependencies:
229 if args.py:
230 # Here the nodes are the actual package names:
231 pysources = [pydeps.get_node(t) for t in targets if pydeps.has_node(t)]
232 g = subgraph(pydeps, pysources, reverse=args.clients,
233 maxdepth=args.recursive, nodegetter=lambda n : n.name)
234
235 graph.add_edges_from(g, style=py_style)
236
237 # Change style of nodes that have only Python dependencies:
238 for n in graph.nodes_iter():
239 if all(e.attr['style']==py_style for e in graph.edges_iter(n)):
240 n.attr['style'] = py_style
241
242 return graph
243
244
245def print_dep_graph(graph, args, package_paths={}):
246 """Output final graph"""
247
248 # txt output
249 if args.batch or not args.dot:
250 f = open(graph.name+'.txt', 'w') if args.batch else sys.stdout
251 nodes = [e[0] for e in graph.in_edges_iter()] if args.clients \
252 else [e[1] for e in graph.out_edges_iter()]
253
254 output = []
255 for p in set(nodes):
256 suffix = ':py' if p.attr['style']==py_style else ''
257 output.append('%s%s' % (package_paths.get(p,p), suffix))
258 print('\n'.join(sorted(output)), file=f)
259
260 # dot output
261 if args.batch or args.dot:
262 f = open(graph.name+'.dot', 'w') if args.batch else sys.stdout
263 if args.legend:
264 add_legend(graph)
265 print(graph, file=f)
266
267
268#
269# Main function and command line arguments
270#
271@acmdlib.command(name='cmake.depends',
272 description=__doc__)
273
274@acmdlib.argument('names', nargs='+', metavar='NAME',
275 help='package/target name or regular expression')
276
277@acmdlib.argument('-t', '--target', action='store_true',
278 help='treat NAME as target instead of package name')
279
280@acmdlib.argument('-c', '--clients', action='store_true',
281 help='show clients (instead of dependencies)')
282
283@acmdlib.argument('-e', '--externals', action='store_true',
284 help='include external dependencies')
285
286@acmdlib.argument('-l', '--long', action='store_true',
287 help='show full package names (only for txt output)')
288
289@acmdlib.argument('-r', '--recursive', nargs='?', metavar='DEPTH',
290 type=int, default=1, const=None,
291 help='recursively resolve dependencies up to DEPTH (default: unlimited)')
292
293@acmdlib.argument('--py', action='store_true',
294 help=f'add Python dependencies (marked with ":py" in printout, {py_style} in graph)')
295
296@acmdlib.argument('--regex', action='store_true',
297 help='treat NAME as regular expression')
298
299@acmdlib.argument('--all', action='store_true',
300 help='do not apply any target filter (e.g. custom targets)')
301
302@acmdlib.argument('-d', '--dot', action='store_true',
303 help='print DOT graph')
304
305@acmdlib.argument('--legend', action='store_true',
306 help='add legend to graph')
307
308@acmdlib.argument('--batch', nargs='?', metavar='N', type=int, const=1,
309 help='Batch mode using N jobs (default: 1). Create dot and txt dependencies '
310 'for all NAMEs and store them in separate files.')
311
312
313# Debugging/expert options:
314@acmdlib.argument('--cmakedot', help=argparse.SUPPRESS)
315@acmdlib.argument('--pydot', help=argparse.SUPPRESS)
316
317
318def run(args):
319 """Inspect cmake build dependencies"""
320
321 # Find packages.dot:
322 if not args.cmakedot:
323 try:
324 args.cmakedot = os.path.join(os.environ['AtlasArea'],'InstallArea',
325 os.environ['BINARY_TAG'],'packages.dot')
326 except KeyError:
327 main.parser.error("Cannot find 'packages.dot'. Setup a release or use --cmakedot.")
328
329 # Find packages.py.dot:
330 pydeps = None
331 if args.py:
332 if args.target:
333 main.parser.error("Python dependencies not possible in target mode.")
334
335 args.pydot = args.pydot or args.cmakedot.replace('.dot','.py.dot')
336 try:
337 pydeps = pygraphviz.AGraph(args.pydot)
338 except Exception:
339 main.parser.error(f"Cannot read '{args.pydot}'. Setup a release or use --pydot.")
340
341 # Read packages.txt if needed:
342 package_paths = {}
343 if args.long:
344 try:
345 package_paths = read_package_list(os.path.join(os.environ['AtlasArea'],'InstallArea',
346 os.environ['BINARY_TAG'],'packages.txt'))
347 except Exception:
348 main.parser.error("Cannot read 'packages.txt'. Setup a release or run without -l/--long.")
349
350 # Read dependencies:
351 deps = AthGraph(args.cmakedot)
352
353 # Create combined graph for all given targets:
354 if not args.batch:
355 subgraphs = [create_dep_graph(target, deps, pydeps, args) for target in args.names]
356 if len(subgraphs)>1:
357 graph = pygraphviz.AGraph(name='AthGraph', directed=True, strict=False)
358 for g in subgraphs:
359 graph.add_subgraph(name=g.get_name())
360 copy_graph(g, graph.subgraphs()[-1])
361 else:
362 graph = subgraphs[0]
363
364 print_dep_graph(graph, args, package_paths)
365
366 # Batch mode: create separte graph for each target:
367 else:
368 import multiprocessing
369 global doit # required for use in multiprocessing
370 def doit(target):
371 graph = create_dep_graph(target, deps, pydeps, args)
372 print_dep_graph(graph, args, package_paths)
373
374 pool = multiprocessing.Pool(args.batch)
375 pool.map(doit, args.names)
376
377
378def main(args):
379 try:
380 run(args)
381 except RuntimeError as e:
382 print(e)
383 return 1
void print(char *figname, TCanvas *c1)
STL class.
traverse(graph, root, reverse=False, maxdepth=None, nodegetter=lambda n:n)
create_dep_graph(target, deps, pydeps, args)
subgraph(graph, sources, reverse=False, maxdepth=None, nodegetter=lambda n :n.attr.get('label'))
lrstrip(s, prefix, postfix)
print_dep_graph(graph, args, package_paths={})
Definition run.py:1