1############################################################################# 2## 3## Copyright (C) 2017 The Qt Company Ltd. 4## Contact: https://www.qt.io/licensing/ 5## 6## This file is part of Qt for Python. 7## 8## $QT_BEGIN_LICENSE:LGPL$ 9## Commercial License Usage 10## Licensees holding valid commercial Qt licenses may use this file in 11## accordance with the commercial license agreement provided with the 12## Software or, alternatively, in accordance with the terms contained in 13## a written agreement between you and The Qt Company. For licensing terms 14## and conditions see https://www.qt.io/terms-conditions. For further 15## information use the contact form at https://www.qt.io/contact-us. 16## 17## GNU Lesser General Public License Usage 18## Alternatively, this file may be used under the terms of the GNU Lesser 19## General Public License version 3 as published by the Free Software 20## Foundation and appearing in the file LICENSE.LGPL3 included in the 21## packaging of this file. Please review the following information to 22## ensure the GNU Lesser General Public License version 3 requirements 23## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24## 25## GNU General Public License Usage 26## Alternatively, this file may be used under the terms of the GNU 27## General Public License version 2.0 or (at your option) the GNU General 28## Public license version 3 or any later version approved by the KDE Free 29## Qt Foundation. The licenses are as published by the Free Software 30## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31## included in the packaging of this file. Please review the following 32## information to ensure the GNU General Public License requirements will 33## be met: https://www.gnu.org/licenses/gpl-2.0.html and 34## https://www.gnu.org/licenses/gpl-3.0.html. 35## 36## $QT_END_LICENSE$ 37## 38############################################################################# 39 40#!/usr/bin/env python 41# 42# checklibs.py 43# 44# Check Mach-O dependencies. 45# 46# See http://www.entropy.ch/blog/Developer/2011/03/05/2011-Update-to-checklibs-Script-for-dynamic-library-dependencies.html 47# 48# Written by Marc Liyanage <http://www.entropy.ch> 49# 50# 51 52import subprocess, sys, re, os.path, optparse, collections 53from pprint import pprint 54 55 56class MachOFile: 57 58 def __init__(self, image_path, arch, parent = None): 59 self.image_path = image_path 60 self._dependencies = [] 61 self._cache = dict(paths = {}, order = []) 62 self.arch = arch 63 self.parent = parent 64 self.header_info = {} 65 self.load_info() 66 self.add_to_cache() 67 68 def load_info(self): 69 if not self.image_path.exists(): 70 return 71 self.load_header() 72 self.load_rpaths() 73 74 def load_header(self): 75 # Get the mach-o header info, we're interested in the file type 76 # (executable, dylib) 77 cmd = 'otool -arch {0} -h "{1}"' 78 output = self.shell(cmd, [self.arch, self.image_path.resolved_path], 79 fatal = True) 80 if not output: 81 print("Unable to load mach header for {} ({}), architecture " 82 "mismatch? Use --arch option to pick architecture".format( 83 self.image_path.resolved_path, self.arch), file=sys.stderr) 84 exit() 85 (keys, values) = output.splitlines()[2:] 86 self.header_info = dict(zip(keys.split(), values.split())) 87 88 def load_rpaths(self): 89 output = self.shell('otool -arch {0} -l "{1}"', 90 [self.arch, self.image_path.resolved_path], fatal = True) 91 # skip file name on first line 92 load_commands = re.split('Load command (\d+)', output)[1:] 93 self._rpaths = [] 94 load_commands = collections.deque(load_commands) 95 while load_commands: 96 load_commands.popleft() # command index 97 command = load_commands.popleft().strip().splitlines() 98 if command[0].find('LC_RPATH') == -1: 99 continue 100 101 path = re.findall('path (.+) \(offset \d+\)$', command[2])[0] 102 image_path = self.image_path_for_recorded_path(path) 103 image_path.rpath_source = self 104 self._rpaths.append(image_path) 105 106 def ancestors(self): 107 ancestors = [] 108 parent = self.parent 109 while parent: 110 ancestors.append(parent) 111 parent = parent.parent 112 113 return ancestors 114 115 def self_and_ancestors(self): 116 return [self] + self.ancestors() 117 118 def rpaths(self): 119 return self._rpaths 120 121 def all_rpaths(self): 122 rpaths = [] 123 for image in self.self_and_ancestors(): 124 rpaths.extend(image.rpaths()) 125 return rpaths 126 127 def root(self): 128 if not self.parent: 129 return self 130 return self.ancestors()[-1] 131 132 def executable_path(self): 133 root = self.root() 134 if root.is_executable(): 135 return root.image_path 136 return None 137 138 def filetype(self): 139 return long(self.header_info.get('filetype', 0)) 140 141 def is_dylib(self): 142 return self.filetype() == MachOFile.MH_DYLIB 143 144 def is_executable(self): 145 return self.filetype() == MachOFile.MH_EXECUTE 146 147 def all_dependencies(self): 148 self.walk_dependencies() 149 return self.cache()['order'] 150 151 def walk_dependencies(self, known = {}): 152 if known.get(self.image_path.resolved_path): 153 return 154 155 known[self.image_path.resolved_path] = self 156 157 for item in self.dependencies(): 158 item.walk_dependencies(known) 159 160 def dependencies(self): 161 if not self.image_path.exists(): 162 return [] 163 164 if self._dependencies: 165 return self._dependencies 166 167 output = self.shell('otool -arch {0} -L "{1}"', 168 [self.arch, self.image_path.resolved_path], fatal = True) 169 output = [line.strip() for line in output.splitlines()] 170 del(output[0]) 171 if self.is_dylib(): 172 # In the case of dylibs, the first line is the id line 173 del(output[0]) 174 175 self._dependencies = [] 176 for line in output: 177 match = re.match('^(.+)\s+(\(.+)\)$', line) 178 if not match: 179 continue 180 recorded_path = match.group(1) 181 image_path = self.image_path_for_recorded_path(recorded_path) 182 image = self.lookup_or_make_item(image_path) 183 self._dependencies.append(image) 184 185 return self._dependencies 186 187 # The root item holds the cache, all lower-level requests bubble up 188 # the parent chain 189 def cache(self): 190 if self.parent: 191 return self.parent.cache() 192 return self._cache 193 194 def add_to_cache(self): 195 cache = self.cache() 196 cache['paths'][self.image_path.resolved_path] = self 197 cache['order'].append(self) 198 199 def cached_item_for_path(self, path): 200 if not path: 201 return None 202 return self.cache()['paths'].get(path) 203 204 def lookup_or_make_item(self, image_path): 205 image = self.cached_item_for_path(image_path.resolved_path) 206 if not image: # cache miss 207 image = MachOFile(image_path, self.arch, parent = self) 208 return image 209 210 def image_path_for_recorded_path(self, recorded_path): 211 path = ImagePath(None, recorded_path) 212 213 # handle @executable_path 214 if recorded_path.startswith(ImagePath.EXECUTABLE_PATH_TOKEN): 215 executable_image_path = self.executable_path() 216 if executable_image_path: 217 path.resolved_path = os.path.normpath( 218 recorded_path.replace( 219 ImagePath.EXECUTABLE_PATH_TOKEN, 220 os.path.dirname(executable_image_path.resolved_path))) 221 222 # handle @loader_path 223 elif recorded_path.startswith(ImagePath.LOADER_PATH_TOKEN): 224 path.resolved_path = os.path.normpath(recorded_path.replace( 225 ImagePath.LOADER_PATH_TOKEN, 226 os.path.dirname(self.image_path.resolved_path))) 227 228 # handle @rpath 229 elif recorded_path.startswith(ImagePath.RPATH_TOKEN): 230 for rpath in self.all_rpaths(): 231 resolved_path = os.path.normpath(recorded_path.replace( 232 ImagePath.RPATH_TOKEN, rpath.resolved_path)) 233 if os.path.exists(resolved_path): 234 path.resolved_path = resolved_path 235 path.rpath_source = rpath.rpath_source 236 break 237 238 # handle absolute path 239 elif recorded_path.startswith('/'): 240 path.resolved_path = recorded_path 241 242 return path 243 244 def __repr__(self): 245 return str(self.image_path) 246 247 def dump(self): 248 print(self.image_path) 249 for dependency in self.dependencies(): 250 print('\t{0}'.format(dependency)) 251 252 @staticmethod 253 def shell(cmd_format, args, fatal = False): 254 cmd = cmd_format.format(*args) 255 popen = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE) 256 output = popen.communicate()[0] 257 if popen.returncode and fatal: 258 print("Nonzero exit status for shell command '{}'".format(cmd), 259 file=sys.stderr) 260 sys.exit(1) 261 262 return output 263 264 @classmethod 265 def architectures_for_image_at_path(cls, path): 266 output = cls.shell('file "{}"', [path]) 267 file_architectures = re.findall(r' executable (\w+)', output) 268 ordering = 'x86_64 i386'.split() 269 file_architectures = sorted(file_architectures, lambda a, b: cmp( 270 ordering.index(a), ordering.index(b))) 271 return file_architectures 272 273 MH_EXECUTE = 0x2 274 MH_DYLIB = 0x6 275 MH_BUNDLE = 0x8 276 277 278# ANSI terminal coloring sequences 279class Color: 280 HEADER = '\033[95m' 281 BLUE = '\033[94m' 282 GREEN = '\033[92m' 283 RED = '\033[91m' 284 ENDC = '\033[0m' 285 286 @staticmethod 287 def red(string): 288 return Color.wrap(string, Color.RED) 289 290 @staticmethod 291 def blue(string): 292 return Color.wrap(string, Color.BLUE) 293 294 @staticmethod 295 def wrap(string, color): 296 return Color.HEADER + color + string + Color.ENDC 297 298 299# This class holds path information for a mach-0 image file. 300# It holds the path as it was recorded in the loading binary as well as 301# the effective, resolved file system path. 302# The former can contain @-replacement tokens. 303# In the case where the recorded path contains an @rpath token that was 304# resolved successfully, we also capture the path of the binary that 305# supplied the rpath value that was used. 306# That path itself can contain replacement tokens such as @loader_path. 307class ImagePath: 308 309 def __init__(self, resolved_path, recorded_path = None): 310 self.recorded_path = recorded_path 311 self.resolved_path = resolved_path 312 self.rpath_source = None 313 314 def __repr__(self): 315 description = None 316 317 if self.resolved_equals_recorded() or self.recorded_path == None: 318 description = self.resolved_path 319 else: 320 description = '{0} ({1})'.format(self.resolved_path, 321 self.recorded_path) 322 323 if (not self.is_system_location()) and (not self.uses_dyld_token()): 324 description = Color.blue(description) 325 326 if self.rpath_source: 327 description += ' (rpath source: {0})'.format( 328 self.rpath_source.image_path.resolved_path) 329 330 if not self.exists(): 331 description += Color.red(' (missing)') 332 333 return description 334 335 def exists(self): 336 return self.resolved_path and os.path.exists(self.resolved_path) 337 338 def resolved_equals_recorded(self): 339 return (self.resolved_path and self.recorded_path and 340 self.resolved_path == self.recorded_path) 341 342 def uses_dyld_token(self): 343 return self.recorded_path and self.recorded_path.startswith('@') 344 345 def is_system_location(self): 346 system_prefixes = ['/System/Library', '/usr/lib'] 347 for prefix in system_prefixes: 348 if self.resolved_path and self.resolved_path.startswith(prefix): 349 return True 350 351 EXECUTABLE_PATH_TOKEN = '@executable_path' 352 LOADER_PATH_TOKEN = '@loader_path' 353 RPATH_TOKEN = '@rpath' 354 355 356# Command line driver 357parser = optparse.OptionParser( 358 usage = "Usage: %prog [options] path_to_mach_o_file") 359parser.add_option( 360 "--arch", dest = "arch", help = "architecture", metavar = "ARCH") 361parser.add_option( 362 "--all", dest = "include_system_libraries", 363 help = "Include system frameworks and libraries", action="store_true") 364(options, args) = parser.parse_args() 365 366if len(args) < 1: 367 parser.print_help() 368 sys.exit(1) 369 370archs = MachOFile.architectures_for_image_at_path(args[0]) 371if archs and not options.arch: 372 print('Analyzing architecture {}, override with --arch if needed'.format( 373 archs[0]), file=sys.stderr) 374 options.arch = archs[0] 375 376toplevel_image = MachOFile(ImagePath(args[0]), options.arch) 377 378for dependency in toplevel_image.all_dependencies(): 379 if (dependency.image_path.exists() and 380 (not options.include_system_libraries) and 381 dependency.image_path.is_system_location()): 382 continue 383 384 dependency.dump() 385 print("\n") 386 387