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