include <limits> for std::numeric_limits
[chromium-blink-merge.git] / build / mac / strip_save_dsym
blobc9cf22663757912c16895c55abd7ce346355079d
1 #!/usr/bin/env python
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.
7 # Usage: strip_save_dsym <whatever-arguments-you-would-pass-to-strip>
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.
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.
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.
31 import errno
32 import os
33 import re
34 import shutil
35 import subprocess
36 import sys
37 import time
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) + ")"
51 file_cmd = subprocess.Popen(["/usr/bin/file", "-b", "--", macho],
52 stdout=subprocess.PIPE)
54 archs = []
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))
73 if file_cmd.wait() != 0:
74 archs = []
76 if len(archs) == 0:
77 print >> sys.stderr, "No architectures in %s" % macho
79 return archs
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 = {}
88 archs = macho_archs(macho)
89 if len(archs) == 0:
90 return uuids
92 for arch in archs:
93 if arch == "":
94 continue
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)
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
162 if otool_cmd.wait() != 0:
163 state = 6
165 if state == 5:
166 uuids[arch] = uuid.upper()
168 if len(uuids) == 0:
169 print >> sys.stderr, "No UUIDs in %s" % macho
171 return uuids
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
189 dsym += ".dSYM"
191 return dsym
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
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)
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>
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,
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()
254 return True
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
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
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)
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))
292 return True
294 def main(argv=None):
295 if argv is None:
296 argv = sys.argv
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
325 if macho is None:
326 print >> sys.stderr, "Nothing to strip"
327 return 1
329 if not strip_and_make_fake_dsym(macho):
330 return 1
332 return 0
334 if __name__ == "__main__":
335 sys.exit(main(sys.argv))