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 'xib': 'file.xib', 1516 'y': 'sourcecode.yacc', 1517 } 1518 1519 prop_map = { 1520 'dart': 'explicitFileType', 1521 'gyp': 'explicitFileType', 1522 'gypi': 'explicitFileType', 1523 } 1524 1525 if is_dir: 1526 file_type = 'folder' 1527 prop_name = 'lastKnownFileType' 1528 else: 1529 basename = posixpath.basename(self._properties['path']) 1530 (root, ext) = posixpath.splitext(basename) 1531 # Check the map using a lowercase extension. 1532 # TODO(mark): Maybe it should try with the original case first and fall 1533 # back to lowercase, in case there are any instances where case 1534 # matters. There currently aren't. 1535 if ext != '': 1536 ext = ext[1:].lower() 1537 1538 # TODO(mark): "text" is the default value, but "file" is appropriate 1539 # for unrecognized files not containing text. Xcode seems to choose 1540 # based on content. 1541 file_type = extension_map.get(ext, 'text') 1542 prop_name = prop_map.get(ext, 'lastKnownFileType') 1543 1544 self._properties[prop_name] = file_type 1545 1546 1547 class PBXVariantGroup(PBXGroup, XCFileLikeElement): 1548 """PBXVariantGroup is used by Xcode to represent localizations.""" 1549 # No additions to the schema relative to PBXGroup. 1550 pass 1551 1552 1553 # PBXReferenceProxy is also an XCFileLikeElement subclass. It is defined below 1554 # because it uses PBXContainerItemProxy, defined below. 1555 1556 1557 class XCBuildConfiguration(XCObject): 1558 _schema = XCObject._schema.copy() 1559 _schema.update({ 1560 'baseConfigurationReference': [0, PBXFileReference, 0, 0], 1561 'buildSettings': [0, dict, 0, 1, {}], 1562 'name': [0, str, 0, 1], 1563 }) 1564 1565 def HasBuildSetting(self, key): 1566 return key in self._properties['buildSettings'] 1567 1568 def GetBuildSetting(self, key): 1569 return self._properties['buildSettings'][key] 1570 1571 def SetBuildSetting(self, key, value): 1572 # TODO(mark): If a list, copy? 1573 self._properties['buildSettings'][key] = value 1574 1575 def AppendBuildSetting(self, key, value): 1576 if not key in self._properties['buildSettings']: 1577 self._properties['buildSettings'][key] = [] 1578 self._properties['buildSettings'][key].append(value) 1579 1580 def DelBuildSetting(self, key): 1581 if key in self._properties['buildSettings']: 1582 del self._properties['buildSettings'][key] 1583 1584 def SetBaseConfiguration(self, value): 1585 self._properties['baseConfigurationReference'] = value 1586 1587 class XCConfigurationList(XCObject): 1588 # _configs is the default list of configurations. 1589 _configs = [ XCBuildConfiguration({'name': 'Debug'}), 1590 XCBuildConfiguration({'name': 'Release'}) ] 1591 1592 _schema = XCObject._schema.copy() 1593 _schema.update({ 1594 'buildConfigurations': [1, XCBuildConfiguration, 1, 1, _configs], 1595 'defaultConfigurationIsVisible': [0, int, 0, 1, 1], 1596 'defaultConfigurationName': [0, str, 0, 1, 'Release'], 1597 }) 1598 1599 def Name(self): 1600 return 'Build configuration list for ' + \ 1601 self.parent.__class__.__name__ + ' "' + self.parent.Name() + '"' 1602 1603 def ConfigurationNamed(self, name): 1604 """Convenience accessor to obtain an XCBuildConfiguration by name.""" 1605 for configuration in self._properties['buildConfigurations']: 1606 if configuration._properties['name'] == name: 1607 return configuration 1608 1609 raise KeyError, name 1610 1611 def DefaultConfiguration(self): 1612 """Convenience accessor to obtain the default XCBuildConfiguration.""" 1613 return self.ConfigurationNamed(self._properties['defaultConfigurationName']) 1614 1615 def HasBuildSetting(self, key): 1616 """Determines the state of a build setting in all XCBuildConfiguration 1617 child objects. 1618 1619 If all child objects have key in their build settings, and the value is the 1620 same in all child objects, returns 1. 1621 1622 If no child objects have the key in their build settings, returns 0. 1623 1624 If some, but not all, child objects have the key in their build settings, 1625 or if any children have different values for the key, returns -1. 1626 """ 1627 1628 has = None 1629 value = None 1630 for configuration in self._properties['buildConfigurations']: 1631 configuration_has = configuration.HasBuildSetting(key) 1632 if has is None: 1633 has = configuration_has 1634 elif has != configuration_has: 1635 return -1 1636 1637 if configuration_has: 1638 configuration_value = configuration.GetBuildSetting(key) 1639 if value is None: 1640 value = configuration_value 1641 elif value != configuration_value: 1642 return -1 1643 1644 if not has: 1645 return 0 1646 1647 return 1 1648 1649 def GetBuildSetting(self, key): 1650 """Gets the build setting for key. 1651 1652 All child XCConfiguration objects must have the same value set for the 1653 setting, or a ValueError will be raised. 1654 """ 1655 1656 # TODO(mark): This is wrong for build settings that are lists. The list 1657 # contents should be compared (and a list copy returned?) 1658 1659 value = None 1660 for configuration in self._properties['buildConfigurations']: 1661 configuration_value = configuration.GetBuildSetting(key) 1662 if value is None: 1663 value = configuration_value 1664 else: 1665 if value != configuration_value: 1666 raise ValueError, 'Variant values for ' + key 1667 1668 return value 1669 1670 def SetBuildSetting(self, key, value): 1671 """Sets the build setting for key to value in all child 1672 XCBuildConfiguration objects. 1673 """ 1674 1675 for configuration in self._properties['buildConfigurations']: 1676 configuration.SetBuildSetting(key, value) 1677 1678 def AppendBuildSetting(self, key, value): 1679 """Appends value to the build setting for key, which is treated as a list, 1680 in all child XCBuildConfiguration objects. 1681 """ 1682 1683 for configuration in self._properties['buildConfigurations']: 1684 configuration.AppendBuildSetting(key, value) 1685 1686 def DelBuildSetting(self, key): 1687 """Deletes the build setting key from all child XCBuildConfiguration 1688 objects. 1689 """ 1690 1691 for configuration in self._properties['buildConfigurations']: 1692 configuration.DelBuildSetting(key) 1693 1694 def SetBaseConfiguration(self, value): 1695 """Sets the build configuration in all child XCBuildConfiguration objects. 1696 """ 1697 1698 for configuration in self._properties['buildConfigurations']: 1699 configuration.SetBaseConfiguration(value) 1700 1701 1702 class PBXBuildFile(XCObject): 1703 _schema = XCObject._schema.copy() 1704 _schema.update({ 1705 'fileRef': [0, XCFileLikeElement, 0, 1], 1706 'settings': [0, str, 0, 0], # hack, it's a dict 1707 }) 1708 1709 # Weird output rules for PBXBuildFile. 1710 _should_print_single_line = True 1711 _encode_transforms = XCObject._alternate_encode_transforms 1712 1713 def Name(self): 1714 # Example: "main.cc in Sources" 1715 return self._properties['fileRef'].Name() + ' in ' + self.parent.Name() 1716 1717 def Hashables(self): 1718 # super 1719 hashables = XCObject.Hashables(self) 1720 1721 # It is not sufficient to just rely on Name() to get the 1722 # XCFileLikeElement's name, because that is not a complete pathname. 1723 # PathHashables returns hashables unique enough that no two 1724 # PBXBuildFiles should wind up with the same set of hashables, unless 1725 # someone adds the same file multiple times to the same target. That 1726 # would be considered invalid anyway. 1727 hashables.extend(self._properties['fileRef'].PathHashables()) 1728 1729 return hashables 1730 1731 1732 class XCBuildPhase(XCObject): 1733 """Abstract base for build phase classes. Not represented in a project 1734 file. 1735 1736 Attributes: 1737 _files_by_path: A dict mapping each path of a child in the files list by 1738 path (keys) to the corresponding PBXBuildFile children (values). 1739 _files_by_xcfilelikeelement: A dict mapping each XCFileLikeElement (keys) 1740 to the corresponding PBXBuildFile children (values). 1741 """ 1742 1743 # TODO(mark): Some build phase types, like PBXShellScriptBuildPhase, don't 1744 # actually have a "files" list. XCBuildPhase should not have "files" but 1745 # another abstract subclass of it should provide this, and concrete build 1746 # phase types that do have "files" lists should be derived from that new 1747 # abstract subclass. XCBuildPhase should only provide buildActionMask and 1748 # runOnlyForDeploymentPostprocessing, and not files or the various 1749 # file-related methods and attributes. 1750 1751 _schema = XCObject._schema.copy() 1752 _schema.update({ 1753 'buildActionMask': [0, int, 0, 1, 0x7fffffff], 1754 'files': [1, PBXBuildFile, 1, 1, []], 1755 'runOnlyForDeploymentPostprocessing': [0, int, 0, 1, 0], 1756 }) 1757 1758 def __init__(self, properties=None, id=None, parent=None): 1759 # super 1760 XCObject.__init__(self, properties, id, parent) 1761 1762 self._files_by_path = {} 1763 self._files_by_xcfilelikeelement = {} 1764 for pbxbuildfile in self._properties.get('files', []): 1765 self._AddBuildFileToDicts(pbxbuildfile) 1766 1767 def FileGroup(self, path): 1768 # Subclasses must override this by returning a two-element tuple. The 1769 # first item in the tuple should be the PBXGroup to which "path" should be 1770 # added, either as a child or deeper descendant. The second item should 1771 # be a boolean indicating whether files should be added into hierarchical 1772 # groups or one single flat group. 1773 raise NotImplementedError, \ 1774 self.__class__.__name__ + ' must implement FileGroup' 1775 1776 def _AddPathToDict(self, pbxbuildfile, path): 1777 """Adds path to the dict tracking paths belonging to this build phase. 1778 1779 If the path is already a member of this build phase, raises an exception. 1780 """ 1781 1782 if path in self._files_by_path: 1783 raise ValueError, 'Found multiple build files with path ' + path 1784 self._files_by_path[path] = pbxbuildfile 1785 1786 def _AddBuildFileToDicts(self, pbxbuildfile, path=None): 1787 """Maintains the _files_by_path and _files_by_xcfilelikeelement dicts. 1788 1789 If path is specified, then it is the path that is being added to the 1790 phase, and pbxbuildfile must contain either a PBXFileReference directly 1791 referencing that path, or it must contain a PBXVariantGroup that itself 1792 contains a PBXFileReference referencing the path. 1793 1794 If path is not specified, either the PBXFileReference's path or the paths 1795 of all children of the PBXVariantGroup are taken as being added to the 1796 phase. 1797 1798 If the path is already present in the phase, raises an exception. 1799 1800 If the PBXFileReference or PBXVariantGroup referenced by pbxbuildfile 1801 are already present in the phase, referenced by a different PBXBuildFile 1802 object, raises an exception. This does not raise an exception when 1803 a PBXFileReference or PBXVariantGroup reappear and are referenced by the 1804 same PBXBuildFile that has already introduced them, because in the case 1805 of PBXVariantGroup objects, they may correspond to multiple paths that are 1806 not all added simultaneously. When this situation occurs, the path needs 1807 to be added to _files_by_path, but nothing needs to change in 1808 _files_by_xcfilelikeelement, and the caller should have avoided adding 1809 the PBXBuildFile if it is already present in the list of children. 1810 """ 1811 1812 xcfilelikeelement = pbxbuildfile._properties['fileRef'] 1813 1814 paths = [] 1815 if path != None: 1816 # It's best when the caller provides the path. 1817 if isinstance(xcfilelikeelement, PBXVariantGroup): 1818 paths.append(path) 1819 else: 1820 # If the caller didn't provide a path, there can be either multiple 1821 # paths (PBXVariantGroup) or one. 1822 if isinstance(xcfilelikeelement, PBXVariantGroup): 1823 for variant in xcfilelikeelement._properties['children']: 1824 paths.append(variant.FullPath()) 1825 else: 1826 paths.append(xcfilelikeelement.FullPath()) 1827 1828 # Add the paths first, because if something's going to raise, the 1829 # messages provided by _AddPathToDict are more useful owing to its 1830 # having access to a real pathname and not just an object's Name(). 1831 for a_path in paths: 1832 self._AddPathToDict(pbxbuildfile, a_path) 1833 1834 # If another PBXBuildFile references this XCFileLikeElement, there's a 1835 # problem. 1836 if xcfilelikeelement in self._files_by_xcfilelikeelement and \ 1837 self._files_by_xcfilelikeelement[xcfilelikeelement] != pbxbuildfile: 1838 raise ValueError, 'Found multiple build files for ' + \ 1839 xcfilelikeelement.Name() 1840 self._files_by_xcfilelikeelement[xcfilelikeelement] = pbxbuildfile 1841 1842 def AppendBuildFile(self, pbxbuildfile, path=None): 1843 # Callers should use this instead of calling 1844 # AppendProperty('files', pbxbuildfile) directly because this function 1845 # maintains the object's dicts. Better yet, callers can just call AddFile 1846 # with a pathname and not worry about building their own PBXBuildFile 1847 # objects. 1848 self.AppendProperty('files', pbxbuildfile) 1849 self._AddBuildFileToDicts(pbxbuildfile, path) 1850 1851 def AddFile(self, path, settings=None): 1852 (file_group, hierarchical) = self.FileGroup(path) 1853 file_ref = file_group.AddOrGetFileByPath(path, hierarchical) 1854 1855 if file_ref in self._files_by_xcfilelikeelement and \ 1856 isinstance(file_ref, PBXVariantGroup): 1857 # There's already a PBXBuildFile in this phase corresponding to the 1858 # PBXVariantGroup. path just provides a new variant that belongs to 1859 # the group. Add the path to the dict. 1860 pbxbuildfile = self._files_by_xcfilelikeelement[file_ref] 1861 self._AddBuildFileToDicts(pbxbuildfile, path) 1862 else: 1863 # Add a new PBXBuildFile to get file_ref into the phase. 1864 if settings is None: 1865 pbxbuildfile = PBXBuildFile({'fileRef': file_ref}) 1866 else: 1867 pbxbuildfile = PBXBuildFile({'fileRef': file_ref, 'settings': settings}) 1868 self.AppendBuildFile(pbxbuildfile, path) 1869 1870 1871 class PBXHeadersBuildPhase(XCBuildPhase): 1872 # No additions to the schema relative to XCBuildPhase. 1873 1874 def Name(self): 1875 return 'Headers' 1876 1877 def FileGroup(self, path): 1878 return self.PBXProjectAncestor().RootGroupForPath(path) 1879 1880 1881 class PBXResourcesBuildPhase(XCBuildPhase): 1882 # No additions to the schema relative to XCBuildPhase. 1883 1884 def Name(self): 1885 return 'Resources' 1886 1887 def FileGroup(self, path): 1888 return self.PBXProjectAncestor().RootGroupForPath(path) 1889 1890 1891 class PBXSourcesBuildPhase(XCBuildPhase): 1892 # No additions to the schema relative to XCBuildPhase. 1893 1894 def Name(self): 1895 return 'Sources' 1896 1897 def FileGroup(self, path): 1898 return self.PBXProjectAncestor().RootGroupForPath(path) 1899 1900 1901 class PBXFrameworksBuildPhase(XCBuildPhase): 1902 # No additions to the schema relative to XCBuildPhase. 1903 1904 def Name(self): 1905 return 'Frameworks' 1906 1907 def FileGroup(self, path): 1908 (root, ext) = posixpath.splitext(path) 1909 if ext != '': 1910 ext = ext[1:].lower() 1911 if ext == 'o': 1912 # .o files are added to Xcode Frameworks phases, but conceptually aren't 1913 # frameworks, they're more like sources or intermediates. Redirect them 1914 # to show up in one of those other groups. 1915 return self.PBXProjectAncestor().RootGroupForPath(path) 1916 else: 1917 return (self.PBXProjectAncestor().FrameworksGroup(), False) 1918 1919 1920 class PBXShellScriptBuildPhase(XCBuildPhase): 1921 _schema = XCBuildPhase._schema.copy() 1922 _schema.update({ 1923 'inputPaths': [1, str, 0, 1, []], 1924 'name': [0, str, 0, 0], 1925 'outputPaths': [1, str, 0, 1, []], 1926 'shellPath': [0, str, 0, 1, '/bin/sh'], 1927 'shellScript': [0, str, 0, 1], 1928 'showEnvVarsInLog': [0, int, 0, 0], 1929 }) 1930 1931 def Name(self): 1932 if 'name' in self._properties: 1933 return self._properties['name'] 1934 1935 return 'ShellScript' 1936 1937 1938 class PBXCopyFilesBuildPhase(XCBuildPhase): 1939 _schema = XCBuildPhase._schema.copy() 1940 _schema.update({ 1941 'dstPath': [0, str, 0, 1], 1942 'dstSubfolderSpec': [0, int, 0, 1], 1943 'name': [0, str, 0, 0], 1944 }) 1945 1946 # path_tree_re matches "$(DIR)/path" or just "$(DIR)". Match group 1 is 1947 # "DIR", match group 3 is "path" or None. 1948 path_tree_re = re.compile('^\\$\\((.*)\\)(/(.*)|)$') 1949 1950 # path_tree_to_subfolder maps names of Xcode variables to the associated 1951 # dstSubfolderSpec property value used in a PBXCopyFilesBuildPhase object. 1952 path_tree_to_subfolder = { 1953 'BUILT_PRODUCTS_DIR': 16, # Products Directory 1954 # Other types that can be chosen via the Xcode UI. 1955 # TODO(mark): Map Xcode variable names to these. 1956 # : 1, # Wrapper 1957 # : 6, # Executables: 6 1958 # : 7, # Resources 1959 # : 15, # Java Resources 1960 # : 10, # Frameworks 1961 # : 11, # Shared Frameworks 1962 # : 12, # Shared Support 1963 # : 13, # PlugIns 1964 } 1965 1966 def Name(self): 1967 if 'name' in self._properties: 1968 return self._properties['name'] 1969 1970 return 'CopyFiles' 1971 1972 def FileGroup(self, path): 1973 return self.PBXProjectAncestor().RootGroupForPath(path) 1974 1975 def SetDestination(self, path): 1976 """Set the dstSubfolderSpec and dstPath properties from path. 1977 1978 path may be specified in the same notation used for XCHierarchicalElements, 1979 specifically, "$(DIR)/path". 1980 """ 1981 1982 path_tree_match = self.path_tree_re.search(path) 1983 if path_tree_match: 1984 # Everything else needs to be relative to an Xcode variable. 1985 path_tree = path_tree_match.group(1) 1986 relative_path = path_tree_match.group(3) 1987 1988 if path_tree in self.path_tree_to_subfolder: 1989 subfolder = self.path_tree_to_subfolder[path_tree] 1990 if relative_path is None: 1991 relative_path = '' 1992 else: 1993 # The path starts with an unrecognized Xcode variable 1994 # name like $(SRCROOT). Xcode will still handle this 1995 # as an "absolute path" that starts with the variable. 1996 subfolder = 0 1997 relative_path = path 1998 elif path.startswith('/'): 1999 # Special case. Absolute paths are in dstSubfolderSpec 0. 2000 subfolder = 0 2001 relative_path = path[1:] 2002 else: 2003 raise ValueError, 'Can\'t use path %s in a %s' % \ 2004 (path, self.__class__.__name__) 2005 2006 self._properties['dstPath'] = relative_path 2007 self._properties['dstSubfolderSpec'] = subfolder 2008 2009 2010 class PBXBuildRule(XCObject): 2011 _schema = XCObject._schema.copy() 2012 _schema.update({ 2013 'compilerSpec': [0, str, 0, 1], 2014 'filePatterns': [0, str, 0, 0], 2015 'fileType': [0, str, 0, 1], 2016 'isEditable': [0, int, 0, 1, 1], 2017 'outputFiles': [1, str, 0, 1, []], 2018 'script': [0, str, 0, 0], 2019 }) 2020 2021 def Name(self): 2022 # Not very inspired, but it's what Xcode uses. 2023 return self.__class__.__name__ 2024 2025 def Hashables(self): 2026 # super 2027 hashables = XCObject.Hashables(self) 2028 2029 # Use the hashables of the weak objects that this object refers to. 2030 hashables.append(self._properties['fileType']) 2031 if 'filePatterns' in self._properties: 2032 hashables.append(self._properties['filePatterns']) 2033 return hashables 2034 2035 2036 class PBXContainerItemProxy(XCObject): 2037 # When referencing an item in this project file, containerPortal is the 2038 # PBXProject root object of this project file. When referencing an item in 2039 # another project file, containerPortal is a PBXFileReference identifying 2040 # the other project file. 2041 # 2042 # When serving as a proxy to an XCTarget (in this project file or another), 2043 # proxyType is 1. When serving as a proxy to a PBXFileReference (in another 2044 # project file), proxyType is 2. Type 2 is used for references to the 2045 # producs of the other project file's targets. 2046 # 2047 # Xcode is weird about remoteGlobalIDString. Usually, it's printed without 2048 # a comment, indicating that it's tracked internally simply as a string, but 2049 # sometimes it's printed with a comment (usually when the object is initially 2050 # created), indicating that it's tracked as a project file object at least 2051 # sometimes. This module always tracks it as an object, but contains a hack 2052 # to prevent it from printing the comment in the project file output. See 2053 # _XCKVPrint. 2054 _schema = XCObject._schema.copy() 2055 _schema.update({ 2056 'containerPortal': [0, XCContainerPortal, 0, 1], 2057 'proxyType': [0, int, 0, 1], 2058 'remoteGlobalIDString': [0, XCRemoteObject, 0, 1], 2059 'remoteInfo': [0, str, 0, 1], 2060 }) 2061 2062 def __repr__(self): 2063 props = self._properties 2064 name = '%s.gyp:%s' % (props['containerPortal'].Name(), props['remoteInfo']) 2065 return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self)) 2066 2067 def Name(self): 2068 # Admittedly not the best name, but it's what Xcode uses. 2069 return self.__class__.__name__ 2070 2071 def Hashables(self): 2072 # super 2073 hashables = XCObject.Hashables(self) 2074 2075 # Use the hashables of the weak objects that this object refers to. 2076 hashables.extend(self._properties['containerPortal'].Hashables()) 2077 hashables.extend(self._properties['remoteGlobalIDString'].Hashables()) 2078 return hashables 2079 2080 2081 class PBXTargetDependency(XCObject): 2082 # The "target" property accepts an XCTarget object, and obviously not 2083 # NoneType. But XCTarget is defined below, so it can't be put into the 2084 # schema yet. The definition of PBXTargetDependency can't be moved below 2085 # XCTarget because XCTarget's own schema references PBXTargetDependency. 2086 # Python doesn't deal well with this circular relationship, and doesn't have 2087 # a real way to do forward declarations. To work around, the type of 2088 # the "target" property is reset below, after XCTarget is defined. 2089 # 2090 # At least one of "name" and "target" is required. 2091 _schema = XCObject._schema.copy() 2092 _schema.update({ 2093 'name': [0, str, 0, 0], 2094 'target': [0, None.__class__, 0, 0], 2095 'targetProxy': [0, PBXContainerItemProxy, 1, 1], 2096 }) 2097 2098 def __repr__(self): 2099 name = self._properties.get('name') or self._properties['target'].Name() 2100 return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self)) 2101 2102 def Name(self): 2103 # Admittedly not the best name, but it's what Xcode uses. 2104 return self.__class__.__name__ 2105 2106 def Hashables(self): 2107 # super 2108 hashables = XCObject.Hashables(self) 2109 2110 # Use the hashables of the weak objects that this object refers to. 2111 hashables.extend(self._properties['targetProxy'].Hashables()) 2112 return hashables 2113 2114 2115 class PBXReferenceProxy(XCFileLikeElement): 2116 _schema = XCFileLikeElement._schema.copy() 2117 _schema.update({ 2118 'fileType': [0, str, 0, 1], 2119 'path': [0, str, 0, 1], 2120 'remoteRef': [0, PBXContainerItemProxy, 1, 1], 2121 }) 2122 2123 2124 class XCTarget(XCRemoteObject): 2125 # An XCTarget is really just an XCObject, the XCRemoteObject thing is just 2126 # to allow PBXProject to be used in the remoteGlobalIDString property of 2127 # PBXContainerItemProxy. 2128 # 2129 # Setting a "name" property at instantiation may also affect "productName", 2130 # which may in turn affect the "PRODUCT_NAME" build setting in children of 2131 # "buildConfigurationList". See __init__ below. 2132 _schema = XCRemoteObject._schema.copy() 2133 _schema.update({ 2134 'buildConfigurationList': [0, XCConfigurationList, 1, 1, 2135 XCConfigurationList()], 2136 'buildPhases': [1, XCBuildPhase, 1, 1, []], 2137 'dependencies': [1, PBXTargetDependency, 1, 1, []], 2138 'name': [0, str, 0, 1], 2139 'productName': [0, str, 0, 1], 2140 }) 2141 2142 def __init__(self, properties=None, id=None, parent=None, 2143 force_outdir=None, force_prefix=None, force_extension=None): 2144 # super 2145 XCRemoteObject.__init__(self, properties, id, parent) 2146 2147 # Set up additional defaults not expressed in the schema. If a "name" 2148 # property was supplied, set "productName" if it is not present. Also set 2149 # the "PRODUCT_NAME" build setting in each configuration, but only if 2150 # the setting is not present in any build configuration. 2151 if 'name' in self._properties: 2152 if not 'productName' in self._properties: 2153 self.SetProperty('productName', self._properties['name']) 2154 2155 if 'productName' in self._properties: 2156 if 'buildConfigurationList' in self._properties: 2157 configs = self._properties['buildConfigurationList'] 2158 if configs.HasBuildSetting('PRODUCT_NAME') == 0: 2159 configs.SetBuildSetting('PRODUCT_NAME', 2160 self._properties['productName']) 2161 2162 def AddDependency(self, other): 2163 pbxproject = self.PBXProjectAncestor() 2164 other_pbxproject = other.PBXProjectAncestor() 2165 if pbxproject == other_pbxproject: 2166 # Add a dependency to another target in the same project file. 2167 container = PBXContainerItemProxy({'containerPortal': pbxproject, 2168 'proxyType': 1, 2169 'remoteGlobalIDString': other, 2170 'remoteInfo': other.Name()}) 2171 dependency = PBXTargetDependency({'target': other, 2172 'targetProxy': container}) 2173 self.AppendProperty('dependencies', dependency) 2174 else: 2175 # Add a dependency to a target in a different project file. 2176 other_project_ref = \ 2177 pbxproject.AddOrGetProjectReference(other_pbxproject)[1] 2178 container = PBXContainerItemProxy({ 2179 'containerPortal': other_project_ref, 2180 'proxyType': 1, 2181 'remoteGlobalIDString': other, 2182 'remoteInfo': other.Name(), 2183 }) 2184 dependency = PBXTargetDependency({'name': other.Name(), 2185 'targetProxy': container}) 2186 self.AppendProperty('dependencies', dependency) 2187 2188 # Proxy all of these through to the build configuration list. 2189 2190 def ConfigurationNamed(self, name): 2191 return self._properties['buildConfigurationList'].ConfigurationNamed(name) 2192 2193 def DefaultConfiguration(self): 2194 return self._properties['buildConfigurationList'].DefaultConfiguration() 2195 2196 def HasBuildSetting(self, key): 2197 return self._properties['buildConfigurationList'].HasBuildSetting(key) 2198 2199 def GetBuildSetting(self, key): 2200 return self._properties['buildConfigurationList'].GetBuildSetting(key) 2201 2202 def SetBuildSetting(self, key, value): 2203 return self._properties['buildConfigurationList'].SetBuildSetting(key, \ 2204 value) 2205 2206 def AppendBuildSetting(self, key, value): 2207 return self._properties['buildConfigurationList'].AppendBuildSetting(key, \ 2208 value) 2209 2210 def DelBuildSetting(self, key): 2211 return self._properties['buildConfigurationList'].DelBuildSetting(key) 2212 2213 2214 # Redefine the type of the "target" property. See PBXTargetDependency._schema 2215 # above. 2216 PBXTargetDependency._schema['target'][1] = XCTarget 2217 2218 2219 class PBXNativeTarget(XCTarget): 2220 # buildPhases is overridden in the schema to be able to set defaults. 2221 # 2222 # NOTE: Contrary to most objects, it is advisable to set parent when 2223 # constructing PBXNativeTarget. A parent of an XCTarget must be a PBXProject 2224 # object. A parent reference is required for a PBXNativeTarget during 2225 # construction to be able to set up the target defaults for productReference, 2226 # because a PBXBuildFile object must be created for the target and it must 2227 # be added to the PBXProject's mainGroup hierarchy. 2228 _schema = XCTarget._schema.copy() 2229 _schema.update({ 2230 'buildPhases': [1, XCBuildPhase, 1, 1, 2231 [PBXSourcesBuildPhase(), PBXFrameworksBuildPhase()]], 2232 'buildRules': [1, PBXBuildRule, 1, 1, []], 2233 'productReference': [0, PBXFileReference, 0, 1], 2234 'productType': [0, str, 0, 1], 2235 }) 2236 2237 # Mapping from Xcode product-types to settings. The settings are: 2238 # filetype : used for explicitFileType in the project file 2239 # prefix : the prefix for the file name 2240 # suffix : the suffix for the filen ame 2241 _product_filetypes = { 2242 'com.apple.product-type.application': ['wrapper.application', 2243 '', '.app'], 2244 'com.apple.product-type.bundle': ['wrapper.cfbundle', 2245 '', '.bundle'], 2246 'com.apple.product-type.framework': ['wrapper.framework', 2247 '', '.framework'], 2248 'com.apple.product-type.library.dynamic': ['compiled.mach-o.dylib', 2249 'lib', '.dylib'], 2250 'com.apple.product-type.library.static': ['archive.ar', 2251 'lib', '.a'], 2252 'com.apple.product-type.tool': ['compiled.mach-o.executable', 2253 '', ''], 2254 'com.apple.product-type.bundle.unit-test': ['wrapper.cfbundle', 2255 '', '.xctest'], 2256 'com.googlecode.gyp.xcode.bundle': ['compiled.mach-o.dylib', 2257 '', '.so'], 2258 } 2259 2260 def __init__(self, properties=None, id=None, parent=None, 2261 force_outdir=None, force_prefix=None, force_extension=None): 2262 # super 2263 XCTarget.__init__(self, properties, id, parent) 2264 2265 if 'productName' in self._properties and \ 2266 'productType' in self._properties and \ 2267 not 'productReference' in self._properties and \ 2268 self._properties['productType'] in self._product_filetypes: 2269 products_group = None 2270 pbxproject = self.PBXProjectAncestor() 2271 if pbxproject != None: 2272 products_group = pbxproject.ProductsGroup() 2273 2274 if products_group != None: 2275 (filetype, prefix, suffix) = \ 2276 self._product_filetypes[self._properties['productType']] 2277 # Xcode does not have a distinct type for loadable modules that are 2278 # pure BSD targets (not in a bundle wrapper). GYP allows such modules 2279 # to be specified by setting a target type to loadable_module without 2280 # having mac_bundle set. These are mapped to the pseudo-product type 2281 # com.googlecode.gyp.xcode.bundle. 2282 # 2283 # By picking up this special type and converting it to a dynamic 2284 # library (com.apple.product-type.library.dynamic) with fix-ups, 2285 # single-file loadable modules can be produced. 2286 # 2287 # MACH_O_TYPE is changed to mh_bundle to produce the proper file type 2288 # (as opposed to mh_dylib). In order for linking to succeed, 2289 # DYLIB_CURRENT_VERSION and DYLIB_COMPATIBILITY_VERSION must be 2290 # cleared. They are meaningless for type mh_bundle. 2291 # 2292 # Finally, the .so extension is forcibly applied over the default 2293 # (.dylib), unless another forced extension is already selected. 2294 # .dylib is plainly wrong, and .bundle is used by loadable_modules in 2295 # bundle wrappers (com.apple.product-type.bundle). .so seems an odd 2296 # choice because it's used as the extension on many other systems that 2297 # don't distinguish between linkable shared libraries and non-linkable 2298 # loadable modules, but there's precedent: Python loadable modules on 2299 # Mac OS X use an .so extension. 2300 if self._properties['productType'] == 'com.googlecode.gyp.xcode.bundle': 2301 self._properties['productType'] = \ 2302 'com.apple.product-type.library.dynamic' 2303 self.SetBuildSetting('MACH_O_TYPE', 'mh_bundle') 2304 self.SetBuildSetting('DYLIB_CURRENT_VERSION', '') 2305 self.SetBuildSetting('DYLIB_COMPATIBILITY_VERSION', '') 2306 if force_extension is None: 2307 force_extension = suffix[1:] 2308 2309 if self._properties['productType'] == \ 2310 'com.apple.product-type-bundle.unit.test': 2311 if force_extension is None: 2312 force_extension = suffix[1:] 2313 2314 if force_extension is not None: 2315 # If it's a wrapper (bundle), set WRAPPER_EXTENSION. 2316 if filetype.startswith('wrapper.'): 2317 self.SetBuildSetting('WRAPPER_EXTENSION', force_extension) 2318 else: 2319 # Extension override. 2320 suffix = '.' + force_extension 2321 self.SetBuildSetting('EXECUTABLE_EXTENSION', force_extension) 2322 2323 if filetype.startswith('compiled.mach-o.executable'): 2324 product_name = self._properties['productName'] 2325 product_name += suffix 2326 suffix = '' 2327 self.SetProperty('productName', product_name) 2328 self.SetBuildSetting('PRODUCT_NAME', product_name) 2329 2330 # Xcode handles most prefixes based on the target type, however there 2331 # are exceptions. If a "BSD Dynamic Library" target is added in the 2332 # Xcode UI, Xcode sets EXECUTABLE_PREFIX. This check duplicates that 2333 # behavior. 2334 if force_prefix is not None: 2335 prefix = force_prefix 2336 if filetype.startswith('wrapper.'): 2337 self.SetBuildSetting('WRAPPER_PREFIX', prefix) 2338 else: 2339 self.SetBuildSetting('EXECUTABLE_PREFIX', prefix) 2340 2341 if force_outdir is not None: 2342 self.SetBuildSetting('TARGET_BUILD_DIR', force_outdir) 2343 2344 # TODO(tvl): Remove the below hack. 2345 # http://code.google.com/p/gyp/issues/detail?id=122 2346 2347 # Some targets include the prefix in the target_name. These targets 2348 # really should just add a product_name setting that doesn't include 2349 # the prefix. For example: 2350 # target_name = 'libevent', product_name = 'event' 2351 # This check cleans up for them. 2352 product_name = self._properties['productName'] 2353 prefix_len = len(prefix) 2354 if prefix_len and (product_name[:prefix_len] == prefix): 2355 product_name = product_name[prefix_len:] 2356 self.SetProperty('productName', product_name) 2357 self.SetBuildSetting('PRODUCT_NAME', product_name) 2358 2359 ref_props = { 2360 'explicitFileType': filetype, 2361 'includeInIndex': 0, 2362 'path': prefix + product_name + suffix, 2363 'sourceTree': 'BUILT_PRODUCTS_DIR', 2364 } 2365 file_ref = PBXFileReference(ref_props) 2366 products_group.AppendChild(file_ref) 2367 self.SetProperty('productReference', file_ref) 2368 2369 def GetBuildPhaseByType(self, type): 2370 if not 'buildPhases' in self._properties: 2371 return None 2372 2373 the_phase = None 2374 for phase in self._properties['buildPhases']: 2375 if isinstance(phase, type): 2376 # Some phases may be present in multiples in a well-formed project file, 2377 # but phases like PBXSourcesBuildPhase may only be present singly, and 2378 # this function is intended as an aid to GetBuildPhaseByType. Loop 2379 # over the entire list of phases and assert if more than one of the 2380 # desired type is found. 2381 assert the_phase is None 2382 the_phase = phase 2383 2384 return the_phase 2385 2386 def HeadersPhase(self): 2387 headers_phase = self.GetBuildPhaseByType(PBXHeadersBuildPhase) 2388 if headers_phase is None: 2389 headers_phase = PBXHeadersBuildPhase() 2390 2391 # The headers phase should come before the resources, sources, and 2392 # frameworks phases, if any. 2393 insert_at = len(self._properties['buildPhases']) 2394 for index in xrange(0, len(self._properties['buildPhases'])): 2395 phase = self._properties['buildPhases'][index] 2396 if isinstance(phase, PBXResourcesBuildPhase) or \ 2397 isinstance(phase, PBXSourcesBuildPhase) or \ 2398 isinstance(phase, PBXFrameworksBuildPhase): 2399 insert_at = index 2400 break 2401 2402 self._properties['buildPhases'].insert(insert_at, headers_phase) 2403 headers_phase.parent = self 2404 2405 return headers_phase 2406 2407 def ResourcesPhase(self): 2408 resources_phase = self.GetBuildPhaseByType(PBXResourcesBuildPhase) 2409 if resources_phase is None: 2410 resources_phase = PBXResourcesBuildPhase() 2411 2412 # The resources phase should come before the sources and frameworks 2413 # phases, if any. 2414 insert_at = len(self._properties['buildPhases']) 2415 for index in xrange(0, len(self._properties['buildPhases'])): 2416 phase = self._properties['buildPhases'][index] 2417 if isinstance(phase, PBXSourcesBuildPhase) or \ 2418 isinstance(phase, PBXFrameworksBuildPhase): 2419 insert_at = index 2420 break 2421 2422 self._properties['buildPhases'].insert(insert_at, resources_phase) 2423 resources_phase.parent = self 2424 2425 return resources_phase 2426 2427 def SourcesPhase(self): 2428 sources_phase = self.GetBuildPhaseByType(PBXSourcesBuildPhase) 2429 if sources_phase is None: 2430 sources_phase = PBXSourcesBuildPhase() 2431 self.AppendProperty('buildPhases', sources_phase) 2432 2433 return sources_phase 2434 2435 def FrameworksPhase(self): 2436 frameworks_phase = self.GetBuildPhaseByType(PBXFrameworksBuildPhase) 2437 if frameworks_phase is None: 2438 frameworks_phase = PBXFrameworksBuildPhase() 2439 self.AppendProperty('buildPhases', frameworks_phase) 2440 2441 return frameworks_phase 2442 2443 def AddDependency(self, other): 2444 # super 2445 XCTarget.AddDependency(self, other) 2446 2447 static_library_type = 'com.apple.product-type.library.static' 2448 shared_library_type = 'com.apple.product-type.library.dynamic' 2449 framework_type = 'com.apple.product-type.framework' 2450 if isinstance(other, PBXNativeTarget) and \ 2451 'productType' in self._properties and \ 2452 self._properties['productType'] != static_library_type and \ 2453 'productType' in other._properties and \ 2454 (other._properties['productType'] == static_library_type or \ 2455 ((other._properties['productType'] == shared_library_type or \ 2456 other._properties['productType'] == framework_type) and \ 2457 ((not other.HasBuildSetting('MACH_O_TYPE')) or 2458 other.GetBuildSetting('MACH_O_TYPE') != 'mh_bundle'))): 2459 2460 file_ref = other.GetProperty('productReference') 2461 2462 pbxproject = self.PBXProjectAncestor() 2463 other_pbxproject = other.PBXProjectAncestor() 2464 if pbxproject != other_pbxproject: 2465 other_project_product_group = \ 2466 pbxproject.AddOrGetProjectReference(other_pbxproject)[0] 2467 file_ref = other_project_product_group.GetChildByRemoteObject(file_ref) 2468 2469 self.FrameworksPhase().AppendProperty('files', 2470 PBXBuildFile({'fileRef': file_ref})) 2471 2472 2473 class PBXAggregateTarget(XCTarget): 2474 pass 2475 2476 2477 class PBXProject(XCContainerPortal): 2478 # A PBXProject is really just an XCObject, the XCContainerPortal thing is 2479 # just to allow PBXProject to be used in the containerPortal property of 2480 # PBXContainerItemProxy. 2481 """ 2482 2483 Attributes: 2484 path: "sample.xcodeproj". TODO(mark) Document me! 2485 _other_pbxprojects: A dictionary, keyed by other PBXProject objects. Each 2486 value is a reference to the dict in the 2487 projectReferences list associated with the keyed 2488 PBXProject. 2489 """ 2490 2491 _schema = XCContainerPortal._schema.copy() 2492 _schema.update({ 2493 'attributes': [0, dict, 0, 0], 2494 'buildConfigurationList': [0, XCConfigurationList, 1, 1, 2495 XCConfigurationList()], 2496 'compatibilityVersion': [0, str, 0, 1, 'Xcode 3.2'], 2497 'hasScannedForEncodings': [0, int, 0, 1, 1], 2498 'mainGroup': [0, PBXGroup, 1, 1, PBXGroup()], 2499 'projectDirPath': [0, str, 0, 1, ''], 2500 'projectReferences': [1, dict, 0, 0], 2501 'projectRoot': [0, str, 0, 1, ''], 2502 'targets': [1, XCTarget, 1, 1, []], 2503 }) 2504 2505 def __init__(self, properties=None, id=None, parent=None, path=None): 2506 self.path = path 2507 self._other_pbxprojects = {} 2508 # super 2509 return XCContainerPortal.__init__(self, properties, id, parent) 2510 2511 def Name(self): 2512 name = self.path 2513 if name[-10:] == '.xcodeproj': 2514 name = name[:-10] 2515 return posixpath.basename(name) 2516 2517 def Path(self): 2518 return self.path 2519 2520 def Comment(self): 2521 return 'Project object' 2522 2523 def Children(self): 2524 # super 2525 children = XCContainerPortal.Children(self) 2526 2527 # Add children that the schema doesn't know about. Maybe there's a more 2528 # elegant way around this, but this is the only case where we need to own 2529 # objects in a dictionary (that is itself in a list), and three lines for 2530 # a one-off isn't that big a deal. 2531 if 'projectReferences' in self._properties: 2532 for reference in self._properties['projectReferences']: 2533 children.append(reference['ProductGroup']) 2534 2535 return children 2536 2537 def PBXProjectAncestor(self): 2538 return self 2539 2540 def _GroupByName(self, name): 2541 if not 'mainGroup' in self._properties: 2542 self.SetProperty('mainGroup', PBXGroup()) 2543 2544 main_group = self._properties['mainGroup'] 2545 group = main_group.GetChildByName(name) 2546 if group is None: 2547 group = PBXGroup({'name': name}) 2548 main_group.AppendChild(group) 2549 2550 return group 2551 2552 # SourceGroup and ProductsGroup are created by default in Xcode's own 2553 # templates. 2554 def SourceGroup(self): 2555 return self._GroupByName('Source') 2556 2557 def ProductsGroup(self): 2558 return self._GroupByName('Products') 2559 2560 # IntermediatesGroup is used to collect source-like files that are generated 2561 # by rules or script phases and are placed in intermediate directories such 2562 # as DerivedSources. 2563 def IntermediatesGroup(self): 2564 return self._GroupByName('Intermediates') 2565 2566 # FrameworksGroup and ProjectsGroup are top-level groups used to collect 2567 # frameworks and projects. 2568 def FrameworksGroup(self): 2569 return self._GroupByName('Frameworks') 2570 2571 def ProjectsGroup(self): 2572 return self._GroupByName('Projects') 2573 2574 def RootGroupForPath(self, path): 2575 """Returns a PBXGroup child of this object to which path should be added. 2576 2577 This method is intended to choose between SourceGroup and 2578 IntermediatesGroup on the basis of whether path is present in a source 2579 directory or an intermediates directory. For the purposes of this 2580 determination, any path located within a derived file directory such as 2581 PROJECT_DERIVED_FILE_DIR is treated as being in an intermediates 2582 directory. 2583 2584 The returned value is a two-element tuple. The first element is the 2585 PBXGroup, and the second element specifies whether that group should be 2586 organized hierarchically (True) or as a single flat list (False). 2587 """ 2588 2589 # TODO(mark): make this a class variable and bind to self on call? 2590 # Also, this list is nowhere near exhaustive. 2591 # INTERMEDIATE_DIR and SHARED_INTERMEDIATE_DIR are used by 2592 # gyp.generator.xcode. There should probably be some way for that module 2593 # to push the names in, rather than having to hard-code them here. 2594 source_tree_groups = { 2595 'DERIVED_FILE_DIR': (self.IntermediatesGroup, True), 2596 'INTERMEDIATE_DIR': (self.IntermediatesGroup, True), 2597 'PROJECT_DERIVED_FILE_DIR': (self.IntermediatesGroup, True), 2598 'SHARED_INTERMEDIATE_DIR': (self.IntermediatesGroup, True), 2599 } 2600 2601 (source_tree, path) = SourceTreeAndPathFromPath(path) 2602 if source_tree != None and source_tree in source_tree_groups: 2603 (group_func, hierarchical) = source_tree_groups[source_tree] 2604 group = group_func() 2605 return (group, hierarchical) 2606 2607 # TODO(mark): make additional choices based on file extension. 2608 2609 return (self.SourceGroup(), True) 2610 2611 def AddOrGetFileInRootGroup(self, path): 2612 """Returns a PBXFileReference corresponding to path in the correct group 2613 according to RootGroupForPath's heuristics. 2614 2615 If an existing PBXFileReference for path exists, it will be returned. 2616 Otherwise, one will be created and returned. 2617 """ 2618 2619 (group, hierarchical) = self.RootGroupForPath(path) 2620 return group.AddOrGetFileByPath(path, hierarchical) 2621 2622 def RootGroupsTakeOverOnlyChildren(self, recurse=False): 2623 """Calls TakeOverOnlyChild for all groups in the main group.""" 2624 2625 for group in self._properties['mainGroup']._properties['children']: 2626 if isinstance(group, PBXGroup): 2627 group.TakeOverOnlyChild(recurse) 2628 2629 def SortGroups(self): 2630 # Sort the children of the mainGroup (like "Source" and "Products") 2631 # according to their defined order. 2632 self._properties['mainGroup']._properties['children'] = \ 2633 sorted(self._properties['mainGroup']._properties['children'], 2634 cmp=lambda x,y: x.CompareRootGroup(y)) 2635 2636 # Sort everything else by putting group before files, and going 2637 # alphabetically by name within sections of groups and files. SortGroup 2638 # is recursive. 2639 for group in self._properties['mainGroup']._properties['children']: 2640 if not isinstance(group, PBXGroup): 2641 continue 2642 2643 if group.Name() == 'Products': 2644 # The Products group is a special case. Instead of sorting 2645 # alphabetically, sort things in the order of the targets that 2646 # produce the products. To do this, just build up a new list of 2647 # products based on the targets. 2648 products = [] 2649 for target in self._properties['targets']: 2650 if not isinstance(target, PBXNativeTarget): 2651 continue 2652 product = target._properties['productReference'] 2653 # Make sure that the product is already in the products group. 2654 assert product in group._properties['children'] 2655 products.append(product) 2656 2657 # Make sure that this process doesn't miss anything that was already 2658 # in the products group. 2659 assert len(products) == len(group._properties['children']) 2660 group._properties['children'] = products 2661 else: 2662 group.SortGroup() 2663 2664 def AddOrGetProjectReference(self, other_pbxproject): 2665 """Add a reference to another project file (via PBXProject object) to this 2666 one. 2667 2668 Returns [ProductGroup, ProjectRef]. ProductGroup is a PBXGroup object in 2669 this project file that contains a PBXReferenceProxy object for each 2670 product of each PBXNativeTarget in the other project file. ProjectRef is 2671 a PBXFileReference to the other project file. 2672 2673 If this project file already references the other project file, the 2674 existing ProductGroup and ProjectRef are returned. The ProductGroup will 2675 still be updated if necessary. 2676 """ 2677 2678 if not 'projectReferences' in self._properties: 2679 self._properties['projectReferences'] = [] 2680 2681 product_group = None 2682 project_ref = None 2683 2684 if not other_pbxproject in self._other_pbxprojects: 2685 # This project file isn't yet linked to the other one. Establish the 2686 # link. 2687 product_group = PBXGroup({'name': 'Products'}) 2688 2689 # ProductGroup is strong. 2690 product_group.parent = self 2691 2692 # There's nothing unique about this PBXGroup, and if left alone, it will 2693 # wind up with the same set of hashables as all other PBXGroup objects 2694 # owned by the projectReferences list. Add the hashables of the 2695 # remote PBXProject that it's related to. 2696 product_group._hashables.extend(other_pbxproject.Hashables()) 2697 2698 # The other project reports its path as relative to the same directory 2699 # that this project's path is relative to. The other project's path 2700 # is not necessarily already relative to this project. Figure out the 2701 # pathname that this project needs to use to refer to the other one. 2702 this_path = posixpath.dirname(self.Path()) 2703 projectDirPath = self.GetProperty('projectDirPath') 2704 if projectDirPath: 2705 if posixpath.isabs(projectDirPath[0]): 2706 this_path = projectDirPath 2707 else: 2708 this_path = posixpath.join(this_path, projectDirPath) 2709 other_path = gyp.common.RelativePath(other_pbxproject.Path(), this_path) 2710 2711 # ProjectRef is weak (it's owned by the mainGroup hierarchy). 2712 project_ref = PBXFileReference({ 2713 'lastKnownFileType': 'wrapper.pb-project', 2714 'path': other_path, 2715 'sourceTree': 'SOURCE_ROOT', 2716 }) 2717 self.ProjectsGroup().AppendChild(project_ref) 2718 2719 ref_dict = {'ProductGroup': product_group, 'ProjectRef': project_ref} 2720 self._other_pbxprojects[other_pbxproject] = ref_dict 2721 self.AppendProperty('projectReferences', ref_dict) 2722 2723 # Xcode seems to sort this list case-insensitively 2724 self._properties['projectReferences'] = \ 2725 sorted(self._properties['projectReferences'], cmp=lambda x,y: 2726 cmp(x['ProjectRef'].Name().lower(), 2727 y['ProjectRef'].Name().lower())) 2728 else: 2729 # The link already exists. Pull out the relevnt data. 2730 project_ref_dict = self._other_pbxprojects[other_pbxproject] 2731 product_group = project_ref_dict['ProductGroup'] 2732 project_ref = project_ref_dict['ProjectRef'] 2733 2734 self._SetUpProductReferences(other_pbxproject, product_group, project_ref) 2735 2736 return [product_group, project_ref] 2737 2738 def _SetUpProductReferences(self, other_pbxproject, product_group, 2739 project_ref): 2740 # TODO(mark): This only adds references to products in other_pbxproject 2741 # when they don't exist in this pbxproject. Perhaps it should also 2742 # remove references from this pbxproject that are no longer present in 2743 # other_pbxproject. Perhaps it should update various properties if they 2744 # change. 2745 for target in other_pbxproject._properties['targets']: 2746 if not isinstance(target, PBXNativeTarget): 2747 continue 2748 2749 other_fileref = target._properties['productReference'] 2750 if product_group.GetChildByRemoteObject(other_fileref) is None: 2751 # Xcode sets remoteInfo to the name of the target and not the name 2752 # of its product, despite this proxy being a reference to the product. 2753 container_item = PBXContainerItemProxy({ 2754 'containerPortal': project_ref, 2755 'proxyType': 2, 2756 'remoteGlobalIDString': other_fileref, 2757 'remoteInfo': target.Name() 2758 }) 2759 # TODO(mark): Does sourceTree get copied straight over from the other 2760 # project? Can the other project ever have lastKnownFileType here 2761 # instead of explicitFileType? (Use it if so?) Can path ever be 2762 # unset? (I don't think so.) Can other_fileref have name set, and 2763 # does it impact the PBXReferenceProxy if so? These are the questions 2764 # that perhaps will be answered one day. 2765 reference_proxy = PBXReferenceProxy({ 2766 'fileType': other_fileref._properties['explicitFileType'], 2767 'path': other_fileref._properties['path'], 2768 'sourceTree': other_fileref._properties['sourceTree'], 2769 'remoteRef': container_item, 2770 }) 2771 2772 product_group.AppendChild(reference_proxy) 2773 2774 def SortRemoteProductReferences(self): 2775 # For each remote project file, sort the associated ProductGroup in the 2776 # same order that the targets are sorted in the remote project file. This 2777 # is the sort order used by Xcode. 2778 2779 def CompareProducts(x, y, remote_products): 2780 # x and y are PBXReferenceProxy objects. Go through their associated 2781 # PBXContainerItem to get the remote PBXFileReference, which will be 2782 # present in the remote_products list. 2783 x_remote = x._properties['remoteRef']._properties['remoteGlobalIDString'] 2784 y_remote = y._properties['remoteRef']._properties['remoteGlobalIDString'] 2785 x_index = remote_products.index(x_remote) 2786 y_index = remote_products.index(y_remote) 2787 2788 # Use the order of each remote PBXFileReference in remote_products to 2789 # determine the sort order. 2790 return cmp(x_index, y_index) 2791 2792 for other_pbxproject, ref_dict in self._other_pbxprojects.iteritems(): 2793 # Build up a list of products in the remote project file, ordered the 2794 # same as the targets that produce them. 2795 remote_products = [] 2796 for target in other_pbxproject._properties['targets']: 2797 if not isinstance(target, PBXNativeTarget): 2798 continue 2799 remote_products.append(target._properties['productReference']) 2800 2801 # Sort the PBXReferenceProxy children according to the list of remote 2802 # products. 2803 product_group = ref_dict['ProductGroup'] 2804 product_group._properties['children'] = sorted( 2805 product_group._properties['children'], 2806 cmp=lambda x, y: CompareProducts(x, y, remote_products)) 2807 2808 2809 class XCProjectFile(XCObject): 2810 _schema = XCObject._schema.copy() 2811 _schema.update({ 2812 'archiveVersion': [0, int, 0, 1, 1], 2813 'classes': [0, dict, 0, 1, {}], 2814 'objectVersion': [0, int, 0, 1, 45], 2815 'rootObject': [0, PBXProject, 1, 1], 2816 }) 2817 2818 def SetXcodeVersion(self, version): 2819 version_to_object_version = { 2820 '2.4': 45, 2821 '3.0': 45, 2822 '3.1': 45, 2823 '3.2': 46, 2824 } 2825 if not version in version_to_object_version: 2826 supported_str = ', '.join(sorted(version_to_object_version.keys())) 2827 raise Exception( 2828 'Unsupported Xcode version %s (supported: %s)' % 2829 ( version, supported_str ) ) 2830 compatibility_version = 'Xcode %s' % version 2831 self._properties['rootObject'].SetProperty('compatibilityVersion', 2832 compatibility_version) 2833 self.SetProperty('objectVersion', version_to_object_version[version]); 2834 2835 def ComputeIDs(self, recursive=True, overwrite=True, hash=None): 2836 # Although XCProjectFile is implemented here as an XCObject, it's not a 2837 # proper object in the Xcode sense, and it certainly doesn't have its own 2838 # ID. Pass through an attempt to update IDs to the real root object. 2839 if recursive: 2840 self._properties['rootObject'].ComputeIDs(recursive, overwrite, hash) 2841 2842 def Print(self, file=sys.stdout): 2843 self.VerifyHasRequiredProperties() 2844 2845 # Add the special "objects" property, which will be caught and handled 2846 # separately during printing. This structure allows a fairly standard 2847 # loop do the normal printing. 2848 self._properties['objects'] = {} 2849 self._XCPrint(file, 0, '// !$*UTF8*$!\n') 2850 if self._should_print_single_line: 2851 self._XCPrint(file, 0, '{ ') 2852 else: 2853 self._XCPrint(file, 0, '{\n') 2854 for property, value in sorted(self._properties.iteritems(), 2855 cmp=lambda x, y: cmp(x, y)): 2856 if property == 'objects': 2857 self._PrintObjects(file) 2858 else: 2859 self._XCKVPrint(file, 1, property, value) 2860 self._XCPrint(file, 0, '}\n') 2861 del self._properties['objects'] 2862 2863 def _PrintObjects(self, file): 2864 if self._should_print_single_line: 2865 self._XCPrint(file, 0, 'objects = {') 2866 else: 2867 self._XCPrint(file, 1, 'objects = {\n') 2868 2869 objects_by_class = {} 2870 for object in self.Descendants(): 2871 if object == self: 2872 continue 2873 class_name = object.__class__.__name__ 2874 if not class_name in objects_by_class: 2875 objects_by_class[class_name] = [] 2876 objects_by_class[class_name].append(object) 2877 2878 for class_name in sorted(objects_by_class): 2879 self._XCPrint(file, 0, '\n') 2880 self._XCPrint(file, 0, '/* Begin ' + class_name + ' section */\n') 2881 for object in sorted(objects_by_class[class_name], 2882 cmp=lambda x, y: cmp(x.id, y.id)): 2883 object.Print(file) 2884 self._XCPrint(file, 0, '/* End ' + class_name + ' section */\n') 2885 2886 if self._should_print_single_line: 2887 self._XCPrint(file, 0, '}; ') 2888 else: 2889 self._XCPrint(file, 1, '};\n') 2890