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