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