|  | #!/usr/bin/env python | 
|  |  | 
|  | # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | # Usage: strip_save_dsym <whatever-arguments-you-would-pass-to-strip> | 
|  | # | 
|  | # strip_save_dsym is a wrapper around the standard strip utility.  Given an | 
|  | # input Mach-O file, strip_save_dsym will save a copy of the file in a "fake" | 
|  | # .dSYM bundle for debugging, and then call strip to strip the Mach-O file. | 
|  | # Note that the .dSYM file is a "fake" in that it's not a self-contained | 
|  | # .dSYM bundle, it just contains a copy of the original (unstripped) Mach-O | 
|  | # file, and therefore contains references to object files on the filesystem. | 
|  | # The generated .dSYM bundle is therefore unsuitable for debugging in the | 
|  | # absence of these .o files. | 
|  | # | 
|  | # If a .dSYM already exists and has a newer timestamp than the Mach-O file, | 
|  | # this utility does nothing.  That allows strip_save_dsym to be run on a file | 
|  | # that has already been stripped without trashing the .dSYM. | 
|  | # | 
|  | # Rationale: the "right" way to generate dSYM bundles, dsymutil, is incredibly | 
|  | # slow.  On the other hand, doing a file copy (which is really all that | 
|  | # dsymutil does) is comparatively fast.  Since we usually just want to strip | 
|  | # a release-mode executable but still be able to debug it, and we don't care | 
|  | # so much about generating a hermetic dSYM bundle, we'll prefer the file copy. | 
|  | # If a real dSYM is ever needed, it's still possible to create one by running | 
|  | # dsymutil and pointing it at the original Mach-O file inside the "fake" | 
|  | # bundle, provided that the object files are available. | 
|  |  | 
|  | import errno | 
|  | import os | 
|  | import re | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  | import time | 
|  |  | 
|  | # Returns a list of architectures contained in a Mach-O file.  The file can be | 
|  | # a universal (fat) file, in which case there will be one list element for | 
|  | # each contained architecture, or it can be a thin single-architecture Mach-O | 
|  | # file, in which case the list will contain a single element identifying the | 
|  | # architecture.  On error, returns an empty list.  Determines the architecture | 
|  | # list by calling file. | 
|  | def macho_archs(macho): | 
|  | macho_types = ["executable", | 
|  | "dynamically linked shared library", | 
|  | "bundle"] | 
|  | macho_types_re = "Mach-O (?:64-bit )?(?:" + "|".join(macho_types) + ")" | 
|  |  | 
|  | file_cmd = subprocess.Popen(["/usr/bin/file", "-b", "--", macho], | 
|  | stdout=subprocess.PIPE) | 
|  |  | 
|  | archs = [] | 
|  |  | 
|  | type_line = file_cmd.stdout.readline() | 
|  | type_match = re.match("^%s (.*)$" % macho_types_re, type_line) | 
|  | if type_match: | 
|  | archs.append(type_match.group(1)) | 
|  | return [type_match.group(1)] | 
|  | else: | 
|  | type_match = re.match("^Mach-O universal binary with (.*) architectures$", | 
|  | type_line) | 
|  | if type_match: | 
|  | for i in range(0, int(type_match.group(1))): | 
|  | arch_line = file_cmd.stdout.readline() | 
|  | arch_match = re.match( | 
|  | "^.* \(for architecture (.*)\):\t%s .*$" % macho_types_re, | 
|  | arch_line) | 
|  | if arch_match: | 
|  | archs.append(arch_match.group(1)) | 
|  |  | 
|  | if file_cmd.wait() != 0: | 
|  | archs = [] | 
|  |  | 
|  | if len(archs) == 0: | 
|  | print >> sys.stderr, "No architectures in %s" % macho | 
|  |  | 
|  | return archs | 
|  |  | 
|  | # Returns a dictionary mapping architectures contained in the file as returned | 
|  | # by macho_archs to the LC_UUID load command for that architecture. | 
|  | # Architectures with no LC_UUID load command are omitted from the dictionary. | 
|  | # Determines the UUID value by calling otool. | 
|  | def macho_uuids(macho): | 
|  | uuids = {} | 
|  |  | 
|  | archs = macho_archs(macho) | 
|  | if len(archs) == 0: | 
|  | return uuids | 
|  |  | 
|  | for arch in archs: | 
|  | if arch == "": | 
|  | continue | 
|  |  | 
|  | otool_cmd = subprocess.Popen(["/usr/bin/otool", "-arch", arch, "-l", "-", | 
|  | macho], | 
|  | stdout=subprocess.PIPE) | 
|  | # state 0 is when nothing UUID-related has been seen yet.  State 1 is | 
|  | # entered after a load command begins, but it may not be an LC_UUID load | 
|  | # command.  States 2, 3, and 4 are intermediate states while reading an | 
|  | # LC_UUID command.  State 5 is the terminal state for a successful LC_UUID | 
|  | # read.  State 6 is the error state. | 
|  | state = 0 | 
|  | uuid = "" | 
|  | for otool_line in otool_cmd.stdout: | 
|  | if state == 0: | 
|  | if re.match("^Load command .*$", otool_line): | 
|  | state = 1 | 
|  | elif state == 1: | 
|  | if re.match("^     cmd LC_UUID$", otool_line): | 
|  | state = 2 | 
|  | else: | 
|  | state = 0 | 
|  | elif state == 2: | 
|  | if re.match("^ cmdsize 24$", otool_line): | 
|  | state = 3 | 
|  | else: | 
|  | state = 6 | 
|  | elif state == 3: | 
|  | # The UUID display format changed in the version of otool shipping | 
|  | # with the Xcode 3.2.2 prerelease.  The new format is traditional: | 
|  | #    uuid 4D7135B2-9C56-C5F5-5F49-A994258E0955 | 
|  | # and with Xcode 3.2.6, then line is indented one more space: | 
|  | #     uuid 4D7135B2-9C56-C5F5-5F49-A994258E0955 | 
|  | # The old format, from cctools-750 and older's otool, breaks the UUID | 
|  | # up into a sequence of bytes: | 
|  | #    uuid 0x4d 0x71 0x35 0xb2 0x9c 0x56 0xc5 0xf5 | 
|  | #         0x5f 0x49 0xa9 0x94 0x25 0x8e 0x09 0x55 | 
|  | new_uuid_match = re.match("^ {3,4}uuid (.{8}-.{4}-.{4}-.{4}-.{12})$", | 
|  | otool_line) | 
|  | if new_uuid_match: | 
|  | uuid = new_uuid_match.group(1) | 
|  |  | 
|  | # Skip state 4, there is no second line to read. | 
|  | state = 5 | 
|  | else: | 
|  | old_uuid_match = re.match("^   uuid 0x(..) 0x(..) 0x(..) 0x(..) " | 
|  | "0x(..) 0x(..) 0x(..) 0x(..)$", | 
|  | otool_line) | 
|  | if old_uuid_match: | 
|  | state = 4 | 
|  | uuid = old_uuid_match.group(1) + old_uuid_match.group(2) + \ | 
|  | old_uuid_match.group(3) + old_uuid_match.group(4) + "-" + \ | 
|  | old_uuid_match.group(5) + old_uuid_match.group(6) + "-" + \ | 
|  | old_uuid_match.group(7) + old_uuid_match.group(8) + "-" | 
|  | else: | 
|  | state = 6 | 
|  | elif state == 4: | 
|  | old_uuid_match = re.match("^        0x(..) 0x(..) 0x(..) 0x(..) " | 
|  | "0x(..) 0x(..) 0x(..) 0x(..)$", | 
|  | otool_line) | 
|  | if old_uuid_match: | 
|  | state = 5 | 
|  | uuid += old_uuid_match.group(1) + old_uuid_match.group(2) + "-" + \ | 
|  | old_uuid_match.group(3) + old_uuid_match.group(4) + \ | 
|  | old_uuid_match.group(5) + old_uuid_match.group(6) + \ | 
|  | old_uuid_match.group(7) + old_uuid_match.group(8) | 
|  | else: | 
|  | state = 6 | 
|  |  | 
|  | if otool_cmd.wait() != 0: | 
|  | state = 6 | 
|  |  | 
|  | if state == 5: | 
|  | uuids[arch] = uuid.upper() | 
|  |  | 
|  | if len(uuids) == 0: | 
|  | print >> sys.stderr, "No UUIDs in %s" % macho | 
|  |  | 
|  | return uuids | 
|  |  | 
|  | # Given a path to a Mach-O file and possible information from the environment, | 
|  | # determines the desired path to the .dSYM. | 
|  | def dsym_path(macho): | 
|  | # If building a bundle, the .dSYM should be placed next to the bundle.  Use | 
|  | # WRAPPER_NAME to make this determination.  If called from xcodebuild, | 
|  | # WRAPPER_NAME will be set to the name of the bundle. | 
|  | dsym = "" | 
|  | if "WRAPPER_NAME" in os.environ: | 
|  | if "BUILT_PRODUCTS_DIR" in os.environ: | 
|  | dsym = os.path.join(os.environ["BUILT_PRODUCTS_DIR"], | 
|  | os.environ["WRAPPER_NAME"]) | 
|  | else: | 
|  | dsym = os.environ["WRAPPER_NAME"] | 
|  | else: | 
|  | dsym = macho | 
|  |  | 
|  | dsym += ".dSYM" | 
|  |  | 
|  | return dsym | 
|  |  | 
|  | # Creates a fake .dSYM bundle at dsym for macho, a Mach-O image with the | 
|  | # architectures and UUIDs specified by the uuids map. | 
|  | def make_fake_dsym(macho, dsym): | 
|  | uuids = macho_uuids(macho) | 
|  | if len(uuids) == 0: | 
|  | return False | 
|  |  | 
|  | dwarf_dir = os.path.join(dsym, "Contents", "Resources", "DWARF") | 
|  | dwarf_file = os.path.join(dwarf_dir, os.path.basename(macho)) | 
|  | try: | 
|  | os.makedirs(dwarf_dir) | 
|  | except OSError, (err, error_string): | 
|  | if err != errno.EEXIST: | 
|  | raise | 
|  | shutil.copyfile(macho, dwarf_file) | 
|  |  | 
|  | # info_template is the same as what dsymutil would have written, with the | 
|  | # addition of the fake_dsym key. | 
|  | info_template = \ | 
|  | '''<?xml version="1.0" encoding="UTF-8"?> | 
|  | <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | 
|  | <plist version="1.0"> | 
|  | <dict> | 
|  | <key>CFBundleDevelopmentRegion</key> | 
|  | <string>English</string> | 
|  | <key>CFBundleIdentifier</key> | 
|  | <string>com.apple.xcode.dsym.%(root_name)s</string> | 
|  | <key>CFBundleInfoDictionaryVersion</key> | 
|  | <string>6.0</string> | 
|  | <key>CFBundlePackageType</key> | 
|  | <string>dSYM</string> | 
|  | <key>CFBundleSignature</key> | 
|  | <string>????</string> | 
|  | <key>CFBundleShortVersionString</key> | 
|  | <string>1.0</string> | 
|  | <key>CFBundleVersion</key> | 
|  | <string>1</string> | 
|  | <key>dSYM_UUID</key> | 
|  | <dict> | 
|  | %(uuid_dict)s		</dict> | 
|  | <key>fake_dsym</key> | 
|  | <true/> | 
|  | </dict> | 
|  | </plist> | 
|  | ''' | 
|  |  | 
|  | root_name = os.path.basename(dsym)[:-5]  # whatever.dSYM without .dSYM | 
|  | uuid_dict = "" | 
|  | for arch in sorted(uuids): | 
|  | uuid_dict += "\t\t\t<key>" + arch + "</key>\n"\ | 
|  | "\t\t\t<string>" + uuids[arch] + "</string>\n" | 
|  | info_dict = { | 
|  | "root_name": root_name, | 
|  | "uuid_dict": uuid_dict, | 
|  | } | 
|  | info_contents = info_template % info_dict | 
|  | info_file = os.path.join(dsym, "Contents", "Info.plist") | 
|  | info_fd = open(info_file, "w") | 
|  | info_fd.write(info_contents) | 
|  | info_fd.close() | 
|  |  | 
|  | return True | 
|  |  | 
|  | # For a Mach-O file, determines where the .dSYM bundle should be located.  If | 
|  | # the bundle does not exist or has a modification time older than the Mach-O | 
|  | # file, calls make_fake_dsym to create a fake .dSYM bundle there, then strips | 
|  | # the Mach-O file and sets the modification time on the .dSYM bundle and Mach-O | 
|  | # file to be identical. | 
|  | def strip_and_make_fake_dsym(macho): | 
|  | dsym = dsym_path(macho) | 
|  | macho_stat = os.stat(macho) | 
|  | dsym_stat = None | 
|  | try: | 
|  | dsym_stat = os.stat(dsym) | 
|  | except OSError, (err, error_string): | 
|  | if err != errno.ENOENT: | 
|  | raise | 
|  |  | 
|  | if dsym_stat is None or dsym_stat.st_mtime < macho_stat.st_mtime: | 
|  | # Make a .dSYM bundle | 
|  | if not make_fake_dsym(macho, dsym): | 
|  | return False | 
|  |  | 
|  | # Strip the Mach-O file | 
|  | remove_dsym = True | 
|  | try: | 
|  | strip_cmdline = ['xcrun', 'strip'] + sys.argv[1:] | 
|  | strip_cmd = subprocess.Popen(strip_cmdline) | 
|  | if strip_cmd.wait() == 0: | 
|  | remove_dsym = False | 
|  | finally: | 
|  | if remove_dsym: | 
|  | shutil.rmtree(dsym) | 
|  |  | 
|  | # Update modification time on the Mach-O file and .dSYM bundle | 
|  | now = time.time() | 
|  | os.utime(macho, (now, now)) | 
|  | os.utime(dsym, (now, now)) | 
|  |  | 
|  | return True | 
|  |  | 
|  | def main(argv=None): | 
|  | if argv is None: | 
|  | argv = sys.argv | 
|  |  | 
|  | # This only supports operating on one file at a time.  Look at the arguments | 
|  | # to strip to figure out what the source to be stripped is.  Arguments are | 
|  | # processed in the same way that strip does, although to reduce complexity, | 
|  | # this doesn't do all of the same checking as strip.  For example, strip | 
|  | # has no -Z switch and would treat -Z on the command line as an error.  For | 
|  | # the purposes this is needed for, that's fine. | 
|  | macho = None | 
|  | process_switches = True | 
|  | ignore_argument = False | 
|  | for arg in argv[1:]: | 
|  | if ignore_argument: | 
|  | ignore_argument = False | 
|  | continue | 
|  | if process_switches: | 
|  | if arg == "-": | 
|  | process_switches = False | 
|  | # strip has these switches accept an argument: | 
|  | if arg in ["-s", "-R", "-d", "-o", "-arch"]: | 
|  | ignore_argument = True | 
|  | if arg[0] == "-": | 
|  | continue | 
|  | if macho is None: | 
|  | macho = arg | 
|  | else: | 
|  | print >> sys.stderr, "Too many things to strip" | 
|  | return 1 | 
|  |  | 
|  | if macho is None: | 
|  | print >> sys.stderr, "Nothing to strip" | 
|  | return 1 | 
|  |  | 
|  | if not strip_and_make_fake_dsym(macho): | 
|  | return 1 | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | sys.exit(main(sys.argv)) |