Home | History | Annotate | Download | only in mac
      1 #!/usr/bin/env python
      2 
      3 # Copyright (c) 2011 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_cmdline = ['xcrun', 'strip'] + sys.argv[1:]
    280       strip_cmd = subprocess.Popen(strip_cmdline)
    281       if strip_cmd.wait() == 0:
    282         remove_dsym = False
    283     finally:
    284       if remove_dsym:
    285         shutil.rmtree(dsym)
    286 
    287     # Update modification time on the Mach-O file and .dSYM bundle
    288     now = time.time()
    289     os.utime(macho, (now, now))
    290     os.utime(dsym, (now, now))
    291 
    292   return True
    293 
    294 def main(argv=None):
    295   if argv is None:
    296     argv = sys.argv
    297 
    298   # This only supports operating on one file at a time.  Look at the arguments
    299   # to strip to figure out what the source to be stripped is.  Arguments are
    300   # processed in the same way that strip does, although to reduce complexity,
    301   # this doesn't do all of the same checking as strip.  For example, strip
    302   # has no -Z switch and would treat -Z on the command line as an error.  For
    303   # the purposes this is needed for, that's fine.
    304   macho = None
    305   process_switches = True
    306   ignore_argument = False
    307   for arg in argv[1:]:
    308     if ignore_argument:
    309       ignore_argument = False
    310       continue
    311     if process_switches:
    312       if arg == "-":
    313         process_switches = False
    314       # strip has these switches accept an argument:
    315       if arg in ["-s", "-R", "-d", "-o", "-arch"]:
    316         ignore_argument = True
    317       if arg[0] == "-":
    318         continue
    319     if macho is None:
    320       macho = arg
    321     else:
    322       print >> sys.stderr, "Too many things to strip"
    323       return 1
    324 
    325   if macho is None:
    326     print >> sys.stderr, "Nothing to strip"
    327     return 1
    328 
    329   if not strip_and_make_fake_dsym(macho):
    330     return 1
    331 
    332   return 0
    333 
    334 if __name__ == "__main__":
    335   sys.exit(main(sys.argv))
    336