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