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