1 # Copyright (c) 2012 Google Inc. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Xcode project file generator. 6 7 This module is both an Xcode project file generator and a documentation of the 8 Xcode project file format. Knowledge of the project file format was gained 9 based on extensive experience with Xcode, and by making changes to projects in 10 Xcode.app and observing the resultant changes in the associated project files. 11 12 XCODE PROJECT FILES 13 14 The generator targets the file format as written by Xcode 3.2 (specifically, 15 3.2.6), but past experience has taught that the format has not changed 16 significantly in the past several years, and future versions of Xcode are able 17 to read older project files. 18 19 Xcode project files are "bundled": the project "file" from an end-user's 20 perspective is actually a directory with an ".xcodeproj" extension. The 21 project file from this module's perspective is actually a file inside this 22 directory, always named "project.pbxproj". This file contains a complete 23 description of the project and is all that is needed to use the xcodeproj. 24 Other files contained in the xcodeproj directory are simply used to store 25 per-user settings, such as the state of various UI elements in the Xcode 26 application. 27 28 The project.pbxproj file is a property list, stored in a format almost 29 identical to the NeXTstep property list format. The file is able to carry 30 Unicode data, and is encoded in UTF-8. The root element in the property list 31 is a dictionary that contains several properties of minimal interest, and two 32 properties of immense interest. The most important property is a dictionary 33 named "objects". The entire structure of the project is represented by the 34 children of this property. The objects dictionary is keyed by unique 96-bit 35 values represented by 24 uppercase hexadecimal characters. Each value in the 36 objects dictionary is itself a dictionary, describing an individual object. 37 38 Each object in the dictionary is a member of a class, which is identified by 39 the "isa" property of each object. A variety of classes are represented in a 40 project file. Objects can refer to other objects by ID, using the 24-character 41 hexadecimal object key. A project's objects form a tree, with a root object 42 of class PBXProject at the root. As an example, the PBXProject object serves 43 as parent to an XCConfigurationList object defining the build configurations 44 used in the project, a PBXGroup object serving as a container for all files 45 referenced in the project, and a list of target objects, each of which defines 46 a target in the project. There are several different types of target object, 47 such as PBXNativeTarget and PBXAggregateTarget. In this module, this 48 relationship is expressed by having each target type derive from an abstract 49 base named XCTarget. 50 51 The project.pbxproj file's root dictionary also contains a property, sibling to 52 the "objects" dictionary, named "rootObject". The value of rootObject is a 53 24-character object key referring to the root PBXProject object in the 54 objects dictionary. 55 56 In Xcode, every file used as input to a target or produced as a final product 57 of a target must appear somewhere in the hierarchy rooted at the PBXGroup 58 object referenced by the PBXProject's mainGroup property. A PBXGroup is 59 generally represented as a folder in the Xcode application. PBXGroups can 60 contain other PBXGroups as well as PBXFileReferences, which are pointers to 61 actual files. 62 63 Each XCTarget contains a list of build phases, represented in this module by 64 the abstract base XCBuildPhase. Examples of concrete XCBuildPhase derivations 65 are PBXSourcesBuildPhase and PBXFrameworksBuildPhase, which correspond to the 66 "Compile Sources" and "Link Binary With Libraries" phases displayed in the 67 Xcode application. Files used as input to these phases (for example, source 68 files in the former case and libraries and frameworks in the latter) are 69 represented by PBXBuildFile objects, referenced by elements of "files" lists 70 in XCTarget objects. Each PBXBuildFile object refers to a PBXBuildFile 71 object as a "weak" reference: it does not "own" the PBXBuildFile, which is 72 owned by the root object's mainGroup or a descendant group. In most cases, the 73 layer of indirection between an XCBuildPhase and a PBXFileReference via a 74 PBXBuildFile appears extraneous, but there's actually one reason for this: 75 file-specific compiler flags are added to the PBXBuildFile object so as to 76 allow a single file to be a member of multiple targets while having distinct 77 compiler flags for each. These flags can be modified in the Xcode applciation 78 in the "Build" tab of a File Info window. 79 80 When a project is open in the Xcode application, Xcode will rewrite it. As 81 such, this module is careful to adhere to the formatting used by Xcode, to 82 avoid insignificant changes appearing in the file when it is used in the 83 Xcode application. This will keep version control repositories happy, and 84 makes it possible to compare a project file used in Xcode to one generated by 85 this module to determine if any significant changes were made in the 86 application. 87 88 Xcode has its own way of assigning 24-character identifiers to each object, 89 which is not duplicated here. Because the identifier only is only generated 90 once, when an object is created, and is then left unchanged, there is no need 91 to attempt to duplicate Xcode's behavior in this area. The generator is free 92 to select any identifier, even at random, to refer to the objects it creates, 93 and Xcode will retain those identifiers and use them when subsequently 94 rewriting the project file. However, the generator would choose new random 95 identifiers each time the project files are generated, leading to difficulties 96 comparing "used" project files to "pristine" ones produced by this module, 97 and causing the appearance of changes as every object identifier is changed 98 when updated projects are checked in to a version control repository. To 99 mitigate this problem, this module chooses identifiers in a more deterministic 100 way, by hashing a description of each object as well as its parent and ancestor 101 objects. This strategy should result in minimal "shift" in IDs as successive 102 generations of project files are produced. 103 104 THIS MODULE 105 106 This module introduces several classes, all derived from the XCObject class. 107 Nearly all of the "brains" are built into the XCObject class, which understands 108 how to create and modify objects, maintain the proper tree structure, compute 109 identifiers, and print objects. For the most part, classes derived from 110 XCObject need only provide a _schema class object, a dictionary that 111 expresses what properties objects of the class may contain. 112 113 Given this structure, it's possible to build a minimal project file by creating 114 objects of the appropriate types and making the proper connections: 115 116 config_list = XCConfigurationList() 117 group = PBXGroup() 118 project = PBXProject({'buildConfigurationList': config_list, 119 'mainGroup': group}) 120 121 With the project object set up, it can be added to an XCProjectFile object. 122 XCProjectFile is a pseudo-class in the sense that it is a concrete XCObject 123 subclass that does not actually correspond to a class type found in a project 124 file. Rather, it is used to represent the project file's root dictionary. 125 Printing an XCProjectFile will print the entire project file, including the 126 full "objects" dictionary. 127 128 project_file = XCProjectFile({'rootObject': project}) 129 project_file.ComputeIDs() 130 project_file.Print() 131 132 Xcode project files are always encoded in UTF-8. This module will accept 133 strings of either the str class or the unicode class. Strings of class str 134 are assumed to already be encoded in UTF-8. Obviously, if you're just using 135 ASCII, you won't encounter difficulties because ASCII is a UTF-8 subset. 136 Strings of class unicode are handled properly and encoded in UTF-8 when 137 a project file is output. 138 """ 139 140 import gyp.common 141 import posixpath 142 import re 143 import struct 144 import sys 145 146 # hashlib is supplied as of Python 2.5 as the replacement interface for sha 147 # and other secure hashes. In 2.6, sha is deprecated. Import hashlib if 148 # available, avoiding a deprecation warning under 2.6. Import sha otherwise, 149 # preserving 2.4 compatibility. 150 try: 151 import hashlib 152 _new_sha1 = hashlib.sha1 153 except ImportError: 154 import sha 155 _new_sha1 = sha.new 156 157 158 # See XCObject._EncodeString. This pattern is used to determine when a string 159 # can be printed unquoted. Strings that match this pattern may be printed 160 # unquoted. Strings that do not match must be quoted and may be further 161 # transformed to be properly encoded. Note that this expression matches the 162 # characters listed with "+", for 1 or more occurrences: if a string is empty, 163 # it must not match this pattern, because it needs to be encoded as "". 164 _unquoted = re.compile('^[A-Za-z0-9$./_]+$') 165 166 # Strings that match this pattern are quoted regardless of what _unquoted says. 167 # Oddly, Xcode will quote any string with a run of three or more underscores. 168 _quoted = re.compile('___') 169 170 # This pattern should match any character that needs to be escaped by 171 # XCObject._EncodeString. See that function. 172 _escaped = re.compile('[\\\\"]|[\x00-\x1f]') 173 174 175 # Used by SourceTreeAndPathFromPath 176 _path_leading_variable = re.compile('^\$\((.*?)\)(/(.*))?$') 177 178 def SourceTreeAndPathFromPath(input_path): 179 """Given input_path, returns a tuple with sourceTree and path values. 180 181 Examples: 182 input_path (source_tree, output_path) 183 '$(VAR)/path' ('VAR', 'path') 184 '$(VAR)' ('VAR', None) 185 'path' (None, 'path') 186 """ 187 188 source_group_match = _path_leading_variable.match(input_path) 189 if source_group_match: 190 source_tree = source_group_match.group(1) 191 output_path = source_group_match.group(3) # This may be None. 192 else: 193 source_tree = None 194 output_path = input_path 195 196 return (source_tree, output_path) 197 198 def ConvertVariablesToShellSyntax(input_string): 199 return re.sub('\$\((.*?)\)', '${\\1}', input_string) 200 201 class XCObject(object): 202 """The abstract base of all class types used in Xcode project files. 203 204 Class variables: 205 _schema: A dictionary defining the properties of this class. The keys to 206 _schema are string property keys as used in project files. Values 207 are a list of four or five elements: 208 [ is_list, property_type, is_strong, is_required, default ] 209 is_list: True if the property described is a list, as opposed 210 to a single element. 211 property_type: The type to use as the value of the property, 212 or if is_list is True, the type to use for each 213 element of the value's list. property_type must 214 be an XCObject subclass, or one of the built-in 215 types str, int, or dict. 216 is_strong: If property_type is an XCObject subclass, is_strong 217 is True to assert that this class "owns," or serves 218 as parent, to the property value (or, if is_list is 219 True, values). is_strong must be False if 220 property_type is not an XCObject subclass. 221 is_required: True if the property is required for the class. 222 Note that is_required being True does not preclude 223 an empty string ("", in the case of property_type 224 str) or list ([], in the case of is_list True) from 225 being set for the property. 226 default: Optional. If is_requried is True, default may be set 227 to provide a default value for objects that do not supply 228 their own value. If is_required is True and default 229 is not provided, users of the class must supply their own 230 value for the property. 231 Note that although the values of the array are expressed in 232 boolean terms, subclasses provide values as integers to conserve 233 horizontal space. 234 _should_print_single_line: False in XCObject. Subclasses whose objects 235 should be written to the project file in the 236 alternate single-line format, such as 237 PBXFileReference and PBXBuildFile, should 238 set this to True. 239 _encode_transforms: Used by _EncodeString to encode unprintable characters. 240 The index into this list is the ordinal of the 241 character to transform; each value is a string 242 used to represent the character in the output. XCObject 243 provides an _encode_transforms list suitable for most 244 XCObject subclasses. 245 _alternate_encode_transforms: Provided for subclasses that wish to use 246 the alternate encoding rules. Xcode seems 247 to use these rules when printing objects in 248 single-line format. Subclasses that desire 249 this behavior should set _encode_transforms 250 to _alternate_encode_transforms. 251 _hashables: A list of XCObject subclasses that can be hashed by ComputeIDs 252 to construct this object's ID. Most classes that need custom 253 hashing behavior should do it by overriding Hashables, 254 but in some cases an object's parent may wish to push a 255 hashable value into its child, and it can do so by appending 256 to _hashables. 257 Attributes: 258 id: The object's identifier, a 24-character uppercase hexadecimal string. 259 Usually, objects being created should not set id until the entire 260 project file structure is built. At that point, UpdateIDs() should 261 be called on the root object to assign deterministic values for id to 262 each object in the tree. 263 parent: The object's parent. This is set by a parent XCObject when a child 264 object is added to it. 265 _properties: The object's property dictionary. An object's properties are 266 described by its class' _schema variable. 267 """ 268 269 _schema = {} 270 _should_print_single_line = False 271 272 # See _EncodeString. 273 _encode_transforms = [] 274 i = 0 275 while i < ord(' '): 276 _encode_transforms.append('\\U%04x' % i) 277 i = i + 1 278 _encode_transforms[7] = '\\a' 279 _encode_transforms[8] = '\\b' 280 _encode_transforms[9] = '\\t' 281 _encode_transforms[10] = '\\n' 282 _encode_transforms[11] = '\\v' 283 _encode_transforms[12] = '\\f' 284 _encode_transforms[13] = '\\n' 285 286 _alternate_encode_transforms = list(_encode_transforms) 287 _alternate_encode_transforms[9] = chr(9) 288 _alternate_encode_transforms[10] = chr(10) 289 _alternate_encode_transforms[11] = chr(11) 290 291 def __init__(self, properties=None, id=None, parent=None): 292 self.id = id 293 self.parent = parent 294 self._properties = {} 295 self._hashables = [] 296 self._SetDefaultsFromSchema() 297 self.UpdateProperties(properties) 298 299 def __repr__(self): 300 try: 301 name = self.Name() 302 except NotImplementedError: 303 return '<%s at 0x%x>' % (self.__class__.__name__, id(self)) 304 return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self)) 305 306 def Copy(self): 307 """Make a copy of this object. 308 309 The new object will have its own copy of lists and dicts. Any XCObject 310 objects owned by this object (marked "strong") will be copied in the 311 new object, even those found in lists. If this object has any weak 312 references to other XCObjects, the same references are added to the new 313 object without making a copy. 314 """ 315 316 that = self.__class__(id=self.id, parent=self.parent) 317 for key, value in self._properties.iteritems(): 318 is_strong = self._schema[key][2] 319 320 if isinstance(value, XCObject): 321 if is_strong: 322 new_value = value.Copy() 323 new_value.parent = that 324 that._properties[key] = new_value 325 else: 326 that._properties[key] = value 327 elif isinstance(value, str) or isinstance(value, unicode) or \ 328 isinstance(value, int): 329 that._properties[key] = value 330 elif isinstance(value, list): 331 if is_strong: 332 # If is_strong is True, each element is an XCObject, so it's safe to 333 # call Copy. 334 that._properties[key] = [] 335 for item in value: 336 new_item = item.Copy() 337 new_item.parent = that 338 that._properties[key].append(new_item) 339 else: 340 that._properties[key] = value[:] 341 elif isinstance(value, dict): 342 # dicts are never strong. 343 if is_strong: 344 raise TypeError, 'Strong dict for key ' + key + ' in ' + \ 345 self.__class__.__name__ 346 else: 347 that._properties[key] = value.copy() 348 else: 349 raise TypeError, 'Unexpected type ' + value.__class__.__name__ + \ 350 ' for key ' + key + ' in ' + self.__class__.__name__ 351 352 return that 353 354 def Name(self): 355 """Return the name corresponding to an object. 356 357 Not all objects necessarily need to be nameable, and not all that do have 358 a "name" property. Override as needed. 359 """ 360 361 # If the schema indicates that "name" is required, try to access the 362 # property even if it doesn't exist. This will result in a KeyError 363 # being raised for the property that should be present, which seems more 364 # appropriate than NotImplementedError in this case. 365 if 'name' in self._properties or \ 366 ('name' in self._schema and self._schema['name'][3]): 367 return self._properties['name'] 368 369 raise NotImplementedError, \ 370 self.__class__.__name__ + ' must implement Name' 371 372 def Comment(self): 373 """Return a comment string for the object. 374 375 Most objects just use their name as the comment, but PBXProject uses 376 different values. 377 378 The returned comment is not escaped and does not have any comment marker 379 strings applied to it. 380 """ 381 382 return self.Name() 383 384 def Hashables(self): 385 hashables = [self.__class__.__name__] 386 387 name = self.Name() 388 if name != None: 389 hashables.append(name) 390 391 hashables.extend(self._hashables) 392 393 return hashables 394 395 def HashablesForChild(self): 396 return None 397 398 def ComputeIDs(self, recursive=True, overwrite=True, seed_hash=None): 399 """Set "id" properties deterministically. 400 401 An object's "id" property is set based on a hash of its class type and 402 name, as well as the class type and name of all ancestor objects. As 403 such, it is only advisable to call ComputeIDs once an entire project file 404 tree is built. 405 406 If recursive is True, recurse into all descendant objects and update their 407 hashes. 408 409 If overwrite is True, any existing value set in the "id" property will be 410 replaced. 411 """ 412 413 def _HashUpdate(hash, data): 414 """Update hash with data's length and contents. 415 416 If the hash were updated only with the value of data, it would be 417 possible for clowns to induce collisions by manipulating the names of 418 their objects. By adding the length, it's exceedingly less likely that 419 ID collisions will be encountered, intentionally or not. 420 """ 421 422 hash.update(struct.pack('>i', len(data))) 423 hash.update(data) 424 425 if seed_hash is None: 426 seed_hash = _new_sha1() 427 428 hash = seed_hash.copy() 429 430 hashables = self.Hashables() 431 assert len(hashables) > 0 432 for hashable in hashables: 433 _HashUpdate(hash, hashable) 434 435 if recursive: 436 hashables_for_child = self.HashablesForChild() 437 if hashables_for_child is None: 438 child_hash = hash 439 else: 440 assert len(hashables_for_child) > 0 441 child_hash = seed_hash.copy() 442 for hashable in hashables_for_child: 443 _HashUpdate(child_hash, hashable) 444 445 for child in self.Children(): 446 child.ComputeIDs(recursive, overwrite, child_hash) 447 448 if overwrite or self.id is None: 449 # Xcode IDs are only 96 bits (24 hex characters), but a SHA-1 digest is 450 # is 160 bits. Instead of throwing out 64 bits of the digest, xor them 451 # into the portion that gets used. 452 assert hash.digest_size % 4 == 0 453 digest_int_count = hash.digest_size / 4 454 digest_ints = struct.unpack('>' + 'I' * digest_int_count, hash.digest()) 455 id_ints = [0, 0, 0] 456 for index in xrange(0, digest_int_count): 457 id_ints[index % 3] ^= digest_ints[index] 458 self.id = '%08X%08X%08X' % tuple(id_ints) 459 460 def EnsureNoIDCollisions(self): 461 """Verifies that no two objects have the same ID. Checks all descendants. 462 """ 463 464 ids = {} 465 descendants = self.Descendants() 466 for descendant in descendants: 467 if descendant.id in ids: 468 other = ids[descendant.id] 469 raise KeyError, \ 470 'Duplicate ID %s, objects "%s" and "%s" in "%s"' % \ 471 (descendant.id, str(descendant._properties), 472 str(other._properties), self._properties['rootObject'].Name()) 473 ids[descendant.id] = descendant 474 475 def Children(self): 476 """Returns a list of all of this object's owned (strong) children.""" 477 478 children = [] 479 for property, attributes in self._schema.iteritems(): 480 (is_list, property_type, is_strong) = attributes[0:3] 481 if is_strong and property in self._properties: 482 if not is_list: 483 children.append(self._properties[property]) 484 else: 485 children.extend(self._properties[property]) 486 return children 487 488 def Descendants(self): 489 """Returns a list of all of this object's descendants, including this 490 object. 491 """ 492 493 children = self.Children() 494 descendants = [self] 495 for child in children: 496 descendants.extend(child.Descendants()) 497 return descendants 498 499 def PBXProjectAncestor(self): 500 # The base case for recursion is defined at PBXProject.PBXProjectAncestor. 501 if self.parent: 502 return self.parent.PBXProjectAncestor() 503 return None 504 505 def _EncodeComment(self, comment): 506 """Encodes a comment to be placed in the project file output, mimicing 507 Xcode behavior. 508 """ 509 510 # This mimics Xcode behavior by wrapping the comment in "/*" and "*/". If 511 # the string already contains a "*/", it is turned into "(*)/". This keeps 512 # the file writer from outputting something that would be treated as the 513 # end of a comment in the middle of something intended to be entirely a 514 # comment. 515 516 return '/* ' + comment.replace('*/', '(*)/') + ' */' 517 518 def _EncodeTransform(self, match): 519 # This function works closely with _EncodeString. It will only be called 520 # by re.sub with match.group(0) containing a character matched by the 521 # the _escaped expression. 522 char = match.group(0) 523 524 # Backslashes (\) and quotation marks (") are always replaced with a 525 # backslash-escaped version of the same. Everything else gets its 526 # replacement from the class' _encode_transforms array. 527 if char == '\\': 528 return '\\\\' 529 if char == '"': 530 return '\\"' 531 return self._encode_transforms[ord(char)] 532 533 def _EncodeString(self, value): 534 """Encodes a string to be placed in the project file output, mimicing 535 Xcode behavior. 536 """ 537 538 # Use quotation marks when any character outside of the range A-Z, a-z, 0-9, 539 # $ (dollar sign), . (period), and _ (underscore) is present. Also use 540 # quotation marks to represent empty strings. 541 # 542 # Escape " (double-quote) and \ (backslash) by preceding them with a 543 # backslash. 544 # 545 # Some characters below the printable ASCII range are encoded specially: 546 # 7 ^G BEL is encoded as "\a" 547 # 8 ^H BS is encoded as "\b" 548 # 11 ^K VT is encoded as "\v" 549 # 12 ^L NP is encoded as "\f" 550 # 127 ^? DEL is passed through as-is without escaping 551 # - In PBXFileReference and PBXBuildFile objects: 552 # 9 ^I HT is passed through as-is without escaping 553 # 10 ^J NL is passed through as-is without escaping 554 # 13 ^M CR is passed through as-is without escaping 555 # - In other objects: 556 # 9 ^I HT is encoded as "\t" 557 # 10 ^J NL is encoded as "\n" 558 # 13 ^M CR is encoded as "\n" rendering it indistinguishable from 559 # 10 ^J NL 560 # All other characters within the ASCII control character range (0 through 561 # 31 inclusive) are encoded as "\U001f" referring to the Unicode code point 562 # in hexadecimal. For example, character 14 (^N SO) is encoded as "\U000e". 563 # Characters above the ASCII range are passed through to the output encoded 564 # as UTF-8 without any escaping. These mappings are contained in the 565 # class' _encode_transforms list. 566 567 if _unquoted.search(value) and not _quoted.search(value): 568 return value 569 570 return '"' + _escaped.sub(self._EncodeTransform, value) + '"' 571 572 def _XCPrint(self, file, tabs, line): 573 file.write('\t' * tabs + line) 574 575 def _XCPrintableValue(self, tabs, value, flatten_list=False): 576 """Returns a representation of value that may be printed in a project file, 577 mimicing Xcode's behavior. 578 579 _XCPrintableValue can handle str and int values, XCObjects (which are 580 made printable by returning their id property), and list and dict objects 581 composed of any of the above types. When printing a list or dict, and 582 _should_print_single_line is False, the tabs parameter is used to determine 583 how much to indent the lines corresponding to the items in the list or 584 dict. 585 586 If flatten_list is True, single-element lists will be transformed into 587 strings. 588 """ 589 590 printable = '' 591 comment = None 592 593 if self._should_print_single_line: 594 sep = ' ' 595 element_tabs = '' 596 end_tabs = '' 597 else: 598 sep = '\n' 599 element_tabs = '\t' * (tabs + 1) 600 end_tabs = '\t' * tabs 601 602 if isinstance(value, XCObject): 603 printable += value.id 604 comment = value.Comment() 605 elif isinstance(value, str): 606 printable += self._EncodeString(value) 607 elif isinstance(value, unicode): 608 printable += self._EncodeString(value.encode('utf-8')) 609 elif isinstance(value, int): 610 printable += str(value) 611 elif isinstance(value, list): 612 if flatten_list and len(value) <= 1: 613 if len(value) == 0: 614 printable += self._EncodeString('') 615 else: 616 printable += self._EncodeString(value[0]) 617 else: 618 printable = '(' + sep 619 for item in value: 620 printable += element_tabs + \ 621 self._XCPrintableValue(tabs + 1, item, flatten_list) + \ 622 ',' + sep 623 printable += end_tabs + ')' 624 elif isinstance(value, dict): 625 printable = '{' + sep 626 for item_key, item_value in sorted(value.iteritems()): 627 printable += element_tabs + \ 628 self._XCPrintableValue(tabs + 1, item_key, flatten_list) + ' = ' + \ 629 self._XCPrintableValue(tabs + 1, item_value, flatten_list) + ';' + \ 630 sep 631 printable += end_tabs + '}' 632 else: 633 raise TypeError, "Can't make " + value.__class__.__name__ + ' printable' 634 635 if comment != None: 636 printable += ' ' + self._EncodeComment(comment) 637 638 return printable 639 640 def _XCKVPrint(self, file, tabs, key, value): 641 """Prints a key and value, members of an XCObject's _properties dictionary, 642 to file. 643 644 tabs is an int identifying the indentation level. If the class' 645 _should_print_single_line variable is True, tabs is ignored and the 646 key-value pair will be followed by a space insead of a newline. 647 """ 648 649 if self._should_print_single_line: 650 printable = '' 651 after_kv = ' ' 652 else: 653 printable = '\t' * tabs 654 after_kv = '\n' 655 656 # Xcode usually prints remoteGlobalIDString values in PBXContainerItemProxy 657 # objects without comments. Sometimes it prints them with comments, but 658 # the majority of the time, it doesn't. To avoid unnecessary changes to 659 # the project file after Xcode opens it, don't write comments for 660 # remoteGlobalIDString. This is a sucky hack and it would certainly be 661 # cleaner to extend the schema to indicate whether or not a comment should 662 # be printed, but since this is the only case where the problem occurs and 663 # Xcode itself can't seem to make up its mind, the hack will suffice. 664 # 665 # Also see PBXContainerItemProxy._schema['remoteGlobalIDString']. 666 if key == 'remoteGlobalIDString' and isinstance(self, 667 PBXContainerItemProxy): 668 value_to_print = value.id 669 else: 670 value_to_print = value 671 672 # PBXBuildFile's settings property is represented in the output as a dict, 673 # but a hack here has it represented as a string. Arrange to strip off the 674 # quotes so that it shows up in the output as expected. 675 if key == 'settings' and isinstance(self, PBXBuildFile): 676 strip_value_quotes = True 677 else: 678 strip_value_quotes = False 679 680 # In another one-off, let's set flatten_list on buildSettings properties 681 # of XCBuildConfiguration objects, because that's how Xcode treats them. 682 if key == 'buildSettings' and isinstance(self, XCBuildConfiguration): 683 flatten_list = True 684 else: 685 flatten_list = False 686 687 try: 688 printable_key = self._XCPrintableValue(tabs, key, flatten_list) 689 printable_value = self._XCPrintableValue(tabs, value_to_print, 690 flatten_list) 691 if strip_value_quotes and len(printable_value) > 1 and \ 692 printable_value[0] == '"' and printable_value[-1] == '"': 693 printable_value = printable_value[1:-1] 694 printable += printable_key + ' = ' + printable_value + ';' + after_kv 695 except TypeError, e: 696 gyp.common.ExceptionAppend(e, 697 'while printing key "%s"' % key) 698 raise 699 700 self._XCPrint(file, 0, printable) 701 702 def Print(self, file=sys.stdout): 703 """Prints a reprentation of this object to file, adhering to Xcode output 704 formatting. 705 """ 706 707 self.VerifyHasRequiredProperties() 708 709 if self._should_print_single_line: 710 # When printing an object in a single line, Xcode doesn't put any space 711 # between the beginning of a dictionary (or presumably a list) and the 712 # first contained item, so you wind up with snippets like 713 # ...CDEF = {isa = PBXFileReference; fileRef = 0123... 714 # If it were me, I would have put a space in there after the opening 715 # curly, but I guess this is just another one of those inconsistencies 716 # between how Xcode prints PBXFileReference and PBXBuildFile objects as 717 # compared to other objects. Mimic Xcode's behavior here by using an 718 # empty string for sep. 719 sep = '' 720 end_tabs = 0 721 else: 722 sep = '\n' 723 end_tabs = 2 724 725 # Start the object. For example, '\t\tPBXProject = {\n'. 726 self._XCPrint(file, 2, self._XCPrintableValue(2, self) + ' = {' + sep) 727 728 # "isa" isn't in the _properties dictionary, it's an intrinsic property 729 # of the class which the object belongs to. Xcode always outputs "isa" 730 # as the first element of an object dictionary. 731 self._XCKVPrint(file, 3, 'isa', self.__class__.__name__) 732 733 # The remaining elements of an object dictionary are sorted alphabetically. 734 for property, value in sorted(self._properties.iteritems()): 735 self._XCKVPrint(file, 3, property, value) 736 737 # End the object. 738 self._XCPrint(file, end_tabs, '};\n') 739 740 def UpdateProperties(self, properties, do_copy=False): 741 """Merge the supplied properties into the _properties dictionary. 742 743 The input properties must adhere to the class schema or a KeyError or 744 TypeError exception will be raised. If adding an object of an XCObject 745 subclass and the schema indicates a strong relationship, the object's 746 parent will be set to this object. 747 748 If do_copy is True, then lists, dicts, strong-owned XCObjects, and 749 strong-owned XCObjects in lists will be copied instead of having their 750 references added. 751 """ 752 753 if properties is None: 754 return 755 756 for property, value in properties.iteritems(): 757 # Make sure the property is in the schema. 758 if not property in self._schema: 759 raise KeyError, property + ' not in ' + self.__class__.__name__ 760 761 # Make sure the property conforms to the schema. 762 (is_list, property_type, is_strong) = self._schema[property][0:3] 763 if is_list: 764 if value.__class__ != list: 765 raise TypeError, \ 766 property + ' of ' + self.__class__.__name__ + \ 767 ' must be list, not ' + value.__class__.__name__ 768 for item in value: 769 if not isinstance(item, property_type) and \ 770 not (item.__class__ == unicode and property_type == str): 771 # Accept unicode where str is specified. str is treated as 772 # UTF-8-encoded. 773 raise TypeError, \ 774 'item of ' + property + ' of ' + self.__class__.__name__ + \ 775 ' must be ' + property_type.__name__ + ', not ' + \ 776 item.__class__.__name__ 777 elif not isinstance(value, property_type) and \ 778 not (value.__class__ == unicode and property_type == str): 779 # Accept unicode where str is specified. str is treated as 780 # UTF-8-encoded. 781 raise TypeError, \ 782 property + ' of ' + self.__class__.__name__ + ' must be ' + \ 783 property_type.__name__ + ', not ' + value.__class__.__name__ 784 785 # Checks passed, perform the assignment. 786 if do_copy: 787 if isinstance(value, XCObject): 788 if is_strong: 789 self._properties[property] = value.Copy() 790 else: 791 self._properties[property] = value 792 elif isinstance(value, str) or isinstance(value, unicode) or \ 793 isinstance(value, int): 794 self._properties[property] = value 795 elif isinstance(value, list): 796 if is_strong: 797 # If is_strong is True, each element is an XCObject, so it's safe 798 # to call Copy. 799 self._properties[property] = [] 800 for item in value: 801 self._properties[property].append(item.Copy()) 802 else: 803 self._properties[property] = value[:] 804 elif isinstance(value, dict): 805 self._properties[property] = value.copy() 806 else: 807 raise TypeError, "Don't know how to copy a " + \ 808 value.__class__.__name__ + ' object for ' + \ 809 property + ' in ' + self.__class__.__name__ 810 else: 811 self._properties[property] = value 812 813 # Set up the child's back-reference to this object. Don't use |value| 814 # any more because it may not be right if do_copy is true. 815 if is_strong: 816 if not is_list: 817 self._properties[property].parent = self 818 else: 819 for item in self._properties[property]: 820 item.parent = self 821 822 def HasProperty(self, key): 823 return key in self._properties 824 825 def GetProperty(self, key): 826 return self._properties[key] 827 828 def SetProperty(self, key, value): 829 self.UpdateProperties({key: value}) 830 831 def DelProperty(self, key): 832 if key in self._properties: 833 del self._properties[key] 834 835 def AppendProperty(self, key, value): 836 # TODO(mark): Support ExtendProperty too (and make this call that)? 837 838 # Schema validation. 839 if not key in self._schema: 840 raise KeyError, key + ' not in ' + self.__class__.__name__ 841 842 (is_list, property_type, is_strong) = self._schema[key][0:3] 843 if not is_list: 844 raise TypeError, key + ' of ' + self.__class__.__name__ + ' must be list' 845 if not isinstance(value, property_type): 846 raise TypeError, 'item of ' + key + ' of ' + self.__class__.__name__ + \ 847 ' must be ' + property_type.__name__ + ', not ' + \ 848 value.__class__.__name__ 849 850 # If the property doesn't exist yet, create a new empty list to receive the 851 # item. 852 if not key in self._properties: 853 self._properties[key] = [] 854 855 # Set up the ownership link. 856 if is_strong: 857 value.parent = self 858 859 # Store the item. 860 self._properties[key].append(value) 861 862 def VerifyHasRequiredProperties(self): 863 """Ensure that all properties identified as required by the schema are 864 set. 865 """ 866 867 # TODO(mark): A stronger verification mechanism is needed. Some 868 # subclasses need to perform validation beyond what the schema can enforce. 869 for property, attributes in self._schema.iteritems(): 870 (is_list, property_type, is_strong, is_required) = attributes[0:4] 871 if is_required and not property in self._properties: 872 raise KeyError, self.__class__.__name__ + ' requires ' + property 873 874 def _SetDefaultsFromSchema(self): 875 """Assign object default values according to the schema. This will not 876 overwrite properties that have already been set.""" 877 878 defaults = {} 879 for property, attributes in self._schema.iteritems(): 880 (is_list, property_type, is_strong, is_required) = attributes[0:4] 881 if is_required and len(attributes) >= 5 and \ 882 not property in self._properties: 883 default = attributes[4] 884 885 defaults[property] = default 886 887 if len(defaults) > 0: 888 # Use do_copy=True so that each new object gets its own copy of strong 889 # objects, lists, and dicts. 890 self.UpdateProperties(defaults, do_copy=True) 891 892 893 class XCHierarchicalElement(XCObject): 894 """Abstract base for PBXGroup and PBXFileReference. Not represented in a 895 project file.""" 896 897 # TODO(mark): Do name and path belong here? Probably so. 898 # If path is set and name is not, name may have a default value. Name will 899 # be set to the basename of path, if the basename of path is different from 900 # the full value of path. If path is already just a leaf name, name will 901 # not be set. 902 _schema = XCObject._schema.copy() 903 _schema.update({ 904 'comments': [0, str, 0, 0], 905 'fileEncoding': [0, str, 0, 0], 906 'includeInIndex': [0, int, 0, 0], 907 'indentWidth': [0, int, 0, 0], 908 'lineEnding': [0, int, 0, 0], 909 'sourceTree': [0, str, 0, 1, '<group>'], 910 'tabWidth': [0, int, 0, 0], 911 'usesTabs': [0, int, 0, 0], 912 'wrapsLines': [0, int, 0, 0], 913 }) 914 915 def __init__(self, properties=None, id=None, parent=None): 916 # super 917 XCObject.__init__(self, properties, id, parent) 918 if 'path' in self._properties and not 'name' in self._properties: 919 path = self._properties['path'] 920 name = posixpath.basename(path) 921 if name != '' and path != name: 922 self.SetProperty('name', name) 923 924 if 'path' in self._properties and \ 925 (not 'sourceTree' in self._properties or \ 926 self._properties['sourceTree'] == '<group>'): 927 # If the pathname begins with an Xcode variable like "$(SDKROOT)/", take 928 # the variable out and make the path be relative to that variable by 929 # assigning the variable name as the sourceTree. 930 (source_tree, path) = SourceTreeAndPathFromPath(self._properties['path']) 931 if source_tree != None: 932 self._properties['sourceTree'] = source_tree 933 if path != None: 934 self._properties['path'] = path 935 if source_tree != None and path is None and \ 936 not 'name' in self._properties: 937 # The path was of the form "$(SDKROOT)" with no path following it. 938 # This object is now relative to that variable, so it has no path 939 # attribute of its own. It does, however, keep a name. 940 del self._properties['path'] 941 self._properties['name'] = source_tree 942 943 def Name(self): 944 if 'name' in self._properties: 945 return self._properties['name'] 946 elif 'path' in self._properties: 947 return self._properties['path'] 948 else: 949 # This happens in the case of the root PBXGroup. 950 return None 951 952 def Hashables(self): 953 """Custom hashables for XCHierarchicalElements. 954 955 XCHierarchicalElements are special. Generally, their hashes shouldn't 956 change if the paths don't change. The normal XCObject implementation of 957 Hashables adds a hashable for each object, which means that if 958 the hierarchical structure changes (possibly due to changes caused when 959 TakeOverOnlyChild runs and encounters slight changes in the hierarchy), 960 the hashes will change. For example, if a project file initially contains 961 a/b/f1 and a/b becomes collapsed into a/b, f1 will have a single parent 962 a/b. If someone later adds a/f2 to the project file, a/b can no longer be 963 collapsed, and f1 winds up with parent b and grandparent a. That would 964 be sufficient to change f1's hash. 965 966 To counteract this problem, hashables for all XCHierarchicalElements except 967 for the main group (which has neither a name nor a path) are taken to be 968 just the set of path components. Because hashables are inherited from 969 parents, this provides assurance that a/b/f1 has the same set of hashables 970 whether its parent is b or a/b. 971 972 The main group is a special case. As it is permitted to have no name or 973 path, it is permitted to use the standard XCObject hash mechanism. This 974 is not considered a problem because there can be only one main group. 975 """ 976 977 if self == self.PBXProjectAncestor()._properties['mainGroup']: 978 # super 979 return XCObject.Hashables(self) 980 981 hashables = [] 982 983 # Put the name in first, ensuring that if TakeOverOnlyChild collapses 984 # children into a top-level group like "Source", the name always goes 985 # into the list of hashables without interfering with path components. 986 if 'name' in self._properties: 987 # Make it less likely for people to manipulate hashes by following the 988 # pattern of always pushing an object type value onto the list first. 989 hashables.append(self.__class__.__name__ + '.name') 990 hashables.append(self._properties['name']) 991 992 # NOTE: This still has the problem that if an absolute path is encountered, 993 # including paths with a sourceTree, they'll still inherit their parents' 994 # hashables, even though the paths aren't relative to their parents. This 995 # is not expected to be much of a problem in practice. 996 path = self.PathFromSourceTreeAndPath() 997 if path != None: 998 components = path.split(posixpath.sep) 999 for component in components: 1000 hashables.append(self.__class__.__name__ + '.path') 1001 hashables.append(component) 1002 1003 hashables.extend(self._hashables) 1004 1005 return hashables 1006 1007 def Compare(self, other): 1008 # Allow comparison of these types. PBXGroup has the highest sort rank; 1009 # PBXVariantGroup is treated as equal to PBXFileReference. 1010 valid_class_types = { 1011 PBXFileReference: 'file', 1012 PBXGroup: 'group', 1013 PBXVariantGroup: 'file', 1014 } 1015 self_type = valid_class_types[self.__class__] 1016 other_type = valid_class_types[other.__class__] 1017 1018 if self_type == other_type: 1019 # If the two objects are of the same sort rank, compare their names. 1020 return cmp(self.Name(), other.Name()) 1021 1022 # Otherwise, sort groups before everything else. 1023 if self_type == 'group': 1024 return -1 1025 return 1 1026 1027 def CompareRootGroup(self, other): 1028 # This function should be used only to compare direct children of the 1029 # containing PBXProject's mainGroup. These groups should appear in the 1030 # listed order. 1031 # TODO(mark): "Build" is used by gyp.generator.xcode, perhaps the 1032 # generator should have a way of influencing this list rather than having 1033 # to hardcode for the generator here. 1034 order = ['Source', 'Intermediates', 'Projects', 'Frameworks', 'Products', 1035 'Build'] 1036 1037 # If the groups aren't in the listed order, do a name comparison. 1038 # Otherwise, groups in the listed order should come before those that 1039 # aren't. 1040 self_name = self.Name() 1041 other_name = other.Name() 1042 self_in = isinstance(self, PBXGroup) and self_name in order 1043 other_in = isinstance(self, PBXGroup) and other_name in order 1044 if not self_in and not other_in: 1045 return self.Compare(other) 1046 if self_name in order and not other_name in order: 1047 return -1 1048 if other_name in order and not self_name in order: 1049 return 1 1050 1051 # If both groups are in the listed order, go by the defined order. 1052 self_index = order.index(self_name) 1053 other_index = order.index(other_name) 1054 if self_index < other_index: 1055 return -1 1056 if self_index > other_index: 1057 return 1 1058 return 0 1059 1060 def PathFromSourceTreeAndPath(self): 1061 # Turn the object's sourceTree and path properties into a single flat 1062 # string of a form comparable to the path parameter. If there's a 1063 # sourceTree property other than "<group>", wrap it in $(...) for the 1064 # comparison. 1065 components = [] 1066 if self._properties['sourceTree'] != '<group>': 1067 components.append('$(' + self._properties['sourceTree'] + ')') 1068 if 'path' in self._properties: 1069 components.append(self._properties['path']) 1070 1071 if len(components) > 0: 1072 return posixpath.join(*components) 1073 1074 return None 1075 1076 def FullPath(self): 1077 # Returns a full path to self relative to the project file, or relative 1078 # to some other source tree. Start with self, and walk up the chain of 1079 # parents prepending their paths, if any, until no more parents are 1080 # available (project-relative path) or until a path relative to some 1081 # source tree is found. 1082 xche = self 1083 path = None 1084 while isinstance(xche, XCHierarchicalElement) and \ 1085 (path is None or \ 1086 (not path.startswith('/') and not path.startswith('$'))): 1087 this_path = xche.PathFromSourceTreeAndPath() 1088 if this_path != None and path != None: 1089 path = posixpath.join(this_path, path) 1090 elif this_path != None: 1091 path = this_path 1092 xche = xche.parent 1093 1094 return path 1095 1096 1097 class PBXGroup(XCHierarchicalElement): 1098 """ 1099 Attributes: 1100 _children_by_path: Maps pathnames of children of this PBXGroup to the 1101 actual child XCHierarchicalElement objects. 1102 _variant_children_by_name_and_path: Maps (name, path) tuples of 1103 PBXVariantGroup children to the actual child PBXVariantGroup objects. 1104 """ 1105 1106 _schema = XCHierarchicalElement._schema.copy() 1107 _schema.update({ 1108 'children': [1, XCHierarchicalElement, 1, 1, []], 1109 'name': [0, str, 0, 0], 1110 'path': [0, str, 0, 0], 1111 }) 1112 1113 def __init__(self, properties=None, id=None, parent=None): 1114 # super 1115 XCHierarchicalElement.__init__(self, properties, id, parent) 1116 self._children_by_path = {} 1117 self._variant_children_by_name_and_path = {} 1118 for child in self._properties.get('children', []): 1119 self._AddChildToDicts(child) 1120 1121 def Hashables(self): 1122 # super 1123 hashables = XCHierarchicalElement.Hashables(self) 1124 1125 # It is not sufficient to just rely on name and parent to build a unique 1126 # hashable : a node could have two child PBXGroup sharing a common name. 1127 # To add entropy the hashable is enhanced with the names of all its 1128 # children. 1129 for child in self._properties.get('children', []): 1130 child_name = child.Name() 1131 if child_name != None: 1132 hashables.append(child_name) 1133 1134 return hashables 1135 1136 def HashablesForChild(self): 1137 # To avoid a circular reference the hashables used to compute a child id do 1138 # not include the child names. 1139 return XCHierarchicalElement.Hashables(self) 1140 1141 def _AddChildToDicts(self, child): 1142 # Sets up this PBXGroup object's dicts to reference the child properly. 1143 child_path = child.PathFromSourceTreeAndPath() 1144 if child_path: 1145 if child_path in self._children_by_path: 1146 raise ValueError, 'Found multiple children with path ' + child_path 1147 self._children_by_path[child_path] = child 1148 1149 if isinstance(child, PBXVariantGroup): 1150 child_name = child._properties.get('name', None) 1151 key = (child_name, child_path) 1152 if key in self._variant_children_by_name_and_path: 1153 raise ValueError, 'Found multiple PBXVariantGroup children with ' + \ 1154 'name ' + str(child_name) + ' and path ' + \ 1155 str(child_path) 1156 self._variant_children_by_name_and_path[key] = child 1157 1158 def AppendChild(self, child): 1159 # Callers should use this instead of calling 1160 # AppendProperty('children', child) directly because this function 1161 # maintains the group's dicts. 1162 self.AppendProperty('children', child) 1163 self._AddChildToDicts(child) 1164 1165 def GetChildByName(self, name): 1166 # This is not currently optimized with a dict as GetChildByPath is because 1167 # it has few callers. Most callers probably want GetChildByPath. This 1168 # function is only useful to get children that have names but no paths, 1169 # which is rare. The children of the main group ("Source", "Products", 1170 # etc.) is pretty much the only case where this likely to come up. 1171 # 1172 # TODO(mark): Maybe this should raise an error if more than one child is 1173 # present with the same name. 1174 if not 'children' in self._properties: 1175 return None 1176 1177 for child in self._properties['children']: 1178 if child.Name() == name: 1179 return child 1180 1181 return None 1182 1183 def GetChildByPath(self, path): 1184 if not path: 1185 return None 1186 1187 if path in self._children_by_path: 1188 return self._children_by_path[path] 1189 1190 return None 1191 1192 def GetChildByRemoteObject(self, remote_object): 1193 # This method is a little bit esoteric. Given a remote_object, which 1194 # should be a PBXFileReference in another project file, this method will 1195 # return this group's PBXReferenceProxy object serving as a local proxy 1196 # for the remote PBXFileReference. 1197 # 1198 # This function might benefit from a dict optimization as GetChildByPath 1199 # for some workloads, but profiling shows that it's not currently a 1200 # problem. 1201 if not 'children' in self._properties: 1202 return None 1203 1204 for child in self._properties['children']: 1205 if not isinstance(child, PBXReferenceProxy): 1206 continue 1207 1208 container_proxy = child._properties['remoteRef'] 1209 if container_proxy._properties['remoteGlobalIDString'] == remote_object: 1210 return child 1211 1212 return None 1213 1214 def AddOrGetFileByPath(self, path, hierarchical): 1215 """Returns an existing or new file reference corresponding to path. 1216 1217 If hierarchical is True, this method will create or use the necessary 1218 hierarchical group structure corresponding to path. Otherwise, it will 1219 look in and create an item in the current group only. 1220 1221 If an existing matching reference is found, it is returned, otherwise, a 1222 new one will be created, added to the correct group, and returned. 1223 1224 If path identifies a directory by virtue of carrying a trailing slash, 1225 this method returns a PBXFileReference of "folder" type. If path 1226 identifies a variant, by virtue of it identifying a file inside a directory 1227 with an ".lproj" extension, this method returns a PBXVariantGroup 1228 containing the variant named by path, and possibly other variants. For 1229 all other paths, a "normal" PBXFileReference will be returned. 1230 """ 1231 1232 # Adding or getting a directory? Directories end with a trailing slash. 1233 is_dir = False 1234 if path.endswith('/'): 1235 is_dir = True 1236 path = posixpath.normpath(path) 1237 if is_dir: 1238 path = path + '/' 1239 1240 # Adding or getting a variant? Variants are files inside directories 1241 # with an ".lproj" extension. Xcode uses variants for localization. For 1242 # a variant path/to/Language.lproj/MainMenu.nib, put a variant group named 1243 # MainMenu.nib inside path/to, and give it a variant named Language. In 1244 # this example, grandparent would be set to path/to and parent_root would 1245 # be set to Language. 1246 variant_name = None 1247 parent = posixpath.dirname(path) 1248 grandparent = posixpath.dirname(parent) 1249 parent_basename = posixpath.basename(parent) 1250 (parent_root, parent_ext) = posixpath.splitext(parent_basename) 1251 if parent_ext == '.lproj': 1252 variant_name = parent_root 1253 if grandparent == '': 1254 grandparent = None 1255 1256 # Putting a directory inside a variant group is not currently supported. 1257 assert not is_dir or variant_name is None 1258 1259 path_split = path.split(posixpath.sep) 1260 if len(path_split) == 1 or \ 1261 ((is_dir or variant_name != None) and len(path_split) == 2) or \ 1262 not hierarchical: 1263 # The PBXFileReference or PBXVariantGroup will be added to or gotten from 1264 # this PBXGroup, no recursion necessary. 1265 if variant_name is None: 1266 # Add or get a PBXFileReference. 1267 file_ref = self.GetChildByPath(path) 1268 if file_ref != None: 1269 assert file_ref.__class__ == PBXFileReference 1270 else: 1271 file_ref = PBXFileReference({'path': path}) 1272 self.AppendChild(file_ref) 1273 else: 1274 # Add or get a PBXVariantGroup. The variant group name is the same 1275 # as the basename (MainMenu.nib in the example above). grandparent 1276 # specifies the path to the variant group itself, and path_split[-2:] 1277 # is the path of the specific variant relative to its group. 1278 variant_group_name = posixpath.basename(path) 1279 variant_group_ref = self.AddOrGetVariantGroupByNameAndPath( 1280 variant_group_name, grandparent) 1281 variant_path = posixpath.sep.join(path_split[-2:]) 1282 variant_ref = variant_group_ref.GetChildByPath(variant_path) 1283 if variant_ref != None: 1284 assert variant_ref.__class__ == PBXFileReference 1285 else: 1286 variant_ref = PBXFileReference({'name': variant_name, 1287 'path': variant_path}) 1288 variant_group_ref.AppendChild(variant_ref) 1289 # The caller is interested in the variant group, not the specific 1290 # variant file. 1291 file_ref = variant_group_ref 1292 return file_ref 1293 else: 1294 # Hierarchical recursion. Add or get a PBXGroup corresponding to the 1295 # outermost path component, and then recurse into it, chopping off that 1296 # path component. 1297 next_dir = path_split[0] 1298 group_ref = self.GetChildByPath(next_dir) 1299 if group_ref != None: 1300 assert group_ref.__class__ == PBXGroup 1301 else: 1302 group_ref = PBXGroup({'path': next_dir}) 1303 self.AppendChild(group_ref) 1304 return group_ref.AddOrGetFileByPath(posixpath.sep.join(path_split[1:]), 1305 hierarchical) 1306 1307 def AddOrGetVariantGroupByNameAndPath(self, name, path): 1308 """Returns an existing or new PBXVariantGroup for name and path. 1309 1310 If a PBXVariantGroup identified by the name and path arguments is already 1311 present as a child of this object, it is returned. Otherwise, a new 1312 PBXVariantGroup with the correct properties is created, added as a child, 1313 and returned. 1314 1315 This method will generally be called by AddOrGetFileByPath, which knows 1316 when to create a variant group based on the structure of the pathnames 1317 passed to it. 1318 """ 1319 1320 key = (name, path) 1321 if key in self._variant_children_by_name_and_path: 1322 variant_group_ref = self._variant_children_by_name_and_path[key] 1323 assert variant_group_ref.__class__ == PBXVariantGroup 1324 return variant_group_ref 1325 1326 variant_group_properties = {'name': name} 1327 if path != None: 1328 variant_group_properties['path'] = path 1329 variant_group_ref = PBXVariantGroup(variant_group_properties) 1330 self.AppendChild(variant_group_ref) 1331 1332 return variant_group_ref 1333 1334 def TakeOverOnlyChild(self, recurse=False): 1335 """If this PBXGroup has only one child and it's also a PBXGroup, take 1336 it over by making all of its children this object's children. 1337 1338 This function will continue to take over only children when those children 1339 are groups. If there are three PBXGroups representing a, b, and c, with 1340 c inside b and b inside a, and a and b have no other children, this will 1341 result in a taking over both b and c, forming a PBXGroup for a/b/c. 1342 1343 If recurse is True, this function will recurse into children and ask them 1344 to collapse themselves by taking over only children as well. Assuming 1345 an example hierarchy with files at a/b/c/d1, a/b/c/d2, and a/b/c/d3/e/f 1346 (d1, d2, and f are files, the rest are groups), recursion will result in 1347 a group for a/b/c containing a group for d3/e. 1348 """ 1349 1350 # At this stage, check that child class types are PBXGroup exactly, 1351 # instead of using isinstance. The only subclass of PBXGroup, 1352 # PBXVariantGroup, should not participate in reparenting in the same way: 1353 # reparenting by merging different object types would be wrong. 1354 while len(self._properties['children']) == 1 and \ 1355 self._properties['children'][0].__class__ == PBXGroup: 1356 # Loop to take over the innermost only-child group possible. 1357 1358 child = self._properties['children'][0] 1359 1360 # Assume the child's properties, including its children. Save a copy 1361 # of this object's old properties, because they'll still be needed. 1362 # This object retains its existing id and parent attributes. 1363 old_properties = self._properties 1364 self._properties = child._properties 1365 self._children_by_path = child._children_by_path 1366 1367 if not 'sourceTree' in self._properties or \ 1368 self._properties['sourceTree'] == '<group>': 1369 # The child was relative to its parent. Fix up the path. Note that 1370 # children with a sourceTree other than "<group>" are not relative to 1371 # their parents, so no path fix-up is needed in that case. 1372 if 'path' in old_properties: 1373 if 'path' in self._properties: 1374 # Both the original parent and child have paths set. 1375 self._properties['path'] = posixpath.join(old_properties['path'], 1376 self._properties['path']) 1377 else: 1378 # Only the original parent has a path, use it. 1379 self._properties['path'] = old_properties['path'] 1380 if 'sourceTree' in old_properties: 1381 # The original parent had a sourceTree set, use it. 1382 self._properties['sourceTree'] = old_properties['sourceTree'] 1383 1384 # If the original parent had a name set, keep using it. If the original 1385 # parent didn't have a name but the child did, let the child's name 1386 # live on. If the name attribute seems unnecessary now, get rid of it. 1387 if 'name' in old_properties and old_properties['name'] != None and \ 1388 old_properties['name'] != self.Name(): 1389 self._properties['name'] = old_properties['name'] 1390 if 'name' in self._properties and 'path' in self._properties and \ 1391 self._properties['name'] == self._properties['path']: 1392 del self._properties['name'] 1393 1394 # Notify all children of their new parent. 1395 for child in self._properties['children']: 1396 child.parent = self 1397 1398 # If asked to recurse, recurse. 1399 if recurse: 1400 for child in self._properties['children']: 1401 if child.__class__ == PBXGroup: 1402 child.TakeOverOnlyChild(recurse) 1403 1404 def SortGroup(self): 1405 self._properties['children'] = \ 1406 sorted(self._properties['children'], cmp=lambda x,y: x.Compare(y)) 1407 1408 # Recurse. 1409 for child in self._properties['children']: 1410 if isinstance(child, PBXGroup): 1411 child.SortGroup() 1412 1413 1414 class XCFileLikeElement(XCHierarchicalElement): 1415 # Abstract base for objects that can be used as the fileRef property of 1416 # PBXBuildFile. 1417 1418 def PathHashables(self): 1419 # A PBXBuildFile that refers to this object will call this method to 1420 # obtain additional hashables specific to this XCFileLikeElement. Don't 1421 # just use this object's hashables, they're not specific and unique enough 1422 # on their own (without access to the parent hashables.) Instead, provide 1423 # hashables that identify this object by path by getting its hashables as 1424 # well as the hashables of ancestor XCHierarchicalElement objects. 1425 1426 hashables = [] 1427 xche = self 1428 while xche != None and isinstance(xche, XCHierarchicalElement): 1429 xche_hashables = xche.Hashables() 1430 for index in xrange(0, len(xche_hashables)): 1431 hashables.insert(index, xche_hashables[index]) 1432 xche = xche.parent 1433 return hashables 1434 1435 1436 class XCContainerPortal(XCObject): 1437 # Abstract base for objects that can be used as the containerPortal property 1438 # of PBXContainerItemProxy. 1439 pass 1440 1441 1442 class XCRemoteObject(XCObject): 1443 # Abstract base for objects that can be used as the remoteGlobalIDString 1444 # property of PBXContainerItemProxy. 1445 pass 1446 1447 1448 class PBXFileReference(XCFileLikeElement, XCContainerPortal, XCRemoteObject): 1449 _schema = XCFileLikeElement._schema.copy() 1450 _schema.update({ 1451 'explicitFileType': [0, str, 0, 0], 1452 'lastKnownFileType': [0, str, 0, 0], 1453 'name': [0, str, 0, 0], 1454 'path': [0, str, 0, 1], 1455 }) 1456 1457 # Weird output rules for PBXFileReference. 1458 _should_print_single_line = True 1459 # super 1460 _encode_transforms = XCFileLikeElement._alternate_encode_transforms 1461 1462 def __init__(self, properties=None, id=None, parent=None): 1463 # super 1464 XCFileLikeElement.__init__(self, properties, id, parent) 1465 if 'path' in self._properties and self._properties['path'].endswith('/'): 1466 self._properties['path'] = self._properties['path'][:-1] 1467 is_dir = True 1468 else: 1469 is_dir = False 1470 1471 if 'path' in self._properties and \ 1472 not 'lastKnownFileType' in self._properties and \ 1473 not 'explicitFileType' in self._properties: 1474 # TODO(mark): This is the replacement for a replacement for a quick hack. 1475 # It is no longer incredibly sucky, but this list needs to be extended. 1476 extension_map = { 1477 'a': 'archive.ar', 1478 'app': 'wrapper.application', 1479 'bdic': 'file', 1480 'bundle': 'wrapper.cfbundle', 1481 'c': 'sourcecode.c.c', 1482 'cc': 'sourcecode.cpp.cpp', 1483 'cpp': 'sourcecode.cpp.cpp', 1484 'css': 'text.css', 1485 'cxx': 'sourcecode.cpp.cpp', 1486 'dart': 'sourcecode', 1487 'dylib': 'compiled.mach-o.dylib', 1488 'framework': 'wrapper.framework', 1489 'gyp': 'sourcecode', 1490 'gypi': 'sourcecode', 1491 'h': 'sourcecode.c.h', 1492 'hxx': 'sourcecode.cpp.h', 1493 'icns': 'image.icns', 1494 'java': 'sourcecode.java', 1495 'js': 'sourcecode.javascript', 1496 'm': 'sourcecode.c.objc', 1497 'mm': 'sourcecode.cpp.objcpp', 1498 'nib': 'wrapper.nib', 1499 'o': 'compiled.mach-o.objfile', 1500 'pdf': 'image.pdf', 1501 'pl': 'text.script.perl', 1502 'plist': 'text.plist.xml', 1503 'pm': 'text.script.perl', 1504 'png': 'image.png', 1505 'py': 'text.script.python', 1506 'r': 'sourcecode.rez', 1507 'rez': 'sourcecode.rez', 1508 's': 'sourcecode.asm', 1509 'storyboard': 'file.storyboard', 1510 'strings': 'text.plist.strings', 1511 'ttf': 'file', 1512 'xcassets': 'folder.assetcatalog', 1513 'xcconfig': 'text.xcconfig', 1514 'xcdatamodel': 'wrapper.xcdatamodel', 1515 'xcdatamodeld':'wrapper.xcdatamodeld', 1516 'xib': 'file.xib', 1517 'y': 'sourcecode.yacc', 1518 } 1519 1520 prop_map = { 1521 'dart': 'explicitFileType', 1522 'gyp': 'explicitFileType', 1523 'gypi': 'explicitFileType', 1524 } 1525 1526 if is_dir: 1527 file_type = 'folder' 1528 prop_name = 'lastKnownFileType' 1529 else: 1530 basename = posixpath.basename(self._properties['path']) 1531 (root, ext) = posixpath.splitext(basename) 1532 # Check the map using a lowercase extension. 1533 # TODO(mark): Maybe it should try with the original case first and fall 1534 # back to lowercase, in case there are any instances where case 1535 # matters. There currently aren't. 1536 if ext != '': 1537 ext = ext[1:].lower() 1538 1539 # TODO(mark): "text" is the default value, but "file" is appropriate 1540 # for unrecognized files not containing text. Xcode seems to choose 1541 # based on content. 1542 file_type = extension_map.get(ext, 'text') 1543 prop_name = prop_map.get(ext, 'lastKnownFileType') 1544 1545 self._properties[prop_name] = file_type 1546 1547 1548 class PBXVariantGroup(PBXGroup, XCFileLikeElement): 1549 """PBXVariantGroup is used by Xcode to represent localizations.""" 1550 # No additions to the schema relative to PBXGroup. 1551 pass 1552 1553 1554 # PBXReferenceProxy is also an XCFileLikeElement subclass. It is defined below 1555 # because it uses PBXContainerItemProxy, defined below. 1556 1557 1558 class XCBuildConfiguration(XCObject): 1559 _schema = XCObject._schema.copy() 1560 _schema.update({ 1561 'baseConfigurationReference': [0, PBXFileReference, 0, 0], 1562 'buildSettings': [0, dict, 0, 1, {}], 1563 'name': [0, str, 0, 1], 1564 }) 1565 1566 def HasBuildSetting(self, key): 1567 return key in self._properties['buildSettings'] 1568 1569 def GetBuildSetting(self, key): 1570 return self._properties['buildSettings'][key] 1571 1572 def SetBuildSetting(self, key, value): 1573 # TODO(mark): If a list, copy? 1574 self._properties['buildSettings'][key] = value 1575 1576 def AppendBuildSetting(self, key, value): 1577 if not key in self._properties['buildSettings']: 1578 self._properties['buildSettings'][key] = [] 1579 self._properties['buildSettings'][key].append(value) 1580 1581 def DelBuildSetting(self, key): 1582 if key in self._properties['buildSettings']: 1583 del self._properties['buildSettings'][key] 1584 1585 def SetBaseConfiguration(self, value): 1586 self._properties['baseConfigurationReference'] = value 1587 1588 class XCConfigurationList(XCObject): 1589 # _configs is the default list of configurations. 1590 _configs = [ XCBuildConfiguration({'name': 'Debug'}), 1591 XCBuildConfiguration({'name': 'Release'}) ] 1592 1593 _schema = XCObject._schema.copy() 1594 _schema.update({ 1595 'buildConfigurations': [1, XCBuildConfiguration, 1, 1, _configs], 1596 'defaultConfigurationIsVisible': [0, int, 0, 1, 1], 1597 'defaultConfigurationName': [0, str, 0, 1, 'Release'], 1598 }) 1599 1600 def Name(self): 1601 return 'Build configuration list for ' + \ 1602 self.parent.__class__.__name__ + ' "' + self.parent.Name() + '"' 1603 1604 def ConfigurationNamed(self, name): 1605 """Convenience accessor to obtain an XCBuildConfiguration by name.""" 1606 for configuration in self._properties['buildConfigurations']: 1607 if configuration._properties['name'] == name: 1608 return configuration 1609 1610 raise KeyError, name 1611 1612 def DefaultConfiguration(self): 1613 """Convenience accessor to obtain the default XCBuildConfiguration.""" 1614 return self.ConfigurationNamed(self._properties['defaultConfigurationName']) 1615 1616 def HasBuildSetting(self, key): 1617 """Determines the state of a build setting in all XCBuildConfiguration 1618 child objects. 1619 1620 If all child objects have key in their build settings, and the value is the 1621 same in all child objects, returns 1. 1622 1623 If no child objects have the key in their build settings, returns 0. 1624 1625 If some, but not all, child objects have the key in their build settings, 1626 or if any children have different values for the key, returns -1. 1627 """ 1628 1629 has = None 1630 value = None 1631 for configuration in self._properties['buildConfigurations']: 1632 configuration_has = configuration.HasBuildSetting(key) 1633 if has is None: 1634 has = configuration_has 1635 elif has != configuration_has: 1636 return -1 1637 1638 if configuration_has: 1639 configuration_value = configuration.GetBuildSetting(key) 1640 if value is None: 1641 value = configuration_value 1642 elif value != configuration_value: 1643 return -1 1644 1645 if not has: 1646 return 0 1647 1648 return 1 1649 1650 def GetBuildSetting(self, key): 1651 """Gets the build setting for key. 1652 1653 All child XCConfiguration objects must have the same value set for the 1654 setting, or a ValueError will be raised. 1655 """ 1656 1657 # TODO(mark): This is wrong for build settings that are lists. The list 1658 # contents should be compared (and a list copy returned?) 1659 1660 value = None 1661 for configuration in self._properties['buildConfigurations']: 1662 configuration_value = configuration.GetBuildSetting(key) 1663 if value is None: 1664 value = configuration_value 1665 else: 1666 if value != configuration_value: 1667 raise ValueError, 'Variant values for ' + key 1668 1669 return value 1670 1671 def SetBuildSetting(self, key, value): 1672 """Sets the build setting for key to value in all child 1673 XCBuildConfiguration objects. 1674 """ 1675 1676 for configuration in self._properties['buildConfigurations']: 1677 configuration.SetBuildSetting(key, value) 1678 1679 def AppendBuildSetting(self, key, value): 1680 """Appends value to the build setting for key, which is treated as a list, 1681 in all child XCBuildConfiguration objects. 1682 """ 1683 1684 for configuration in self._properties['buildConfigurations']: 1685 configuration.AppendBuildSetting(key, value) 1686 1687 def DelBuildSetting(self, key): 1688 """Deletes the build setting key from all child XCBuildConfiguration 1689 objects. 1690 """ 1691 1692 for configuration in self._properties['buildConfigurations']: 1693 configuration.DelBuildSetting(key) 1694 1695 def SetBaseConfiguration(self, value): 1696 """Sets the build configuration in all child XCBuildConfiguration objects. 1697 """ 1698 1699 for configuration in self._properties['buildConfigurations']: 1700 configuration.SetBaseConfiguration(value) 1701 1702 1703 class PBXBuildFile(XCObject): 1704 _schema = XCObject._schema.copy() 1705 _schema.update({ 1706 'fileRef': [0, XCFileLikeElement, 0, 1], 1707 'settings': [0, str, 0, 0], # hack, it's a dict 1708 }) 1709 1710 # Weird output rules for PBXBuildFile. 1711 _should_print_single_line = True 1712 _encode_transforms = XCObject._alternate_encode_transforms 1713 1714 def Name(self): 1715 # Example: "main.cc in Sources" 1716 return self._properties['fileRef'].Name() + ' in ' + self.parent.Name() 1717 1718 def Hashables(self): 1719 # super 1720 hashables = XCObject.Hashables(self) 1721 1722 # It is not sufficient to just rely on Name() to get the 1723 # XCFileLikeElement's name, because that is not a complete pathname. 1724 # PathHashables returns hashables unique enough that no two 1725 # PBXBuildFiles should wind up with the same set of hashables, unless 1726 # someone adds the same file multiple times to the same target. That 1727 # would be considered invalid anyway. 1728 hashables.extend(self._properties['fileRef'].PathHashables()) 1729 1730 return hashables 1731 1732 1733 class XCBuildPhase(XCObject): 1734 """Abstract base for build phase classes. Not represented in a project 1735 file. 1736 1737 Attributes: 1738 _files_by_path: A dict mapping each path of a child in the files list by 1739 path (keys) to the corresponding PBXBuildFile children (values). 1740 _files_by_xcfilelikeelement: A dict mapping each XCFileLikeElement (keys) 1741 to the corresponding PBXBuildFile children (values). 1742 """ 1743 1744 # TODO(mark): Some build phase types, like PBXShellScriptBuildPhase, don't 1745 # actually have a "files" list. XCBuildPhase should not have "files" but 1746 # another abstract subclass of it should provide this, and concrete build 1747 # phase types that do have "files" lists should be derived from that new 1748 # abstract subclass. XCBuildPhase should only provide buildActionMask and 1749 # runOnlyForDeploymentPostprocessing, and not files or the various 1750 # file-related methods and attributes. 1751 1752 _schema = XCObject._schema.copy() 1753 _schema.update({ 1754 'buildActionMask': [0, int, 0, 1, 0x7fffffff], 1755 'files': [1, PBXBuildFile, 1, 1, []], 1756 'runOnlyForDeploymentPostprocessing': [0, int, 0, 1, 0], 1757 }) 1758 1759 def __init__(self, properties=None, id=None, parent=None): 1760 # super 1761 XCObject.__init__(self, properties, id, parent) 1762 1763 self._files_by_path = {} 1764 self._files_by_xcfilelikeelement = {} 1765 for pbxbuildfile in self._properties.get('files', []): 1766 self._AddBuildFileToDicts(pbxbuildfile) 1767 1768 def FileGroup(self, path): 1769 # Subclasses must override this by returning a two-element tuple. The 1770 # first item in the tuple should be the PBXGroup to which "path" should be 1771 # added, either as a child or deeper descendant. The second item should 1772 # be a boolean indicating whether files should be added into hierarchical 1773 # groups or one single flat group. 1774 raise NotImplementedError, \ 1775 self.__class__.__name__ + ' must implement FileGroup' 1776 1777 def _AddPathToDict(self, pbxbuildfile, path): 1778 """Adds path to the dict tracking paths belonging to this build phase. 1779 1780 If the path is already a member of this build phase, raises an exception. 1781 """ 1782 1783 if path in self._files_by_path: 1784 raise ValueError, 'Found multiple build files with path ' + path 1785 self._files_by_path[path] = pbxbuildfile 1786 1787 def _AddBuildFileToDicts(self, pbxbuildfile, path=None): 1788 """Maintains the _files_by_path and _files_by_xcfilelikeelement dicts. 1789 1790 If path is specified, then it is the path that is being added to the 1791 phase, and pbxbuildfile must contain either a PBXFileReference directly 1792 referencing that path, or it must contain a PBXVariantGroup that itself 1793 contains a PBXFileReference referencing the path. 1794 1795 If path is not specified, either the PBXFileReference's path or the paths 1796 of all children of the PBXVariantGroup are taken as being added to the 1797 phase. 1798 1799 If the path is already present in the phase, raises an exception. 1800 1801 If the PBXFileReference or PBXVariantGroup referenced by pbxbuildfile 1802 are already present in the phase, referenced by a different PBXBuildFile 1803 object, raises an exception. This does not raise an exception when 1804 a PBXFileReference or PBXVariantGroup reappear and are referenced by the 1805 same PBXBuildFile that has already introduced them, because in the case 1806 of PBXVariantGroup objects, they may correspond to multiple paths that are 1807 not all added simultaneously. When this situation occurs, the path needs 1808 to be added to _files_by_path, but nothing needs to change in 1809 _files_by_xcfilelikeelement, and the caller should have avoided adding 1810 the PBXBuildFile if it is already present in the list of children. 1811 """ 1812 1813 xcfilelikeelement = pbxbuildfile._properties['fileRef'] 1814 1815 paths = [] 1816 if path != None: 1817 # It's best when the caller provides the path. 1818 if isinstance(xcfilelikeelement, PBXVariantGroup): 1819 paths.append(path) 1820 else: 1821 # If the caller didn't provide a path, there can be either multiple 1822 # paths (PBXVariantGroup) or one. 1823 if isinstance(xcfilelikeelement, PBXVariantGroup): 1824 for variant in xcfilelikeelement._properties['children']: 1825 paths.append(variant.FullPath()) 1826 else: 1827 paths.append(xcfilelikeelement.FullPath()) 1828 1829 # Add the paths first, because if something's going to raise, the 1830 # messages provided by _AddPathToDict are more useful owing to its 1831 # having access to a real pathname and not just an object's Name(). 1832 for a_path in paths: 1833 self._AddPathToDict(pbxbuildfile, a_path) 1834 1835 # If another PBXBuildFile references this XCFileLikeElement, there's a 1836 # problem. 1837 if xcfilelikeelement in self._files_by_xcfilelikeelement and \ 1838 self._files_by_xcfilelikeelement[xcfilelikeelement] != pbxbuildfile: 1839 raise ValueError, 'Found multiple build files for ' + \ 1840 xcfilelikeelement.Name() 1841 self._files_by_xcfilelikeelement[xcfilelikeelement] = pbxbuildfile 1842 1843 def AppendBuildFile(self, pbxbuildfile, path=None): 1844 # Callers should use this instead of calling 1845 # AppendProperty('files', pbxbuildfile) directly because this function 1846 # maintains the object's dicts. Better yet, callers can just call AddFile 1847 # with a pathname and not worry about building their own PBXBuildFile 1848 # objects. 1849 self.AppendProperty('files', pbxbuildfile) 1850 self._AddBuildFileToDicts(pbxbuildfile, path) 1851 1852 def AddFile(self, path, settings=None): 1853 (file_group, hierarchical) = self.FileGroup(path) 1854 file_ref = file_group.AddOrGetFileByPath(path, hierarchical) 1855 1856 if file_ref in self._files_by_xcfilelikeelement and \ 1857 isinstance(file_ref, PBXVariantGroup): 1858 # There's already a PBXBuildFile in this phase corresponding to the 1859 # PBXVariantGroup. path just provides a new variant that belongs to 1860 # the group. Add the path to the dict. 1861 pbxbuildfile = self._files_by_xcfilelikeelement[file_ref] 1862 self._AddBuildFileToDicts(pbxbuildfile, path) 1863 else: 1864 # Add a new PBXBuildFile to get file_ref into the phase. 1865 if settings is None: 1866 pbxbuildfile = PBXBuildFile({'fileRef': file_ref}) 1867 else: 1868 pbxbuildfile = PBXBuildFile({'fileRef': file_ref, 'settings': settings}) 1869 self.AppendBuildFile(pbxbuildfile, path) 1870 1871 1872 class PBXHeadersBuildPhase(XCBuildPhase): 1873 # No additions to the schema relative to XCBuildPhase. 1874 1875 def Name(self): 1876 return 'Headers' 1877 1878 def FileGroup(self, path): 1879 return self.PBXProjectAncestor().RootGroupForPath(path) 1880 1881 1882 class PBXResourcesBuildPhase(XCBuildPhase): 1883 # No additions to the schema relative to XCBuildPhase. 1884 1885 def Name(self): 1886 return 'Resources' 1887 1888 def FileGroup(self, path): 1889 return self.PBXProjectAncestor().RootGroupForPath(path) 1890 1891 1892 class PBXSourcesBuildPhase(XCBuildPhase): 1893 # No additions to the schema relative to XCBuildPhase. 1894 1895 def Name(self): 1896 return 'Sources' 1897 1898 def FileGroup(self, path): 1899 return self.PBXProjectAncestor().RootGroupForPath(path) 1900 1901 1902 class PBXFrameworksBuildPhase(XCBuildPhase): 1903 # No additions to the schema relative to XCBuildPhase. 1904 1905 def Name(self): 1906 return 'Frameworks' 1907 1908 def FileGroup(self, path): 1909 (root, ext) = posixpath.splitext(path) 1910 if ext != '': 1911 ext = ext[1:].lower() 1912 if ext == 'o': 1913 # .o files are added to Xcode Frameworks phases, but conceptually aren't 1914 # frameworks, they're more like sources or intermediates. Redirect them 1915 # to show up in one of those other groups. 1916 return self.PBXProjectAncestor().RootGroupForPath(path) 1917 else: 1918 return (self.PBXProjectAncestor().FrameworksGroup(), False) 1919 1920 1921 class PBXShellScriptBuildPhase(XCBuildPhase): 1922 _schema = XCBuildPhase._schema.copy() 1923 _schema.update({ 1924 'inputPaths': [1, str, 0, 1, []], 1925 'name': [0, str, 0, 0], 1926 'outputPaths': [1, str, 0, 1, []], 1927 'shellPath': [0, str, 0, 1, '/bin/sh'], 1928 'shellScript': [0, str, 0, 1], 1929 'showEnvVarsInLog': [0, int, 0, 0], 1930 }) 1931 1932 def Name(self): 1933 if 'name' in self._properties: 1934 return self._properties['name'] 1935 1936 return 'ShellScript' 1937 1938 1939 class PBXCopyFilesBuildPhase(XCBuildPhase): 1940 _schema = XCBuildPhase._schema.copy() 1941 _schema.update({ 1942 'dstPath': [0, str, 0, 1], 1943 'dstSubfolderSpec': [0, int, 0, 1], 1944 'name': [0, str, 0, 0], 1945 }) 1946 1947 # path_tree_re matches "$(DIR)/path" or just "$(DIR)". Match group 1 is 1948 # "DIR", match group 3 is "path" or None. 1949 path_tree_re = re.compile('^\\$\\((.*)\\)(/(.*)|)$') 1950 1951 # path_tree_to_subfolder maps names of Xcode variables to the associated 1952 # dstSubfolderSpec property value used in a PBXCopyFilesBuildPhase object. 1953 path_tree_to_subfolder = { 1954 'BUILT_PRODUCTS_DIR': 16, # Products Directory 1955 # Other types that can be chosen via the Xcode UI. 1956 # TODO(mark): Map Xcode variable names to these. 1957 # : 1, # Wrapper 1958 # : 6, # Executables: 6 1959 # : 7, # Resources 1960 # : 15, # Java Resources 1961 # : 10, # Frameworks 1962 # : 11, # Shared Frameworks 1963 # : 12, # Shared Support 1964 # : 13, # PlugIns 1965 } 1966 1967 def Name(self): 1968 if 'name' in self._properties: 1969 return self._properties['name'] 1970 1971 return 'CopyFiles' 1972 1973 def FileGroup(self, path): 1974 return self.PBXProjectAncestor().RootGroupForPath(path) 1975 1976 def SetDestination(self, path): 1977 """Set the dstSubfolderSpec and dstPath properties from path. 1978 1979 path may be specified in the same notation used for XCHierarchicalElements, 1980 specifically, "$(DIR)/path". 1981 """ 1982 1983 path_tree_match = self.path_tree_re.search(path) 1984 if path_tree_match: 1985 # Everything else needs to be relative to an Xcode variable. 1986 path_tree = path_tree_match.group(1) 1987 relative_path = path_tree_match.group(3) 1988 1989 if path_tree in self.path_tree_to_subfolder: 1990 subfolder = self.path_tree_to_subfolder[path_tree] 1991 if relative_path is None: 1992 relative_path = '' 1993 else: 1994 # The path starts with an unrecognized Xcode variable 1995 # name like $(SRCROOT). Xcode will still handle this 1996 # as an "absolute path" that starts with the variable. 1997 subfolder = 0 1998 relative_path = path 1999 elif path.startswith('/'): 2000 # Special case. Absolute paths are in dstSubfolderSpec 0. 2001 subfolder = 0 2002 relative_path = path[1:] 2003 else: 2004 raise ValueError, 'Can\'t use path %s in a %s' % \ 2005 (path, self.__class__.__name__) 2006 2007 self._properties['dstPath'] = relative_path 2008 self._properties['dstSubfolderSpec'] = subfolder 2009 2010 2011 class PBXBuildRule(XCObject): 2012 _schema = XCObject._schema.copy() 2013 _schema.update({ 2014 'compilerSpec': [0, str, 0, 1], 2015 'filePatterns': [0, str, 0, 0], 2016 'fileType': [0, str, 0, 1], 2017 'isEditable': [0, int, 0, 1, 1], 2018 'outputFiles': [1, str, 0, 1, []], 2019 'script': [0, str, 0, 0], 2020 }) 2021 2022 def Name(self): 2023 # Not very inspired, but it's what Xcode uses. 2024 return self.__class__.__name__ 2025 2026 def Hashables(self): 2027 # super 2028 hashables = XCObject.Hashables(self) 2029 2030 # Use the hashables of the weak objects that this object refers to. 2031 hashables.append(self._properties['fileType']) 2032 if 'filePatterns' in self._properties: 2033 hashables.append(self._properties['filePatterns']) 2034 return hashables 2035 2036 2037 class PBXContainerItemProxy(XCObject): 2038 # When referencing an item in this project file, containerPortal is the 2039 # PBXProject root object of this project file. When referencing an item in 2040 # another project file, containerPortal is a PBXFileReference identifying 2041 # the other project file. 2042 # 2043 # When serving as a proxy to an XCTarget (in this project file or another), 2044 # proxyType is 1. When serving as a proxy to a PBXFileReference (in another 2045 # project file), proxyType is 2. Type 2 is used for references to the 2046 # producs of the other project file's targets. 2047 # 2048 # Xcode is weird about remoteGlobalIDString. Usually, it's printed without 2049 # a comment, indicating that it's tracked internally simply as a string, but 2050 # sometimes it's printed with a comment (usually when the object is initially 2051 # created), indicating that it's tracked as a project file object at least 2052 # sometimes. This module always tracks it as an object, but contains a hack 2053 # to prevent it from printing the comment in the project file output. See 2054 # _XCKVPrint. 2055 _schema = XCObject._schema.copy() 2056 _schema.update({ 2057 'containerPortal': [0, XCContainerPortal, 0, 1], 2058 'proxyType': [0, int, 0, 1], 2059 'remoteGlobalIDString': [0, XCRemoteObject, 0, 1], 2060 'remoteInfo': [0, str, 0, 1], 2061 }) 2062 2063 def __repr__(self): 2064 props = self._properties 2065 name = '%s.gyp:%s' % (props['containerPortal'].Name(), props['remoteInfo']) 2066 return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self)) 2067 2068 def Name(self): 2069 # Admittedly not the best name, but it's what Xcode uses. 2070 return self.__class__.__name__ 2071 2072 def Hashables(self): 2073 # super 2074 hashables = XCObject.Hashables(self) 2075 2076 # Use the hashables of the weak objects that this object refers to. 2077 hashables.extend(self._properties['containerPortal'].Hashables()) 2078 hashables.extend(self._properties['remoteGlobalIDString'].Hashables()) 2079 return hashables 2080 2081 2082 class PBXTargetDependency(XCObject): 2083 # The "target" property accepts an XCTarget object, and obviously not 2084 # NoneType. But XCTarget is defined below, so it can't be put into the 2085 # schema yet. The definition of PBXTargetDependency can't be moved below 2086 # XCTarget because XCTarget's own schema references PBXTargetDependency. 2087 # Python doesn't deal well with this circular relationship, and doesn't have 2088 # a real way to do forward declarations. To work around, the type of 2089 # the "target" property is reset below, after XCTarget is defined. 2090 # 2091 # At least one of "name" and "target" is required. 2092 _schema = XCObject._schema.copy() 2093 _schema.update({ 2094 'name': [0, str, 0, 0], 2095 'target': [0, None.__class__, 0, 0], 2096 'targetProxy': [0, PBXContainerItemProxy, 1, 1], 2097 }) 2098 2099 def __repr__(self): 2100 name = self._properties.get('name') or self._properties['target'].Name() 2101 return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self)) 2102 2103 def Name(self): 2104 # Admittedly not the best name, but it's what Xcode uses. 2105 return self.__class__.__name__ 2106 2107 def Hashables(self): 2108 # super 2109 hashables = XCObject.Hashables(self) 2110 2111 # Use the hashables of the weak objects that this object refers to. 2112 hashables.extend(self._properties['targetProxy'].Hashables()) 2113 return hashables 2114 2115 2116 class PBXReferenceProxy(XCFileLikeElement): 2117 _schema = XCFileLikeElement._schema.copy() 2118 _schema.update({ 2119 'fileType': [0, str, 0, 1], 2120 'path': [0, str, 0, 1], 2121 'remoteRef': [0, PBXContainerItemProxy, 1, 1], 2122 }) 2123 2124 2125 class XCTarget(XCRemoteObject): 2126 # An XCTarget is really just an XCObject, the XCRemoteObject thing is just 2127 # to allow PBXProject to be used in the remoteGlobalIDString property of 2128 # PBXContainerItemProxy. 2129 # 2130 # Setting a "name" property at instantiation may also affect "productName", 2131 # which may in turn affect the "PRODUCT_NAME" build setting in children of 2132 # "buildConfigurationList". See __init__ below. 2133 _schema = XCRemoteObject._schema.copy() 2134 _schema.update({ 2135 'buildConfigurationList': [0, XCConfigurationList, 1, 1, 2136 XCConfigurationList()], 2137 'buildPhases': [1, XCBuildPhase, 1, 1, []], 2138 'dependencies': [1, PBXTargetDependency, 1, 1, []], 2139 'name': [0, str, 0, 1], 2140 'productName': [0, str, 0, 1], 2141 }) 2142 2143 def __init__(self, properties=None, id=None, parent=None, 2144 force_outdir=None, force_prefix=None, force_extension=None): 2145 # super 2146 XCRemoteObject.__init__(self, properties, id, parent) 2147 2148 # Set up additional defaults not expressed in the schema. If a "name" 2149 # property was supplied, set "productName" if it is not present. Also set 2150 # the "PRODUCT_NAME" build setting in each configuration, but only if 2151 # the setting is not present in any build configuration. 2152 if 'name' in self._properties: 2153 if not 'productName' in self._properties: 2154 self.SetProperty('productName', self._properties['name']) 2155 2156 if 'productName' in self._properties: 2157 if 'buildConfigurationList' in self._properties: 2158 configs = self._properties['buildConfigurationList'] 2159 if configs.HasBuildSetting('PRODUCT_NAME') == 0: 2160 configs.SetBuildSetting('PRODUCT_NAME', 2161 self._properties['productName']) 2162 2163 def AddDependency(self, other): 2164 pbxproject = self.PBXProjectAncestor() 2165 other_pbxproject = other.PBXProjectAncestor() 2166 if pbxproject == other_pbxproject: 2167 # Add a dependency to another target in the same project file. 2168 container = PBXContainerItemProxy({'containerPortal': pbxproject, 2169 'proxyType': 1, 2170 'remoteGlobalIDString': other, 2171 'remoteInfo': other.Name()}) 2172 dependency = PBXTargetDependency({'target': other, 2173 'targetProxy': container}) 2174 self.AppendProperty('dependencies', dependency) 2175 else: 2176 # Add a dependency to a target in a different project file. 2177 other_project_ref = \ 2178 pbxproject.AddOrGetProjectReference(other_pbxproject)[1] 2179 container = PBXContainerItemProxy({ 2180 'containerPortal': other_project_ref, 2181 'proxyType': 1, 2182 'remoteGlobalIDString': other, 2183 'remoteInfo': other.Name(), 2184 }) 2185 dependency = PBXTargetDependency({'name': other.Name(), 2186 'targetProxy': container}) 2187 self.AppendProperty('dependencies', dependency) 2188 2189 # Proxy all of these through to the build configuration list. 2190 2191 def ConfigurationNamed(self, name): 2192 return self._properties['buildConfigurationList'].ConfigurationNamed(name) 2193 2194 def DefaultConfiguration(self): 2195 return self._properties['buildConfigurationList'].DefaultConfiguration() 2196 2197 def HasBuildSetting(self, key): 2198 return self._properties['buildConfigurationList'].HasBuildSetting(key) 2199 2200 def GetBuildSetting(self, key): 2201 return self._properties['buildConfigurationList'].GetBuildSetting(key) 2202 2203 def SetBuildSetting(self, key, value): 2204 return self._properties['buildConfigurationList'].SetBuildSetting(key, \ 2205 value) 2206 2207 def AppendBuildSetting(self, key, value): 2208 return self._properties['buildConfigurationList'].AppendBuildSetting(key, \ 2209 value) 2210 2211 def DelBuildSetting(self, key): 2212 return self._properties['buildConfigurationList'].DelBuildSetting(key) 2213 2214 2215 # Redefine the type of the "target" property. See PBXTargetDependency._schema 2216 # above. 2217 PBXTargetDependency._schema['target'][1] = XCTarget 2218 2219 2220 class PBXNativeTarget(XCTarget): 2221 # buildPhases is overridden in the schema to be able to set defaults. 2222 # 2223 # NOTE: Contrary to most objects, it is advisable to set parent when 2224 # constructing PBXNativeTarget. A parent of an XCTarget must be a PBXProject 2225 # object. A parent reference is required for a PBXNativeTarget during 2226 # construction to be able to set up the target defaults for productReference, 2227 # because a PBXBuildFile object must be created for the target and it must 2228 # be added to the PBXProject's mainGroup hierarchy. 2229 _schema = XCTarget._schema.copy() 2230 _schema.update({ 2231 'buildPhases': [1, XCBuildPhase, 1, 1, 2232 [PBXSourcesBuildPhase(), PBXFrameworksBuildPhase()]], 2233 'buildRules': [1, PBXBuildRule, 1, 1, []], 2234 'productReference': [0, PBXFileReference, 0, 1], 2235 'productType': [0, str, 0, 1], 2236 }) 2237 2238 # Mapping from Xcode product-types to settings. The settings are: 2239 # filetype : used for explicitFileType in the project file 2240 # prefix : the prefix for the file name 2241 # suffix : the suffix for the file name 2242 _product_filetypes = { 2243 'com.apple.product-type.application': ['wrapper.application', 2244 '', '.app'], 2245 'com.apple.product-type.app-extension': ['wrapper.app-extension', 2246 '', '.appex'], 2247 'com.apple.product-type.bundle': ['wrapper.cfbundle', 2248 '', '.bundle'], 2249 'com.apple.product-type.framework': ['wrapper.framework', 2250 '', '.framework'], 2251 'com.apple.product-type.library.dynamic': ['compiled.mach-o.dylib', 2252 'lib', '.dylib'], 2253 'com.apple.product-type.library.static': ['archive.ar', 2254 'lib', '.a'], 2255 'com.apple.product-type.tool': ['compiled.mach-o.executable', 2256 '', ''], 2257 'com.apple.product-type.bundle.unit-test': ['wrapper.cfbundle', 2258 '', '.xctest'], 2259 'com.googlecode.gyp.xcode.bundle': ['compiled.mach-o.dylib', 2260 '', '.so'], 2261 } 2262 2263 def __init__(self, properties=None, id=None, parent=None, 2264 force_outdir=None, force_prefix=None, force_extension=None): 2265 # super 2266 XCTarget.__init__(self, properties, id, parent) 2267 2268 if 'productName' in self._properties and \ 2269 'productType' in self._properties and \ 2270 not 'productReference' in self._properties and \ 2271 self._properties['productType'] in self._product_filetypes: 2272 products_group = None 2273 pbxproject = self.PBXProjectAncestor() 2274 if pbxproject != None: 2275 products_group = pbxproject.ProductsGroup() 2276 2277 if products_group != None: 2278 (filetype, prefix, suffix) = \ 2279 self._product_filetypes[self._properties['productType']] 2280 # Xcode does not have a distinct type for loadable modules that are 2281 # pure BSD targets (not in a bundle wrapper). GYP allows such modules 2282 # to be specified by setting a target type to loadable_module without 2283 # having mac_bundle set. These are mapped to the pseudo-product type 2284 # com.googlecode.gyp.xcode.bundle. 2285 # 2286 # By picking up this special type and converting it to a dynamic 2287 # library (com.apple.product-type.library.dynamic) with fix-ups, 2288 # single-file loadable modules can be produced. 2289 # 2290 # MACH_O_TYPE is changed to mh_bundle to produce the proper file type 2291 # (as opposed to mh_dylib). In order for linking to succeed, 2292 # DYLIB_CURRENT_VERSION and DYLIB_COMPATIBILITY_VERSION must be 2293 # cleared. They are meaningless for type mh_bundle. 2294 # 2295 # Finally, the .so extension is forcibly applied over the default 2296 # (.dylib), unless another forced extension is already selected. 2297 # .dylib is plainly wrong, and .bundle is used by loadable_modules in 2298 # bundle wrappers (com.apple.product-type.bundle). .so seems an odd 2299 # choice because it's used as the extension on many other systems that 2300 # don't distinguish between linkable shared libraries and non-linkable 2301 # loadable modules, but there's precedent: Python loadable modules on 2302 # Mac OS X use an .so extension. 2303 if self._properties['productType'] == 'com.googlecode.gyp.xcode.bundle': 2304 self._properties['productType'] = \ 2305 'com.apple.product-type.library.dynamic' 2306 self.SetBuildSetting('MACH_O_TYPE', 'mh_bundle') 2307 self.SetBuildSetting('DYLIB_CURRENT_VERSION', '') 2308 self.SetBuildSetting('DYLIB_COMPATIBILITY_VERSION', '') 2309 if force_extension is None: 2310 force_extension = suffix[1:] 2311 2312 if self._properties['productType'] == \ 2313 'com.apple.product-type-bundle.unit.test': 2314 if force_extension is None: 2315 force_extension = suffix[1:] 2316 2317 if force_extension is not None: 2318 # If it's a wrapper (bundle), set WRAPPER_EXTENSION. 2319 # Extension override. 2320 suffix = '.' + force_extension 2321 if filetype.startswith('wrapper.'): 2322 self.SetBuildSetting('WRAPPER_EXTENSION', force_extension) 2323 else: 2324 self.SetBuildSetting('EXECUTABLE_EXTENSION', force_extension) 2325 2326 if filetype.startswith('compiled.mach-o.executable'): 2327 product_name = self._properties['productName'] 2328 product_name += suffix 2329 suffix = '' 2330 self.SetProperty('productName', product_name) 2331 self.SetBuildSetting('PRODUCT_NAME', product_name) 2332 2333 # Xcode handles most prefixes based on the target type, however there 2334 # are exceptions. If a "BSD Dynamic Library" target is added in the 2335 # Xcode UI, Xcode sets EXECUTABLE_PREFIX. This check duplicates that 2336 # behavior. 2337 if force_prefix is not None: 2338 prefix = force_prefix 2339 if filetype.startswith('wrapper.'): 2340 self.SetBuildSetting('WRAPPER_PREFIX', prefix) 2341 else: 2342 self.SetBuildSetting('EXECUTABLE_PREFIX', prefix) 2343 2344 if force_outdir is not None: 2345 self.SetBuildSetting('TARGET_BUILD_DIR', force_outdir) 2346 2347 # TODO(tvl): Remove the below hack. 2348 # http://code.google.com/p/gyp/issues/detail?id=122 2349 2350 # Some targets include the prefix in the target_name. These targets 2351 # really should just add a product_name setting that doesn't include 2352 # the prefix. For example: 2353 # target_name = 'libevent', product_name = 'event' 2354 # This check cleans up for them. 2355 product_name = self._properties['productName'] 2356 prefix_len = len(prefix) 2357 if prefix_len and (product_name[:prefix_len] == prefix): 2358 product_name = product_name[prefix_len:] 2359 self.SetProperty('productName', product_name) 2360 self.SetBuildSetting('PRODUCT_NAME', product_name) 2361 2362 ref_props = { 2363 'explicitFileType': filetype, 2364 'includeInIndex': 0, 2365 'path': prefix + product_name + suffix, 2366 'sourceTree': 'BUILT_PRODUCTS_DIR', 2367 } 2368 file_ref = PBXFileReference(ref_props) 2369 products_group.AppendChild(file_ref) 2370 self.SetProperty('productReference', file_ref) 2371 2372 def GetBuildPhaseByType(self, type): 2373 if not 'buildPhases' in self._properties: 2374 return None 2375 2376 the_phase = None 2377 for phase in self._properties['buildPhases']: 2378 if isinstance(phase, type): 2379 # Some phases may be present in multiples in a well-formed project file, 2380 # but phases like PBXSourcesBuildPhase may only be present singly, and 2381 # this function is intended as an aid to GetBuildPhaseByType. Loop 2382 # over the entire list of phases and assert if more than one of the 2383 # desired type is found. 2384 assert the_phase is None 2385 the_phase = phase 2386 2387 return the_phase 2388 2389 def HeadersPhase(self): 2390 headers_phase = self.GetBuildPhaseByType(PBXHeadersBuildPhase) 2391 if headers_phase is None: 2392 headers_phase = PBXHeadersBuildPhase() 2393 2394 # The headers phase should come before the resources, sources, and 2395 # frameworks phases, if any. 2396 insert_at = len(self._properties['buildPhases']) 2397 for index in xrange(0, len(self._properties['buildPhases'])): 2398 phase = self._properties['buildPhases'][index] 2399 if isinstance(phase, PBXResourcesBuildPhase) or \ 2400 isinstance(phase, PBXSourcesBuildPhase) or \ 2401 isinstance(phase, PBXFrameworksBuildPhase): 2402 insert_at = index 2403 break 2404 2405 self._properties['buildPhases'].insert(insert_at, headers_phase) 2406 headers_phase.parent = self 2407 2408 return headers_phase 2409 2410 def ResourcesPhase(self): 2411 resources_phase = self.GetBuildPhaseByType(PBXResourcesBuildPhase) 2412 if resources_phase is None: 2413 resources_phase = PBXResourcesBuildPhase() 2414 2415 # The resources phase should come before the sources and frameworks 2416 # phases, if any. 2417 insert_at = len(self._properties['buildPhases']) 2418 for index in xrange(0, len(self._properties['buildPhases'])): 2419 phase = self._properties['buildPhases'][index] 2420 if isinstance(phase, PBXSourcesBuildPhase) or \ 2421 isinstance(phase, PBXFrameworksBuildPhase): 2422 insert_at = index 2423 break 2424 2425 self._properties['buildPhases'].insert(insert_at, resources_phase) 2426 resources_phase.parent = self 2427 2428 return resources_phase 2429 2430 def SourcesPhase(self): 2431 sources_phase = self.GetBuildPhaseByType(PBXSourcesBuildPhase) 2432 if sources_phase is None: 2433 sources_phase = PBXSourcesBuildPhase() 2434 self.AppendProperty('buildPhases', sources_phase) 2435 2436 return sources_phase 2437 2438 def FrameworksPhase(self): 2439 frameworks_phase = self.GetBuildPhaseByType(PBXFrameworksBuildPhase) 2440 if frameworks_phase is None: 2441 frameworks_phase = PBXFrameworksBuildPhase() 2442 self.AppendProperty('buildPhases', frameworks_phase) 2443 2444 return frameworks_phase 2445 2446 def AddDependency(self, other): 2447 # super 2448 XCTarget.AddDependency(self, other) 2449 2450 static_library_type = 'com.apple.product-type.library.static' 2451 shared_library_type = 'com.apple.product-type.library.dynamic' 2452 framework_type = 'com.apple.product-type.framework' 2453 if isinstance(other, PBXNativeTarget) and \ 2454 'productType' in self._properties and \ 2455 self._properties['productType'] != static_library_type and \ 2456 'productType' in other._properties and \ 2457 (other._properties['productType'] == static_library_type or \ 2458 ((other._properties['productType'] == shared_library_type or \ 2459 other._properties['productType'] == framework_type) and \ 2460 ((not other.HasBuildSetting('MACH_O_TYPE')) or 2461 other.GetBuildSetting('MACH_O_TYPE') != 'mh_bundle'))): 2462 2463 file_ref = other.GetProperty('productReference') 2464 2465 pbxproject = self.PBXProjectAncestor() 2466 other_pbxproject = other.PBXProjectAncestor() 2467 if pbxproject != other_pbxproject: 2468 other_project_product_group = \ 2469 pbxproject.AddOrGetProjectReference(other_pbxproject)[0] 2470 file_ref = other_project_product_group.GetChildByRemoteObject(file_ref) 2471 2472 self.FrameworksPhase().AppendProperty('files', 2473 PBXBuildFile({'fileRef': file_ref})) 2474 2475 2476 class PBXAggregateTarget(XCTarget): 2477 pass 2478 2479 2480 class PBXProject(XCContainerPortal): 2481 # A PBXProject is really just an XCObject, the XCContainerPortal thing is 2482 # just to allow PBXProject to be used in the containerPortal property of 2483 # PBXContainerItemProxy. 2484 """ 2485 2486 Attributes: 2487 path: "sample.xcodeproj". TODO(mark) Document me! 2488 _other_pbxprojects: A dictionary, keyed by other PBXProject objects. Each 2489 value is a reference to the dict in the 2490 projectReferences list associated with the keyed 2491 PBXProject. 2492 """ 2493 2494 _schema = XCContainerPortal._schema.copy() 2495 _schema.update({ 2496 'attributes': [0, dict, 0, 0], 2497 'buildConfigurationList': [0, XCConfigurationList, 1, 1, 2498 XCConfigurationList()], 2499 'compatibilityVersion': [0, str, 0, 1, 'Xcode 3.2'], 2500 'hasScannedForEncodings': [0, int, 0, 1, 1], 2501 'mainGroup': [0, PBXGroup, 1, 1, PBXGroup()], 2502 'projectDirPath': [0, str, 0, 1, ''], 2503 'projectReferences': [1, dict, 0, 0], 2504 'projectRoot': [0, str, 0, 1, ''], 2505 'targets': [1, XCTarget, 1, 1, []], 2506 }) 2507 2508 def __init__(self, properties=None, id=None, parent=None, path=None): 2509 self.path = path 2510 self._other_pbxprojects = {} 2511 # super 2512 return XCContainerPortal.__init__(self, properties, id, parent) 2513 2514 def Name(self): 2515 name = self.path 2516 if name[-10:] == '.xcodeproj': 2517 name = name[:-10] 2518 return posixpath.basename(name) 2519 2520 def Path(self): 2521 return self.path 2522 2523 def Comment(self): 2524 return 'Project object' 2525 2526 def Children(self): 2527 # super 2528 children = XCContainerPortal.Children(self) 2529 2530 # Add children that the schema doesn't know about. Maybe there's a more 2531 # elegant way around this, but this is the only case where we need to own 2532 # objects in a dictionary (that is itself in a list), and three lines for 2533 # a one-off isn't that big a deal. 2534 if 'projectReferences' in self._properties: 2535 for reference in self._properties['projectReferences']: 2536 children.append(reference['ProductGroup']) 2537 2538 return children 2539 2540 def PBXProjectAncestor(self): 2541 return self 2542 2543 def _GroupByName(self, name): 2544 if not 'mainGroup' in self._properties: 2545 self.SetProperty('mainGroup', PBXGroup()) 2546 2547 main_group = self._properties['mainGroup'] 2548 group = main_group.GetChildByName(name) 2549 if group is None: 2550 group = PBXGroup({'name': name}) 2551 main_group.AppendChild(group) 2552 2553 return group 2554 2555 # SourceGroup and ProductsGroup are created by default in Xcode's own 2556 # templates. 2557 def SourceGroup(self): 2558 return self._GroupByName('Source') 2559 2560 def ProductsGroup(self): 2561 return self._GroupByName('Products') 2562 2563 # IntermediatesGroup is used to collect source-like files that are generated 2564 # by rules or script phases and are placed in intermediate directories such 2565 # as DerivedSources. 2566 def IntermediatesGroup(self): 2567 return self._GroupByName('Intermediates') 2568 2569 # FrameworksGroup and ProjectsGroup are top-level groups used to collect 2570 # frameworks and projects. 2571 def FrameworksGroup(self): 2572 return self._GroupByName('Frameworks') 2573 2574 def ProjectsGroup(self): 2575 return self._GroupByName('Projects') 2576 2577 def RootGroupForPath(self, path): 2578 """Returns a PBXGroup child of this object to which path should be added. 2579 2580 This method is intended to choose between SourceGroup and 2581 IntermediatesGroup on the basis of whether path is present in a source 2582 directory or an intermediates directory. For the purposes of this 2583 determination, any path located within a derived file directory such as 2584 PROJECT_DERIVED_FILE_DIR is treated as being in an intermediates 2585 directory. 2586 2587 The returned value is a two-element tuple. The first element is the 2588 PBXGroup, and the second element specifies whether that group should be 2589 organized hierarchically (True) or as a single flat list (False). 2590 """ 2591 2592 # TODO(mark): make this a class variable and bind to self on call? 2593 # Also, this list is nowhere near exhaustive. 2594 # INTERMEDIATE_DIR and SHARED_INTERMEDIATE_DIR are used by 2595 # gyp.generator.xcode. There should probably be some way for that module 2596 # to push the names in, rather than having to hard-code them here. 2597 source_tree_groups = { 2598 'DERIVED_FILE_DIR': (self.IntermediatesGroup, True), 2599 'INTERMEDIATE_DIR': (self.IntermediatesGroup, True), 2600 'PROJECT_DERIVED_FILE_DIR': (self.IntermediatesGroup, True), 2601 'SHARED_INTERMEDIATE_DIR': (self.IntermediatesGroup, True), 2602 } 2603 2604 (source_tree, path) = SourceTreeAndPathFromPath(path) 2605 if source_tree != None and source_tree in source_tree_groups: 2606 (group_func, hierarchical) = source_tree_groups[source_tree] 2607 group = group_func() 2608 return (group, hierarchical) 2609 2610 # TODO(mark): make additional choices based on file extension. 2611 2612 return (self.SourceGroup(), True) 2613 2614 def AddOrGetFileInRootGroup(self, path): 2615 """Returns a PBXFileReference corresponding to path in the correct group 2616 according to RootGroupForPath's heuristics. 2617 2618 If an existing PBXFileReference for path exists, it will be returned. 2619 Otherwise, one will be created and returned. 2620 """ 2621 2622 (group, hierarchical) = self.RootGroupForPath(path) 2623 return group.AddOrGetFileByPath(path, hierarchical) 2624 2625 def RootGroupsTakeOverOnlyChildren(self, recurse=False): 2626 """Calls TakeOverOnlyChild for all groups in the main group.""" 2627 2628 for group in self._properties['mainGroup']._properties['children']: 2629 if isinstance(group, PBXGroup): 2630 group.TakeOverOnlyChild(recurse) 2631 2632 def SortGroups(self): 2633 # Sort the children of the mainGroup (like "Source" and "Products") 2634 # according to their defined order. 2635 self._properties['mainGroup']._properties['children'] = \ 2636 sorted(self._properties['mainGroup']._properties['children'], 2637 cmp=lambda x,y: x.CompareRootGroup(y)) 2638 2639 # Sort everything else by putting group before files, and going 2640 # alphabetically by name within sections of groups and files. SortGroup 2641 # is recursive. 2642 for group in self._properties['mainGroup']._properties['children']: 2643 if not isinstance(group, PBXGroup): 2644 continue 2645 2646 if group.Name() == 'Products': 2647 # The Products group is a special case. Instead of sorting 2648 # alphabetically, sort things in the order of the targets that 2649 # produce the products. To do this, just build up a new list of 2650 # products based on the targets. 2651 products = [] 2652 for target in self._properties['targets']: 2653 if not isinstance(target, PBXNativeTarget): 2654 continue 2655 product = target._properties['productReference'] 2656 # Make sure that the product is already in the products group. 2657 assert product in group._properties['children'] 2658 products.append(product) 2659 2660 # Make sure that this process doesn't miss anything that was already 2661 # in the products group. 2662 assert len(products) == len(group._properties['children']) 2663 group._properties['children'] = products 2664 else: 2665 group.SortGroup() 2666 2667 def AddOrGetProjectReference(self, other_pbxproject): 2668 """Add a reference to another project file (via PBXProject object) to this 2669 one. 2670 2671 Returns [ProductGroup, ProjectRef]. ProductGroup is a PBXGroup object in 2672 this project file that contains a PBXReferenceProxy object for each 2673 product of each PBXNativeTarget in the other project file. ProjectRef is 2674 a PBXFileReference to the other project file. 2675 2676 If this project file already references the other project file, the 2677 existing ProductGroup and ProjectRef are returned. The ProductGroup will 2678 still be updated if necessary. 2679 """ 2680 2681 if not 'projectReferences' in self._properties: 2682 self._properties['projectReferences'] = [] 2683 2684 product_group = None 2685 project_ref = None 2686 2687 if not other_pbxproject in self._other_pbxprojects: 2688 # This project file isn't yet linked to the other one. Establish the 2689 # link. 2690 product_group = PBXGroup({'name': 'Products'}) 2691 2692 # ProductGroup is strong. 2693 product_group.parent = self 2694 2695 # There's nothing unique about this PBXGroup, and if left alone, it will 2696 # wind up with the same set of hashables as all other PBXGroup objects 2697 # owned by the projectReferences list. Add the hashables of the 2698 # remote PBXProject that it's related to. 2699 product_group._hashables.extend(other_pbxproject.Hashables()) 2700 2701 # The other project reports its path as relative to the same directory 2702 # that this project's path is relative to. The other project's path 2703 # is not necessarily already relative to this project. Figure out the 2704 # pathname that this project needs to use to refer to the other one. 2705 this_path = posixpath.dirname(self.Path()) 2706 projectDirPath = self.GetProperty('projectDirPath') 2707 if projectDirPath: 2708 if posixpath.isabs(projectDirPath[0]): 2709 this_path = projectDirPath 2710 else: 2711 this_path = posixpath.join(this_path, projectDirPath) 2712 other_path = gyp.common.RelativePath(other_pbxproject.Path(), this_path) 2713 2714 # ProjectRef is weak (it's owned by the mainGroup hierarchy). 2715 project_ref = PBXFileReference({ 2716 'lastKnownFileType': 'wrapper.pb-project', 2717 'path': other_path, 2718 'sourceTree': 'SOURCE_ROOT', 2719 }) 2720 self.ProjectsGroup().AppendChild(project_ref) 2721 2722 ref_dict = {'ProductGroup': product_group, 'ProjectRef': project_ref} 2723 self._other_pbxprojects[other_pbxproject] = ref_dict 2724 self.AppendProperty('projectReferences', ref_dict) 2725 2726 # Xcode seems to sort this list case-insensitively 2727 self._properties['projectReferences'] = \ 2728 sorted(self._properties['projectReferences'], cmp=lambda x,y: 2729 cmp(x['ProjectRef'].Name().lower(), 2730 y['ProjectRef'].Name().lower())) 2731 else: 2732 # The link already exists. Pull out the relevnt data. 2733 project_ref_dict = self._other_pbxprojects[other_pbxproject] 2734 product_group = project_ref_dict['ProductGroup'] 2735 project_ref = project_ref_dict['ProjectRef'] 2736 2737 self._SetUpProductReferences(other_pbxproject, product_group, project_ref) 2738 2739 return [product_group, project_ref] 2740 2741 def _SetUpProductReferences(self, other_pbxproject, product_group, 2742 project_ref): 2743 # TODO(mark): This only adds references to products in other_pbxproject 2744 # when they don't exist in this pbxproject. Perhaps it should also 2745 # remove references from this pbxproject that are no longer present in 2746 # other_pbxproject. Perhaps it should update various properties if they 2747 # change. 2748 for target in other_pbxproject._properties['targets']: 2749 if not isinstance(target, PBXNativeTarget): 2750 continue 2751 2752 other_fileref = target._properties['productReference'] 2753 if product_group.GetChildByRemoteObject(other_fileref) is None: 2754 # Xcode sets remoteInfo to the name of the target and not the name 2755 # of its product, despite this proxy being a reference to the product. 2756 container_item = PBXContainerItemProxy({ 2757 'containerPortal': project_ref, 2758 'proxyType': 2, 2759 'remoteGlobalIDString': other_fileref, 2760 'remoteInfo': target.Name() 2761 }) 2762 # TODO(mark): Does sourceTree get copied straight over from the other 2763 # project? Can the other project ever have lastKnownFileType here 2764 # instead of explicitFileType? (Use it if so?) Can path ever be 2765 # unset? (I don't think so.) Can other_fileref have name set, and 2766 # does it impact the PBXReferenceProxy if so? These are the questions 2767 # that perhaps will be answered one day. 2768 reference_proxy = PBXReferenceProxy({ 2769 'fileType': other_fileref._properties['explicitFileType'], 2770 'path': other_fileref._properties['path'], 2771 'sourceTree': other_fileref._properties['sourceTree'], 2772 'remoteRef': container_item, 2773 }) 2774 2775 product_group.AppendChild(reference_proxy) 2776 2777 def SortRemoteProductReferences(self): 2778 # For each remote project file, sort the associated ProductGroup in the 2779 # same order that the targets are sorted in the remote project file. This 2780 # is the sort order used by Xcode. 2781 2782 def CompareProducts(x, y, remote_products): 2783 # x and y are PBXReferenceProxy objects. Go through their associated 2784 # PBXContainerItem to get the remote PBXFileReference, which will be 2785 # present in the remote_products list. 2786 x_remote = x._properties['remoteRef']._properties['remoteGlobalIDString'] 2787 y_remote = y._properties['remoteRef']._properties['remoteGlobalIDString'] 2788 x_index = remote_products.index(x_remote) 2789 y_index = remote_products.index(y_remote) 2790 2791 # Use the order of each remote PBXFileReference in remote_products to 2792 # determine the sort order. 2793 return cmp(x_index, y_index) 2794 2795 for other_pbxproject, ref_dict in self._other_pbxprojects.iteritems(): 2796 # Build up a list of products in the remote project file, ordered the 2797 # same as the targets that produce them. 2798 remote_products = [] 2799 for target in other_pbxproject._properties['targets']: 2800 if not isinstance(target, PBXNativeTarget): 2801 continue 2802 remote_products.append(target._properties['productReference']) 2803 2804 # Sort the PBXReferenceProxy children according to the list of remote 2805 # products. 2806 product_group = ref_dict['ProductGroup'] 2807 product_group._properties['children'] = sorted( 2808 product_group._properties['children'], 2809 cmp=lambda x, y: CompareProducts(x, y, remote_products)) 2810 2811 2812 class XCProjectFile(XCObject): 2813 _schema = XCObject._schema.copy() 2814 _schema.update({ 2815 'archiveVersion': [0, int, 0, 1, 1], 2816 'classes': [0, dict, 0, 1, {}], 2817 'objectVersion': [0, int, 0, 1, 45], 2818 'rootObject': [0, PBXProject, 1, 1], 2819 }) 2820 2821 def SetXcodeVersion(self, version): 2822 version_to_object_version = { 2823 '2.4': 45, 2824 '3.0': 45, 2825 '3.1': 45, 2826 '3.2': 46, 2827 } 2828 if not version in version_to_object_version: 2829 supported_str = ', '.join(sorted(version_to_object_version.keys())) 2830 raise Exception( 2831 'Unsupported Xcode version %s (supported: %s)' % 2832 ( version, supported_str ) ) 2833 compatibility_version = 'Xcode %s' % version 2834 self._properties['rootObject'].SetProperty('compatibilityVersion', 2835 compatibility_version) 2836 self.SetProperty('objectVersion', version_to_object_version[version]); 2837 2838 def ComputeIDs(self, recursive=True, overwrite=True, hash=None): 2839 # Although XCProjectFile is implemented here as an XCObject, it's not a 2840 # proper object in the Xcode sense, and it certainly doesn't have its own 2841 # ID. Pass through an attempt to update IDs to the real root object. 2842 if recursive: 2843 self._properties['rootObject'].ComputeIDs(recursive, overwrite, hash) 2844 2845 def Print(self, file=sys.stdout): 2846 self.VerifyHasRequiredProperties() 2847 2848 # Add the special "objects" property, which will be caught and handled 2849 # separately during printing. This structure allows a fairly standard 2850 # loop do the normal printing. 2851 self._properties['objects'] = {} 2852 self._XCPrint(file, 0, '// !$*UTF8*$!\n') 2853 if self._should_print_single_line: 2854 self._XCPrint(file, 0, '{ ') 2855 else: 2856 self._XCPrint(file, 0, '{\n') 2857 for property, value in sorted(self._properties.iteritems(), 2858 cmp=lambda x, y: cmp(x, y)): 2859 if property == 'objects': 2860 self._PrintObjects(file) 2861 else: 2862 self._XCKVPrint(file, 1, property, value) 2863 self._XCPrint(file, 0, '}\n') 2864 del self._properties['objects'] 2865 2866 def _PrintObjects(self, file): 2867 if self._should_print_single_line: 2868 self._XCPrint(file, 0, 'objects = {') 2869 else: 2870 self._XCPrint(file, 1, 'objects = {\n') 2871 2872 objects_by_class = {} 2873 for object in self.Descendants(): 2874 if object == self: 2875 continue 2876 class_name = object.__class__.__name__ 2877 if not class_name in objects_by_class: 2878 objects_by_class[class_name] = [] 2879 objects_by_class[class_name].append(object) 2880 2881 for class_name in sorted(objects_by_class): 2882 self._XCPrint(file, 0, '\n') 2883 self._XCPrint(file, 0, '/* Begin ' + class_name + ' section */\n') 2884 for object in sorted(objects_by_class[class_name], 2885 cmp=lambda x, y: cmp(x.id, y.id)): 2886 object.Print(file) 2887 self._XCPrint(file, 0, '/* End ' + class_name + ' section */\n') 2888 2889 if self._should_print_single_line: 2890 self._XCPrint(file, 0, '}; ') 2891 else: 2892 self._XCPrint(file, 1, '};\n') 2893