ATLAS Offline Software
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 """
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
9 depth is configurable.
10 """
11 
12 import sys
13 import os
14 import re
15 from collections import deque
16 import PyUtils.acmdlib as acmdlib
17 import argparse
18 import pygraphviz
19 
20 # Style for python dependencies:
21 py_style = 'dashed'
22 
23 def 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 
32 def externals_name(lib):
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 
44 def 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 
51 def 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 
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.
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 
99 def 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 
109 def 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 
117 class AthGraph:
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 
171 def 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 
245 def 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 
318 def 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 
378 def main(args):
379  try:
380  run(args)
381  except RuntimeError as e:
382  print(e)
383  return 1
python.scripts.cmake_depends.create_dep_graph
def create_dep_graph(target, deps, pydeps, args)
Definition: cmake_depends.py:171
python.scripts.cmake_depends.copy_graph
def copy_graph(source, dest)
Definition: cmake_depends.py:109
python.scripts.cmake_depends.traverse
def traverse(graph, root, reverse=False, maxdepth=None, nodegetter=lambda n:n)
Definition: cmake_depends.py:51
python.scripts.cmake_depends.AthGraph.__init__
def __init__(self, dotfile)
Definition: cmake_depends.py:120
Cut::all
@ all
Definition: SUSYToolsAlg.cxx:67
covarianceTool.filter
filter
Definition: covarianceTool.py:514
python.scripts.cmake_depends.print_dep_graph
def print_dep_graph(graph, args, package_paths={})
Definition: cmake_depends.py:245
python.scripts.cmake_depends.AthGraph.get_node
def get_node(self, label)
Definition: cmake_depends.py:159
fillPileUpNoiseLumi.next
next
Definition: fillPileUpNoiseLumi.py:52
python.scripts.cmake_depends.AthGraph.graph
graph
Definition: cmake_depends.py:124
python.scripts.cmake_depends.read_package_list
def read_package_list(package_file)
Definition: cmake_depends.py:23
python.scripts.cmake_depends.subgraph
def subgraph(graph, sources, reverse=False, maxdepth=None, nodegetter=lambda n :n.attr.get('label'))
Definition: cmake_depends.py:85
python.scripts.cmake_depends.externals_name
def externals_name(lib)
Definition: cmake_depends.py:32
run
Definition: run.py:1
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename T::value_type > sorted(T begin, T end)
Helper function to create a sorted vector from an unsorted one.
CxxUtils::set
constexpr std::enable_if_t< is_bitmask_v< E >, E & > set(E &lhs, E rhs)
Convenience function to set bits in a class enum bitmask.
Definition: bitmask.h:232
TCS::join
std::string join(const std::vector< std::string > &v, const char c=',')
Definition: Trigger/TrigT1/L1Topo/L1TopoCommon/Root/StringUtils.cxx:10
python.scripts.cmake_depends.AthGraph.node
node
Definition: cmake_depends.py:131
python.scripts.cmake_depends.AthGraph
Definition: cmake_depends.py:117
Trk::open
@ open
Definition: BinningType.h:40
python.scripts.cmake_depends.lrstrip
def lrstrip(s, prefix, postfix)
Definition: cmake_depends.py:44
dbg::print
void print(std::FILE *stream, std::format_string< Args... > fmt, Args &&... args)
Definition: SGImplSvc.cxx:70
python.scripts.cmake_depends.add_legend
def add_legend(graph)
Definition: cmake_depends.py:99
python.scripts.cmake_depends.AthGraph.types
types
Definition: cmake_depends.py:128
python.scripts.cmake_depends.AthGraph.ignore_target
def ignore_target(self, node)
Definition: cmake_depends.py:163
python.scripts.cmake_depends.main
def main(args)
Definition: cmake_depends.py:378
python.scripts.cmake_depends.run
def run(args)
Definition: cmake_depends.py:318