| 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 |