7 Print target/package dependencies of ATLAS releases. For a given target/package
8 name, the dependencies are printed as a plain list or DOT graph. The recursion
15 from collections
import deque
16 import PyUtils.acmdlib
as acmdlib
24 """Read packages.txt as a source for the full package path"""
26 with open(package_file)
as f:
27 packages = [line.rstrip()
for line
in f
if not line.startswith(
'#')]
29 return dict([(p.split(
'/')[-1],p)
for p
in packages])
33 """Return a short name for an external library"""
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
41 return os.path.basename(lib)
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)]
51 def traverse(graph, root, reverse=False, maxdepth=None, nodegetter=lambda n:n):
52 """Depth-limited BFS edge traversal of graph starting at root.
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)
61 Inspired by https://github.com/networkx/networkx/tree/master/networkx/algorithms/traversal
65 queue = deque([(root,root,0)])
66 neighbors = graph.iterpred
if reverse
else graph.itersucc
68 parent,node,level = queue.popleft()
70 if node
not in visited_nodes:
71 visited_nodes.add(node)
74 if maxdepth
is None or level < maxdepth:
75 queue.extend((node,n,level+1)
for n
in neighbors(node))
78 queue.extend((node,n,level+1)
for n
in neighbors(node)
if n
in visited_nodes)
80 if (parent,node)
not in visited_edges:
81 visited_edges.add((parent,node))
82 yield nodegetter(parent), nodegetter(node)
85 def 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.
91 for a,b
in traverse(graph, root, reverse=reverse, maxdepth=maxdepth, nodegetter=nodegetter):
93 if reverse: edges.add((b,a))
94 else: edges.add((a,b))
100 """Add legend to graph"""
101 graph.add_subgraph(name=
'clusterLegend', label=
'Legend')
102 l = graph.subgraphs()[-1]
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)
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)
118 """Class to hold dependency information for release"""
121 """Read dotfile and and optionally transform package names to full paths"""
124 self.
graph = pygraphviz.AGraph(dotfile)
127 legend = self.
graph.get_subgraph(
'clusterLegend')
128 self.
types = { n.attr[
'label'] : n.attr[
'shape']
for n
in legend.nodes_iter() }
131 self.
node = { n.attr[
'label'] : n.get_name()
for n
in self.
graph.nodes_iter() }
133 def decorate_package(n0, n1=None):
134 """Assign package name to n0 -> n1 if n0 is a package target"""
137 if p.startswith(
'Package_'):
138 n0.attr[
'package'] =
lrstrip(p,
'Package_',
'_tests')
140 n1.attr[
'package'] = n0.attr[
'package']
143 for e
in self.
graph.edges_iter():
144 decorate_package(e[0], e[1])
147 for n
in self.
graph.nodes_iter():
151 external_nodes =
filter(
lambda n :
'package' not in n.attr.keys(),
152 self.
graph.nodes_iter())
153 for n
in external_nodes:
155 n.attr[
'package'] = name.split(
'::')[0]
156 n.attr[
'label'] = name
157 n.attr[
'external'] =
'yes'
160 """Return graph node for label/target"""
164 """Check if target should be ignored"""
165 label = node.attr[
'label']
166 return True if (label.startswith(
'__')
or
167 label.startswith(
'-')
or
168 node.attr[
'shape']==self.
types[
'Custom Target'])
else False
172 """Create and return dependency graph.
174 @param target name of target
175 @param deps AthGraph cmake dependencies
176 @param pydeps python dependencies
177 @param args command line arguments
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'
186 target = target.split(
'/')[-1]
189 depth = args.recursive
190 if not args.target
and not args.clients
and args.recursive
is not None:
195 r = re.compile(target)
196 targets = [getnode(n)
for n
in deps.graph.nodes_iter()
if r.match(n.attr[
'label'])]
198 targets += [n
for n
in pydeps.nodes_iter()
if r.match(n)]
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.")
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)])
217 sources.extend([deps.get_node(l)])
219 raise RuntimeError(f
"Target with name {l} does not exist.")
222 g =
subgraph(deps.graph, sources, reverse=args.clients,
223 maxdepth=depth, nodegetter=getnode)
225 graph = pygraphviz.AGraph(name=target, directed=
True, strict=
False)
226 graph.add_edges_from(g)
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)
235 graph.add_edges_from(g, style=py_style)
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
246 """Output final graph"""
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()]
256 suffix =
':py' if p.attr[
'style']==py_style
else ''
257 output.append(
'%s%s' % (package_paths.get(p,p), suffix))
261 if args.batch
or args.dot:
262 f =
open(graph.name+
'.dot',
'w')
if args.batch
else sys.stdout
271 @acmdlib.command(name=
'cmake.depends',
274 @acmdlib.argument(
'names', nargs=
'+', metavar=
'NAME',
275 help=
'package/target name or regular expression')
277 @acmdlib.argument(
'-t',
'--target', action=
'store_true',
278 help=
'treat NAME as target instead of package name')
280 @acmdlib.argument(
'-c',
'--clients', action=
'store_true',
281 help=
'show clients (instead of dependencies)')
283 @acmdlib.argument(
'-e',
'--externals', action=
'store_true',
284 help=
'include external dependencies')
286 @acmdlib.argument(
'-l',
'--long', action=
'store_true',
287 help=
'show full package names (only for txt output)')
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)')
293 @acmdlib.argument(
'--py', action=
'store_true',
294 help=f
'add Python dependencies (marked with ":py" in printout, {py_style} in graph)')
296 @acmdlib.argument(
'--regex', action=
'store_true',
297 help=
'treat NAME as regular expression')
299 @acmdlib.argument(
'--all', action=
'store_true',
300 help=
'do not apply any target filter (e.g. custom targets)')
302 @acmdlib.argument(
'-d',
'--dot', action=
'store_true',
303 help=
'print DOT graph')
305 @acmdlib.argument(
'--legend', action=
'store_true',
306 help=
'add legend to graph')
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.')
314 @acmdlib.argument(
'--cmakedot', help=argparse.SUPPRESS)
315 @acmdlib.argument(
'--pydot', help=argparse.SUPPRESS)
319 """Inspect cmake build dependencies"""
322 if not args.cmakedot:
324 args.cmakedot = os.path.join(os.environ[
'AtlasArea'],
'InstallArea',
325 os.environ[
'BINARY_TAG'],
'packages.dot')
327 main.parser.error(
"Cannot find 'packages.dot'. Setup a release or use --cmakedot.")
333 main.parser.error(
"Python dependencies not possible in target mode.")
335 args.pydot = args.pydot
or args.cmakedot.replace(
'.dot',
'.py.dot')
337 pydeps = pygraphviz.AGraph(args.pydot)
339 main.parser.error(f
"Cannot read '{args.pydot}'. Setup a release or use --pydot.")
345 package_paths =
read_package_list(os.path.join(os.environ[
'AtlasArea'],
'InstallArea',
346 os.environ[
'BINARY_TAG'],
'packages.txt'))
348 main.parser.error(
"Cannot read 'packages.txt'. Setup a release or run without -l/--long.")
355 subgraphs = [
create_dep_graph(target, deps, pydeps, args)
for target
in args.names]
357 graph = pygraphviz.AGraph(name=
'AthGraph', directed=
True, strict=
False)
359 graph.add_subgraph(name=g.get_name())
368 import multiprocessing
374 pool = multiprocessing.Pool(args.batch)
375 pool.map(doit, args.names)
381 except RuntimeError
as e: