#!/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 # # 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 = \ ''' CFBundleDevelopmentRegion English CFBundleIdentifier com.apple.xcode.dsym.%(root_name)s CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType dSYM CFBundleSignature ???? CFBundleShortVersionString 1.0 CFBundleVersion 1 dSYM_UUID %(uuid_dict)s fake_dsym ''' root_name = os.path.basename(dsym)[:-5] # whatever.dSYM without .dSYM uuid_dict = "" for arch in sorted(uuids): uuid_dict += "\t\t\t" + arch + "\n"\ "\t\t\t" + uuids[arch] + "\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))