5059c60a45ca532687d50b2566b937914b375502
[samba.git] / buildtools / wafsamba / symbols.py
1 # a waf tool to extract symbols from object files or libraries
2 # using nm, producing a set of exposed defined/undefined symbols
3
4 import Utils, Build, subprocess, Logs
5 from samba_wildcard import fake_build_environment
6 from samba_utils import *
7
8 # these are the data structures used in symbols.py:
9 #
10 # bld.env.symbol_map : dictionary mapping public symbol names to list of
11 #                      subsystem names where that symbol exists
12 #
13 # t.in_library       : list of libraries that t is in
14 #
15 # bld.env.public_symbols: set of public symbols for each subsystem
16 # bld.env.used_symbols  : set of used symbols for each subsystem
17 #
18 # bld.env.syslib_symbols: dictionary mapping system library name to set of symbols
19 #                         for that library
20 # bld.env.library_dict  : dictionary mapping built library paths to subsystem names
21 #
22 # LOCAL_CACHE(bld, 'TARGET_TYPE') : dictionary mapping subsystem name to target type
23
24 def symbols_extract(objfiles, dynamic=False):
25     '''extract symbols from objfile, returning a dictionary containing
26        the set of undefined and public symbols for each file'''
27
28     ret = {}
29
30     cmd = ["nm"]
31     if dynamic:
32         # needed for some .so files
33         cmd.append("-D")
34     cmd.extend(list(objfiles))
35
36     nmpipe = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout
37     if len(objfiles) == 1:
38         filename = list(objfiles)[0]
39         ret[filename] = { "PUBLIC": set(), "UNDEFINED" : set()}
40
41     for line in nmpipe:
42         line = line.strip()
43         if line.endswith(':'):
44             filename = line[:-1]
45             ret[filename] = { "PUBLIC": set(), "UNDEFINED" : set() }
46             continue
47         cols = line.split(" ")
48         if cols == ['']:
49             continue
50         # see if the line starts with an address
51         if len(cols) == 3:
52             symbol_type = cols[1]
53             symbol = cols[2]
54         else:
55             symbol_type = cols[0]
56             symbol = cols[1]
57         if symbol_type in "BDGTRVWSi":
58             # its a public symbol
59             ret[filename]["PUBLIC"].add(symbol)
60         elif symbol_type in "U":
61             ret[filename]["UNDEFINED"].add(symbol)
62
63     return ret
64
65
66 def real_name(name):
67     if name.find(".objlist") != -1:
68         name = name[:-8]
69     return name
70
71
72 def get_ldd_libs(bld, binname):
73     '''find the list of linked libraries for any binary or library
74     binname is the path to the binary/library on disk
75     '''
76     ret = set()
77     lddpipe = subprocess.Popen(['ldd', binname], stdout=subprocess.PIPE).stdout
78     for line in lddpipe:
79         line = line.strip()
80         cols = line.split(" ")
81         if len(cols) < 3 or cols[1] != "=>" or cols[2] == '':
82             continue
83         ret.add(os.path.realpath(cols[2]))
84     return ret
85
86
87 def get_ldd_libs_recursive(bld, binname, seen=set()):
88     '''find the recursive list of linked libraries for any binary or library
89     binname is the path to the binary/library on disk. seen is a set used
90     to prevent loops
91     '''
92     if binname in seen:
93         return set()
94     ret = get_ldd_libs(bld, binname)
95     seen.add(binname)
96     for lib in ret:
97         ret = ret.union(get_ldd_libs_recursive(bld, lib, seen))
98     return ret
99
100
101 def find_syslib_path(bld, libname, deps):
102     '''find the path to the syslib we will link against'''
103     # the strategy is to use the targets that depend on the library, and run ldd
104     # on it to find the real location of the library that is used
105
106     linkpath = deps[0].link_task.outputs[0].abspath(bld.env)
107
108     if libname == "python":
109         libname += bld.env.PYTHON_VERSION
110
111     ret = None
112
113     lddpipe = subprocess.Popen(['ldd', linkpath], stdout=subprocess.PIPE).stdout
114     for line in lddpipe:
115         line = line.strip()
116         cols = line.split(" ")
117         if len(cols) < 3 or cols[1] != "=>":
118             continue
119         if cols[0].startswith("lib%s." % libname.lower()):
120             ret = cols[2]
121         if cols[0].startswith("libc."):
122             # save this one too
123             bld.env.libc_path = cols[2]
124     return ret
125
126
127 def build_symbol_sets(bld, tgt_list):
128     '''build the public_symbols and undefined_symbols attributes for each target'''
129
130     if bld.env.public_symbols:
131         return
132
133     objlist = []  # list of object file
134     objmap = {}   # map from object filename to target (subsystem) name
135
136     for t in tgt_list:
137         t.public_symbols = set()
138         t.undefined_symbols = set()
139         t.used_symbols = set()
140         for tsk in getattr(t, 'compiled_tasks', []):
141             for output in tsk.outputs:
142                 objpath = output.abspath(bld.env)
143                 objlist.append(objpath)
144                 objmap[objpath] = t
145
146     symbols = symbols_extract(objlist)
147     for obj in objlist:
148         t = objmap[obj]
149         t.public_symbols = t.public_symbols.union(symbols[obj]["PUBLIC"])
150         t.undefined_symbols = t.undefined_symbols.union(symbols[obj]["UNDEFINED"])
151         t.used_symbols = t.used_symbols.union(symbols[obj]["UNDEFINED"])
152
153     t.undefined_symbols = t.undefined_symbols.difference(t.public_symbols)
154
155     # and the reverse map of public symbols to subsystem name
156     bld.env.symbol_map = {}
157
158     for t in tgt_list:
159         for s in t.public_symbols:
160             if not s in bld.env.symbol_map:
161                 bld.env.symbol_map[s] = []
162             bld.env.symbol_map[s].append(real_name(t.sname))
163
164     targets = LOCAL_CACHE(bld, 'TARGET_TYPE')
165
166     bld.env.public_symbols = {}
167     for t in tgt_list:
168         name = real_name(t.sname)
169         if name in bld.env.public_symbols:
170             bld.env.public_symbols[name] = bld.env.public_symbols[name].union(t.public_symbols)
171         else:
172             bld.env.public_symbols[name] = t.public_symbols
173         if t.samba_type == 'LIBRARY':
174             for dep in t.add_objects:
175                 t2 = bld.name_to_obj(dep, bld.env)
176                 bld.ASSERT(t2 is not None, "Library '%s' has unknown dependency '%s'" % (name, dep))
177                 bld.env.public_symbols[name] = bld.env.public_symbols[name].union(t2.public_symbols)
178
179     bld.env.used_symbols = {}
180     for t in tgt_list:
181         name = real_name(t.sname)
182         if name in bld.env.used_symbols:
183             bld.env.used_symbols[name] = bld.env.used_symbols[name].union(t.used_symbols)
184         else:
185             bld.env.used_symbols[name] = t.used_symbols
186         if t.samba_type == 'LIBRARY':
187             for dep in t.add_objects:
188                 t2 = bld.name_to_obj(dep, bld.env)
189                 bld.ASSERT(t2 is not None, "Library '%s' has unknown dependency '%s'" % (name, dep))
190                 bld.env.used_symbols[name] = bld.env.used_symbols[name].union(t2.used_symbols)
191
192
193 def build_library_dict(bld, tgt_list):
194     '''build the library_dict dictionary'''
195
196     if bld.env.library_dict:
197         return
198
199     bld.env.library_dict = {}
200
201     for t in tgt_list:
202         if t.samba_type in [ 'LIBRARY', 'PYTHON' ]:
203             linkpath = os.path.realpath(t.link_task.outputs[0].abspath(bld.env))
204             bld.env.library_dict[linkpath] = t.sname
205
206
207 def build_syslib_sets(bld, tgt_list):
208     '''build the public_symbols for all syslibs'''
209
210     if bld.env.syslib_symbols:
211         return
212
213     # work out what syslibs we depend on, and what targets those are used in
214     syslibs = {}
215     objmap = {}
216     for t in tgt_list:
217         if getattr(t, 'uselib', []) and t.samba_type in [ 'LIBRARY', 'BINARY', 'PYTHON' ]:
218             for lib in t.uselib:
219                 if lib in ['PYEMBED', 'PYEXT']:
220                     lib = "python"
221                 if not lib in syslibs:
222                     syslibs[lib] = []
223                 syslibs[lib].append(t)
224
225     # work out the paths to each syslib
226     syslib_paths = []
227     for lib in syslibs:
228         path = find_syslib_path(bld, lib, syslibs[lib])
229         if path is None:
230             Logs.warn("Unable to find syslib path for %s" % lib)
231         if path is not None:
232             syslib_paths.append(path)
233             objmap[path] = lib.lower()
234
235     # add in libc
236     syslib_paths.append(bld.env.libc_path)
237     objmap[bld.env.libc_path] = 'c'
238
239     symbols = symbols_extract(syslib_paths, dynamic=True)
240
241     # keep a map of syslib names to public symbols
242     bld.env.syslib_symbols = {}
243     for lib in symbols:
244         bld.env.syslib_symbols[lib] = symbols[lib]["PUBLIC"]
245
246     # add to the map of symbols to dependencies
247     for lib in symbols:
248         for sym in symbols[lib]["PUBLIC"]:
249             if not sym in bld.env.symbol_map:
250                 bld.env.symbol_map[sym] = []
251             bld.env.symbol_map[sym].append(objmap[lib])
252
253     # keep the libc symbols as well, as these are useful for some of the
254     # sanity checks
255     bld.env.libc_symbols = symbols[bld.env.libc_path]["PUBLIC"]
256
257     # add to the combined map of dependency name to public_symbols
258     for lib in bld.env.syslib_symbols:
259         bld.env.public_symbols[objmap[lib]] = bld.env.syslib_symbols[lib]
260
261
262 def build_autodeps(bld, t):
263     '''build the set of dependencies for a target'''
264     deps = set()
265     name = real_name(t.sname)
266
267     targets    = LOCAL_CACHE(bld, 'TARGET_TYPE')
268
269     for sym in t.undefined_symbols:
270         if sym in t.public_symbols:
271             continue
272         if sym in bld.env.symbol_map:
273             depname = bld.env.symbol_map[sym]
274             if depname == [ name ]:
275                 # self dependencies aren't interesting
276                 continue
277             if t.in_library == depname:
278                 # no need to depend on the library we are part of
279                 continue
280             if depname[0] in ['c', 'python']:
281                 # these don't go into autodeps
282                 continue
283             if targets[depname[0]] in [ 'SYSLIB' ]:
284                 deps.add(depname[0])
285                 continue
286             t2 = bld.name_to_obj(depname[0], bld.env)
287             if len(t2.in_library) != 1:
288                 deps.add(depname[0])
289                 continue
290             if t2.in_library == t.in_library:
291                 # if we're part of the same library, we don't need to autodep
292                 continue
293             deps.add(t2.in_library[0])
294     t.autodeps = deps
295
296
297 def build_library_names(bld, tgt_list):
298     '''add a in_library attribute to all targets that are part of a library'''
299
300     if bld.env.done_build_library_names:
301         return
302
303     for t in tgt_list:
304         t.in_library = []
305
306     for t in tgt_list:
307         if t.samba_type in [ 'LIBRARY' ]:
308             for obj in t.samba_deps_extended:
309                 t2 = bld.name_to_obj(obj, bld.env)
310                 if t2 and t2.samba_type in [ 'SUBSYSTEM', 'ASN1' ]:
311                     if not t.sname in t2.in_library:
312                         t2.in_library.append(t.sname)
313     bld.env.done_build_library_names = True
314
315
316 def check_library_deps(bld, t):
317     '''check that all the autodeps that have mutual dependency of this
318     target are in the same library as the target'''
319
320     name = real_name(t.sname)
321
322     if len(t.in_library) > 1:
323         Logs.warn("WARNING: Target '%s' in multiple libraries: %s" % (t.sname, t.in_library))
324
325     for dep in t.autodeps:
326         t2 = bld.name_to_obj(dep, bld.env)
327         if t2 is None:
328             continue
329         for dep2 in t2.autodeps:
330             if dep2 == name and t.in_library != t2.in_library:
331                 Logs.warn("WARNING: mutual dependency %s <=> %s" % (name, real_name(t2.sname)))
332                 Logs.warn("Libraries should match. %s != %s" % (t.in_library, t2.in_library))
333                 # raise Utils.WafError("illegal mutual dependency")
334
335
336 def check_syslib_collisions(bld, tgt_list):
337     '''check if a target has any symbol collisions with a syslib
338
339     We do not want any code in Samba to use a symbol name from a
340     system library. The chance of that causing problems is just too
341     high. Note that libreplace uses a rep_XX approach of renaming
342     symbols via macros
343     '''
344
345     has_error = False
346     for t in tgt_list:
347         for lib in bld.env.syslib_symbols:
348             common = t.public_symbols.intersection(bld.env.syslib_symbols[lib])
349             if common:
350                 Logs.error("ERROR: Target '%s' has symbols '%s' which is also in syslib '%s'" % (t.sname, common, lib))
351                 has_error = True
352     if has_error:
353         raise Utils.WafError("symbols in common with system libraries")
354
355
356 def check_dependencies(bld, t):
357     '''check for depenencies that should be changed'''
358
359     if bld.name_to_obj(t.sname + ".objlist", bld.env):
360         return
361
362     targets = LOCAL_CACHE(bld, 'TARGET_TYPE')
363
364     remaining = t.undefined_symbols.copy()
365     remaining = remaining.difference(t.public_symbols)
366
367     sname = real_name(t.sname)
368
369     deps = set(t.samba_deps)
370     for d in t.samba_deps:
371         if targets[d] in [ 'EMPTY', 'DISABLED', 'SYSLIB' ]:
372             continue
373         bld.ASSERT(d in bld.env.public_symbols, "Failed to find symbol list for dependency '%s'" % d)
374         diff = remaining.intersection(bld.env.public_symbols[d])
375         if not diff and targets[sname] != 'LIBRARY':
376             Logs.info("Target '%s' has no dependency on %s" % (sname, d))
377         else:
378             remaining = remaining.difference(diff)
379
380     t.unsatisfied_symbols = set()
381     needed = {}
382     for sym in remaining:
383         if sym in bld.env.symbol_map:
384             dep = bld.env.symbol_map[sym]
385             if not dep[0] in needed:
386                 needed[dep[0]] = set()
387             needed[dep[0]].add(sym)
388         else:
389             t.unsatisfied_symbols.add(sym)
390
391     for dep in needed:
392         Logs.info("Target '%s' should add dep '%s' for symbols %s" % (sname, dep, " ".join(needed[dep])))
393
394
395
396 def check_syslib_dependencies(bld, t):
397     '''check for syslib depenencies'''
398
399     if bld.name_to_obj(t.sname + ".objlist", bld.env):
400         return
401
402     sname = real_name(t.sname)
403
404     remaining = set()
405
406     features = TO_LIST(t.features)
407     if 'pyembed' in features or 'pyext' in features:
408         if 'python' in bld.env.public_symbols:
409             t.unsatisfied_symbols = t.unsatisfied_symbols.difference(bld.env.public_symbols['python'])
410
411     needed = {}
412     for sym in t.unsatisfied_symbols:
413         if sym in bld.env.symbol_map:
414             dep = bld.env.symbol_map[sym][0]
415             if dep == 'c':
416                 continue
417             if not dep in needed:
418                 needed[dep] = set()
419             needed[dep].add(sym)
420         else:
421             remaining.add(sym)
422
423     for dep in needed:
424         Logs.info("Target '%s' should add syslib dep '%s' for symbols %s" % (sname, dep, " ".join(needed[dep])))
425
426     if remaining:
427         debug("deps: Target '%s' has unsatisfied symbols: %s" % (sname, " ".join(remaining)))
428
429
430
431 def symbols_symbolcheck(task):
432     '''check the internal dependency lists'''
433     bld = task.env.bld
434     tgt_list = get_tgt_list(bld)
435
436     build_symbol_sets(bld, tgt_list)
437     build_library_names(bld, tgt_list)
438
439     for t in tgt_list:
440         t.autodeps = set()
441         if getattr(t, 'source', ''):
442             build_autodeps(bld, t)
443
444     for t in tgt_list:
445         check_dependencies(bld, t)
446
447     for t in tgt_list:
448         check_library_deps(bld, t)
449
450 def symbols_syslibcheck(task):
451     '''check the syslib dependencies'''
452     bld = task.env.bld
453     tgt_list = get_tgt_list(bld)
454
455     build_syslib_sets(bld, tgt_list)
456     check_syslib_collisions(bld, tgt_list)
457
458     for t in tgt_list:
459         check_syslib_dependencies(bld, t)
460
461
462 def symbols_whyneeded(task):
463     """check why 'target' needs to link to 'subsystem'"""
464     bld = task.env.bld
465     tgt_list = get_tgt_list(bld)
466
467     why = Options.options.WHYNEEDED.split(":")
468     if len(why) != 2:
469         raise Utils.WafError("usage: WHYNEEDED=TARGET:DEPENDENCY")
470     target = why[0]
471     subsystem = why[1]
472
473     build_symbol_sets(bld, tgt_list)
474     build_library_names(bld, tgt_list)
475     build_syslib_sets(bld, tgt_list)
476
477     Logs.info("Checking why %s needs to link to %s" % (target, subsystem))
478     if not target in bld.env.used_symbols:
479         Logs.warn("unable to find target '%s' in used_symbols dict" % target)
480         return
481     if not subsystem in bld.env.public_symbols:
482         Logs.warn("unable to find subsystem '%s' in public_symbols dict" % subsystem)
483         return
484     overlap = bld.env.used_symbols[target].intersection(bld.env.public_symbols[subsystem])
485     if not overlap:
486         Logs.info("target '%s' doesn't use any public symbols from '%s'" % (target, subsystem))
487     else:
488         Logs.info("target '%s' uses symbols %s from '%s'" % (target, overlap, subsystem))
489
490
491 def report_duplicate(bld, binname, sym, libs):
492     '''report duplicated symbols'''
493     if sym in ['_init', '_fini']:
494         return
495     libnames = []
496     for lib in libs:
497         if lib in bld.env.library_dict:
498             libnames.append(bld.env.library_dict[lib])
499         else:
500             libnames.append(lib)
501     print("%s: Symbol %s linked in multiple libraries %s" % (binname, sym, libnames))
502
503
504 def symbols_dupcheck_binary(bld, binname):
505     '''check for duplicated symbols in one binary'''
506     libs = get_ldd_libs_recursive(bld, binname)
507
508     symlist = symbols_extract(libs, dynamic=True)
509
510     symmap = {}
511     for libpath in symlist:
512         for sym in symlist[libpath]['PUBLIC']:
513             if not sym in symmap:
514                 symmap[sym] = set()
515             symmap[sym].add(libpath)
516     for sym in symmap:
517         if len(symmap[sym]) > 1:
518             for libpath in symmap[sym]:
519                 if libpath in bld.env.library_dict:
520                     report_duplicate(bld, binname, sym, symmap[sym])
521                     break
522
523 def symbols_dupcheck(task):
524     '''check for symbols defined in two different subsystems'''
525     bld = task.env.bld
526     tgt_list = get_tgt_list(bld)
527
528     targets = LOCAL_CACHE(bld, 'TARGET_TYPE')
529
530     build_library_dict(bld, tgt_list)
531     for t in tgt_list:
532         if t.samba_type == 'BINARY':
533             binname = os_path_relpath(t.link_task.outputs[0].abspath(bld.env), os.getcwd())
534             symbols_dupcheck_binary(bld, binname)
535
536
537
538 def SYMBOL_CHECK(bld):
539     '''check our dependency lists'''
540     if Options.options.SYMBOLCHECK:
541         bld.SET_BUILD_GROUP('symbolcheck')
542         task = bld(rule=symbols_symbolcheck, always=True, name='symbol checking')
543         task.env.bld = bld
544
545         bld.SET_BUILD_GROUP('syslibcheck')
546         task = bld(rule=symbols_syslibcheck, always=True, name='syslib checking')
547         task.env.bld = bld
548
549         bld.SET_BUILD_GROUP('syslibcheck')
550         task = bld(rule=symbols_dupcheck, always=True, name='symbol duplicate checking')
551         task.env.bld = bld
552
553     if Options.options.WHYNEEDED:
554         bld.SET_BUILD_GROUP('syslibcheck')
555         task = bld(rule=symbols_whyneeded, always=True, name='check why a dependency is needed')
556         task.env.bld = bld
557
558
559 Build.BuildContext.SYMBOL_CHECK = SYMBOL_CHECK