Index: build/mac/strip_save_dsym |
diff --git a/build/mac/strip_save_dsym b/build/mac/strip_save_dsym |
new file mode 100755 |
index 0000000000000000000000000000000000000000..c9cf22663757912c16895c55abd7ce346355079d |
--- /dev/null |
+++ b/build/mac/strip_save_dsym |
@@ -0,0 +1,335 @@ |
+#!/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)) |