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