OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # Copyright (c) 2008 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 """ | |
6 Commandline modification of Xcode project files | |
7 """ | |
8 | |
9 import sys | |
10 import os | |
11 import optparse | |
12 import re | |
13 import tempfile | |
14 import random | |
15 import subprocess | |
16 | |
17 random.seed() # Seed the generator | |
18 | |
19 | |
20 # All known project build path source tree path reference types | |
21 PBX_VALID_SOURCE_TREE_TYPES = ('"<group>"', | |
22 'SOURCE_ROOT', | |
23 '"<absolute>"', | |
24 'BUILT_PRODUCTS_DIR', | |
25 'DEVELOPER_DIR', | |
26 'SDKROOT', | |
27 'CONFIGURATION_TEMP_DIR') | |
28 # Paths with some characters appear quoted | |
29 QUOTE_PATH_RE = re.compile('\s|-|\+') | |
30 | |
31 # Supported Xcode file types | |
32 EXTENSION_TO_XCODE_FILETYPE = { | |
33 '.h' : 'sourcecode.c.h', | |
34 '.c' : 'sourcecode.c.c', | |
35 '.cpp' : 'sourcecode.cpp.cpp', | |
36 '.cc' : 'sourcecode.cpp.cpp', | |
37 '.cxx' : 'sourcecode.cpp.cpp', | |
38 '.m' : 'sourcecode.c.objc', | |
39 '.mm' : 'sourcecode.c.objcpp', | |
40 } | |
41 | |
42 # File types that can be added to a Sources phase | |
43 SOURCES_XCODE_FILETYPES = ( 'sourcecode.c.c', | |
44 'sourcecode.cpp.cpp', | |
45 'sourcecode.c.objc', | |
46 'sourcecode.c.objcpp' ) | |
47 | |
48 # Avoid inserting source files into these common Xcode group names. Because | |
49 # Xcode allows any names for these groups this list cannot be authoritative, | |
50 # but these are common names in the Xcode templates. | |
51 NON_SOURCE_GROUP_NAMES = ( 'Frameworks', | |
52 'Resources', | |
53 'Products', | |
54 'Derived Sources', | |
55 'Configurations', | |
56 'Documentation', | |
57 'Frameworks and Libraries', | |
58 'External Frameworks and Libraries', | |
59 'Libraries' ) | |
60 | |
61 | |
62 def NewUUID(): | |
63 """Create a new random Xcode UUID""" | |
64 __pychecker__ = 'unusednames=i' | |
65 elements = [] | |
66 for i in range(24): | |
67 elements.append(hex(random.randint(0, 15))[-1].upper()) | |
68 return ''.join(elements) | |
69 | |
70 def CygwinPathClean(path): | |
71 """Folks use Cygwin shells with standard Win32 Python which can't handle | |
72 Cygwin paths. Run everything through cygpath if we can (conveniently | |
73 cygpath does the right thing with normal Win32 paths). | |
74 """ | |
75 # Look for Unix-like path with Win32 Python | |
76 if sys.platform == 'win32' and path.startswith('/'): | |
77 cygproc = subprocess.Popen(('cygpath', '-a', '-w', path), | |
78 stdout=subprocess.PIPE) | |
79 (stdout_content, stderr_content) = cygproc.communicate() | |
80 return stdout_content.rstrip() | |
81 # Convert all paths to cygpaths if we're using cygwin python | |
82 if sys.platform == 'cygwin': | |
83 cygproc = subprocess.Popen(('cygpath', '-a', '-u', path), | |
84 stdout=subprocess.PIPE) | |
85 (stdout_content, stderr_content) = cygproc.communicate() | |
86 return stdout_content.rstrip() | |
87 # Fallthrough for all other cases | |
88 return path | |
89 | |
90 class XcodeProject(object): | |
91 """Class for reading/writing Xcode project files. | |
92 This is not a general parser or representation. It is restricted to just | |
93 the Xcode internal objects we need. | |
94 | |
95 Args: | |
96 path: Absolute path to Xcode project file (including project.pbxproj | |
97 filename) | |
98 Attributes: | |
99 path: Full path to the project.pbxproj file | |
100 name: Project name (wrapper directory basename without extension) | |
101 source_root_path: Absolute path for Xcode's SOURCE_ROOT | |
102 """ | |
103 | |
104 EXPECTED_PROJECT_HEADER_RE = re.compile( | |
105 r'^// !\$\*UTF8\*\$!\n' \ | |
106 '\{\n' \ | |
107 '\tarchiveVersion = 1;\n' \ | |
108 '\tclasses = \{\n' \ | |
109 '\t\};\n' \ | |
110 '\tobjectVersion = \d+;\n' \ | |
111 '\tobjects = \{\n' \ | |
112 '\n') | |
113 SECTION_BEGIN_RE = re.compile(r'^/\* Begin (.*) section \*/\n$') | |
114 SECTION_END_RE = re.compile(r'^/\* End (.*) section \*/\n$') | |
115 PROJECT_ROOT_OBJECT_RE = re.compile( | |
116 r'^\trootObject = ([0-9A-F]{24}) /\* Project object \*/;\n$') | |
117 | |
118 def __init__(self, path): | |
119 self.path = path | |
120 self.name = os.path.splitext(os.path.basename(os.path.dirname(path)))[0] | |
121 | |
122 # Load project. Ideally we would use plistlib, but sadly that only reads | |
123 # XML plists. A real parser with pyparsing | |
124 # (http://pyparsing.wikispaces.com/) might be another option, but for now | |
125 # we'll do the simple (?!?) thing. | |
126 project_fh = open(self.path, 'rU') | |
127 self._raw_content = project_fh.readlines() | |
128 project_fh.close() | |
129 | |
130 # Store and check header | |
131 if len(self._raw_content) < 8: | |
132 print >> sys.stderr, ''.join(self._raw_content) | |
133 raise RuntimeError('XcodeProject file "%s" too short' % path) | |
134 self._header = tuple(self._raw_content[:8]) | |
135 if not self.__class__.EXPECTED_PROJECT_HEADER_RE.match(''.join(self._header)
): | |
136 print >> sys.stderr, ''.join(self._header) | |
137 raise RuntimeError('XcodeProject file "%s" wrong header' % path) | |
138 | |
139 # Find and store tail (some projects have additional whitespace at end) | |
140 self._tail = [] | |
141 for tail_line in reversed(self._raw_content): | |
142 self._tail.insert(0, tail_line) | |
143 if tail_line == '\t};\n': break | |
144 | |
145 # Ugly ugly project parsing, turn each commented section into a separate | |
146 # set of objects. For types we don't have a custom representation for, | |
147 # store the raw lines. | |
148 self._section_order = [] | |
149 self._sections = {} | |
150 parse_line_no = len(self._header) | |
151 while parse_line_no < (len(self._raw_content) - len(self._tail)): | |
152 section_header_match = self.__class__.SECTION_BEGIN_RE.match( | |
153 self._raw_content[parse_line_no]) | |
154 # Loop to next section header | |
155 if not section_header_match: | |
156 parse_line_no += 1 | |
157 continue | |
158 | |
159 section = section_header_match.group(1) | |
160 self._section_order.append(section) | |
161 self._sections[section] = [] | |
162 | |
163 # Advance to first line of the section | |
164 parse_line_no += 1 | |
165 | |
166 # Read in the section, using custom classes where we need them | |
167 section_end_match = self.__class__.SECTION_END_RE.match( | |
168 self._raw_content[parse_line_no]) | |
169 while not section_end_match: | |
170 # Unhandled lines | |
171 content = self._raw_content[parse_line_no] | |
172 # Sections we can parse line-by-line | |
173 if section in ('PBXBuildFile', 'PBXFileReference'): | |
174 content = eval('%s.FromContent(content)' % section) | |
175 # Multiline sections | |
176 elif section in ('PBXGroup', 'PBXVariantGroup', 'PBXProject', | |
177 'PBXNativeTarget', 'PBXSourcesBuildPhase'): | |
178 # Accumulate lines | |
179 content_lines = [] | |
180 while 1: | |
181 content_lines.append(content) | |
182 if content == '\t\t};\n': break | |
183 parse_line_no += 1 | |
184 content = self._raw_content[parse_line_no] | |
185 content = eval('%s.FromContent(content_lines)' % section) | |
186 | |
187 self._sections[section].append(content) | |
188 parse_line_no += 1 | |
189 section_end_match = self.__class__.SECTION_END_RE.match( | |
190 self._raw_content[parse_line_no]) | |
191 # Validate section end | |
192 if section_header_match.group(1) != section: | |
193 raise RuntimeError( | |
194 'XcodeProject parse, section "%s" ended inside section "%s"' % | |
195 (section_end_match.group(1), section)) | |
196 # Back around parse loop | |
197 | |
198 # Sanity overall group structure | |
199 if (not self._sections.has_key('PBXProject') or | |
200 len(self._sections['PBXProject']) != 1): | |
201 raise RuntimeError('PBXProject section insane') | |
202 root_obj_parsed = self.__class__.PROJECT_ROOT_OBJECT_RE.match( | |
203 self._tail[1]) | |
204 if not root_obj_parsed: | |
205 raise RuntimeError('XcodeProject unable to parse project root object:\n%s' | |
206 % self._tail[1]) | |
207 if root_obj_parsed.group(1) != self._sections['PBXProject'][0].uuid: | |
208 raise RuntimeError('XcodeProject root object does not match PBXProject') | |
209 self._root_group_uuid = self._sections['PBXProject'][0].main_group_uuid | |
210 | |
211 # Source root | |
212 self.source_root_path = os.path.abspath( | |
213 os.path.join( | |
214 # Directory that contains the project package | |
215 os.path.dirname(os.path.dirname(path)), | |
216 # Any relative path | |
217 self._sections['PBXProject'][0].project_root)) | |
218 | |
219 # Build the absolute paths of the groups with these helpers | |
220 def GroupAbsRealPath(*elements): | |
221 return os.path.abspath(os.path.realpath(os.path.join(*elements))) | |
222 def GroupPathRecurse(group, parent_path): | |
223 descend = False | |
224 if group.source_tree == '"<absolute>"': | |
225 group.abs_path = GroupAbsRealPath(group.path) | |
226 descend = True | |
227 elif group.source_tree == '"<group>"': | |
228 if group.path: | |
229 group.abs_path = GroupAbsRealPath(parent_path, group.path) | |
230 else: | |
231 group.abs_path = parent_path | |
232 descend = True | |
233 elif group.source_tree == 'SOURCE_ROOT': | |
234 if group.path: | |
235 group.abs_path = GroupAbsRealPath(self.source_root_path, group.path) | |
236 else: | |
237 group.abs_path = GroupAbsRealPath(self.source_root_path) | |
238 descend = True | |
239 if descend: | |
240 for child_uuid in group.child_uuids: | |
241 # Try a group first | |
242 found_uuid = False | |
243 for other_group in self._sections['PBXGroup']: | |
244 if other_group.uuid == child_uuid: | |
245 found_uuid = True | |
246 GroupPathRecurse(other_group, group.abs_path) | |
247 break | |
248 if self._sections.has_key('PBXVariantGroup'): | |
249 for other_group in self._sections['PBXVariantGroup']: | |
250 if other_group.uuid == child_uuid: | |
251 found_uuid = True | |
252 GroupPathRecurse(other_group, group.abs_path) | |
253 break | |
254 if not found_uuid: | |
255 for file_ref in self._sections['PBXFileReference']: | |
256 if file_ref.uuid == child_uuid: | |
257 found_uuid = True | |
258 if file_ref.source_tree == '"<absolute>"': | |
259 file_ref.abs_path = GroupAbsRealPath(file_ref.path) | |
260 elif group.source_tree == '"<group>"': | |
261 file_ref.abs_path = GroupAbsRealPath(group.abs_path, | |
262 file_ref.path) | |
263 elif group.source_tree == 'SOURCE_ROOT': | |
264 file_ref.abs_path = GroupAbsRealPath(self.source_root_path, | |
265 file_ref.path) | |
266 break | |
267 if not found_uuid: | |
268 raise RuntimeError('XcodeProject group descent failed to find %s' % | |
269 child_uuid) | |
270 self._root_group = None | |
271 for group in self._sections['PBXGroup']: | |
272 if group.uuid == self._root_group_uuid: | |
273 self._root_group = group | |
274 GroupPathRecurse(group, self.source_root_path) | |
275 if not self._root_group: | |
276 raise RuntimeError('XcodeProject failed to find root group by UUID') | |
277 | |
278 def FileContent(self): | |
279 """Generate and return the project file content as a list of lines""" | |
280 content = [] | |
281 content.extend(self._header[:-1]) | |
282 for section in self._section_order: | |
283 content.append('\n/* Begin %s section */\n' % section) | |
284 for section_content in self._sections[section]: | |
285 content.append(str(section_content)) | |
286 content.append('/* End %s section */\n' % section) | |
287 content.extend(self._tail) | |
288 return content | |
289 | |
290 def Update(self): | |
291 """Rewrite the project file in place with all updated metadata""" | |
292 __pychecker__ = 'no-deprecated' | |
293 # Not concerned with temp_path security here, just needed a unique name | |
294 temp_path = tempfile.mktemp(dir=os.path.dirname(self.path)) | |
295 outfile = open(temp_path, 'w') | |
296 outfile.writelines(self.FileContent()) | |
297 outfile.close() | |
298 # Rename is weird on Win32, see the docs, | |
299 os.unlink(self.path) | |
300 os.rename(temp_path, self.path) | |
301 | |
302 def NativeTargets(self): | |
303 """Obtain all PBXNativeTarget instances for this project | |
304 | |
305 Returns: | |
306 List of PBXNativeTarget instances | |
307 """ | |
308 if self._sections.has_key('PBXNativeTarget'): | |
309 return self._sections['PBXNativeTarget'] | |
310 else: | |
311 return [] | |
312 | |
313 def NativeTargetForName(self, name): | |
314 """Obtain the target with a given name. | |
315 | |
316 Args: | |
317 name: Target name | |
318 | |
319 Returns: | |
320 PBXNativeTarget instance or None | |
321 """ | |
322 for target in self.NativeTargets(): | |
323 if target.name == name: | |
324 return target | |
325 return None | |
326 | |
327 def FileReferences(self): | |
328 """Obtain all PBXFileReference instances for this project | |
329 | |
330 Returns: | |
331 List of PBXFileReference instances | |
332 """ | |
333 return self._sections['PBXFileReference'] | |
334 | |
335 def SourcesBuildPhaseForTarget(self, target): | |
336 """Obtain the PBXSourcesBuildPhase instance for a target. Xcode allows | |
337 only one PBXSourcesBuildPhase per target and each target has a unique | |
338 PBXSourcesBuildPhase. | |
339 | |
340 Args: | |
341 target: PBXNativeTarget instance | |
342 | |
343 Returns: | |
344 PBXSourcesBuildPhase instance | |
345 """ | |
346 sources_uuid = None | |
347 for i in range(len(target.build_phase_names)): | |
348 if target.build_phase_names[i] == 'Sources': | |
349 sources_uuid = target.build_phase_uuids[i] | |
350 break | |
351 if not sources_uuid: | |
352 raise RuntimeError('Missing PBXSourcesBuildPhase for target "%s"' % | |
353 target.name) | |
354 for sources_phase in self._sections['PBXSourcesBuildPhase']: | |
355 if sources_phase.uuid == sources_uuid: | |
356 return sources_phase | |
357 raise RuntimeError('Missing PBXSourcesBuildPhase for UUID "%s"' % | |
358 sources_uuid) | |
359 | |
360 def BuildFileForUUID(self, uuid): | |
361 """Look up a PBXBuildFile by UUID | |
362 | |
363 Args: | |
364 uuid: UUID of the PBXBuildFile to find | |
365 | |
366 Raises: | |
367 RuntimeError if no PBXBuildFile exists for |uuid| | |
368 | |
369 Returns: | |
370 PBXBuildFile instance | |
371 """ | |
372 for build_file in self._sections['PBXBuildFile']: | |
373 if build_file.uuid == uuid: | |
374 return build_file | |
375 raise RuntimeError('Missing PBXBuildFile for UUID "%s"' % uuid) | |
376 | |
377 def FileReferenceForUUID(self, uuid): | |
378 """Look up a PBXFileReference by UUID | |
379 | |
380 Args: | |
381 uuid: UUID of the PBXFileReference to find | |
382 | |
383 Raises: | |
384 RuntimeError if no PBXFileReference exists for |uuid| | |
385 | |
386 Returns: | |
387 PBXFileReference instance | |
388 """ | |
389 for file_ref in self._sections['PBXFileReference']: | |
390 if file_ref.uuid == uuid: | |
391 return file_ref | |
392 raise RuntimeError('Missing PBXFileReference for UUID "%s"' % uuid) | |
393 | |
394 def RemoveSourceFileReference(self, file_ref): | |
395 """Remove a source file's PBXFileReference from the project, cleaning up all | |
396 PBXGroup and PBXBuildFile references to that PBXFileReference and | |
397 furthermore, removing any PBXBuildFiles from all PBXNativeTarget source | |
398 lists. | |
399 | |
400 Args: | |
401 file_ref: PBXFileReference instance | |
402 | |
403 Raises: | |
404 RuntimeError if |file_ref| is not a source file reference in PBXBuildFile | |
405 """ | |
406 self._sections['PBXFileReference'].remove(file_ref) | |
407 # Clean up build files | |
408 removed_build_files = [] | |
409 for build_file in self._sections['PBXBuildFile']: | |
410 if build_file.file_ref_uuid == file_ref.uuid: | |
411 if build_file.type != 'Sources': | |
412 raise RuntimeError('Removing PBXBuildFile not of "Sources" type') | |
413 removed_build_files.append(build_file) | |
414 removed_build_file_uuids = [] | |
415 for build_file in removed_build_files: | |
416 removed_build_file_uuids.append(build_file.uuid) | |
417 self._sections['PBXBuildFile'].remove(build_file) | |
418 # Clean up source references to the removed build files | |
419 for source_phase in self._sections['PBXSourcesBuildPhase']: | |
420 removal_indexes = [] | |
421 for i in range(len(source_phase.file_uuids)): | |
422 if source_phase.file_uuids[i] in removed_build_file_uuids: | |
423 removal_indexes.append(i) | |
424 for removal_index in removal_indexes: | |
425 del source_phase.file_uuids[removal_index] | |
426 del source_phase.file_names[removal_index] | |
427 # Clean up group references | |
428 for group in self._sections['PBXGroup']: | |
429 removal_indexes = [] | |
430 for i in range(len(group.child_uuids)): | |
431 if group.child_uuids[i] == file_ref.uuid: | |
432 removal_indexes.append(i) | |
433 for removal_index in removal_indexes: | |
434 del group.child_uuids[removal_index] | |
435 del group.child_names[removal_index] | |
436 | |
437 def RelativeSourceRootPath(self, abs_path): | |
438 """Convert a path to one relative to the project's SOURCE_ROOT if possible. | |
439 Generally this follows Xcode semantics, that is, a path is only converted | |
440 if it is a subpath of SOURCE_ROOT. | |
441 | |
442 Args: | |
443 abs_path: Absolute path to convert | |
444 | |
445 Returns: | |
446 String SOURCE_ROOT relative path if possible or None if not relative | |
447 to SOURCE_ROOT. | |
448 """ | |
449 if abs_path.startswith(self.source_root_path + os.path.sep): | |
450 return abs_path[len(self.source_root_path + os.path.sep):] | |
451 else: | |
452 # Try to construct a relative path (bodged from ActiveState recipe | |
453 # 302594 since we can't assume Python 2.5 with os.path.relpath() | |
454 source_root_parts = self.source_root_path.split(os.path.sep) | |
455 target_parts = abs_path.split(os.path.sep) | |
456 # Guard against drive changes on Win32 and cygwin | |
457 if sys.platform == 'win32' and source_root_parts[0] <> target_parts[0]: | |
458 return None | |
459 if sys.platform == 'cygwin' and source_root_parts[2] <> target_parts[2]: | |
460 return None | |
461 for i in range(min(len(source_root_parts), len(target_parts))): | |
462 if source_root_parts[i] <> target_parts[i]: break | |
463 else: | |
464 i += 1 | |
465 rel_parts = [os.path.pardir] * (len(source_root_parts) - i) | |
466 rel_parts.extend(target_parts[i:]) | |
467 return os.path.join(*rel_parts) | |
468 | |
469 def RelativeGroupPath(self, abs_path): | |
470 """Convert a path to a group-relative path if possible | |
471 | |
472 Args: | |
473 abs_path: Absolute path to convert | |
474 | |
475 Returns: | |
476 Parent PBXGroup instance if possible or None | |
477 """ | |
478 needed_path = os.path.dirname(abs_path) | |
479 possible_groups = [ g for g in self._sections['PBXGroup'] | |
480 if g.abs_path == needed_path and | |
481 not g.name in NON_SOURCE_GROUP_NAMES ] | |
482 if len(possible_groups) < 1: | |
483 return None | |
484 elif len(possible_groups) == 1: | |
485 return possible_groups[0] | |
486 # Multiple groups match, try to find the best using some simple | |
487 # heuristics. Does only one group contain source? | |
488 groups_with_source = [] | |
489 for group in possible_groups: | |
490 for child_uuid in group.child_uuids: | |
491 try: | |
492 self.FileReferenceForUUID(child_uuid) | |
493 except RuntimeError: | |
494 pass | |
495 else: | |
496 groups_with_source.append(group) | |
497 break | |
498 if len(groups_with_source) == 1: | |
499 return groups_with_source[0] | |
500 # Is only one _not_ the root group? | |
501 non_root_groups = [ g for g in possible_groups | |
502 if g is not self._root_group ] | |
503 if len(non_root_groups) == 1: | |
504 return non_root_groups[0] | |
505 # Best guess | |
506 if len(non_root_groups): | |
507 return non_root_groups[0] | |
508 elif len(groups_with_source): | |
509 return groups_with_source[0] | |
510 else: | |
511 return possible_groups[0] | |
512 | |
513 def AddSourceFile(self, path): | |
514 """Add a source file to the project, attempting to position it | |
515 in the GUI group hierarchy reasonably. | |
516 | |
517 NOTE: Adding a source file does not add it to any targets | |
518 | |
519 Args: | |
520 path: Absolute path to the file to add | |
521 | |
522 Returns: | |
523 PBXFileReference instance for the newly added source. | |
524 """ | |
525 # Guess at file type | |
526 root, extension = os.path.splitext(path) | |
527 if EXTENSION_TO_XCODE_FILETYPE.has_key(extension): | |
528 source_type = EXTENSION_TO_XCODE_FILETYPE[extension] | |
529 else: | |
530 raise RuntimeError('Unknown source file extension "%s"' % extension) | |
531 | |
532 # Is group-relative possible for an existing group? | |
533 parent_group = self.RelativeGroupPath(os.path.abspath(path)) | |
534 if parent_group: | |
535 new_file_ref = PBXFileReference(NewUUID(), | |
536 os.path.basename(path), | |
537 source_type, | |
538 None, | |
539 os.path.basename(path), | |
540 '"<group>"', | |
541 None) | |
542 # Chrome tries to keep its lists name sorted, try to match | |
543 i = 0 | |
544 while i < len(parent_group.child_uuids): | |
545 # Only files are sorted, they keep groups at the top | |
546 try: | |
547 self.FileReferenceForUUID(parent_group.child_uuids[i]) | |
548 if new_file_ref.name.lower() < parent_group.child_names[i].lower(): | |
549 break | |
550 except RuntimeError: | |
551 pass # Must be a child group | |
552 i += 1 | |
553 parent_group.child_names.insert(i, new_file_ref.name) | |
554 parent_group.child_uuids.insert(i, new_file_ref.uuid) | |
555 # Add file ref uuid sorted | |
556 self._sections['PBXFileReference'].append(new_file_ref) | |
557 self._sections['PBXFileReference'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid
)) | |
558 return new_file_ref | |
559 | |
560 # Group-relative failed, how about SOURCE_ROOT relative in the main group | |
561 src_rel_path = self.RelativeSourceRootPath(os.path.abspath(path)) | |
562 if src_rel_path: | |
563 src_rel_path = src_rel_path.replace('\\', '/') # Convert to Unix | |
564 new_file_ref = PBXFileReference(NewUUID(), | |
565 os.path.basename(path), | |
566 source_type, | |
567 None, | |
568 src_rel_path, | |
569 'SOURCE_ROOT', | |
570 None) | |
571 self._root_group.child_uuids.append(new_file_ref.uuid) | |
572 self._root_group.child_names.append(new_file_ref.name) | |
573 # Add file ref uuid sorted | |
574 self._sections['PBXFileReference'].append(new_file_ref) | |
575 self._sections['PBXFileReference'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid
)) | |
576 return new_file_ref | |
577 | |
578 # Win to Unix absolute paths probably not practical | |
579 raise RuntimeError('Could not construct group or source PBXFileReference ' | |
580 'for path "%s"' % path) | |
581 | |
582 def AddSourceFileToSourcesBuildPhase(self, source_ref, source_phase): | |
583 """Add a PBXFileReference to a PBXSourcesBuildPhase, creating a new | |
584 PBXBuildFile as needed. | |
585 | |
586 Args: | |
587 source_ref: PBXFileReference instance appropriate for use in | |
588 PBXSourcesBuildPhase | |
589 source_phase: PBXSourcesBuildPhase instance | |
590 """ | |
591 # Prevent duplication | |
592 for source_uuid in source_phase.file_uuids: | |
593 build_file = self.BuildFileForUUID(source_uuid) | |
594 if build_file.file_ref_uuid == source_ref.uuid: | |
595 return | |
596 # Create PBXBuildFile | |
597 new_build_file = PBXBuildFile(NewUUID(), | |
598 source_ref.name, | |
599 'Sources', | |
600 source_ref.uuid, | |
601 '') | |
602 # Add to build file list (uuid sorted) | |
603 self._sections['PBXBuildFile'].append(new_build_file) | |
604 self._sections['PBXBuildFile'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid)) | |
605 # Add to sources phase list (name sorted) | |
606 i = 0 | |
607 while i < len(source_phase.file_names): | |
608 if source_ref.name.lower() < source_phase.file_names[i].lower(): | |
609 break | |
610 i += 1 | |
611 source_phase.file_names.insert(i, new_build_file.name) | |
612 source_phase.file_uuids.insert(i, new_build_file.uuid) | |
613 | |
614 | |
615 class PBXProject(object): | |
616 """Class for PBXProject data section of an Xcode project file. | |
617 | |
618 Attributes: | |
619 uuid: Project UUID | |
620 main_group_uuid: UUID of the top-level PBXGroup | |
621 project_root: Relative path from project file wrapper to source_root_path | |
622 """ | |
623 | |
624 PBXPROJECT_HEADER_RE = re.compile( | |
625 r'^\t\t([0-9A-F]{24}) /\* Project object \*/ = {\n$') | |
626 PBXPROJECT_MAIN_GROUP_RE = re.compile( | |
627 r'^\t\t\tmainGroup = ([0-9A-F]{24})(?: /\* .* \*/)?;\n$') | |
628 PBXPROJECT_ROOT_RE = re.compile( | |
629 r'^\t\t\tprojectRoot = (.*);\n$') | |
630 | |
631 @classmethod | |
632 def FromContent(klass, content_lines): | |
633 header_parsed = klass.PBXPROJECT_HEADER_RE.match(content_lines[0]) | |
634 if not header_parsed: | |
635 raise RuntimeError('PBXProject unable to parse header content:\n%s' | |
636 % content_lines[0]) | |
637 main_group_uuid = None | |
638 project_root = '' | |
639 for content_line in content_lines: | |
640 group_parsed = klass.PBXPROJECT_MAIN_GROUP_RE.match(content_line) | |
641 if group_parsed: | |
642 main_group_uuid = group_parsed.group(1) | |
643 root_parsed = klass.PBXPROJECT_ROOT_RE.match(content_line) | |
644 if root_parsed: | |
645 project_root = root_parsed.group(1) | |
646 if project_root.startswith('"'): | |
647 project_root = project_root[1:-1] | |
648 if not main_group_uuid: | |
649 raise RuntimeError('PBXProject missing main group') | |
650 return klass(content_lines, header_parsed.group(1), | |
651 main_group_uuid, project_root) | |
652 | |
653 def __init__(self, raw_lines, uuid, main_group_uuid, project_root): | |
654 self.uuid = uuid | |
655 self._raw_lines = raw_lines | |
656 self.main_group_uuid = main_group_uuid | |
657 self.project_root = project_root | |
658 | |
659 def __str__(self): | |
660 return ''.join(self._raw_lines) | |
661 | |
662 | |
663 class PBXBuildFile(object): | |
664 """Class for PBXBuildFile data from an Xcode project file. | |
665 | |
666 Attributes: | |
667 uuid: UUID for this instance | |
668 name: Basename of the build file | |
669 type: 'Sources' or 'Frameworks' | |
670 file_ref_uuid: UUID of the PBXFileReference for this file | |
671 """ | |
672 | |
673 PBXBUILDFILE_LINE_RE = re.compile( | |
674 r'^\t\t([0-9A-F]{24}) /\* (.+) in (.+) \*/ = ' | |
675 '{isa = PBXBuildFile; fileRef = ([0-9A-F]{24}) /\* (.+) \*/; (.*)};\n$') | |
676 | |
677 @classmethod | |
678 def FromContent(klass, content_line): | |
679 parsed = klass.PBXBUILDFILE_LINE_RE.match(content_line) | |
680 if not parsed: | |
681 raise RuntimeError('PBXBuildFile unable to parse content:\n%s' | |
682 % content_line) | |
683 if parsed.group(2) != parsed.group(5): | |
684 raise RuntimeError('PBXBuildFile name mismatch "%s" vs "%s"' % | |
685 (parsed.group(2), parsed.group(5))) | |
686 if not parsed.group(3) in ('Sources', 'Frameworks', | |
687 'Resources', 'CopyFiles', | |
688 'Headers', 'Copy Into Framework', | |
689 'Rez', 'Copy Generated Headers'): | |
690 raise RuntimeError('PBXBuildFile unknown type "%s"' % parsed.group(3)) | |
691 return klass(parsed.group(1), parsed.group(2), parsed.group(3), | |
692 parsed.group(4), parsed.group(6)) | |
693 | |
694 def __init__(self, uuid, name, type, file_ref_uuid, raw_extras): | |
695 self.uuid = uuid | |
696 self.name = name | |
697 self.type = type | |
698 self.file_ref_uuid = file_ref_uuid | |
699 self._raw_extras = raw_extras | |
700 | |
701 def __str__(self): | |
702 return '\t\t%s /* %s in %s */ = ' \ | |
703 '{isa = PBXBuildFile; fileRef = %s /* %s */; %s};\n' % ( | |
704 self.uuid, self.name, self.type, self.file_ref_uuid, self.name, | |
705 self._raw_extras) | |
706 | |
707 | |
708 class PBXFileReference(object): | |
709 """Class for PBXFileReference data from an Xcode project file. | |
710 | |
711 Attributes: | |
712 uuid: UUID for this instance | |
713 name: Basename of the file | |
714 file_type: current active file type (explicit or assumed) | |
715 path: source_tree relative path (or absolute if source_tree is absolute) | |
716 source_tree: Source tree type (see PBX_VALID_SOURCE_TREE_TYPES) | |
717 abs_path: Absolute path to the file | |
718 """ | |
719 PBXFILEREFERENCE_HEADER_RE = re.compile( | |
720 r'^\t\t([0-9A-F]{24}) /\* (.+) \*/ = {isa = PBXFileReference; ') | |
721 PBXFILEREFERENCE_FILETYPE_RE = re.compile( | |
722 r' (lastKnownFileType|explicitFileType) = ([^\;]+); ') | |
723 PBXFILEREFERENCE_PATH_RE = re.compile(r' path = ([^\;]+); ') | |
724 PBXFILEREFERENCE_SOURCETREE_RE = re.compile(r' sourceTree = ([^\;]+); ') | |
725 | |
726 @classmethod | |
727 def FromContent(klass, content_line): | |
728 header_parsed = klass.PBXFILEREFERENCE_HEADER_RE.match(content_line) | |
729 if not header_parsed: | |
730 raise RuntimeError('PBXFileReference unable to parse header content:\n%s' | |
731 % content_line) | |
732 type_parsed = klass.PBXFILEREFERENCE_FILETYPE_RE.search(content_line) | |
733 if not type_parsed: | |
734 raise RuntimeError('PBXFileReference unable to parse type content:\n%s' | |
735 % content_line) | |
736 if type_parsed.group(1) == 'lastKnownFileType': | |
737 last_known_type = type_parsed.group(2) | |
738 explicit_type = None | |
739 else: | |
740 last_known_type = None | |
741 explicit_type = type_parsed.group(2) | |
742 path_parsed = klass.PBXFILEREFERENCE_PATH_RE.search(content_line) | |
743 if not path_parsed: | |
744 raise RuntimeError('PBXFileReference unable to parse path content:\n%s' | |
745 % content_line) | |
746 tree_parsed = klass.PBXFILEREFERENCE_SOURCETREE_RE.search(content_line) | |
747 if not tree_parsed: | |
748 raise RuntimeError( | |
749 'PBXFileReference unable to parse source tree content:\n%s' | |
750 % content_line) | |
751 return klass(header_parsed.group(1), header_parsed.group(2), | |
752 last_known_type, explicit_type, path_parsed.group(1), | |
753 tree_parsed.group(1), content_line) | |
754 | |
755 def __init__(self, uuid, name, last_known_file_type, explicit_file_type, | |
756 path, source_tree, raw_line): | |
757 self.uuid = uuid | |
758 self.name = name | |
759 self._last_known_file_type = last_known_file_type | |
760 self._explicit_file_type = explicit_file_type | |
761 if explicit_file_type: | |
762 self.file_type = explicit_file_type | |
763 else: | |
764 self.file_type = last_known_file_type | |
765 self.path = path | |
766 self.source_tree = source_tree | |
767 self.abs_path = None | |
768 self._raw_line = raw_line | |
769 | |
770 def __str__(self): | |
771 # Raw available? | |
772 if self._raw_line: return self._raw_line | |
773 # Construct our own | |
774 if self._last_known_file_type: | |
775 print_file_type = 'lastKnownFileType = %s; ' % self._last_known_file_type | |
776 elif self._explicit_file_type: | |
777 print_file_type = 'explicitFileType = %s; ' % self._explicit_file_type | |
778 else: | |
779 raise RuntimeError('No known file type for stringification') | |
780 name_attribute = '' | |
781 if self.name != self.path: | |
782 name_attribute = 'name = %s; ' % self.name | |
783 print_path = self.path | |
784 if QUOTE_PATH_RE.search(print_path): | |
785 print_path = '"%s"' % print_path | |
786 return '\t\t%s /* %s */ = ' \ | |
787 '{isa = PBXFileReference; ' \ | |
788 'fileEncoding = 4; ' \ | |
789 '%s' \ | |
790 '%s' \ | |
791 'path = %s; sourceTree = %s; };\n' % ( | |
792 self.uuid, self.name, print_file_type, | |
793 name_attribute, print_path, self.source_tree) | |
794 | |
795 | |
796 class PBXGroup(object): | |
797 """Class for PBXGroup data from an Xcode project file. | |
798 | |
799 Attributes: | |
800 uuid: UUID for this instance | |
801 name: Group (folder) name | |
802 path: source_tree relative path (or absolute if source_tree is absolute) | |
803 source_tree: Source tree type (see PBX_VALID_SOURCE_TREE_TYPES) | |
804 abs_path: Absolute path to the group | |
805 child_uuids: Ordered list of PBXFileReference UUIDs | |
806 child_names: Ordered list of PBXFileReference names | |
807 """ | |
808 | |
809 PBXGROUP_HEADER_RE = re.compile(r'^\t\t([0-9A-F]{24}) (?:/\* .* \*/ )?= {\n$') | |
810 PBXGROUP_FIELD_RE = re.compile(r'^\t\t\t(.*) = (.*);\n$') | |
811 PBXGROUP_CHILD_RE = re.compile(r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) \*/,\n$') | |
812 | |
813 @classmethod | |
814 def FromContent(klass, content_lines): | |
815 # Header line | |
816 header_parsed = klass.PBXGROUP_HEADER_RE.match(content_lines[0]) | |
817 if not header_parsed: | |
818 raise RuntimeError('PBXGroup unable to parse header content:\n%s' | |
819 % content_lines[0]) | |
820 name = None | |
821 path = '' | |
822 source_tree = None | |
823 tab_width = None | |
824 uses_tabs = None | |
825 indent_width = None | |
826 child_uuids = [] | |
827 child_names = [] | |
828 # Parse line by line | |
829 content_line_no = 0 | |
830 while 1: | |
831 content_line_no += 1 | |
832 content_line = content_lines[content_line_no] | |
833 if content_line == '\t\t};\n': break | |
834 if content_line == '\t\t\tisa = PBXGroup;\n': continue | |
835 if content_line == '\t\t\tisa = PBXVariantGroup;\n': continue | |
836 # Child groups | |
837 if content_line == '\t\t\tchildren = (\n': | |
838 content_line_no += 1 | |
839 content_line = content_lines[content_line_no] | |
840 while content_line != '\t\t\t);\n': | |
841 child_parsed = klass.PBXGROUP_CHILD_RE.match(content_line) | |
842 if not child_parsed: | |
843 raise RuntimeError('PBXGroup unable to parse child content:\n%s' | |
844 % content_line) | |
845 child_uuids.append(child_parsed.group(1)) | |
846 child_names.append(child_parsed.group(2)) | |
847 content_line_no += 1 | |
848 content_line = content_lines[content_line_no] | |
849 continue # Back to top of loop on end of children | |
850 # Other fields | |
851 field_parsed = klass.PBXGROUP_FIELD_RE.match(content_line) | |
852 if not field_parsed: | |
853 raise RuntimeError('PBXGroup unable to parse field content:\n%s' | |
854 % content_line) | |
855 if field_parsed.group(1) == 'name': | |
856 name = field_parsed.group(2) | |
857 elif field_parsed.group(1) == 'path': | |
858 path = field_parsed.group(2) | |
859 elif field_parsed.group(1) == 'sourceTree': | |
860 if not field_parsed.group(2) in PBX_VALID_SOURCE_TREE_TYPES: | |
861 raise RuntimeError('PBXGroup unknown source tree type "%s"' | |
862 % field_parsed.group(2)) | |
863 source_tree = field_parsed.group(2) | |
864 elif field_parsed.group(1) == 'tabWidth': | |
865 tab_width = field_parsed.group(2) | |
866 elif field_parsed.group(1) == 'usesTabs': | |
867 uses_tabs = field_parsed.group(2) | |
868 elif field_parsed.group(1) == 'indentWidth': | |
869 indent_width = field_parsed.group(2) | |
870 else: | |
871 raise RuntimeError('PBXGroup unknown field "%s"' | |
872 % field_parsed.group(1)) | |
873 if path and path.startswith('"'): | |
874 path = path[1:-1] | |
875 if name and name.startswith('"'): | |
876 name = name[1:-1] | |
877 return klass(header_parsed.group(1), name, path, source_tree, child_uuids, | |
878 child_names, tab_width, uses_tabs, indent_width) | |
879 | |
880 def __init__(self, uuid, name, path, source_tree, child_uuids, child_names, | |
881 tab_width, uses_tabs, indent_width): | |
882 self.uuid = uuid | |
883 self.name = name | |
884 self.path = path | |
885 self.source_tree = source_tree | |
886 self.child_uuids = child_uuids | |
887 self.child_names = child_names | |
888 self.abs_path = None | |
889 # Semantically I'm not sure these aren't an error, but they | |
890 # appear in some projects | |
891 self._tab_width = tab_width | |
892 self._uses_tabs = uses_tabs | |
893 self._indent_width = indent_width | |
894 | |
895 def __str__(self): | |
896 if self.name: | |
897 header_comment = '/* %s */ ' % self.name | |
898 elif self.path: | |
899 header_comment = '/* %s */ ' % self.path | |
900 else: | |
901 header_comment = '' | |
902 if self.name: | |
903 if QUOTE_PATH_RE.search(self.name): | |
904 name_attribute = '\t\t\tname = "%s";\n' % self.name | |
905 else: | |
906 name_attribute = '\t\t\tname = %s;\n' % self.name | |
907 else: | |
908 name_attribute = '' | |
909 if self.path: | |
910 if QUOTE_PATH_RE.search(self.path): | |
911 path_attribute = '\t\t\tpath = "%s";\n' % self.path | |
912 else: | |
913 path_attribute = '\t\t\tpath = %s;\n' % self.path | |
914 else: | |
915 path_attribute = '' | |
916 child_lines = [] | |
917 for x in range(len(self.child_uuids)): | |
918 child_lines.append('\t\t\t\t%s /* %s */,\n' % | |
919 (self.child_uuids[x], self.child_names[x])) | |
920 children = ''.join(child_lines) | |
921 tab_width_attribute = '' | |
922 if self._tab_width: | |
923 tab_width_attribute = '\t\t\ttabWidth = %s;\n' % self._tab_width | |
924 uses_tabs_attribute = '' | |
925 if self._uses_tabs: | |
926 uses_tabs_attribute = '\t\t\tusesTabs = %s;\n' % self._uses_tabs | |
927 indent_width_attribute = '' | |
928 if self._indent_width: | |
929 indent_width_attribute = '\t\t\tindentWidth = %s;\n' % self._indent_width | |
930 return '\t\t%s %s= {\n' \ | |
931 '\t\t\tisa = %s;\n' \ | |
932 '\t\t\tchildren = (\n' \ | |
933 '%s' \ | |
934 '\t\t\t);\n' \ | |
935 '%s' \ | |
936 '%s' \ | |
937 '%s' \ | |
938 '\t\t\tsourceTree = %s;\n' \ | |
939 '%s' \ | |
940 '%s' \ | |
941 '\t\t};\n' % ( | |
942 self.uuid, header_comment, | |
943 self.__class__.__name__, | |
944 children, | |
945 indent_width_attribute, | |
946 name_attribute, | |
947 path_attribute, self.source_tree, | |
948 tab_width_attribute, uses_tabs_attribute) | |
949 | |
950 | |
951 class PBXVariantGroup(PBXGroup): | |
952 pass | |
953 | |
954 | |
955 class PBXNativeTarget(object): | |
956 """Class for PBXNativeTarget data from an Xcode project file. | |
957 | |
958 Attributes: | |
959 name: Target name | |
960 build_phase_uuids: Ordered list of build phase UUIDs | |
961 build_phase_names: Ordered list of build phase names | |
962 | |
963 NOTE: We do not have wrapper classes for all build phase data types! | |
964 """ | |
965 | |
966 PBXNATIVETARGET_HEADER_RE = re.compile( | |
967 r'^\t\t([0-9A-F]{24}) /\* (.*) \*/ = {\n$') | |
968 PBXNATIVETARGET_BUILD_PHASE_RE = re.compile( | |
969 r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) \*/,\n$') | |
970 | |
971 @classmethod | |
972 def FromContent(klass, content_lines): | |
973 header_parsed = klass.PBXNATIVETARGET_HEADER_RE.match(content_lines[0]) | |
974 if not header_parsed: | |
975 raise RuntimeError('PBXNativeTarget unable to parse header content:\n%s' | |
976 % content_lines[0]) | |
977 build_phase_uuids = [] | |
978 build_phase_names = [] | |
979 content_line_no = 0 | |
980 while 1: | |
981 content_line_no += 1 | |
982 content_line = content_lines[content_line_no] | |
983 if content_line == '\t\t};\n': break | |
984 if content_line == '\t\t\tisa = PBXNativeTarget;\n': continue | |
985 # Build phases groups | |
986 if content_line == '\t\t\tbuildPhases = (\n': | |
987 content_line_no += 1 | |
988 content_line = content_lines[content_line_no] | |
989 while content_line != '\t\t\t);\n': | |
990 phase_parsed = klass.PBXNATIVETARGET_BUILD_PHASE_RE.match( | |
991 content_line) | |
992 if not phase_parsed: | |
993 raise RuntimeError( | |
994 'PBXNativeTarget unable to parse build phase content:\n%s' | |
995 % content_line) | |
996 build_phase_uuids.append(phase_parsed.group(1)) | |
997 build_phase_names.append(phase_parsed.group(2)) | |
998 content_line_no += 1 | |
999 content_line = content_lines[content_line_no] | |
1000 break # Don't care about the rest of the content | |
1001 return klass(content_lines, header_parsed.group(2), build_phase_uuids, | |
1002 build_phase_names) | |
1003 | |
1004 def __init__(self, raw_lines, name, build_phase_uuids, build_phase_names): | |
1005 self._raw_lines = raw_lines | |
1006 self.name = name | |
1007 self.build_phase_uuids = build_phase_uuids | |
1008 self.build_phase_names = build_phase_names | |
1009 | |
1010 def __str__(self): | |
1011 return ''.join(self._raw_lines) | |
1012 | |
1013 | |
1014 class PBXSourcesBuildPhase(object): | |
1015 """Class for PBXSourcesBuildPhase data from an Xcode project file. | |
1016 | |
1017 Attributes: | |
1018 uuid: UUID for this instance | |
1019 build_action_mask: Xcode magic mask constant | |
1020 run_only_for_deployment_postprocessing: deployment postprocess flag | |
1021 file_uuids: Ordered list of PBXBuildFile UUIDs | |
1022 file_names: Ordered list of PBXBuildFile names (basename) | |
1023 """ | |
1024 | |
1025 PBXSOURCESBUILDPHASE_HEADER_RE = re.compile( | |
1026 r'^\t\t([0-9A-F]{24}) /\* Sources \*/ = {\n$') | |
1027 PBXSOURCESBUILDPHASE_FIELD_RE = re.compile(r'^\t\t\t(.*) = (.*);\n$') | |
1028 PBXSOURCESBUILDPHASE_FILE_RE = re.compile( | |
1029 r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) in Sources \*/,\n$') | |
1030 | |
1031 @classmethod | |
1032 def FromContent(klass, content_lines): | |
1033 header_parsed = klass.PBXSOURCESBUILDPHASE_HEADER_RE.match(content_lines[0]) | |
1034 if not header_parsed: | |
1035 raise RuntimeError( | |
1036 'PBXSourcesBuildPhase unable to parse header content:\n%s' | |
1037 % content_lines[0]) | |
1038 # Parse line by line | |
1039 build_action_mask = None | |
1040 run_only_for_deployment_postprocessing = None | |
1041 file_uuids = [] | |
1042 file_names = [] | |
1043 content_line_no = 0 | |
1044 while 1: | |
1045 content_line_no += 1 | |
1046 content_line = content_lines[content_line_no] | |
1047 if content_line == '\t\t};\n': break | |
1048 if content_line == '\t\t\tisa = PBXSourcesBuildPhase;\n': continue | |
1049 # Files | |
1050 if content_line == '\t\t\tfiles = (\n': | |
1051 content_line_no += 1 | |
1052 content_line = content_lines[content_line_no] | |
1053 while content_line != '\t\t\t);\n': | |
1054 file_parsed = klass.PBXSOURCESBUILDPHASE_FILE_RE.match(content_line) | |
1055 if not file_parsed: | |
1056 raise RuntimeError( | |
1057 'PBXSourcesBuildPhase unable to parse file content:\n%s' | |
1058 % content_line) | |
1059 file_uuids.append(file_parsed.group(1)) | |
1060 file_names.append(file_parsed.group(2)) | |
1061 content_line_no += 1 | |
1062 content_line = content_lines[content_line_no] | |
1063 continue # Back to top of loop on end of files list | |
1064 # Other fields | |
1065 field_parsed = klass.PBXSOURCESBUILDPHASE_FIELD_RE.match(content_line) | |
1066 if not field_parsed: | |
1067 raise RuntimeError( | |
1068 'PBXSourcesBuildPhase unable to parse field content:\n%s' | |
1069 % content_line) | |
1070 if field_parsed.group(1) == 'buildActionMask': | |
1071 build_action_mask = field_parsed.group(2) | |
1072 elif field_parsed.group(1) == 'runOnlyForDeploymentPostprocessing': | |
1073 run_only_for_deployment_postprocessing = field_parsed.group(2) | |
1074 else: | |
1075 raise RuntimeError('PBXSourcesBuildPhase unknown field "%s"' | |
1076 % field_parsed.group(1)) | |
1077 return klass(header_parsed.group(1), build_action_mask, | |
1078 run_only_for_deployment_postprocessing, | |
1079 file_uuids, file_names) | |
1080 | |
1081 def __init__(self, uuid, build_action_mask, | |
1082 run_only_for_deployment_postprocessing, | |
1083 file_uuids, file_names): | |
1084 self.uuid = uuid | |
1085 self.build_action_mask = build_action_mask | |
1086 self.run_only_for_deployment_postprocessing = \ | |
1087 run_only_for_deployment_postprocessing | |
1088 self.file_uuids = file_uuids | |
1089 self.file_names = file_names | |
1090 | |
1091 def __str__(self): | |
1092 file_lines = [] | |
1093 for x in range(len(self.file_uuids)): | |
1094 file_lines.append('\t\t\t\t%s /* %s in Sources */,\n' % | |
1095 (self.file_uuids[x], self.file_names[x])) | |
1096 files = ''.join(file_lines) | |
1097 return '\t\t%s /* Sources */ = {\n' \ | |
1098 '\t\t\tisa = PBXSourcesBuildPhase;\n' \ | |
1099 '\t\t\tbuildActionMask = %s;\n' \ | |
1100 '\t\t\tfiles = (\n' \ | |
1101 '%s' \ | |
1102 '\t\t\t);\n' \ | |
1103 '\t\t\trunOnlyForDeploymentPostprocessing = %s;\n' \ | |
1104 '\t\t};\n' % ( | |
1105 self.uuid, self.build_action_mask, files, | |
1106 self.run_only_for_deployment_postprocessing) | |
1107 | |
1108 | |
1109 def Usage(optparse): | |
1110 optparse.print_help() | |
1111 print '\n' \ | |
1112 'Commands:\n' \ | |
1113 ' list_native_targets: List Xcode "native" (source compilation)\n' \ | |
1114 ' targets by name.\n' \ | |
1115 ' list_target_sources: List project-relative source files in the\n' \ | |
1116 ' specified Xcode "native" target.\n' \ | |
1117 ' remove_source [sourcefile ...]: Remove the specified source files\n'
\ | |
1118 ' from every target in the project (target is ignored).\n' \ | |
1119 ' add_source [sourcefile ...]: Add the specified source files\n' \ | |
1120 ' to the specified target.\n' | |
1121 sys.exit(2) | |
1122 | |
1123 | |
1124 def Main(): | |
1125 # Use argument structure like xcodebuild commandline | |
1126 option_parser = optparse.OptionParser( | |
1127 usage='usage: %prog -p projectname [ -t targetname ] ' \ | |
1128 '<command> [...]', | |
1129 add_help_option=False) | |
1130 option_parser.add_option( | |
1131 '-h', '--help', action='store_true', dest='help', | |
1132 default=False, help=optparse.SUPPRESS_HELP) | |
1133 option_parser.add_option( | |
1134 '-p', '--project', action='store', type='string', | |
1135 dest='project', metavar='projectname', | |
1136 help='Manipulate the project specified by projectname.') | |
1137 option_parser.add_option( | |
1138 '-t', '--target', action='store', type='string', | |
1139 dest='target', metavar='targetname', | |
1140 help='Manipulate the target specified by targetname.') | |
1141 (options, args) = option_parser.parse_args() | |
1142 | |
1143 # Since we have more elaborate commands, handle help | |
1144 if options.help: | |
1145 Usage(option_parser) | |
1146 | |
1147 # Xcode project file | |
1148 if not options.project: | |
1149 option_parser.error('Xcode project file must be specified.') | |
1150 project_path = os.path.abspath(CygwinPathClean(options.project)) | |
1151 if project_path.endswith('.xcodeproj'): | |
1152 project_path = os.path.join(project_path, 'project.pbxproj') | |
1153 if not project_path.endswith(os.sep + 'project.pbxproj'): | |
1154 option_parser.error('Invalid Xcode project file path \"%s\"' % project_path) | |
1155 if not os.path.exists(project_path): | |
1156 option_parser.error('Missing Xcode project file \"%s\"' % project_path) | |
1157 | |
1158 # Construct project object | |
1159 project = XcodeProject(project_path) | |
1160 | |
1161 # Switch on command | |
1162 | |
1163 # List native target names (default command) | |
1164 if len(args) < 1 or args[0] == 'list_native_targets': | |
1165 # Ape xcodebuild output | |
1166 target_names = [] | |
1167 for target in project.NativeTargets(): | |
1168 target_names.append(target.name) | |
1169 print 'Information about project "%s"\n Native Targets:\n %s' % ( | |
1170 project.name, | |
1171 '\n '.join(target_names)) | |
1172 | |
1173 if len(args) < 1: | |
1174 # Be friendly and print some hints for further actions. | |
1175 print | |
1176 print 'To add or remove files from given target, run:' | |
1177 print '\txcodebodge.py -p <project> -t <target> add_source <file_name>' | |
1178 print '\txcodebodge.py -p <project> -t <target> remove_source <file_name>' | |
1179 | |
1180 # List files in a native target | |
1181 elif args[0] == 'list_target_sources': | |
1182 if len(args) != 1: | |
1183 option_parser.error('list_target_sources takes no arguments') | |
1184 if not options.target: | |
1185 option_parser.error('list_target_sources requires a target') | |
1186 # Validate target and get list of files | |
1187 target = project.NativeTargetForName(options.target) | |
1188 if not target: | |
1189 option_parser.error('No native target named "%s"' % options.target) | |
1190 sources_phase = project.SourcesBuildPhaseForTarget(target) | |
1191 target_files = [] | |
1192 for source_uuid in sources_phase.file_uuids: | |
1193 build_file = project.BuildFileForUUID(source_uuid) | |
1194 file_ref = project.FileReferenceForUUID(build_file.file_ref_uuid) | |
1195 pretty_path = project.RelativeSourceRootPath(file_ref.abs_path) | |
1196 if pretty_path: | |
1197 target_files.append(pretty_path) | |
1198 else: | |
1199 target_files.append(file_ref.abs_path) | |
1200 # Ape xcodebuild output | |
1201 print 'Information about project "%s" target "%s"\n' \ | |
1202 ' Files:\n %s' % (project.name, options.target, | |
1203 '\n '.join(target_files)) | |
1204 | |
1205 # Remove source files | |
1206 elif args[0] == 'remove_source': | |
1207 if len(args) < 2: | |
1208 option_parser.error('remove_source needs one or more source files') | |
1209 if options.target: | |
1210 option_parser.error( | |
1211 'remove_source does not support removal from a single target') | |
1212 for source_path in args[1:]: | |
1213 source_path = CygwinPathClean(source_path) | |
1214 found = False | |
1215 for file_ref in project.FileReferences(): | |
1216 # Try undecorated path, abs_path and our prettified paths | |
1217 if (file_ref.path == source_path or ( | |
1218 file_ref.abs_path and ( | |
1219 file_ref.abs_path == os.path.abspath(source_path) or | |
1220 project.RelativeSourceRootPath(file_ref.abs_path) == source_path))
): | |
1221 # Found a matching file ref, remove it | |
1222 found = True | |
1223 project.RemoveSourceFileReference(file_ref) | |
1224 if not found: | |
1225 option_parser.error('No matching source file "%s"' % source_path) | |
1226 project.Update() | |
1227 | |
1228 # Add source files | |
1229 elif args[0] == 'add_source': | |
1230 if len(args) < 2: | |
1231 option_parser.error('add_source needs one or more source files') | |
1232 if not options.target: | |
1233 option_parser.error('add_source requires a target') | |
1234 # Look for the target we want to add too. | |
1235 target = project.NativeTargetForName(options.target) | |
1236 if not target: | |
1237 option_parser.error('No native target named "%s"' % options.target) | |
1238 # Get the sources build phase | |
1239 sources_phase = project.SourcesBuildPhaseForTarget(target) | |
1240 # Loop new sources | |
1241 for source_path in args[1:]: | |
1242 source_path = CygwinPathClean(source_path) | |
1243 if not os.path.exists(os.path.abspath(source_path)): | |
1244 option_parser.error('File "%s" not found' % source_path) | |
1245 # Don't generate duplicate file references if we don't need them | |
1246 source_ref = None | |
1247 for file_ref in project.FileReferences(): | |
1248 # Try undecorated path, abs_path and our prettified paths | |
1249 if (file_ref.path == source_path or ( | |
1250 file_ref.abs_path and ( | |
1251 file_ref.abs_path == os.path.abspath(source_path) or | |
1252 project.RelativeSourceRootPath(file_ref.abs_path) == source_path))
): | |
1253 source_ref = file_ref | |
1254 break | |
1255 if not source_ref: | |
1256 # Create a new source file ref | |
1257 source_ref = project.AddSourceFile(os.path.abspath(source_path)) | |
1258 # Add the new source file reference to the target if its a safe type | |
1259 if source_ref.file_type in SOURCES_XCODE_FILETYPES: | |
1260 project.AddSourceFileToSourcesBuildPhase(source_ref, sources_phase) | |
1261 project.Update() | |
1262 | |
1263 # Private sanity check. On an unmodified project make sure our output is | |
1264 # the same as the input | |
1265 elif args[0] == 'parse_sanity': | |
1266 if ''.join(project.FileContent()) != ''.join(project._raw_content): | |
1267 option_parser.error('Project rewrite sanity fail "%s"' % project.path) | |
1268 | |
1269 else: | |
1270 Usage(option_parser) | |
1271 | |
1272 | |
1273 if __name__ == '__main__': | |
1274 Main() | |
OLD | NEW |