/[gentoo-projects]/pax-utils/lddtree.py
Gentoo

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.18 - (hide annotations) (download) (as text)
Sat Jan 5 20:39:56 2013 UTC (20 months, 1 week ago) by vapier
Branch: MAIN
Changes since 1.17: +15 -3 lines
File MIME type: text/x-python
lddtree.py: leverage the rpaths from the main executable to better locate libs that get used

1 vapier 1.1 #!/usr/bin/python
2     # Copyright 2012 Gentoo Foundation
3     # Copyright 2012 Mike Frysinger <vapier@gentoo.org>
4     # Distributed under the terms of the GNU General Public License v2
5 vapier 1.18 # $Header: /var/cvsroot/gentoo-projects/pax-utils/lddtree.py,v 1.17 2012/12/14 04:22:52 vapier Exp $
6 vapier 1.1
7     """Read the ELF dependency tree and show it
8    
9     This does not work like `ldd` in that we do not execute/load code (only read
10     files on disk), and we should the ELFs as a tree rather than a flat list.
11     """
12    
13     from __future__ import print_function
14    
15     import glob
16 vapier 1.10 import errno
17 vapier 1.1 import optparse
18     import os
19 vapier 1.5 import shutil
20 vapier 1.1 import sys
21    
22     from elftools.elf.elffile import ELFFile
23     from elftools.common import exceptions
24    
25    
26     def warn(msg, prefix='warning'):
27     """Write |msg| to stderr with a |prefix| before it"""
28     print('%s: %s: %s' % (sys.argv[0], prefix, msg), file=sys.stderr)
29    
30    
31     def err(msg, status=1):
32     """Write |msg| to stderr and exit with |status|"""
33     warn(msg, prefix='error')
34     sys.exit(status)
35    
36    
37     def normpath(path):
38     """Normalize a path
39    
40     Python's os.path.normpath() doesn't handle some cases:
41     // -> //
42     //..// -> //
43     //..//..// -> ///
44     """
45     return os.path.normpath(path).replace('//', '/')
46    
47    
48 vapier 1.14 def dedupe(input):
49     """Remove all duplicates from input (keeping order)"""
50     seen = {}
51     return [seen.setdefault(x, x) for x in input if x not in seen]
52    
53    
54 vapier 1.9 def ParseLdPaths(str_ldpaths, root='', path=None):
55 vapier 1.1 """Parse the colon-delimited list of paths and apply ldso rules to each
56    
57     Note the special handling as dictated by the ldso:
58     - Empty paths are equivalent to $PWD
59 vapier 1.9 - $ORIGIN is expanded to the path of the given file
60 vapier 1.6 - (TODO) $LIB and friends
61 vapier 1.1
62     Args:
63     str_ldpath: A colon-delimited string of paths
64     root: The path to prepend to all paths found
65     Returns:
66     list of processed paths
67     """
68     ldpaths = []
69     for ldpath in str_ldpaths.split(':'):
70     if ldpath == '':
71     # The ldso treats "" paths as $PWD.
72     ldpath = os.getcwd()
73 vapier 1.13 else:
74     ldpath = ldpath.replace('$ORIGIN', os.path.dirname(path))
75 vapier 1.14 ldpaths.append(normpath(root + ldpath))
76     return dedupe(ldpaths)
77 vapier 1.1
78    
79     def ParseLdSoConf(ldso_conf, root='/', _first=True):
80     """Load all the paths from a given ldso config file
81    
82     This should handle comments, whitespace, and "include" statements.
83    
84     Args:
85     ldso_conf: The file to scan
86     root: The path to prepend to all paths found
87     _first: Recursive use only; is this the first ELF ?
88     Returns:
89     list of paths found
90     """
91     paths = []
92    
93     try:
94     with open(ldso_conf) as f:
95     for line in f.readlines():
96     line = line.split('#', 1)[0].strip()
97     if not line:
98     continue
99     if line.startswith('include '):
100     line = line[8:]
101     if line[0] == '/':
102     line = root + line.lstrip('/')
103     else:
104     line = os.path.dirname(ldso_conf) + '/' + line
105 vapier 1.7 for path in glob.glob(line):
106     paths += ParseLdSoConf(path, root=root, _first=False)
107 vapier 1.1 else:
108     paths += [normpath(root + line)]
109 vapier 1.10 except IOError as e:
110     if e.errno != errno.ENOENT:
111     warn(e)
112 vapier 1.1
113     if _first:
114 vapier 1.14 # XXX: Load paths from ldso itself.
115 vapier 1.1 # Remove duplicate entries to speed things up.
116 vapier 1.14 paths = dedupe(paths)
117 vapier 1.1
118     return paths
119    
120    
121 vapier 1.6 def LoadLdpaths(root='/'):
122     """Load linker paths from common locations
123    
124     This parses the ld.so.conf and LD_LIBRARY_PATH env var.
125    
126     Args:
127     root: The root tree to prepend to paths
128     Returns:
129     dict containing library paths to search
130     """
131     ldpaths = {
132     'conf': [],
133     'env': [],
134     'interp': [],
135     }
136    
137     # Load up $LD_LIBRARY_PATH.
138     ldpaths['env'] = []
139     env_ldpath = os.environ.get('LD_LIBRARY_PATH')
140     if not env_ldpath is None:
141     if root != '/':
142     warn('ignoring LD_LIBRARY_PATH due to ROOT usage')
143     else:
144 vapier 1.13 # XXX: If this contains $ORIGIN, we probably have to parse this
145     # on a per-ELF basis so it can get turned into the right thing.
146     ldpaths['env'] = ParseLdPaths(env_ldpath, path='')
147 vapier 1.6
148     # Load up /etc/ld.so.conf.
149     ldpaths['conf'] = ParseLdSoConf(root + 'etc/ld.so.conf', root=root)
150    
151     return ldpaths
152    
153    
154 vapier 1.1 def CompatibleELFs(elf1, elf2):
155     """See if two ELFs are compatible
156    
157     This compares the aspects of the ELF to see if they're compatible:
158     bit size, endianness, machine type, and operating system.
159    
160     Args:
161     elf1: an ELFFile object
162     elf2: an ELFFile object
163     Returns:
164     True if compatible, False otherwise
165     """
166 vapier 1.16 osabis = frozenset([e.header['e_ident']['EI_OSABI'] for e in (elf1, elf2)])
167     compat_sets = (
168     frozenset(['ELFOSABI_NONE', 'ELFOSABI_SYSV', 'ELFOSABI_LINUX']),
169     )
170     return ((len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) and
171     elf1.elfclass == elf2.elfclass and
172     elf1.little_endian == elf2.little_endian and
173     elf1.header['e_machine'] == elf2.header['e_machine'])
174 vapier 1.1
175    
176     def FindLib(elf, lib, ldpaths):
177     """Try to locate a |lib| that is compatible to |elf| in the given |ldpaths|
178    
179     Args:
180     elf: the elf which the library should be compatible with (ELF wise)
181     lib: the library (basename) to search for
182     ldpaths: a list of paths to search
183     Returns:
184     the full path to the desired library
185     """
186     for ldpath in ldpaths:
187     path = os.path.join(ldpath, lib)
188     if os.path.exists(path):
189     with open(path) as f:
190     libelf = ELFFile(f)
191     if CompatibleELFs(elf, libelf):
192     return path
193     return None
194    
195    
196 vapier 1.7 def ParseELF(path, root='/', ldpaths={'conf':[], 'env':[], 'interp':[]},
197 vapier 1.1 _first=True, _all_libs={}):
198     """Parse the ELF dependency tree of the specified file
199    
200     Args:
201 vapier 1.7 path: The ELF to scan
202 vapier 1.6 root: The root tree to prepend to paths; this applies to interp and rpaths
203 vapier 1.7 only as |path| and |ldpaths| are expected to be prefixed already
204 vapier 1.1 ldpaths: dict containing library paths to search; should have the keys:
205     conf, env, interp
206     _first: Recursive use only; is this the first ELF ?
207     _all_libs: Recursive use only; dict of all libs we've seen
208     Returns:
209     a dict containing information about all the ELFs; e.g.
210     {
211     'interp': '/lib64/ld-linux.so.2',
212     'needed': ['libc.so.6', 'libcurl.so.4',],
213     'libs': {
214     'libc.so.6': {
215     'path': '/lib64/libc.so.6',
216     'needed': [],
217     },
218     'libcurl.so.4': {
219     'path': '/usr/lib64/libcurl.so.4',
220     'needed': ['libc.so.6', 'librt.so.1',],
221     },
222     },
223 vapier 1.7 }
224 vapier 1.1 """
225 vapier 1.12 if _first:
226     _all_libs = {}
227 vapier 1.18 ldpaths = ldpaths.copy()
228 vapier 1.1 ret = {
229     'interp': None,
230 vapier 1.7 'path': path,
231 vapier 1.1 'needed': [],
232 vapier 1.18 'rpath': [],
233     'runpath': [],
234 vapier 1.1 'libs': _all_libs,
235     }
236    
237 vapier 1.7 with open(path) as f:
238 vapier 1.1 elf = ELFFile(f)
239    
240     # If this is the first ELF, extract the interpreter.
241     if _first:
242     for segment in elf.iter_segments():
243     if segment.header.p_type != 'PT_INTERP':
244     continue
245    
246 vapier 1.8 interp = segment.get_interp_name()
247     ret['interp'] = normpath(root + interp)
248 vapier 1.1 ret['libs'][os.path.basename(interp)] = {
249 vapier 1.8 'path': ret['interp'],
250 vapier 1.1 'needed': [],
251     }
252     # XXX: Should read it and scan for /lib paths.
253     ldpaths['interp'] = [
254     normpath(root + os.path.dirname(interp)),
255     normpath(root + '/usr' + os.path.dirname(interp)),
256     ]
257     break
258    
259     # Parse the ELF's dynamic tags.
260     libs = []
261     rpaths = []
262     runpaths = []
263     for segment in elf.iter_segments():
264     if segment.header.p_type != 'PT_DYNAMIC':
265     continue
266    
267     for t in segment.iter_tags():
268     if t.entry.d_tag == 'DT_RPATH':
269 vapier 1.9 rpaths = ParseLdPaths(t.rpath, root=root, path=path)
270 vapier 1.1 elif t.entry.d_tag == 'DT_RUNPATH':
271 vapier 1.9 runpaths = ParseLdPaths(t.runpath, root=root, path=path)
272 vapier 1.1 elif t.entry.d_tag == 'DT_NEEDED':
273     libs.append(t.needed)
274     if runpaths:
275     # If both RPATH and RUNPATH are set, only the latter is used.
276 vapier 1.17 rpaths = []
277 vapier 1.1
278 vapier 1.18 # XXX: We assume there is only one PT_DYNAMIC. This is
279     # probably fine since the runtime ldso does the same.
280 vapier 1.1 break
281 vapier 1.18 if _first:
282     # Propagate the rpaths used by the main ELF since those will be
283     # used at runtime to locate things.
284     ldpaths['rpath'] = rpaths
285     ldpaths['runpath'] = runpaths
286     ret['rpath'] = rpaths
287     ret['runpath'] = runpaths
288 vapier 1.1 ret['needed'] = libs
289    
290     # Search for the libs this ELF uses.
291     all_ldpaths = None
292     for lib in libs:
293     if lib in _all_libs:
294     continue
295     if all_ldpaths is None:
296 vapier 1.18 all_ldpaths = rpaths + ldpaths['rpath'] + ldpaths['env'] + runpaths + ldpaths['runpath'] + ldpaths['conf'] + ldpaths['interp']
297 vapier 1.1 fullpath = FindLib(elf, lib, all_ldpaths)
298     _all_libs[lib] = {
299     'path': fullpath,
300     'needed': [],
301     }
302     if fullpath:
303     lret = ParseELF(fullpath, root, ldpaths, False, _all_libs)
304     _all_libs[lib]['needed'] = lret['needed']
305    
306     del elf
307    
308     return ret
309    
310    
311 vapier 1.5 def _NormalizePath(option, _opt, value, parser):
312     setattr(parser.values, option.dest, normpath(value))
313 vapier 1.1
314    
315 vapier 1.2 def _ShowVersion(_option, _opt, _value, _parser):
316 vapier 1.18 id = '$Id: lddtree.py,v 1.17 2012/12/14 04:22:52 vapier Exp $'.split()
317 vapier 1.2 print('%s-%s %s %s' % (id[1].split('.')[0], id[2], id[3], id[4]))
318     sys.exit(0)
319    
320    
321 vapier 1.5 def _ActionShow(options, elf):
322     """Show the dependency tree for this ELF"""
323     def _show(lib, depth):
324     chain_libs.append(lib)
325     fullpath = elf['libs'][lib]['path']
326     if options.list:
327     print(fullpath or lib)
328     else:
329     print('%s%s => %s' % (' ' * depth, lib, fullpath))
330    
331     new_libs = []
332     for lib in elf['libs'][lib]['needed']:
333     if lib in chain_libs:
334     if not options.list:
335     print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
336     continue
337     if options.all or not lib in shown_libs:
338     shown_libs.add(lib)
339     new_libs.append(lib)
340    
341     for lib in new_libs:
342     _show(lib, depth + 1)
343     chain_libs.pop()
344    
345     shown_libs = set(elf['needed'])
346     chain_libs = []
347     interp = elf['interp']
348     if interp:
349     shown_libs.add(os.path.basename(interp))
350     if options.list:
351     print(elf['path'])
352     if not interp is None:
353     print(interp)
354     else:
355     print('%s (interpreter => %s)' % (elf['path'], interp))
356     for lib in elf['needed']:
357     _show(lib, 1)
358    
359    
360     def _ActionCopy(options, elf):
361     """Copy the ELF and its dependencies to a destination tree"""
362 vapier 1.7 def _copy(src):
363     if src is None:
364 vapier 1.5 return
365    
366 vapier 1.7 dst = options.dest + src
367     if os.path.exists(dst):
368 vapier 1.5 # See if they're the same file.
369 vapier 1.7 ostat = os.stat(src)
370     nstat = os.stat(dst)
371 vapier 1.5 for field in ('mode', 'mtime', 'size'):
372     if getattr(ostat, 'st_' + field) != \
373     getattr(nstat, 'st_' + field):
374     break
375     else:
376     return
377    
378     if options.verbose:
379 vapier 1.7 print('%s -> %s' % (src, dst))
380 vapier 1.5
381     try:
382 vapier 1.7 os.makedirs(os.path.dirname(dst))
383 vapier 1.5 except OSError as e:
384     if e.errno != os.errno.EEXIST:
385     raise
386     try:
387 vapier 1.7 shutil.copy2(src, dst)
388 vapier 1.5 return
389     except IOError:
390 vapier 1.7 os.unlink(dst)
391     shutil.copy2(src, dst)
392 vapier 1.5
393     _copy(elf['path'])
394     _copy(elf['interp'])
395     for lib in elf['libs']:
396     _copy(elf['libs'][lib]['path'])
397    
398    
399 vapier 1.1 def main(argv):
400     parser = optparse.OptionParser("""%prog [options] <ELFs>
401    
402     Display ELF dependencies as a tree""")
403     parser.add_option('-a', '--all',
404     action='store_true', default=False,
405     help=('Show all duplicated dependencies'))
406     parser.add_option('-R', '--root',
407     dest='root', default=os.environ.get('ROOT', ''), type='string',
408 vapier 1.5 action='callback', callback=_NormalizePath,
409 vapier 1.1 help=('Show all duplicated dependencies'))
410 vapier 1.5 parser.add_option('--copy-to-tree',
411     dest='dest', default=None, type='string',
412     action='callback', callback=_NormalizePath,
413     help=('Copy all files to the specified tree'))
414 vapier 1.3 parser.add_option('-l', '--list',
415     action='store_true', default=False,
416     help=('Display output in a simple list (easy for copying)'))
417 vapier 1.1 parser.add_option('-x', '--debug',
418     action='store_true', default=False,
419     help=('Run with debugging'))
420 vapier 1.5 parser.add_option('-v', '--verbose',
421     action='store_true', default=False,
422     help=('Be verbose'))
423 vapier 1.2 parser.add_option('-V', '--version',
424     action='callback', callback=_ShowVersion,
425     help=('Show version information'))
426 vapier 1.7 (options, paths) = parser.parse_args(argv)
427 vapier 1.1
428 vapier 1.7 # Throw away argv[0].
429     paths.pop(0)
430 vapier 1.5 if options.root != '/':
431     options.root += '/'
432 vapier 1.1
433     if options.debug:
434     print('root =', options.root)
435 vapier 1.5 if options.dest:
436     print('dest =', options.dest)
437 vapier 1.7 if not paths:
438 vapier 1.1 err('missing ELF files to scan')
439    
440 vapier 1.6 ldpaths = LoadLdpaths(options.root)
441 vapier 1.1 if options.debug:
442     print('ldpaths[conf] =', ldpaths['conf'])
443     print('ldpaths[env] =', ldpaths['env'])
444    
445 vapier 1.5 # Process all the files specified.
446 vapier 1.1 ret = 0
447 vapier 1.7 for path in paths:
448 vapier 1.1 try:
449 vapier 1.7 elf = ParseELF(path, options.root, ldpaths)
450 vapier 1.1 except (exceptions.ELFError, IOError) as e:
451     ret = 1
452 vapier 1.7 warn('%s: %s' % (path, e))
453 vapier 1.1 continue
454 vapier 1.5 if options.dest is None:
455     _ActionShow(options, elf)
456 vapier 1.3 else:
457 vapier 1.5 _ActionCopy(options, elf)
458 vapier 1.1 return ret
459    
460    
461     if __name__ == '__main__':
462     sys.exit(main(sys.argv))

  ViewVC Help
Powered by ViewVC 1.1.20