ATLAS Offline Software
dlldep.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
4 #
5 # @file: dlldep.py
6 #
7 # @purpose: Create a graph of the runtime dependencies of a dynamic library using ldd.
8 # The dependencies are printed in DOT (graphviz) language.
9 #
10 # @author: Frank Winklmeier (CERN)
11 # based on Dominik Seichter's 'dependencies.sh':
12 # http://domseichter.blogspot.com/2008/02/visualize-dependencies-of-binaries-and.html
13 #
14 
15 import sys
16 from os.path import basename
17 import subprocess as sp
18 import re
19 
20 import PyUtils.Dso as Dso
21 
22 class Cache:
23  """Global cache of already processed files"""
24 
25  files = {} # Global cache of already processed libs
26  stats = [] # Global statistics
27 
28  def __init__(self):
29  self.myfiles = {} # Dependencies of the currently processed lib
30  self.dotsrc = [] # DOT source code
31 
32  def add(self, shlib):
33  """Add file to local and global cache"""
34  self.myfiles[shlib.lib] = shlib
35  Cache.files[shlib.lib] = shlib
36 
37  def writeDOT(self, file):
38  for d in self.dotsrc: print (file, d, file=file)
39 
40  def dot(self, code, style={}):
41  """Output a line of dot code"""
42  if len(style)>0:
43  code += ' ['
44  for k,v in style.items():
45  code += '%s="%s" ' % (k,v)
46  code += ']'
47 
48  self.dotsrc.append(code)
49  return
50 
51 
52 class SharedLib:
53  """Represent a shared library with name, dependencies and other stats"""
54 
55  def __init__(self, distance, lib):
56  import os.path as osp
57  lib = osp.expanduser(osp.expandvars(lib))
58  if not osp.exists(lib):
59  l = Dso.find_library(lib)
60  if l:
61  lib = l
62  assert osp.exists(lib), "no such path [%s]" % (lib,)
63  self.lib = lib # path of library
64  self.distance = distance # distance from root lib
65  self.deplibs = self._getLibs(lib) # direct dependencies
66 
67  def _getLibs(self, lib):
68  """Get direct dependencies of shared library"""
69 
70  # First check if already in global cache
71  cachedlib = Cache.files.get(lib)
72  if cachedlib: return cachedlib.deplibs
73 
74  # Run readelf to find direct dependencies
75  # Note: ldd itself does recursions so we cannot use it here
76  encargs = {'encoding' : 'utf-8'}
77  p = sp.Popen(["readelf","-d",lib], stdout=sp.PIPE, **encargs)
78  output = p.communicate()[0]
79  if p.returncode != 0:
80  print ("Cannot run 'readelf' on",lib)
81  return []
82 
83  libs = []
84  for l in output.split("\n"):
85  if l.find("NEEDED")==-1: continue
86  libs += [l.split()[-1].strip("[]")]
87 
88  # Run ldd to find full path of libraries
89  p = sp.Popen(["ldd",lib], stdout=sp.PIPE, **encargs)
90  output = p.communicate()[0]
91  if p.returncode != 0:
92  print ("Cannot run 'ldd' on",lib)
93  return []
94 
95  libpaths = []
96  for l in output.split("\n"):
97  fields = l.strip().split()
98  if len(fields)!=4: continue
99  path = fields[2]
100  if (fields[0] in libs) and len(path)>0:
101  libpaths += [path]
102 
103  return libpaths
104 
105 
106 class Color:
107  """Helper class for colored nodes"""
108 
109  default = "white"
110  scheme = "rdbu8"
111  projects = {"DetCommon" : 2,
112  "AtlasCore" : 1,
113  "AtlasEvent" : 3,
114  "AtlasConditions" : 4,
115  "AtlasReconstruction" : 5,
116  "AtlasSimulation" : 6,
117  "AtlasTrigger" : 7,
118  "AtlasAnalysis" : 8
119  }
120 
121  @classmethod
122  def get(cls, lib):
123  for p,c in cls.projects.items():
124  if lib.find(p)!=-1: return "/%s/%s" % (cls.scheme, c)
125  return cls.default
126 
127 
128 class Stats:
129  """Statistics calculated from Cache object"""
130 
131  def __init__(self, lib, cache):
132  self.lib = lib
133  self.depTotal = len(cache.myfiles)-1 # do not count ourselves
134  self.depDirect = 0
135 
136  for lib in cache.myfiles.values():
137  if lib.distance==1: self.depDirect += 1
138  return
139 
140 
141 def anaLib(lib, opt, cache, select = [], ignore = [], depth = 0):
142  """Get dependencies of shared library recursively"""
143 
144  def process(path):
145  """Should this lib be processed?"""
146  for regexp in select:
147  if regexp.match(path): return True
148  if len(select)>0: return False
149 
150  for regexp in ignore:
151  if regexp.match(path): return False
152  return True
153 
154  if opt.maxdepth and depth>=opt.maxdepth: return
155 
156  # Check if we analyzed this lib already
157  cachedlib = cache.myfiles.get(lib)
158  if cachedlib:
159  # Always save minimum distance
160  if depth<cachedlib.distance: cachedlib.distance=depth
161  return
162 
163  shlib = SharedLib(depth, lib)
164  cache.add(shlib)
165 
166  for l in shlib.deplibs:
167  if process(l):
168  cache.dot(' "%s" -> "%s"' % (basename(lib), basename(l)))
169  anaLib(l, opt, cache, select, ignore, depth+1)
170 
171  return
172 
173 
174 def processLib(lib, opt, dotFileName = None):
175  """Process one library"""
176 
177  cache = Cache()
178  dot = cache.dot # shortcut
179 
180  dot('digraph DependencyTree {')
181  dot(' ratio=0.9 nodesep=0.05') # some reasonable default values
182  dot(' "%s" [shape=box]' % basename(lib))
183 
184  select = []
185  ignore = [] # currently not used
186  if opt.filter:
187  for f in opt.filter: select += [re.compile(f)]
188  else:
189  select = [re.compile(".*atlas/software.*")]
190 
191  anaLib(lib, opt, cache, select, ignore)
192 
193  # Declare style of all nodes
194  for l,v in cache.myfiles.items():
195  style = {}
196  # Special style for direct dependencies
197  if v.distance==1:
198  style["shape"] = "box"
199 
200  if not opt.nocolor:
201  style["style"] = "filled"
202  style["fillcolor"] = Color.get(l)
203 
204  dot(' "%s"' % (basename(l)), style)
205 
206  dot('}')
207 
208  # Write output to file
209  if dotFileName: outFile = open(dotFileName, "w")
210  else: outFile = open(basename(lib)+".dot", "w")
211 
212  cache.writeDOT(outFile)
213 
214  # Calculate statistics
215  if opt.stats:
216  st = Stats(lib, cache)
217  Cache.stats += [st]
218  return st
219 
220  return None
221 
222 
223 
225  """Print statistics"""
226  import operator
227 
228  print ("%-50s %7s %7s" % ("Library dependencies","Direct","Total"))
229  print ("-"*70)
230  for s in sorted(Cache.stats, key=operator.attrgetter("depDirect"), reverse=True):
231  print ("%-50s %7d %7d" % (basename(s.lib), s.depDirect, s.depTotal))
232 
233  return
234 
235 
236 def main():
237 
238  import optparse
239  parser = optparse.OptionParser(description="Create runtime dependecy graph for shared library. The output is a graph in DOT language. To visualize it use, e.g. 'dot -O -Tps mygraph.dot'. The rectangular nodes represent direct dependencies. Nodes belonging to the same project have the same color.",
240  usage="%prog [OPTIONS] LIB [LIB...]")
241 
242  parser.add_option("-o", "--output",
243  help="File for DOT source code (default is LIB.dot)")
244 
245  parser.add_option("-d", "--maxdepth", type="int",
246  help="Maximum depth of dependency tree [1..]")
247 
248  parser.add_option("-f", "--filter", action="append",
249  help="Only analyze libraries matching regular expression (can be specified multiple times) [default: .*atlas/software.*]")
250 
251  parser.add_option("--nocolor", action="store_true",
252  help="Do not use colors")
253 
254  parser.add_option("-s", "--stats", action="store_true",
255  help="Print statistics")
256 
257  (opt, args) = parser.parse_args()
258  if len(args)==0:
259  parser.error("Invalid number of arguments specified")
260 
261 
262  if len(args)>1 and opt.output:
263  print ("Multiple libraries specified. Ignoring output file name.")
264  opt.output = None
265 
266  for lib in args:
267  processLib(lib, opt, opt.output)
268 
269  if opt.stats:
270  printStats()
271 
272  return 0
273 
274 
275 if __name__ == "__main__":
276  try:
277  sys.exit(main())
278  except KeyboardInterrupt:
279  sys.exit(1)
280 
dlldep.SharedLib.__init__
def __init__(self, distance, lib)
Definition: dlldep.py:55
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename R::value_type > sorted(const R &r, PROJ proj={})
Helper function to create a sorted vector from an unsorted range.
dlldep.Cache
Definition: dlldep.py:22
dlldep.Stats.lib
lib
Definition: dlldep.py:132
dlldep.Cache.__init__
def __init__(self)
Definition: dlldep.py:28
dlldep.Cache.writeDOT
def writeDOT(self, file)
Definition: dlldep.py:37
dlldep.Stats.__init__
def __init__(self, lib, cache)
Definition: dlldep.py:131
dlldep.Color
Definition: dlldep.py:106
dlldep.SharedLib._getLibs
def _getLibs(self, lib)
Definition: dlldep.py:67
dlldep.processLib
def processLib(lib, opt, dotFileName=None)
Definition: dlldep.py:174
dlldep.anaLib
def anaLib(lib, opt, cache, select=[], ignore=[], depth=0)
Definition: dlldep.py:141
dumpHVPathFromNtuple.append
bool append
Definition: dumpHVPathFromNtuple.py:91
dlldep.Cache.dotsrc
dotsrc
Definition: dlldep.py:30
SUSY_SimplifiedModel_PostInclude.process
string process
Definition: SUSY_SimplifiedModel_PostInclude.py:43
dlldep.Color.default
default
Definition: dlldep.py:109
dlldep.Color.get
def get(cls, lib)
Definition: dlldep.py:122
dlldep.SharedLib
Definition: dlldep.py:52
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:71
dlldep.SharedLib.deplibs
deplibs
Definition: dlldep.py:65
dlldep.printStats
def printStats()
Definition: dlldep.py:224
dlldep.Cache.dot
def dot(self, code, style={})
Definition: dlldep.py:40
Trk::open
@ open
Definition: BinningType.h:40
dlldep.Cache.myfiles
myfiles
Definition: dlldep.py:29
dlldep.Stats
Definition: dlldep.py:128
dlldep.Cache.add
def add(self, shlib)
Definition: dlldep.py:32
dlldep.SharedLib.distance
distance
Definition: dlldep.py:64
dlldep.Stats.depTotal
depTotal
Definition: dlldep.py:133
dlldep.main
def main()
Definition: dlldep.py:236
dlldep.Stats.depDirect
depDirect
Definition: dlldep.py:134
dot
Definition: dot.py:1
dlldep.Color.projects
projects
Definition: dlldep.py:111
dlldep.SharedLib.lib
lib
Definition: dlldep.py:63
dlldep.Color.scheme
scheme
Definition: dlldep.py:110
Trk::split
@ split
Definition: LayerMaterialProperties.h:38
beamspotman.basename
basename
Definition: beamspotman.py:638