ATLAS Offline Software
Toggle main menu visibility
Loading...
Searching...
No Matches
Tools
PyUtils
python
scripts
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
print
void print(char *figname, TCanvas *c1)
Definition
TRTCalib_StrawStatusPlots.cxx:26
python.scripts.cmake_depends.AthGraph
Definition
cmake_depends.py:117
python.scripts.cmake_depends.AthGraph.graph
graph
Definition
cmake_depends.py:124
python.scripts.cmake_depends.AthGraph.get_node
get_node(self, label)
Definition
cmake_depends.py:159
python.scripts.cmake_depends.AthGraph.types
dict types
Definition
cmake_depends.py:128
python.scripts.cmake_depends.AthGraph.ignore_target
ignore_target(self, node)
Definition
cmake_depends.py:163
python.scripts.cmake_depends.AthGraph.__init__
__init__(self, dotfile)
Definition
cmake_depends.py:120
python.scripts.cmake_depends.AthGraph.node
dict node
Definition
cmake_depends.py:131
set
STL class.
python.scripts.cmake_depends.add_legend
add_legend(graph)
Definition
cmake_depends.py:99
python.scripts.cmake_depends.lrstrip
lrstrip(s, prefix, postfix)
Definition
cmake_depends.py:44
python.scripts.cmake_depends.read_package_list
read_package_list(package_file)
Definition
cmake_depends.py:23
python.scripts.cmake_depends.create_dep_graph
create_dep_graph(target, deps, pydeps, args)
Definition
cmake_depends.py:171
python.scripts.cmake_depends.print_dep_graph
print_dep_graph(graph, args, package_paths={})
Definition
cmake_depends.py:245
python.scripts.cmake_depends.traverse
traverse(graph, root, reverse=False, maxdepth=None, nodegetter=lambda n:n)
Definition
cmake_depends.py:51
python.scripts.cmake_depends.copy_graph
copy_graph(source, dest)
Definition
cmake_depends.py:109
python.scripts.cmake_depends.subgraph
subgraph(graph, sources, reverse=False, maxdepth=None, nodegetter=lambda n :n.attr.get('label'))
Definition
cmake_depends.py:85
python.scripts.cmake_depends.run
run(args)
Definition
cmake_depends.py:318
python.scripts.cmake_depends.externals_name
externals_name(lib)
Definition
cmake_depends.py:32
Generated on
for ATLAS Offline Software by
1.17.0