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

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.39 - (show annotations) (download) (as text)
Sun Apr 7 19:20:09 2013 UTC (16 months, 3 weeks ago) by vapier
Branch: MAIN
Changes since 1.38: +3 -3 lines
File MIME type: text/x-python
lddtree: add GNU to the LINUX/SYSV/NONE compat set #464380 by cmuelle8

1 #!/usr/bin/python
2 # Copyright 2012-2013 Gentoo Foundation
3 # Copyright 2012-2013 Mike Frysinger <vapier@gentoo.org>
4 # Use of this source code is governed by a BSD-style license (BSD-3)
5 # pylint: disable=C0301
6 # $Header: /var/cvsroot/gentoo-projects/pax-utils/lddtree.py,v 1.38 2013/04/05 22:26:53 vapier Exp $
7
8 # TODO: Handle symlinks.
9
10 """Read the ELF dependency tree and show it
11
12 This does not work like `ldd` in that we do not execute/load code (only read
13 files on disk), and we should the ELFs as a tree rather than a flat list.
14 """
15
16 from __future__ import print_function
17
18 import glob
19 import errno
20 import optparse
21 import os
22 import shutil
23 import sys
24
25 from elftools.elf.elffile import ELFFile
26 from elftools.common import exceptions
27
28
29 def warn(msg, prefix='warning'):
30 """Write |msg| to stderr with a |prefix| before it"""
31 print('%s: %s: %s' % (sys.argv[0], prefix, msg), file=sys.stderr)
32
33
34 def err(msg, status=1):
35 """Write |msg| to stderr and exit with |status|"""
36 warn(msg, prefix='error')
37 sys.exit(status)
38
39
40 def bstr(buf):
41 """Decode the byte string into a string"""
42 return buf.decode('utf-8')
43
44
45 def normpath(path):
46 """Normalize a path
47
48 Python's os.path.normpath() doesn't handle some cases:
49 // -> //
50 //..// -> //
51 //..//..// -> ///
52 """
53 return os.path.normpath(path).replace('//', '/')
54
55
56 def makedirs(path):
57 """Like os.makedirs(), but ignore EEXIST errors"""
58 try:
59 os.makedirs(path)
60 except OSError as e:
61 if e.errno != os.errno.EEXIST:
62 raise
63
64
65 def dedupe(items):
66 """Remove all duplicates from |items| (keeping order)"""
67 seen = {}
68 return [seen.setdefault(x, x) for x in items if x not in seen]
69
70
71 def GenerateLdsoWrapper(root, path, interp, libpaths=()):
72 """Generate a shell script wrapper which uses local ldso to run the ELF
73
74 Since we cannot rely on the host glibc (or other libraries), we need to
75 execute the local packaged ldso directly and tell it where to find our
76 copies of libraries.
77
78 Args:
79 root: The root tree to generate scripts inside of
80 path: The full path (inside |root|) to the program to wrap
81 interp: The ldso interpreter that we need to execute
82 libpaths: Extra lib paths to search for libraries
83 """
84 basedir = os.path.dirname(path)
85 interp_dir, interp_name = os.path.split(interp)
86 libpaths = dedupe([interp_dir] + list(libpaths))
87 replacements = {
88 'interp': os.path.join(os.path.relpath(interp_dir, basedir),
89 interp_name),
90 'libpaths': ':'.join(['${basedir}/' + os.path.relpath(p, basedir)
91 for p in libpaths]),
92 }
93 wrapper = """#!/bin/sh
94 if ! base=$(realpath "$0" 2>/dev/null); then
95 case $0 in
96 /*) base=$0;;
97 *) base=${PWD:-`pwd`}/$0;;
98 esac
99 fi
100 basedir=${base%%/*}
101 exec \
102 "${basedir}/%(interp)s" \
103 --library-path "%(libpaths)s" \
104 --inhibit-rpath '' \
105 "${base}.elf" \
106 "$@"
107 """
108 wrappath = root + path
109 os.rename(wrappath, wrappath + '.elf')
110 with open(wrappath, 'w') as f:
111 f.write(wrapper % replacements)
112 os.chmod(wrappath, 0o0755)
113
114
115 def ParseLdPaths(str_ldpaths, root='', path=None):
116 """Parse the colon-delimited list of paths and apply ldso rules to each
117
118 Note the special handling as dictated by the ldso:
119 - Empty paths are equivalent to $PWD
120 - $ORIGIN is expanded to the path of the given file
121 - (TODO) $LIB and friends
122
123 Args:
124 str_ldpath: A colon-delimited string of paths
125 root: The path to prepend to all paths found
126 Returns:
127 list of processed paths
128 """
129 ldpaths = []
130 for ldpath in str_ldpaths.split(':'):
131 if ldpath == '':
132 # The ldso treats "" paths as $PWD.
133 ldpath = os.getcwd()
134 else:
135 ldpath = ldpath.replace('$ORIGIN', os.path.dirname(path))
136 ldpaths.append(normpath(root + ldpath))
137 return dedupe(ldpaths)
138
139
140 def ParseLdSoConf(ldso_conf, root='/', _first=True):
141 """Load all the paths from a given ldso config file
142
143 This should handle comments, whitespace, and "include" statements.
144
145 Args:
146 ldso_conf: The file to scan
147 root: The path to prepend to all paths found
148 _first: Recursive use only; is this the first ELF ?
149 Returns:
150 list of paths found
151 """
152 paths = []
153
154 try:
155 with open(ldso_conf) as f:
156 for line in f.readlines():
157 line = line.split('#', 1)[0].strip()
158 if not line:
159 continue
160 if line.startswith('include '):
161 line = line[8:]
162 if line[0] == '/':
163 line = root + line.lstrip('/')
164 else:
165 line = os.path.dirname(ldso_conf) + '/' + line
166 for path in glob.glob(line):
167 paths += ParseLdSoConf(path, root=root, _first=False)
168 else:
169 paths += [normpath(root + line)]
170 except IOError as e:
171 if e.errno != errno.ENOENT:
172 warn(e)
173
174 if _first:
175 # XXX: Load paths from ldso itself.
176 # Remove duplicate entries to speed things up.
177 paths = dedupe(paths)
178
179 return paths
180
181
182 def LoadLdpaths(root='/'):
183 """Load linker paths from common locations
184
185 This parses the ld.so.conf and LD_LIBRARY_PATH env var.
186
187 Args:
188 root: The root tree to prepend to paths
189 Returns:
190 dict containing library paths to search
191 """
192 ldpaths = {
193 'conf': [],
194 'env': [],
195 'interp': [],
196 }
197
198 # Load up $LD_LIBRARY_PATH.
199 ldpaths['env'] = []
200 env_ldpath = os.environ.get('LD_LIBRARY_PATH')
201 if not env_ldpath is None:
202 if root != '/':
203 warn('ignoring LD_LIBRARY_PATH due to ROOT usage')
204 else:
205 # XXX: If this contains $ORIGIN, we probably have to parse this
206 # on a per-ELF basis so it can get turned into the right thing.
207 ldpaths['env'] = ParseLdPaths(env_ldpath, path='')
208
209 # Load up /etc/ld.so.conf.
210 ldpaths['conf'] = ParseLdSoConf(root + 'etc/ld.so.conf', root=root)
211
212 return ldpaths
213
214
215 def CompatibleELFs(elf1, elf2):
216 """See if two ELFs are compatible
217
218 This compares the aspects of the ELF to see if they're compatible:
219 bit size, endianness, machine type, and operating system.
220
221 Args:
222 elf1: an ELFFile object
223 elf2: an ELFFile object
224 Returns:
225 True if compatible, False otherwise
226 """
227 osabis = frozenset([e.header['e_ident']['EI_OSABI'] for e in (elf1, elf2)])
228 compat_sets = (
229 frozenset('ELFOSABI_%s' % x for x in ('NONE', 'SYSV', 'GNU', 'LINUX',)),
230 )
231 return ((len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) and
232 elf1.elfclass == elf2.elfclass and
233 elf1.little_endian == elf2.little_endian and
234 elf1.header['e_machine'] == elf2.header['e_machine'])
235
236
237 def FindLib(elf, lib, ldpaths):
238 """Try to locate a |lib| that is compatible to |elf| in the given |ldpaths|
239
240 Args:
241 elf: the elf which the library should be compatible with (ELF wise)
242 lib: the library (basename) to search for
243 ldpaths: a list of paths to search
244 Returns:
245 the full path to the desired library
246 """
247 for ldpath in ldpaths:
248 path = os.path.join(ldpath, lib)
249 if os.path.exists(path):
250 with open(path, 'rb') as f:
251 libelf = ELFFile(f)
252 if CompatibleELFs(elf, libelf):
253 return path
254 return None
255
256
257 def ParseELF(path, root='/', ldpaths={'conf':[], 'env':[], 'interp':[]},
258 _first=True, _all_libs={}):
259 """Parse the ELF dependency tree of the specified file
260
261 Args:
262 path: The ELF to scan
263 root: The root tree to prepend to paths; this applies to interp and rpaths
264 only as |path| and |ldpaths| are expected to be prefixed already
265 ldpaths: dict containing library paths to search; should have the keys:
266 conf, env, interp
267 _first: Recursive use only; is this the first ELF ?
268 _all_libs: Recursive use only; dict of all libs we've seen
269 Returns:
270 a dict containing information about all the ELFs; e.g.
271 {
272 'interp': '/lib64/ld-linux.so.2',
273 'needed': ['libc.so.6', 'libcurl.so.4',],
274 'libs': {
275 'libc.so.6': {
276 'path': '/lib64/libc.so.6',
277 'needed': [],
278 },
279 'libcurl.so.4': {
280 'path': '/usr/lib64/libcurl.so.4',
281 'needed': ['libc.so.6', 'librt.so.1',],
282 },
283 },
284 }
285 """
286 if _first:
287 _all_libs = {}
288 ldpaths = ldpaths.copy()
289 ret = {
290 'interp': None,
291 'path': path,
292 'needed': [],
293 'rpath': [],
294 'runpath': [],
295 'libs': _all_libs,
296 }
297
298 with open(path, 'rb') as f:
299 elf = ELFFile(f)
300
301 # If this is the first ELF, extract the interpreter.
302 if _first:
303 for segment in elf.iter_segments():
304 if segment.header.p_type != 'PT_INTERP':
305 continue
306
307 interp = bstr(segment.get_interp_name())
308 ret['interp'] = normpath(root + interp)
309 ret['libs'][os.path.basename(interp)] = {
310 'path': ret['interp'],
311 'needed': [],
312 }
313 # XXX: Should read it and scan for /lib paths.
314 ldpaths['interp'] = [
315 normpath(root + os.path.dirname(interp)),
316 normpath(root + '/usr' + os.path.dirname(interp)),
317 ]
318 break
319
320 # Parse the ELF's dynamic tags.
321 libs = []
322 rpaths = []
323 runpaths = []
324 for segment in elf.iter_segments():
325 if segment.header.p_type != 'PT_DYNAMIC':
326 continue
327
328 for t in segment.iter_tags():
329 if t.entry.d_tag == 'DT_RPATH':
330 rpaths = ParseLdPaths(bstr(t.rpath), root=root, path=path)
331 elif t.entry.d_tag == 'DT_RUNPATH':
332 runpaths = ParseLdPaths(bstr(t.runpath), root=root, path=path)
333 elif t.entry.d_tag == 'DT_NEEDED':
334 libs.append(bstr(t.needed))
335 if runpaths:
336 # If both RPATH and RUNPATH are set, only the latter is used.
337 rpaths = []
338
339 # XXX: We assume there is only one PT_DYNAMIC. This is
340 # probably fine since the runtime ldso does the same.
341 break
342 if _first:
343 # Propagate the rpaths used by the main ELF since those will be
344 # used at runtime to locate things.
345 ldpaths['rpath'] = rpaths
346 ldpaths['runpath'] = runpaths
347 ret['rpath'] = rpaths
348 ret['runpath'] = runpaths
349 ret['needed'] = libs
350
351 # Search for the libs this ELF uses.
352 all_ldpaths = None
353 for lib in libs:
354 if lib in _all_libs:
355 continue
356 if all_ldpaths is None:
357 all_ldpaths = rpaths + ldpaths['rpath'] + ldpaths['env'] + runpaths + ldpaths['runpath'] + ldpaths['conf'] + ldpaths['interp']
358 fullpath = FindLib(elf, lib, all_ldpaths)
359 _all_libs[lib] = {
360 'path': fullpath,
361 'needed': [],
362 }
363 if fullpath:
364 lret = ParseELF(fullpath, root, ldpaths, False, _all_libs)
365 _all_libs[lib]['needed'] = lret['needed']
366
367 del elf
368
369 return ret
370
371
372 def _NormalizePath(option, _opt, value, parser):
373 setattr(parser.values, option.dest, normpath(value))
374
375
376 def _ShowVersion(_option, _opt, _value, _parser):
377 d = '$Id: lddtree.py,v 1.38 2013/04/05 22:26:53 vapier Exp $'.split()
378 print('%s-%s %s %s' % (d[1].split('.')[0], d[2], d[3], d[4]))
379 sys.exit(0)
380
381
382 def _ActionShow(options, elf):
383 """Show the dependency tree for this ELF"""
384 def _show(lib, depth):
385 chain_libs.append(lib)
386 fullpath = elf['libs'][lib]['path']
387 if options.list:
388 print(fullpath or lib)
389 else:
390 print('%s%s => %s' % (' ' * depth, lib, fullpath))
391
392 new_libs = []
393 for lib in elf['libs'][lib]['needed']:
394 if lib in chain_libs:
395 if not options.list:
396 print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
397 continue
398 if options.all or not lib in shown_libs:
399 shown_libs.add(lib)
400 new_libs.append(lib)
401
402 for lib in new_libs:
403 _show(lib, depth + 1)
404 chain_libs.pop()
405
406 shown_libs = set(elf['needed'])
407 chain_libs = []
408 interp = elf['interp']
409 if interp:
410 shown_libs.add(os.path.basename(interp))
411 if options.list:
412 print(elf['path'])
413 if not interp is None:
414 print(interp)
415 else:
416 print('%s (interpreter => %s)' % (elf['path'], interp))
417 for lib in elf['needed']:
418 _show(lib, 1)
419
420
421 def _ActionCopy(options, elf):
422 """Copy the ELF and its dependencies to a destination tree"""
423 def _StripRoot(path):
424 return path[len(options.root) - 1:]
425
426 def _copy(src, striproot=True, wrapit=False, libpaths=(), outdir=None):
427 if src is None:
428 return
429
430 if wrapit:
431 # Static ELFs don't need to be wrapped.
432 if not elf['interp']:
433 wrapit = False
434
435 striproot = _StripRoot if striproot else lambda x: x
436
437 if outdir:
438 subdst = os.path.join(outdir, os.path.basename(src))
439 else:
440 subdst = striproot(src)
441 dst = options.dest + subdst
442
443 try:
444 # See if they're the same file.
445 nstat = os.stat(dst + ('.elf' if wrapit else ''))
446 ostat = os.stat(src)
447 for field in ('mode', 'mtime', 'size'):
448 if getattr(ostat, 'st_' + field) != \
449 getattr(nstat, 'st_' + field):
450 break
451 else:
452 return
453 except OSError as e:
454 if e.errno != errno.ENOENT:
455 raise
456
457 if options.verbose:
458 print('%s -> %s' % (src, dst))
459
460 makedirs(os.path.dirname(dst))
461 try:
462 shutil.copy2(src, dst)
463 except IOError:
464 os.unlink(dst)
465 shutil.copy2(src, dst)
466
467 if wrapit:
468 if options.verbose:
469 print('generate wrapper %s' % (dst,))
470
471 if options.libdir:
472 interp = os.path.join(options.libdir, os.path.basename(elf['interp']))
473 else:
474 interp = _StripRoot(elf['interp'])
475 GenerateLdsoWrapper(options.dest, subdst, interp, libpaths)
476
477 # XXX: We should automatically import libgcc_s.so whenever libpthread.so
478 # is copied over (since we know it can be dlopen-ed by NPTL at runtime).
479 # Similarly, we should provide an option for automatically copying over
480 # the libnsl.so and libnss_*.so libraries, as well as an open ended list
481 # for known libs that get loaded (e.g. curl will dlopen(libresolv)).
482 libpaths = set()
483 for lib in elf['libs']:
484 path = elf['libs'][lib]['path']
485 if not options.libdir:
486 libpaths.add(_StripRoot(os.path.dirname(path)))
487 _copy(path, outdir=options.libdir)
488
489 if not options.libdir:
490 libpaths = list(libpaths)
491 if elf['runpath']:
492 libpaths = elf['runpath'] + libpaths
493 else:
494 libpaths = elf['rpath'] + libpaths
495 else:
496 libpaths.add(options.libdir)
497
498 _copy(elf['interp'], outdir=options.libdir)
499 _copy(elf['path'], striproot=options.auto_root,
500 wrapit=options.generate_wrappers, libpaths=libpaths,
501 outdir=options.bindir)
502
503
504 def main(argv):
505 parser = optparse.OptionParser("""%prog [options] <ELFs>
506
507 Display ELF dependencies as a tree
508
509 <ELFs> can be globs that lddtree will take care of expanding.
510 Useful when you want to glob a path under the ROOT path.
511
512 When using the --root option, all paths are implicitly prefixed by that.
513 e.g. lddtree -R /my/magic/root /bin/bash
514 This will load up the ELF found at /my/magic/root/bin/bash and then resolve
515 all libraries via that path. If you wish to actually read /bin/bash (and
516 so use the ROOT path as an alternative library tree), you can specify the
517 --no-auto-root option.
518
519 When pairing --root with --copy-to-tree, the ROOT path will be stripped.
520 e.g. lddtree -R /my/magic/root --copy-to-tree /foo /bin/bash
521 You will see /foo/bin/bash and /foo/lib/libc.so.6 and not paths like
522 /foo/my/magic/root/bin/bash. If you want that, you'll have to manually
523 add the ROOT path to the output path.
524
525 The --bindir and --libdir flags are used to normalize the output subdirs
526 when used with --copy-to-tree.
527 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof
528 This will mirror the input paths in the output. So you will end up with
529 /foo/bin/bash and /foo/usr/sbin/lspci and /foo/usr/bin/lsof. Similarly,
530 the libraries needed will be scattered among /foo/lib/ and /foo/usr/lib/
531 and perhaps other paths (like /foo/lib64/ and /usr/lib/gcc/...). You can
532 collapse all that down into nice directory structure.
533 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof \\
534 --bindir /bin --libdir /lib
535 This will place bash, lspci, and lsof into /foo/bin/. All the libraries
536 they need will be placed into /foo/lib/ only.""")
537 parser.add_option('-a', '--all',
538 action='store_true', default=False,
539 help='Show all duplicated dependencies')
540 parser.add_option('-R', '--root',
541 default=os.environ.get('ROOT', ''), type='string',
542 action='callback', callback=_NormalizePath,
543 help='Search for all files/dependencies in ROOT')
544 parser.add_option('--no-auto-root',
545 dest='auto_root', action='store_false', default=True,
546 help='Do not automatically prefix input ELFs with ROOT')
547 parser.add_option('-l', '--list',
548 action='store_true', default=False,
549 help='Display output in a simple list (easy for copying)')
550 parser.add_option('-x', '--debug',
551 action='store_true', default=False,
552 help='Run with debugging')
553 parser.add_option('-v', '--verbose',
554 action='store_true', default=False,
555 help='Be verbose')
556 parser.add_option('-V', '--version',
557 action='callback', callback=_ShowVersion,
558 help='Show version information')
559
560 group = optparse.OptionGroup(parser, 'Copying options')
561 group.add_option('--copy-to-tree',
562 dest='dest', default=None, type='string',
563 action='callback', callback=_NormalizePath,
564 help='Copy all files to the specified tree')
565 group.add_option('--bindir',
566 default=None, type='string',
567 action='callback', callback=_NormalizePath,
568 help='Dir to store all ELFs specified on the command line')
569 group.add_option('--libdir',
570 default=None, type='string',
571 action='callback', callback=_NormalizePath,
572 help='Dir to store all ELF libs')
573 group.add_option('--generate-wrappers',
574 action='store_true', default=False,
575 help='Wrap executable ELFs with scripts for local ldso')
576 group.add_option('--copy-non-elfs',
577 action='store_true', default=False,
578 help='Copy over plain (non-ELF) files instead of warn+ignore')
579 parser.add_option_group(group)
580
581 (options, paths) = parser.parse_args(argv)
582
583 if options.root != '/':
584 options.root += '/'
585
586 if options.bindir and options.bindir[0] != '/':
587 parser.error('--bindir accepts absolute paths only')
588 if options.libdir and options.libdir[0] != '/':
589 parser.error('--libdir accepts absolute paths only')
590
591 if options.debug:
592 print('root =', options.root)
593 if options.dest:
594 print('dest =', options.dest)
595 if not paths:
596 err('missing ELF files to scan')
597
598 ldpaths = LoadLdpaths(options.root)
599 if options.debug:
600 print('ldpaths[conf] =', ldpaths['conf'])
601 print('ldpaths[env] =', ldpaths['env'])
602
603 # Process all the files specified.
604 ret = 0
605 for path in paths:
606 # Only auto-prefix the path if the ELF is absolute.
607 # If it's a relative path, the user most likely wants
608 # the local path.
609 if options.auto_root and path.startswith('/'):
610 path = options.root + path.lstrip('/')
611
612 matched = False
613 for p in glob.iglob(path):
614 matched = True
615 try:
616 elf = ParseELF(p, options.root, ldpaths)
617 except (exceptions.ELFError, IOError) as e:
618 # XXX: Ugly. Should unify with _Action* somehow.
619 if options.dest is not None and options.copy_non_elfs:
620 if os.path.exists(p):
621 elf = {
622 'interp': None,
623 'libs': [],
624 'runpath': [],
625 'rpath': [],
626 'path': p,
627 }
628 _ActionCopy(options, elf)
629 continue
630 ret = 1
631 warn('%s: %s' % (p, e))
632 continue
633
634 if options.dest is None:
635 _ActionShow(options, elf)
636 else:
637 _ActionCopy(options, elf)
638
639 if not matched:
640 ret = 1
641 warn('%s: did not match any paths' % (path,))
642
643 return ret
644
645
646 if __name__ == '__main__':
647 sys.exit(main(sys.argv[1:]))

  ViewVC Help
Powered by ViewVC 1.1.20