File indexing completed on 2024-06-16 04:06:49

0001 #!/usr/bin/env python2.7
0002 
0003 # Script to list recursive dylib dependencies for binaries/dylibs passed as arguments.
0004 # Modified from https://github.com/mixxxdj/mixxx/blob/master/build/osx/otool.py.
0005 #
0006 # SPDX-FileCopyrightText: 2015      by Shanti <listaccount at revenant dot org>
0007 # SPDX-FileCopyrightText: 2015-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0008 #
0009 # SPDX-License-Identifier: BSD-3-Clause
0010 #
0011 
0012 import sys
0013 import os
0014 import re
0015 import argparse
0016 
0017 # ---------------------------------------------------------------
0018 
0019 def system(s):
0020     "wrap system() to give us feedback on what it's doing"
0021     "anything using this call should be fixed to use SCons's declarative style (once you figure that out, right nick?)"
0022     print s,
0023     sys.stdout.flush() # ignore line buffering.
0024     result = os.system(s)
0025     print
0026     return result
0027 
0028 # ---------------------------------------------------------------
0029 
0030 SYSTEM_FRAMEWORKS = ["/System/Library/Frameworks"]
0031 SYSTEM_LIBPATH    = ["/usr/lib"] # anything else?
0032 
0033 # paths to libs that we should copy in
0034 LOCAL_FRAMEWORKS  = [
0035     os.path.expanduser("~/Library/Frameworks"),
0036                        "/Library/Frameworks",
0037                        "/Network/Library/Frameworks"
0038 ]
0039 
0040 LOCAL_LIBPATH = filter(lambda x:
0041     os.path.isdir(x),
0042     ["/usr/local/lib",
0043      "/opt/local/lib",
0044      "/sw/local/lib"]
0045 )
0046 
0047 # However
0048 FRAMEWORKS = LOCAL_FRAMEWORKS + SYSTEM_FRAMEWORKS
0049 LIBPATH    = LOCAL_LIBPATH + SYSTEM_LIBPATH
0050 
0051 # ---------------------------------------------------------------
0052 
0053 # otool parsing
0054 def otool(binary):
0055 
0056     "return in a list of strings the OS X 'install names' of Mach-O binaries (dylibs and programs)"
0057     "Do not run this on object code archive (.a) files, it is not designed for that."
0058     #if not os.path.exists(binary): raise Exception("'%s' not found." % binary)
0059 
0060     if not type(binary) == str: raise ValueError("otool() requires a path (as a string)")
0061 
0062     stdin, stdout, stderr = os.popen3('otool -L "%s"' % binary)
0063 
0064     try:
0065         # discard the first line since it is just the name of the file or an error message (or if reading .as, the first item on the list)
0066         header = stdout.readline()
0067 
0068         if not binary+":\n" == header:
0069 
0070             # as far as I know otool -L only parses .dylibs and .a (but if it does anything else we should cover that case here)
0071             if header.startswith("Archive : "):
0072                 raise Exception("'%s' an archive (.a) file." % binary)
0073             else:
0074                 raise Exception(stderr.readline().strip())
0075 
0076         def parse(l):
0077             return l[1:l.rfind("(")-1]
0078 
0079         return [parse(l[:-1]) for l in stdout] #[:-1] is for stripping the trailing \n
0080 
0081     finally:
0082         stdin.close()
0083         stdout.close()
0084         stderr.close()
0085 
0086 # ---------------------------------------------------------------
0087 
0088 def dependencies(binary):
0089     l = otool(binary)
0090 
0091     if os.path.basename(l[0]) == os.path.basename(binary):
0092         id = l.pop(0)
0093         #print "Removing -id field result %s from %s" % (id, binary)
0094     return l
0095 
0096 # ---------------------------------------------------------------
0097 
0098 def embed_dependencies(binary,
0099                        # Defaults from
0100                        # http://www.kernelthread.com/mac/osx/programming.html
0101                        LOCAL = [os.path.expanduser("~/Library/Frameworks"),
0102                                                    "/Library/Frameworks",
0103                                                    "/Network/Library/Frameworks",
0104                                                    "/usr/local/lib",
0105                                                    "/opt/local/lib",
0106                                                    "/sw/local/lib"],
0107                        SYSTEM = ["/System/Library/Frameworks",
0108                                  "/Network/Library/Frameworks",
0109                                  "/usr/lib"]):
0110     "LOCAL: a list of paths to consider to be for installed libs that must therefore be bundled. Override this if you are not getting the right libs."
0111     "SYSTEM: a list of system library search paths that should be searched for system libs but should not be recursed into; this is needed. Override this if you want to bunde the system libs for some reason." #XXX it's useful to expose LOCAL but is this really needed?
0112     "this is a badly named function"
0113     "Note: sometimes Mach-O binaries depend on themselves. Deal with it."
0114 
0115     # "ignore_missing means whether to ignore if we can't load a binary for examination
0116     # (e.g. if you have references to plugins) XXX is the list"
0117 
0118     #binary = os.path.abspath(binary)
0119 
0120     todo = dependencies(binary)
0121     done = []
0122     orig = []
0123 
0124     # This code can be factored.
0125 
0126     while todo:
0127 
0128         # because of how this is written, popping from the end is a depth-first search. 
0129         # Popping from the front would be a breadth-first search. Neat!
0130 
0131         e = todo.pop()
0132 
0133         # Figure out the absolute path to the library
0134 
0135         if e.startswith('/'):
0136             p = e
0137         elif e.startswith('@'):
0138             # it's a relative load path
0139             raise Exception("Unable to make heads nor tails, sah, of install name '%s'. Relative paths are for already-bundled binaries, this function does not support them." % e)
0140         else:
0141             # experiments show that giving an unspecified path is asking dyld(1) to find the library for us. This covers that case.
0142 
0143             for P in ['']+LOCAL+SYSTEM: 
0144                 p = os.path.abspath(os.path.join(P, e))
0145                 #print "SEARCHING IN LIBPATH; TRYING", p
0146 
0147                 if os.path.exists(p):
0148                     break
0149             else:
0150                 p = e # fallthrough to the exception below #XXX icky bad logic, there must be a way to avoid saying exists() twice
0151 
0152 #        if not os.path.exists(p):
0153 #            raise Exception("Dependent library '%s' not found. Make sure it is installed." % e)
0154 
0155         if not any(p.startswith(P) for P in SYSTEM):
0156 
0157             if ".framework/Versions/" in p:
0158                 # If dependency is a framework, return
0159                 # framework directory not shared library
0160 
0161                 f = re.sub("/Versions/[0-9]*/.*","",p)
0162 
0163                 if f not in done:
0164                     done.append(f)
0165                     todo.extend(dependencies(p))
0166                     orig.append(e)
0167             elif p not in done:
0168                 done.append(p)
0169                 todo.extend(dependencies(p))
0170                 orig.append(e)
0171 
0172     assert all(e.startswith("/") for e in done), "embed_dependencies() is broken, some path in this list is not absolute: %s" % (done,)
0173 
0174     return sorted(done)
0175 
0176 # ---------------------------------------------------------------
0177 
0178 deplist = []
0179 
0180 for dep in sys.argv[1:]:
0181     deplist = deplist + embed_dependencies(dep)
0182 
0183 depset = set(deplist)
0184 
0185 for dep in depset:
0186    print dep