1#!/usr/bin/env python 2 3# 4# This is used to verify that all the dependent libraries of a Mac OS X executable 5# are present and that they are backwards compatible with at least 10.9. 6# Run with an executable as parameter 7# Will return 0 if the executable an all libraries are OK 8# Returns != 0 and prints some textural description on error 9# 10# Author: Marius Kintel <marius@kintel.net> 11# 12# This script lives here: 13# https://github.com/kintel/MacOSX-tools 14# 15 16import sys 17import os 18import subprocess 19import re 20from distutils.version import StrictVersion 21 22DEBUG = False 23 24cxxlib = None 25 26def usage(): 27 print("Usage: " + sys.argv[0] + " <executable>", sys.stderr) 28 sys.exit(1) 29 30# Try to find the given library by searching in the typical locations 31# Returns the full path to the library or None if the library is not found. 32def lookup_library(file): 33 found = None 34 if re.search("@rpath", file): 35 file = re.sub("^@rpath", lc_rpath, file) 36 if os.path.exists(file): found = file 37 if DEBUG: print("@rpath resolved: " + str(file)) 38 if not found: 39 if re.search("\.app/", file): 40 found = file 41 if DEBUG: print("App found: " + str(found)) 42 elif re.search("@executable_path", file): 43 abs = re.sub("^@executable_path", executable_path, file) 44 if os.path.exists(abs): found = abs 45 if DEBUG: print("Lib in @executable_path found: " + str(found)) 46 elif re.search("\.framework/", file): 47 found = os.path.join("/Library/Frameworks", file) 48 if DEBUG: print("Framework found: " + str(found)) 49 else: 50 for path in os.getenv("DYLD_LIBRARY_PATH", "").split(':'): 51 abs = os.path.join(path, file) 52 if os.path.exists(abs): found = abs 53 if DEBUG: print("Library found: " + str(found)) 54 return found 55 56# Returns a list of dependent libraries, excluding system libs 57def find_dependencies(file): 58 libs = [] 59 60 args = ["otool", "-L", file] 61 if DEBUG: print("Executing " + " ".join(args)) 62 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 63 output,err = p.communicate() 64 if p.returncode != 0: 65 print("Failed with return code " + str(p.returncode) + ":") 66 print(err) 67 return None 68 deps = output.split('\n') 69 for dep in deps: 70 # print(dep) 71 # Fail if libstc++ and libc++ was mixed 72 global cxxlib 73 match = re.search("lib(std)?c\+\+", dep) 74 if match: 75 if not cxxlib: 76 cxxlib = match.group(0) 77 else: 78 if cxxlib != match.group(0): 79 print("Error: Mixing libc++ and libstdc++") 80 return None 81 dep = re.sub(".*:$", "", dep) # Take away header line 82 dep = re.sub("^\t", "", dep) # Remove initial tabs 83 dep = re.sub(" \(.*\)$", "", dep) # Remove trailing parentheses 84 if len(dep) > 0 and not re.search("/System/Library", dep) and not re.search("/usr/lib", dep): 85 libs.append(dep) 86 return libs 87 88def validate_lib(lib): 89 p = subprocess.Popen(["otool", "-l", lib], stdout=subprocess.PIPE, universal_newlines=True) 90 output = p.communicate()[0] 91 if p.returncode != 0: return False 92 # Check deployment target 93 m = re.search("LC_VERSION_MIN_MACOSX.*\n(.*)\n\s+version (.*)", output, re.MULTILINE) 94 if not m: 95 print("Error: LC_VERSION_MIN_MACOSX not found in " + lib) 96 return False 97 deploymenttarget = m.group(2) 98 if StrictVersion(deploymenttarget) > StrictVersion('10.9'): 99 print("Error: Unsupported deployment target " + m.group(2) + " found: " + lib) 100 return False 101# We don't support Snow Leopard anymore 102# if re.search("LC_DYLD_INFO_ONLY", output): 103# print("Error: Requires Snow Leopard: " + lib) 104# return False 105 106 # This is a check for a weak symbols from a build made on 10.12 or newer sneaking into a build for an 107 # earlier deployment target. The 'mkostemp' symbol tends to be introduced by fontconfig. 108 p = subprocess.Popen(["nm", "-g", lib], stdout=subprocess.PIPE, universal_newlines=True) 109 output = p.communicate()[0] 110 if p.returncode != 0: return False 111 match = re.search("mkostemp", output) 112 if match: 113 print("Error: Reference to mkostemp() found - only supported on macOS 10.12->") 114 return None 115 116 p = subprocess.Popen(["lipo", lib, "-verify_arch", "x86_64"], stdout=subprocess.PIPE, universal_newlines=True) 117 output = p.communicate()[0] 118 if p.returncode != 0: 119 print("Error: x86_64 architecture not supported: " + lib) 120 return False 121 122# We don't support 32-bit binaries anymore 123# p = subprocess.Popen(["lipo", lib, "-verify_arch", "i386"], stdout=subprocess.PIPE, universal_newlines=True) 124# output = p.communicate()[0] 125# if p.returncode != 0: 126# print("Error: i386 architecture not supported: " + lib) 127# return False 128 return True 129 130if __name__ == '__main__': 131 error = False 132 if len(sys.argv) != 2: usage() 133 executable = sys.argv[1] 134 if DEBUG: print("Processing " + executable) 135 executable_path = os.path.dirname(executable) 136 137 # Find the Runpath search path (LC_RPATH) 138 p = subprocess.Popen(["otool", "-l", executable], stdout=subprocess.PIPE, universal_newlines=True) 139 output = p.communicate()[0] 140 if p.returncode != 0: 141 print('Error otool -l failed on main executable') 142 sys.exit(1) 143 # Check deployment target 144 m = re.search("LC_RPATH\n(.*)\n\s+path ([^ ]+)", output, re.MULTILINE) 145 lc_rpath = m.group(2) 146 if DEBUG: print('Runpath search path: ' + lc_rpath) 147 148 # processed is a dict {libname : [parents]} - each parent is dependent on libname 149 processed = {} 150 pending = [executable] 151 processed[executable] = [] 152 while len(pending) > 0: 153 dep = pending.pop() 154 if DEBUG: print("Evaluating " + dep) 155 deps = find_dependencies(dep) 156# if DEBUG: print("Deps: " + ' '.join(deps)) 157 assert(deps) 158 for d in deps: 159 absfile = lookup_library(d) 160 if absfile == None: 161 print("Not found: " + d) 162 print(" ..required by " + str(processed[dep])) 163 error = True 164 continue 165 if not re.match(executable_path, absfile): 166 print("Error: External dependency " + d) 167 sys.exit(1) 168 if absfile in processed: 169 processed[absfile].append(dep) 170 else: 171 processed[absfile] = [dep] 172 if DEBUG: print("Pending: " + absfile) 173 pending.append(absfile) 174 175 for dep in processed: 176 if DEBUG: print("Validating: " + dep) 177 if not validate_lib(dep): 178 print("..required by " + str(processed[dep])) 179 error = True 180 if error: sys.exit(1) 181 else: sys.exit(0) 182