OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 | |
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. | |
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 macho_types = ["executable", | |
47 "dynamically linked shared library", | |
48 "bundle"] | |
49 macho_types_re = "Mach-O (?:64-bit )?(?:" + "|".join(macho_types) + ")" | |
50 | |
51 file_cmd = subprocess.Popen(["/usr/bin/file", "-b", "--", macho], | |
52 stdout=subprocess.PIPE) | |
53 | |
54 archs = [] | |
55 | |
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)) | |
72 | |
73 if file_cmd.wait() != 0: | |
74 archs = [] | |
75 | |
76 if len(archs) == 0: | |
77 print >> sys.stderr, "No architectures in %s" % macho | |
78 | |
79 return archs | |
80 | |
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 = {} | |
87 | |
88 archs = macho_archs(macho) | |
89 if len(archs) == 0: | |
90 return uuids | |
91 | |
92 for arch in archs: | |
93 if arch == "": | |
94 continue | |
95 | |
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) | |
134 | |
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 | |
161 | |
162 if otool_cmd.wait() != 0: | |
163 state = 6 | |
164 | |
165 if state == 5: | |
166 uuids[arch] = uuid.upper() | |
167 | |
168 if len(uuids) == 0: | |
169 print >> sys.stderr, "No UUIDs in %s" % macho | |
170 | |
171 return uuids | |
172 | |
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 | |
188 | |
189 dsym += ".dSYM" | |
190 | |
191 return dsym | |
192 | |
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 | |
199 | |
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) | |
208 | |
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> | |
237 ''' | |
238 | |
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, | |
247 } | |
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() | |
253 | |
254 return True | |
255 | |
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 | |
270 | |
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 | |
275 | |
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) | |
286 | |
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)) | |
291 | |
292 return True | |
293 | |
294 def main(argv=None): | |
295 if argv is None: | |
296 argv = sys.argv | |
297 | |
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 | |
324 | |
325 if macho is None: | |
326 print >> sys.stderr, "Nothing to strip" | |
327 return 1 | |
328 | |
329 if not strip_and_make_fake_dsym(macho): | |
330 return 1 | |
331 | |
332 return 0 | |
333 | |
334 if __name__ == "__main__": | |
335 sys.exit(main(sys.argv)) | |
OLD | NEW |