Belle II Software light-2406-ragdoll
check_libraries.py
1#!/usr/bin/env python
2
3
10
11import os
12from SCons.Script import AddOption, GetOption, BUILD_TARGETS
13from SCons import Node
14from SCons.Action import Action
15import re
16import subprocess
17
18
19def 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
37def 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
47def 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
67def 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
73def 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
157def 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
183def generate(env):
184 env.AddMethod(run, "CheckLibraryDependencies")
185
186
187def exists(env):
188 return True