Belle II Software  release-08-01-10
check_libraries.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 
11 
12 import os
13 from SCons.Script import AddOption, GetOption, BUILD_TARGETS
14 from SCons import Node
15 from SCons.Action import Action
16 import re
17 import subprocess
18 
19 
20 def get_dtneeded(filename):
21  """Use readelf (from binutils) to get a list of required libraries for a
22  given shared object file. This will just look at the NEEDED entries in the
23  dynamic table so in contrast to ldd it will not resolve child
24  dependencies recursively. It will also just return the library names, not in
25  which directory they might be found."""
26 
27  try:
28  re_readelf = re.compile(r"^\s*0x0.*\‍(NEEDED\‍).*\[(.*)\]\s*$", re.M)
29  readelf_out = subprocess.check_output(["readelf", "-d", filename], stderr=subprocess.STDOUT,
30  env=dict(os.environ, LC_ALL="C"), encoding="utf-8")
31  needed_entries = re_readelf.findall(readelf_out)
32  return needed_entries
33  except Exception as e:
34  print("Could not get dependencies for library %s: %s" % (filename, e))
35  return None
36 
37 
38 def get_env_list(env, key):
39  """Get a list from the environment by substituting all values and converting
40  them to str. For example to get the list of libraries to be linked in
41 
42  >>> get_env_list(env, "LIBS")
43  ["framework", "framework_dataobjects"]
44  """
45  return map(str, env.subst_list(key)[0])
46 
47 
48 def get_package(env, node):
49  """Determine the package of a node by looking at it's sources
50  Hopefully the sources will be inside the build directory so look for the
51  first one there and determine the package name from its name."""
52  builddir = env.Dir("$BUILDDIR").get_abspath()
53  for n in node.sources:
54  fullpath = n.get_abspath()
55  if fullpath.startswith(builddir):
56  # split the path components inside the build dir
57  components = os.path.relpath(fullpath, builddir).split(os.path.sep)
58  # so if the file is directly in the build dir we take the part in
59  # front of the _
60  if len(components) == 1:
61  return components[0].split("_", 2)[0]
62  else:
63  return components[0]
64 
65  return "none"
66 
67 
68 def print_libs(title, text, pkg, lib, libs):
69  """Print information on extra/missing libraries"""
70  for library in sorted(libs):
71  print("%s:%s:%s -> %s (%s)" % (title, pkg, lib, library, text))
72 
73 
74 def check_libraries(target, source, env):
75  """Check library dependencies. This is called as a builder hence the target and
76  source arguments which are ignored."""
77 
78  libdir = env.Dir("$LIBDIR").get_abspath()
79  # check libraries and modules directories
80  stack = [env.Dir("$LIBDIR"), env.Dir("$MODDIR")]
81  # and make a list of all .so objects we actually built
82  libraries = []
83  while stack:
84  dirnode = stack.pop()
85  # loop over all nodes which are childrens of this directory
86  for node in dirnode.all_children():
87  if isinstance(node, Node.FS.Dir):
88  # and add directories to the list to investigate
89  stack.append(node)
90  elif node.has_explicit_builder() and str(node).endswith(".so"):
91  # FIXME: this one needs some special love because it doesn't
92  # really depend on all the dataobjects libraries, we just force
93  # it to. Skip for now
94  if os.path.basename(node.get_abspath()) == "libdataobjects.so":
95  continue
96  fullname = str(node)
97  name = os.path.basename(fullname)
98  # is it in a modules directory? if so add it to the name
99  if "modules" in fullname.split(os.path.sep):
100  name = "modules/" + name
101  # find out which package the file belongs to
102  pkg = get_package(env, node)
103  # and add lib to the list of libraries
104  libraries.append((pkg, name, node))
105 
106  # now we have them sorted, go through the list
107  for pkg, lib, node in sorted(libraries):
108  # get the list of libraries mentioned in the SConscript
109  given = get_env_list(node.get_build_env(), "$LIBS")
110  # and the list of libraries actually needed by the library
111  needed = get_dtneeded(node.get_abspath())
112  # ignore it if something is wrong with reading the library
113  if needed is None:
114  continue
115 
116  def remove_libprefix(x):
117  """small helper to get rid of lib* prefix for requirements as SCons
118  seems to do this transparently as well"""
119  if x.startswith("lib"):
120  print_libs("LIB_WARNING", "dependency given as lib*, please remove "
121  "'lib' prefix in SConscript", pkg, str(node), [x])
122  x = x[3:]
123  return x
124 
125  # filter lib* from all given libraries and emit a Warning for each
126  given = map(remove_libprefix, given)
127 
128  # TODO: the list of libraries to link against is short name,
129  # e.g. framework instead of libframework.so so we have to fix
130  # these lists. However for external libraries this is usually
131  # complicated by so versions, e.g. the list of linked libraries
132  # will not contain libc.so but libc.so.6 and to make matters
133  # worse libraries like CLHEP will end up as libCLHEP-$version.so
134  # In theory we need to emulate the linker behaviour here to get
135  # a perfect matching. A resonable safe assumption would probably
136  # be to require just the beginning to match.
137 
138  # But for now we just restrict ourselves to libraries in $LIBDIR
139  # because we know they don't have so-versions. So reduce the
140  # lists to libraries which we can actually find in $LIBDIR
141  given_internal = set("lib%s.so" % library for library in given if os.path.exists(os.path.join(libdir,
142  "lib%s.so" % library)))
143  needed_internal = set(library for library in needed if os.path.exists(os.path.join(libdir, library)))
144 
145  # now check for extra or missing direct dependencies using
146  # simple set operations
147  if GetOption("libcheck_extra"):
148  extra = given_internal - needed_internal
149  print_libs("LIB_EXTRA", "dependency not needed and can be removed from SConscript", pkg, lib, extra)
150 
151  if GetOption("libcheck_missing"):
152  missing = needed_internal - given_internal
153  print_libs("LIB_MISSING", "library needed directly, please add to SConscript", pkg, lib, missing)
154 
155  print("*** finished checking library dependencies")
156 
157 
158 def run(env):
159  AddOption("--check-extra-libraries", dest="libcheck_extra", action="store_true", default=False,
160  help="if given all libraries will be checked for dependencies in "
161  "the SConscript which are not actually needed")
162  AddOption("--check-missing-libraries", dest="libcheck_missing", action="store_true", default=False,
163  help="if given all libraries will be checked for missing direct dependencies after build")
164  # check if any of the two options is set and if so run the checker
165  if env.GetOption("libcheck_extra") or env.GetOption("libcheck_missing"):
166  # but we need to run it after the build so add a pseudo build command
167  libcheck = "#.check_libraries_pseudotarget"
168  check_action = Action(check_libraries, "*** Checking library dependencies...")
169  env.Command(libcheck, None, check_action)
170  env.AlwaysBuild(libcheck)
171  # which depends on all other build targets so depending on wether a list
172  # of build targets is defined ...
173  if not BUILD_TARGETS:
174  # we want to run this last so if no targets are specified make sure check is
175  # at least run after libraries and modules are built
176  env.Depends(libcheck, ["lib", "modules"])
177  else:
178  # otherwise let it depend just on all defined targets
179  env.Depends(libcheck, BUILD_TARGETS)
180  # and add make sure it's included in the build targets
181  BUILD_TARGETS.append(libcheck)
182 
183 
184 def generate(env):
185  env.AddMethod(run, "CheckLibraryDependencies")
186 
187 
188 def exists(env):
189  return True