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