OLD | NEW |
(Empty) | |
| 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 file_cmd = subprocess.Popen(["/usr/bin/file", "-b", "--", macho], |
| 47 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 48 |
| 49 archs = [] |
| 50 |
| 51 type_line = file_cmd.stdout.readline() |
| 52 type_match = re.match("^Mach-O executable (.*)$", type_line) |
| 53 if type_match: |
| 54 archs.append(type_match.group(1)) |
| 55 return [type_match.group(1)] |
| 56 else: |
| 57 type_match = re.match("^Mach-O universal binary with (.*) architectures$", |
| 58 type_line) |
| 59 if type_match: |
| 60 for i in range(0, int(type_match.group(1))): |
| 61 arch_line = file_cmd.stdout.readline() |
| 62 arch_match = re.match( |
| 63 "^.* \(for architecture (.*)\):\tMach-O executable .*$", |
| 64 arch_line) |
| 65 if arch_match: |
| 66 archs.append(arch_match.group(1)) |
| 67 |
| 68 if file_cmd.wait() != 0: |
| 69 archs = [] |
| 70 |
| 71 return archs |
| 72 |
| 73 # Returns a dictionary mapping architectures contained in the file as returned |
| 74 # by macho_archs to the LC_UUID load command for that architecture. |
| 75 # Architectures with no LC_UUID load command are omitted from the dictionary. |
| 76 # Determines the UUID value by calling otool. |
| 77 def macho_uuids(macho): |
| 78 archs = macho_archs(macho) |
| 79 |
| 80 uuids = {} |
| 81 |
| 82 for arch in archs: |
| 83 if arch == "": |
| 84 continue |
| 85 |
| 86 otool_cmd = subprocess.Popen(["/usr/bin/otool", "-arch", arch, "-l", "-", |
| 87 macho], |
| 88 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 89 # state 0 is when nothing UUID-related has been seen yet. State 1 is |
| 90 # entered after a load command begins, but it may not be an LC_UUID load |
| 91 # command. States 2, 3, and 4 are intermediate states while reading an |
| 92 # LC_UUID command. State 5 is the terminal state for a successful LC_UUID |
| 93 # read. State 6 is the error state. |
| 94 state = 0 |
| 95 uuid = "" |
| 96 for otool_line in otool_cmd.stdout: |
| 97 if state == 0: |
| 98 if re.match("^Load command .*$", otool_line): |
| 99 state = 1 |
| 100 elif state == 1: |
| 101 if re.match("^ cmd LC_UUID$", otool_line): |
| 102 state = 2 |
| 103 else: |
| 104 state = 0 |
| 105 elif state == 2: |
| 106 if re.match("^ cmdsize 24$", otool_line): |
| 107 state = 3 |
| 108 else: |
| 109 state = 6 |
| 110 elif state == 3: |
| 111 uuid_match = re.match("^ uuid 0x(..) 0x(..) 0x(..) 0x(..) " |
| 112 "0x(..) 0x(..) 0x(..) 0x(..)$", |
| 113 otool_line) |
| 114 if uuid_match: |
| 115 state = 4 |
| 116 uuid = uuid_match.group(1) + uuid_match.group(2) + \ |
| 117 uuid_match.group(3) + uuid_match.group(4) + "-" + \ |
| 118 uuid_match.group(5) + uuid_match.group(6) + "-" + \ |
| 119 uuid_match.group(7) + uuid_match.group(8) + "-" |
| 120 else: |
| 121 state = 6 |
| 122 elif state == 4: |
| 123 uuid_match = re.match("^ 0x(..) 0x(..) 0x(..) 0x(..) " |
| 124 "0x(..) 0x(..) 0x(..) 0x(..)$", |
| 125 otool_line) |
| 126 if uuid_match: |
| 127 state = 5 |
| 128 uuid += uuid_match.group(1) + uuid_match.group(2) + "-" + \ |
| 129 uuid_match.group(3) + uuid_match.group(4) + \ |
| 130 uuid_match.group(5) + uuid_match.group(6) + \ |
| 131 uuid_match.group(7) + uuid_match.group(8) |
| 132 else: |
| 133 state = 6 |
| 134 |
| 135 if otool_cmd.wait() != 0: |
| 136 state = 6 |
| 137 |
| 138 if state == 5: |
| 139 uuids[arch] = uuid.upper() |
| 140 |
| 141 return uuids |
| 142 |
| 143 # Given a path to a Mach-O file and possible information from the environment, |
| 144 # determines the desired path to the .dSYM. |
| 145 def dsym_path(macho): |
| 146 # If building a bundle, the .dSYM should be placed next to the bundle. Use |
| 147 # WRAPPER_NAME to make this determination. If called from xcodebuild, |
| 148 # WRAPPER_NAME will be set to the name of the bundle. |
| 149 dsym = "" |
| 150 if "WRAPPER_NAME" in os.environ: |
| 151 if "BUILT_PRODUCTS_DIR" in os.environ: |
| 152 dsym = os.path.join(os.environ["BUILT_PRODUCTS_DIR"], |
| 153 os.environ["WRAPPER_NAME"]) |
| 154 else: |
| 155 dsym = os.environ["WRAPPER_NAME"] |
| 156 else: |
| 157 dsym = macho |
| 158 |
| 159 dsym += ".dSYM" |
| 160 |
| 161 return dsym |
| 162 |
| 163 # Creates a fake .dSYM bundle at dsym for macho, a Mach-O image with the |
| 164 # architectures and UUIDs specified by the uuids map. |
| 165 def make_fake_dsym(macho, dsym): |
| 166 uuids = macho_uuids(macho) |
| 167 if len(uuids) == 0: |
| 168 return False |
| 169 |
| 170 dwarf_dir = os.path.join(dsym, "Contents", "Resources", "DWARF") |
| 171 dwarf_file = os.path.join(dwarf_dir, os.path.basename(macho)) |
| 172 try: |
| 173 os.makedirs(dwarf_dir) |
| 174 except OSError, (err, error_string): |
| 175 if err != errno.EEXIST: |
| 176 raise |
| 177 shutil.copyfile(macho, dwarf_file) |
| 178 |
| 179 # info_template is the same as what dsymutil would have written, with the |
| 180 # addition of the fake_dsym key. |
| 181 info_template = \ |
| 182 '''<?xml version="1.0" encoding="UTF-8"?> |
| 183 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.
com/DTDs/PropertyList-1.0.dtd"> |
| 184 <plist version="1.0"> |
| 185 <dict> |
| 186 <key>CFBundleDevelopmentRegion</key> |
| 187 <string>English</string> |
| 188 <key>CFBundleIdentifier</key> |
| 189 <string>com.apple.xcode.dsym.%(root_name)s</string> |
| 190 <key>CFBundleInfoDictionaryVersion</key> |
| 191 <string>6.0</string> |
| 192 <key>CFBundlePackageType</key> |
| 193 <string>dSYM</string> |
| 194 <key>CFBundleSignature</key> |
| 195 <string>????</string> |
| 196 <key>CFBundleShortVersionString</key> |
| 197 <string>1.0</string> |
| 198 <key>CFBundleVersion</key> |
| 199 <string>1</string> |
| 200 <key>dSYM_UUID</key> |
| 201 <dict> |
| 202 %(uuid_dict)s </dict> |
| 203 <key>fake_dsym</key> |
| 204 <true/> |
| 205 </dict> |
| 206 </plist> |
| 207 ''' |
| 208 |
| 209 root_name = os.path.basename(dsym)[:-5] # whatever.dSYM without .dSYM |
| 210 uuid_dict = "" |
| 211 for arch in sorted(uuids): |
| 212 uuid_dict += "\t\t\t<key>" + arch + "</key>\n"\ |
| 213 "\t\t\t<string>" + uuids[arch] + "</string>\n" |
| 214 info_dict = { |
| 215 "root_name": root_name, |
| 216 "uuid_dict": uuid_dict, |
| 217 } |
| 218 info_contents = info_template % info_dict |
| 219 info_file = os.path.join(dsym, "Info.plist") |
| 220 info_fd = open(info_file, "w") |
| 221 info_fd.write(info_contents) |
| 222 info_fd.close() |
| 223 |
| 224 return True |
| 225 |
| 226 # For a Mach-O file, determines where the .dSYM bundle should be located. If |
| 227 # the bundle does not exist or has a modification time older than the Mach-O |
| 228 # file, calls make_fake_dsym to create a fake .dSYM bundle there, then strips |
| 229 # the Mach-O file and sets the modification time on the .dSYM bundle and Mach-O |
| 230 # file to be identical. |
| 231 def strip_and_make_fake_dsym(macho): |
| 232 dsym = dsym_path(macho) |
| 233 macho_stat = os.stat(macho) |
| 234 dsym_stat = None |
| 235 try: |
| 236 dsym_stat = os.stat(dsym) |
| 237 except OSError, (err, error_string): |
| 238 if err != errno.ENOENT: |
| 239 raise |
| 240 |
| 241 if dsym_stat is None or dsym_stat.st_mtime < macho_stat.st_mtime: |
| 242 # Make a .dSYM bundle |
| 243 if not make_fake_dsym(macho, dsym): |
| 244 return False |
| 245 |
| 246 # Strip the Mach-O file |
| 247 remove_dsym = True |
| 248 try: |
| 249 strip_path = "" |
| 250 if "SYSTEM_DEVELOPER_BIN_DIR" in os.environ: |
| 251 strip_path = os.environ["SYSTEM_DEVELOPER_BIN_DIR"] |
| 252 else: |
| 253 strip_path = "/usr/bin" |
| 254 strip_path = os.path.join(strip_path, "strip") |
| 255 strip_cmdline = [strip_path] + sys.argv[1:] |
| 256 # Print the strip invocation so that it's obvious something is happening |
| 257 print " ".join(strip_cmdline) |
| 258 strip_cmd = subprocess.Popen(strip_cmdline) |
| 259 if strip_cmd.wait() == 0: |
| 260 remove_dsym = False |
| 261 finally: |
| 262 if remove_dsym: |
| 263 shutil.rmtree(dsym) |
| 264 |
| 265 # Update modification time on the Mach-O file and .dSYM bundle |
| 266 now = time.time() |
| 267 os.utime(macho, (now, now)) |
| 268 os.utime(dsym, (now, now)) |
| 269 |
| 270 return True |
| 271 |
| 272 def main(argv=None): |
| 273 if argv is None: |
| 274 argv = sys.argv |
| 275 |
| 276 # This only supports operating on one file at a time. Look at the arguments |
| 277 # to strip to figure out what the source to be stripped is. Arguments are |
| 278 # processed in the same way that strip does, although to reduce complexity, |
| 279 # this doesn't do all of the same checking as strip. For example, strip |
| 280 # has no -Z switch and would treat -Z on the command line as an error. For |
| 281 # the purposes this is needed for, that's fine. |
| 282 macho = None |
| 283 process_switches = True |
| 284 ignore_argument = False |
| 285 for arg in argv[1:]: |
| 286 if ignore_argument: |
| 287 ignore_argument = False |
| 288 continue |
| 289 if process_switches: |
| 290 if arg == "-": |
| 291 process_switches = False |
| 292 # strip has these switches accept an argument: |
| 293 if arg in ["-s", "-R", "-d", "-o", "-arch"]: |
| 294 ignore_argument = True |
| 295 if arg[0] == "-": |
| 296 continue |
| 297 if macho is None: |
| 298 macho = arg |
| 299 else: |
| 300 print >> sys.stderr, "Too many things to strip" |
| 301 return 1 |
| 302 |
| 303 if macho is None: |
| 304 print >> sys.stderr, "Nothing to strip" |
| 305 return 1 |
| 306 |
| 307 if not strip_and_make_fake_dsym(macho): |
| 308 return 1 |
| 309 |
| 310 return 0 |
| 311 |
| 312 if __name__ == '__main__': |
| 313 sys.exit(main(sys.argv)) |
OLD | NEW |