Home | History | Annotate | Download | only in mac
      1 #!/usr/bin/python
      2 
      3 # Copyright (c) 2008 The Chromium Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 # Usage: strip_save_dsym <whatever-arguments-you-would-pass-to-strip>
      8 #
      9 # strip_save_dsym is a wrapper around the standard strip utility.  Given an
     10 # input Mach-O file, strip_save_dsym will save a copy of the file in a "fake"
     11 # .dSYM bundle for debugging, and then call strip to strip the Mach-O file.
     12 # Note that the .dSYM file is a "fake" in that it's not a self-contained
     13 # .dSYM bundle, it just contains a copy of the original (unstripped) Mach-O
     14 # file, and therefore contains references to object files on the filesystem.
     15 # The generated .dSYM bundle is therefore unsuitable for debugging in the
     16 # absence of these .o files.
     17 #
     18 # If a .dSYM already exists and has a newer timestamp than the Mach-O file,
     19 # this utility does nothing.  That allows strip_save_dsym to be run on a file
     20 # that has already been stripped without trashing the .dSYM.
     21 #
     22 # Rationale: the "right" way to generate dSYM bundles, dsymutil, is incredibly
     23 # slow.  On the other hand, doing a file copy (which is really all that
     24 # dsymutil does) is comparatively fast.  Since we usually just want to strip
     25 # a release-mode executable but still be able to debug it, and we don't care
     26 # so much about generating a hermetic dSYM bundle, we'll prefer the file copy.
     27 # If a real dSYM is ever needed, it's still possible to create one by running
     28 # dsymutil and pointing it at the original Mach-O file inside the "fake"
     29 # bundle, provided that the object files are available.
     30 
     31 import errno
     32 import os
     33 import re
     34 import shutil
     35 import subprocess
     36 import sys
     37 import time
     38 
     39 # Returns a list of architectures contained in a Mach-O file.  The file can be
     40 # a universal (fat) file, in which case there will be one list element for
     41 # each contained architecture, or it can be a thin single-architecture Mach-O
     42 # file, in which case the list will contain a single element identifying the
     43 # architecture.  On error, returns an empty list.  Determines the architecture
     44 # list by calling file.
     45 def macho_archs(macho):
     46   macho_types = ["executable",
     47                  "dynamically linked shared library",
     48                  "bundle"]
     49   macho_types_re = "Mach-O (?:64-bit )?(?:" + "|".join(macho_types) + ")"
     50 
     51   file_cmd = subprocess.Popen(["/usr/bin/file", "-b", "--", macho],
     52                               stdout=subprocess.PIPE)
     53 
     54   archs = []
     55 
     56   type_line = file_cmd.stdout.readline()
     57   type_match = re.match("^%s (.*)$" % macho_types_re, type_line)
     58   if type_match:
     59     archs.append(type_match.group(1))
     60     return [type_match.group(1)]
     61   else:
     62     type_match = re.match("^Mach-O universal binary with (.*) architectures$",
     63                           type_line)
     64     if type_match:
     65       for i in range(0, int(type_match.group(1))):
     66         arch_line = file_cmd.stdout.readline()
     67         arch_match = re.match(
     68                      "^.* \(for architecture (.*)\):\t%s .*$" % macho_types_re,
     69                      arch_line)
     70         if arch_match:
     71           archs.append(arch_match.group(1))
     72 
     73   if file_cmd.wait() != 0:
     74     archs = []
     75 
     76   if len(archs) == 0:
     77     print >> sys.stderr, "No architectures in %s" % macho
     78 
     79   return archs
     80 
     81 # Returns a dictionary mapping architectures contained in the file as returned
     82 # by macho_archs to the LC_UUID load command for that architecture.
     83 # Architectures with no LC_UUID load command are omitted from the dictionary.
     84 # Determines the UUID value by calling otool.
     85 def macho_uuids(macho):
     86   uuids = {}
     87 
     88   archs = macho_archs(macho)
     89   if len(archs) == 0:
     90     return uuids
     91 
     92   for arch in archs:
     93     if arch == "":
     94       continue
     95 
     96     otool_cmd = subprocess.Popen(["/usr/bin/otool", "-arch", arch, "-l", "-",
     97                                   macho],
     98                                  stdout=subprocess.PIPE)
     99     # state 0 is when nothing UUID-related has been seen yet.  State 1 is
    100     # entered after a load command begins, but it may not be an LC_UUID load
    101     # command.  States 2, 3, and 4 are intermediate states while reading an
    102     # LC_UUID command.  State 5 is the terminal state for a successful LC_UUID
    103     # read.  State 6 is the error state.
    104     state = 0
    105     uuid = ""
    106     for otool_line in otool_cmd.stdout:
    107       if state == 0:
    108         if re.match("^Load command .*$", otool_line):
    109           state = 1
    110       elif state == 1:
    111         if re.match("^     cmd LC_UUID$", otool_line):
    112           state = 2
    113         else:
    114           state = 0
    115       elif state == 2:
    116         if re.match("^ cmdsize 24$", otool_line):
    117           state = 3
    118         else:
    119           state = 6
    120       elif state == 3:
    121         # The UUID display format changed in the version of otool shipping
    122         # with the Xcode 3.2.2 prerelease.  The new format is traditional:
    123         #    uuid 4D7135B2-9C56-C5F5-5F49-A994258E0955
    124         # and with Xcode 3.2.6, then line is indented one more space:
    125         #     uuid 4D7135B2-9C56-C5F5-5F49-A994258E0955
    126         # The old format, from cctools-750 and older's otool, breaks the UUID
    127         # up into a sequence of bytes:
    128         #    uuid 0x4d 0x71 0x35 0xb2 0x9c 0x56 0xc5 0xf5
    129         #         0x5f 0x49 0xa9 0x94 0x25 0x8e 0x09 0x55
    130         new_uuid_match = re.match("^ {3,4}uuid (.{8}-.{4}-.{4}-.{4}-.{12})$",
    131                                   otool_line)
    132         if new_uuid_match:
    133           uuid = new_uuid_match.group(1)
    134 
    135           # Skip state 4, there is no second line to read.
    136           state = 5
    137         else:
    138           old_uuid_match = re.match("^   uuid 0x(..) 0x(..) 0x(..) 0x(..) "
    139                                     "0x(..) 0x(..) 0x(..) 0x(..)$",
    140                                     otool_line)
    141           if old_uuid_match:
    142             state = 4
    143             uuid = old_uuid_match.group(1) + old_uuid_match.group(2) + \
    144                    old_uuid_match.group(3) + old_uuid_match.group(4) + "-" + \
    145                    old_uuid_match.group(5) + old_uuid_match.group(6) + "-" + \
    146                    old_uuid_match.group(7) + old_uuid_match.group(8) + "-"
    147           else:
    148             state = 6
    149       elif state == 4:
    150         old_uuid_match = re.match("^        0x(..) 0x(..) 0x(..) 0x(..) "
    151                                   "0x(..) 0x(..) 0x(..) 0x(..)$",
    152                                   otool_line)
    153         if old_uuid_match:
    154           state = 5
    155           uuid += old_uuid_match.group(1) + old_uuid_match.group(2) + "-" + \
    156                   old_uuid_match.group(3) + old_uuid_match.group(4) + \
    157                   old_uuid_match.group(5) + old_uuid_match.group(6) + \
    158                   old_uuid_match.group(7) + old_uuid_match.group(8)
    159         else:
    160           state = 6
    161 
    162     if otool_cmd.wait() != 0:
    163       state = 6
    164 
    165     if state == 5:
    166       uuids[arch] = uuid.upper()
    167 
    168   if len(uuids) == 0:
    169     print >> sys.stderr, "No UUIDs in %s" % macho
    170 
    171   return uuids
    172 
    173 # Given a path to a Mach-O file and possible information from the environment,
    174 # determines the desired path to the .dSYM.
    175 def dsym_path(macho):
    176   # If building a bundle, the .dSYM should be placed next to the bundle.  Use
    177   # WRAPPER_NAME to make this determination.  If called from xcodebuild,
    178   # WRAPPER_NAME will be set to the name of the bundle.
    179   dsym = ""
    180   if "WRAPPER_NAME" in os.environ:
    181     if "BUILT_PRODUCTS_DIR" in os.environ:
    182       dsym = os.path.join(os.environ["BUILT_PRODUCTS_DIR"],
    183                           os.environ["WRAPPER_NAME"])
    184     else:
    185       dsym = os.environ["WRAPPER_NAME"]
    186   else:
    187     dsym = macho
    188 
    189   dsym += ".dSYM"
    190 
    191   return dsym
    192 
    193 # Creates a fake .dSYM bundle at dsym for macho, a Mach-O image with the
    194 # architectures and UUIDs specified by the uuids map.
    195 def make_fake_dsym(macho, dsym):
    196   uuids = macho_uuids(macho)
    197   if len(uuids) == 0:
    198     return False
    199 
    200   dwarf_dir = os.path.join(dsym, "Contents", "Resources", "DWARF")
    201   dwarf_file = os.path.join(dwarf_dir, os.path.basename(macho))
    202   try:
    203     os.makedirs(dwarf_dir)
    204   except OSError, (err, error_string):
    205     if err != errno.EEXIST:
    206       raise
    207   shutil.copyfile(macho, dwarf_file)
    208 
    209   # info_template is the same as what dsymutil would have written, with the
    210   # addition of the fake_dsym key.
    211   info_template = \
    212 '''<?xml version="1.0" encoding="UTF-8"?>
    213 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    214 <plist version="1.0">
    215 	<dict>
    216 		<key>CFBundleDevelopmentRegion</key>
    217 		<string>English</string>
    218 		<key>CFBundleIdentifier</key>
    219 		<string>com.apple.xcode.dsym.%(root_name)s</string>
    220 		<key>CFBundleInfoDictionaryVersion</key>
    221 		<string>6.0</string>
    222 		<key>CFBundlePackageType</key>
    223 		<string>dSYM</string>
    224 		<key>CFBundleSignature</key>
    225 		<string>????</string>
    226 		<key>CFBundleShortVersionString</key>
    227 		<string>1.0</string>
    228 		<key>CFBundleVersion</key>
    229 		<string>1</string>
    230 		<key>dSYM_UUID</key>
    231 		<dict>
    232 %(uuid_dict)s		</dict>
    233 		<key>fake_dsym</key>
    234 		<true/>
    235 	</dict>
    236 </plist>
    237 '''
    238 
    239   root_name = os.path.basename(dsym)[:-5]  # whatever.dSYM without .dSYM
    240   uuid_dict = ""
    241   for arch in sorted(uuids):
    242     uuid_dict += "\t\t\t<key>" + arch + "</key>\n"\
    243                  "\t\t\t<string>" + uuids[arch] + "</string>\n"
    244   info_dict = {
    245     "root_name": root_name,
    246     "uuid_dict": uuid_dict,
    247   }
    248   info_contents = info_template % info_dict
    249   info_file = os.path.join(dsym, "Contents", "Info.plist")
    250   info_fd = open(info_file, "w")
    251   info_fd.write(info_contents)
    252   info_fd.close()
    253 
    254   return True
    255 
    256 # For a Mach-O file, determines where the .dSYM bundle should be located.  If
    257 # the bundle does not exist or has a modification time older than the Mach-O
    258 # file, calls make_fake_dsym to create a fake .dSYM bundle there, then strips
    259 # the Mach-O file and sets the modification time on the .dSYM bundle and Mach-O
    260 # file to be identical.
    261 def strip_and_make_fake_dsym(macho):
    262   dsym = dsym_path(macho)
    263   macho_stat = os.stat(macho)
    264   dsym_stat = None
    265   try:
    266     dsym_stat = os.stat(dsym)
    267   except OSError, (err, error_string):
    268     if err != errno.ENOENT:
    269       raise
    270 
    271   if dsym_stat is None or dsym_stat.st_mtime < macho_stat.st_mtime:
    272     # Make a .dSYM bundle
    273     if not make_fake_dsym(macho, dsym):
    274       return False
    275 
    276     # Strip the Mach-O file
    277     remove_dsym = True
    278     try:
    279       strip_path = ""
    280       if "SYSTEM_DEVELOPER_BIN_DIR" in os.environ:
    281         strip_path = os.environ["SYSTEM_DEVELOPER_BIN_DIR"]
    282       else:
    283         strip_path = "/usr/bin"
    284       strip_path = os.path.join(strip_path, "strip")
    285       strip_cmdline = [strip_path] + sys.argv[1:]
    286       # Print the strip invocation so that it's obvious something is happening
    287       print " ".join(strip_cmdline)
    288       strip_cmd = subprocess.Popen(strip_cmdline)
    289       if strip_cmd.wait() == 0:
    290         remove_dsym = False
    291     finally:
    292       if remove_dsym:
    293         shutil.rmtree(dsym)
    294 
    295     # Update modification time on the Mach-O file and .dSYM bundle
    296     now = time.time()
    297     os.utime(macho, (now, now))
    298     os.utime(dsym, (now, now))
    299 
    300   return True
    301 
    302 def main(argv=None):
    303   if argv is None:
    304     argv = sys.argv
    305 
    306   # This only supports operating on one file at a time.  Look at the arguments
    307   # to strip to figure out what the source to be stripped is.  Arguments are
    308   # processed in the same way that strip does, although to reduce complexity,
    309   # this doesn't do all of the same checking as strip.  For example, strip
    310   # has no -Z switch and would treat -Z on the command line as an error.  For
    311   # the purposes this is needed for, that's fine.
    312   macho = None
    313   process_switches = True
    314   ignore_argument = False
    315   for arg in argv[1:]:
    316     if ignore_argument:
    317       ignore_argument = False
    318       continue
    319     if process_switches:
    320       if arg == "-":
    321         process_switches = False
    322       # strip has these switches accept an argument:
    323       if arg in ["-s", "-R", "-d", "-o", "-arch"]:
    324         ignore_argument = True
    325       if arg[0] == "-":
    326         continue
    327     if macho is None:
    328       macho = arg
    329     else:
    330       print >> sys.stderr, "Too many things to strip"
    331       return 1
    332 
    333   if macho is None:
    334     print >> sys.stderr, "Nothing to strip"
    335     return 1
    336 
    337   if not strip_and_make_fake_dsym(macho):
    338     return 1
    339 
    340   return 0
    341 
    342 if __name__ == "__main__":
    343   sys.exit(main(sys.argv))
    344