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