aboutsummaryrefslogtreecommitdiffstats
path: root/checklibs.py
diff options
context:
space:
mode:
authorChristian Tismer <ctismer@gmail.com>2015-07-05 07:57:10 +0200
committerChristian Tismer <ctismer@gmail.com>2015-07-05 08:02:36 +0200
commitf2e5162cf02047deff1119e73d787b363cb84d04 (patch)
tree7c263d466bc11200588a3c958f8f4b5f4a5e0f64 /checklibs.py
parentcd13ccdfa495e55e4bfb3ecfd01855c348edcbbc (diff)
transition to pyside-examples2
Diffstat (limited to 'checklibs.py')
-rw-r--r--checklibs.py318
1 files changed, 318 insertions, 0 deletions
diff --git a/checklibs.py b/checklibs.py
new file mode 100644
index 00000000..ff4d51ae
--- /dev/null
+++ b/checklibs.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+#
+# checklibs.py
+#
+# Check Mach-O dependencies.
+#
+# See http://www.entropy.ch/blog/Developer/2011/03/05/2011-Update-to-checklibs-Script-for-dynamic-library-dependencies.html
+#
+# Written by Marc Liyanage <http://www.entropy.ch>
+#
+#
+
+import subprocess, sys, re, os.path, optparse, collections
+from pprint import pprint
+
+
+class MachOFile:
+
+ def __init__(self, image_path, arch, parent = None):
+ self.image_path = image_path
+ self._dependencies = []
+ self._cache = dict(paths = {}, order = [])
+ self.arch = arch
+ self.parent = parent
+ self.header_info = {}
+ self.load_info()
+ self.add_to_cache()
+
+ def load_info(self):
+ if not self.image_path.exists():
+ return
+ self.load_header()
+ self.load_rpaths()
+
+ def load_header(self):
+ # Get the mach-o header info, we're interested in the file type (executable, dylib)
+ cmd = 'otool -arch {0} -h "{1}"'
+ output = self.shell(cmd, [self.arch, self.image_path.resolved_path], fatal = True)
+ if not output:
+ print >> sys.stderr, 'Unable to load mach header for {0} ({1}), architecture mismatch? Use --arch option to pick architecture'.format(self.image_path.resolved_path, self.arch)
+ exit()
+ (keys, values) = output.splitlines()[2:]
+ self.header_info = dict(zip(keys.split(), values.split()))
+
+ def load_rpaths(self):
+ output = self.shell('otool -arch {0} -l "{1}"', [self.arch, self.image_path.resolved_path], fatal = True)
+ load_commands = re.split('Load command (\d+)', output)[1:] # skip file name on first line
+ self._rpaths = []
+ load_commands = collections.deque(load_commands)
+ while load_commands:
+ load_commands.popleft() # command index
+ command = load_commands.popleft().strip().splitlines()
+ if command[0].find('LC_RPATH') == -1:
+ continue
+
+ path = re.findall('path (.+) \(offset \d+\)$', command[2])[0]
+ image_path = self.image_path_for_recorded_path(path)
+ image_path.rpath_source = self
+ self._rpaths.append(image_path)
+
+ def ancestors(self):
+ ancestors = []
+ parent = self.parent
+ while parent:
+ ancestors.append(parent)
+ parent = parent.parent
+
+ return ancestors
+
+ def self_and_ancestors(self):
+ return [self] + self.ancestors()
+
+ def rpaths(self):
+ return self._rpaths
+
+ def all_rpaths(self):
+ rpaths = []
+ for image in self.self_and_ancestors():
+ rpaths.extend(image.rpaths())
+ return rpaths
+
+ def root(self):
+ if not self.parent:
+ return self
+ return self.ancestors()[-1]
+
+ def executable_path(self):
+ root = self.root()
+ if root.is_executable():
+ return root.image_path
+ return None
+
+ def filetype(self):
+ return long(self.header_info.get('filetype', 0))
+
+ def is_dylib(self):
+ return self.filetype() == MachOFile.MH_DYLIB
+
+ def is_executable(self):
+ return self.filetype() == MachOFile.MH_EXECUTE
+
+ def all_dependencies(self):
+ self.walk_dependencies()
+ return self.cache()['order']
+
+ def walk_dependencies(self, known = {}):
+ if known.get(self.image_path.resolved_path):
+ return
+
+ known[self.image_path.resolved_path] = self
+
+ for item in self.dependencies():
+ item.walk_dependencies(known)
+
+ def dependencies(self):
+ if not self.image_path.exists():
+ return []
+
+ if self._dependencies:
+ return self._dependencies
+
+ output = self.shell('otool -arch {0} -L "{1}"', [self.arch, self.image_path.resolved_path], fatal = True)
+ output = [line.strip() for line in output.splitlines()]
+ del(output[0])
+ if self.is_dylib():
+ del(output[0]) # In the case of dylibs, the first line is the id line
+
+ self._dependencies = []
+ for line in output:
+ match = re.match('^(.+)\s+(\(.+)\)$', line)
+ if not match:
+ continue
+ recorded_path = match.group(1)
+ image_path = self.image_path_for_recorded_path(recorded_path)
+ image = self.lookup_or_make_item(image_path)
+ self._dependencies.append(image)
+
+ return self._dependencies
+
+ # The root item holds the cache, all lower-level requests bubble up the parent chain
+ def cache(self):
+ if self.parent:
+ return self.parent.cache()
+ return self._cache
+
+ def add_to_cache(self):
+ cache = self.cache()
+ cache['paths'][self.image_path.resolved_path] = self
+ cache['order'].append(self)
+
+ def cached_item_for_path(self, path):
+ if not path:
+ return None
+ return self.cache()['paths'].get(path)
+
+ def lookup_or_make_item(self, image_path):
+ image = self.cached_item_for_path(image_path.resolved_path)
+ if not image: # cache miss
+ image = MachOFile(image_path, self.arch, parent = self)
+ return image
+
+ def image_path_for_recorded_path(self, recorded_path):
+ path = ImagePath(None, recorded_path)
+
+ # handle @executable_path
+ if recorded_path.startswith(ImagePath.EXECUTABLE_PATH_TOKEN):
+ executable_image_path = self.executable_path()
+ if executable_image_path:
+ path.resolved_path = os.path.normpath(recorded_path.replace(ImagePath.EXECUTABLE_PATH_TOKEN, os.path.dirname(executable_image_path.resolved_path)))
+
+ # handle @loader_path
+ elif recorded_path.startswith(ImagePath.LOADER_PATH_TOKEN):
+ path.resolved_path = os.path.normpath(recorded_path.replace(ImagePath.LOADER_PATH_TOKEN, os.path.dirname(self.image_path.resolved_path)))
+
+ # handle @rpath
+ elif recorded_path.startswith(ImagePath.RPATH_TOKEN):
+ for rpath in self.all_rpaths():
+ resolved_path = os.path.normpath(recorded_path.replace(ImagePath.RPATH_TOKEN, rpath.resolved_path))
+ if os.path.exists(resolved_path):
+ path.resolved_path = resolved_path
+ path.rpath_source = rpath.rpath_source
+ break
+
+ # handle absolute path
+ elif recorded_path.startswith('/'):
+ path.resolved_path = recorded_path
+
+ return path
+
+ def __repr__(self):
+ return str(self.image_path)
+
+ def dump(self):
+ print self.image_path
+ for dependency in self.dependencies():
+ print '\t{0}'.format(dependency)
+
+ @staticmethod
+ def shell(cmd_format, args, fatal = False):
+ cmd = cmd_format.format(*args)
+ popen = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE)
+ output = popen.communicate()[0]
+ if popen.returncode and fatal:
+ print >> sys.stderr, 'Nonzero exit status for shell command "{0}"'.format(cmd)
+ sys.exit(1)
+
+ return output
+
+ @classmethod
+ def architectures_for_image_at_path(cls, path):
+ output = cls.shell('file "{}"', [path])
+ file_architectures = re.findall(r' executable (\w+)', output)
+ ordering = 'x86_64 i386'.split()
+ file_architectures = sorted(file_architectures, lambda a, b: cmp(ordering.index(a), ordering.index(b)))
+ return file_architectures
+
+ MH_EXECUTE = 0x2
+ MH_DYLIB = 0x6
+ MH_BUNDLE = 0x8
+
+
+# ANSI terminal coloring sequences
+class Color:
+ HEADER = '\033[95m'
+ BLUE = '\033[94m'
+ GREEN = '\033[92m'
+ RED = '\033[91m'
+ ENDC = '\033[0m'
+
+ @staticmethod
+ def red(string):
+ return Color.wrap(string, Color.RED)
+
+ @staticmethod
+ def blue(string):
+ return Color.wrap(string, Color.BLUE)
+
+ @staticmethod
+ def wrap(string, color):
+ return Color.HEADER + color + string + Color.ENDC
+
+
+# This class holds path information for a mach-0 image file. It holds the path as it was recorded
+# in the loading binary as well as the effective, resolved file system path.
+# The former can contain @-replacement tokens.
+# In the case where the recorded path contains an @rpath token that was resolved successfully, we also
+# capture the path of the binary that supplied the rpath value that was used.
+# That path itself can contain replacement tokens such as @loader_path.
+class ImagePath:
+
+ def __init__(self, resolved_path, recorded_path = None):
+ self.recorded_path = recorded_path
+ self.resolved_path = resolved_path
+ self.rpath_source = None
+
+ def __repr__(self):
+ description = None
+
+ if self.resolved_equals_recorded() or self.recorded_path == None:
+ description = self.resolved_path
+ else:
+ description = '{0} ({1})'.format(self.resolved_path, self.recorded_path)
+
+ if (not self.is_system_location()) and (not self.uses_dyld_token()):
+ description = Color.blue(description)
+
+ if self.rpath_source:
+ description += ' (rpath source: {0})'.format(self.rpath_source.image_path.resolved_path)
+
+ if not self.exists():
+ description += Color.red(' (missing)')
+
+ return description
+
+ def exists(self):
+ return self.resolved_path and os.path.exists(self.resolved_path)
+
+ def resolved_equals_recorded(self):
+ return self.resolved_path and self.recorded_path and self.resolved_path == self.recorded_path
+
+ def uses_dyld_token(self):
+ return self.recorded_path and self.recorded_path.startswith('@')
+
+ def is_system_location(self):
+ system_prefixes = ['/System/Library', '/usr/lib']
+ for prefix in system_prefixes:
+ if self.resolved_path and self.resolved_path.startswith(prefix):
+ return True
+
+ EXECUTABLE_PATH_TOKEN = '@executable_path'
+ LOADER_PATH_TOKEN = '@loader_path'
+ RPATH_TOKEN = '@rpath'
+
+
+# Command line driver
+parser = optparse.OptionParser(usage = "Usage: %prog [options] path_to_mach_o_file")
+parser.add_option("--arch", dest = "arch", help = "architecture", metavar = "ARCH")
+parser.add_option("--all", dest = "include_system_libraries", help = "Include system frameworks and libraries", action="store_true")
+(options, args) = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_help()
+ sys.exit(1)
+
+archs = MachOFile.architectures_for_image_at_path(args[0])
+if archs and not options.arch:
+ print >> sys.stderr, 'Analyzing architecture {}, override with --arch if needed'.format(archs[0])
+ options.arch = archs[0]
+
+toplevel_image = MachOFile(ImagePath(args[0]), options.arch)
+
+for dependency in toplevel_image.all_dependencies():
+ if dependency.image_path.exists() and (not options.include_system_libraries) and dependency.image_path.is_system_location():
+ continue
+
+ dependency.dump()
+ print
+