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