• 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        'xcassets':    'folder.assetcatalog',
1513        'xcconfig':    'text.xcconfig',
1514        'xcdatamodel': 'wrapper.xcdatamodel',
1515        'xib':         'file.xib',
1516        'y':           'sourcecode.yacc',
1517      }
1518
1519      prop_map = {
1520        'dart':        'explicitFileType',
1521        'gyp':         'explicitFileType',
1522        'gypi':        'explicitFileType',
1523      }
1524
1525      if is_dir:
1526        file_type = 'folder'
1527        prop_name = 'lastKnownFileType'
1528      else:
1529        basename = posixpath.basename(self._properties['path'])
1530        (root, ext) = posixpath.splitext(basename)
1531        # Check the map using a lowercase extension.
1532        # TODO(mark): Maybe it should try with the original case first and fall
1533        # back to lowercase, in case there are any instances where case
1534        # matters.  There currently aren't.
1535        if ext != '':
1536          ext = ext[1:].lower()
1537
1538        # TODO(mark): "text" is the default value, but "file" is appropriate
1539        # for unrecognized files not containing text.  Xcode seems to choose
1540        # based on content.
1541        file_type = extension_map.get(ext, 'text')
1542        prop_name = prop_map.get(ext, 'lastKnownFileType')
1543
1544      self._properties[prop_name] = file_type
1545
1546
1547class PBXVariantGroup(PBXGroup, XCFileLikeElement):
1548  """PBXVariantGroup is used by Xcode to represent localizations."""
1549  # No additions to the schema relative to PBXGroup.
1550  pass
1551
1552
1553# PBXReferenceProxy is also an XCFileLikeElement subclass.  It is defined below
1554# because it uses PBXContainerItemProxy, defined below.
1555
1556
1557class XCBuildConfiguration(XCObject):
1558  _schema = XCObject._schema.copy()
1559  _schema.update({
1560    'baseConfigurationReference': [0, PBXFileReference, 0, 0],
1561    'buildSettings':              [0, dict, 0, 1, {}],
1562    'name':                       [0, str,  0, 1],
1563  })
1564
1565  def HasBuildSetting(self, key):
1566    return key in self._properties['buildSettings']
1567
1568  def GetBuildSetting(self, key):
1569    return self._properties['buildSettings'][key]
1570
1571  def SetBuildSetting(self, key, value):
1572    # TODO(mark): If a list, copy?
1573    self._properties['buildSettings'][key] = value
1574
1575  def AppendBuildSetting(self, key, value):
1576    if not key in self._properties['buildSettings']:
1577      self._properties['buildSettings'][key] = []
1578    self._properties['buildSettings'][key].append(value)
1579
1580  def DelBuildSetting(self, key):
1581    if key in self._properties['buildSettings']:
1582      del self._properties['buildSettings'][key]
1583
1584  def SetBaseConfiguration(self, value):
1585    self._properties['baseConfigurationReference'] = value
1586
1587class XCConfigurationList(XCObject):
1588  # _configs is the default list of configurations.
1589  _configs = [ XCBuildConfiguration({'name': 'Debug'}),
1590               XCBuildConfiguration({'name': 'Release'}) ]
1591
1592  _schema = XCObject._schema.copy()
1593  _schema.update({
1594    'buildConfigurations':           [1, XCBuildConfiguration, 1, 1, _configs],
1595    'defaultConfigurationIsVisible': [0, int,                  0, 1, 1],
1596    'defaultConfigurationName':      [0, str,                  0, 1, 'Release'],
1597  })
1598
1599  def Name(self):
1600    return 'Build configuration list for ' + \
1601           self.parent.__class__.__name__ + ' "' + self.parent.Name() + '"'
1602
1603  def ConfigurationNamed(self, name):
1604    """Convenience accessor to obtain an XCBuildConfiguration by name."""
1605    for configuration in self._properties['buildConfigurations']:
1606      if configuration._properties['name'] == name:
1607        return configuration
1608
1609    raise KeyError, name
1610
1611  def DefaultConfiguration(self):
1612    """Convenience accessor to obtain the default XCBuildConfiguration."""
1613    return self.ConfigurationNamed(self._properties['defaultConfigurationName'])
1614
1615  def HasBuildSetting(self, key):
1616    """Determines the state of a build setting in all XCBuildConfiguration
1617    child objects.
1618
1619    If all child objects have key in their build settings, and the value is the
1620    same in all child objects, returns 1.
1621
1622    If no child objects have the key in their build settings, returns 0.
1623
1624    If some, but not all, child objects have the key in their build settings,
1625    or if any children have different values for the key, returns -1.
1626    """
1627
1628    has = None
1629    value = None
1630    for configuration in self._properties['buildConfigurations']:
1631      configuration_has = configuration.HasBuildSetting(key)
1632      if has is None:
1633        has = configuration_has
1634      elif has != configuration_has:
1635        return -1
1636
1637      if configuration_has:
1638        configuration_value = configuration.GetBuildSetting(key)
1639        if value is None:
1640          value = configuration_value
1641        elif value != configuration_value:
1642          return -1
1643
1644    if not has:
1645      return 0
1646
1647    return 1
1648
1649  def GetBuildSetting(self, key):
1650    """Gets the build setting for key.
1651
1652    All child XCConfiguration objects must have the same value set for the
1653    setting, or a ValueError will be raised.
1654    """
1655
1656    # TODO(mark): This is wrong for build settings that are lists.  The list
1657    # contents should be compared (and a list copy returned?)
1658
1659    value = None
1660    for configuration in self._properties['buildConfigurations']:
1661      configuration_value = configuration.GetBuildSetting(key)
1662      if value is None:
1663        value = configuration_value
1664      else:
1665        if value != configuration_value:
1666          raise ValueError, 'Variant values for ' + key
1667
1668    return value
1669
1670  def SetBuildSetting(self, key, value):
1671    """Sets the build setting for key to value in all child
1672    XCBuildConfiguration objects.
1673    """
1674
1675    for configuration in self._properties['buildConfigurations']:
1676      configuration.SetBuildSetting(key, value)
1677
1678  def AppendBuildSetting(self, key, value):
1679    """Appends value to the build setting for key, which is treated as a list,
1680    in all child XCBuildConfiguration objects.
1681    """
1682
1683    for configuration in self._properties['buildConfigurations']:
1684      configuration.AppendBuildSetting(key, value)
1685
1686  def DelBuildSetting(self, key):
1687    """Deletes the build setting key from all child XCBuildConfiguration
1688    objects.
1689    """
1690
1691    for configuration in self._properties['buildConfigurations']:
1692      configuration.DelBuildSetting(key)
1693
1694  def SetBaseConfiguration(self, value):
1695    """Sets the build configuration in all child XCBuildConfiguration objects.
1696    """
1697
1698    for configuration in self._properties['buildConfigurations']:
1699      configuration.SetBaseConfiguration(value)
1700
1701
1702class PBXBuildFile(XCObject):
1703  _schema = XCObject._schema.copy()
1704  _schema.update({
1705    'fileRef':  [0, XCFileLikeElement, 0, 1],
1706    'settings': [0, str,               0, 0],  # hack, it's a dict
1707  })
1708
1709  # Weird output rules for PBXBuildFile.
1710  _should_print_single_line = True
1711  _encode_transforms = XCObject._alternate_encode_transforms
1712
1713  def Name(self):
1714    # Example: "main.cc in Sources"
1715    return self._properties['fileRef'].Name() + ' in ' + self.parent.Name()
1716
1717  def Hashables(self):
1718    # super
1719    hashables = XCObject.Hashables(self)
1720
1721    # It is not sufficient to just rely on Name() to get the
1722    # XCFileLikeElement's name, because that is not a complete pathname.
1723    # PathHashables returns hashables unique enough that no two
1724    # PBXBuildFiles should wind up with the same set of hashables, unless
1725    # someone adds the same file multiple times to the same target.  That
1726    # would be considered invalid anyway.
1727    hashables.extend(self._properties['fileRef'].PathHashables())
1728
1729    return hashables
1730
1731
1732class XCBuildPhase(XCObject):
1733  """Abstract base for build phase classes.  Not represented in a project
1734  file.
1735
1736  Attributes:
1737    _files_by_path: A dict mapping each path of a child in the files list by
1738      path (keys) to the corresponding PBXBuildFile children (values).
1739    _files_by_xcfilelikeelement: A dict mapping each XCFileLikeElement (keys)
1740      to the corresponding PBXBuildFile children (values).
1741  """
1742
1743  # TODO(mark): Some build phase types, like PBXShellScriptBuildPhase, don't
1744  # actually have a "files" list.  XCBuildPhase should not have "files" but
1745  # another abstract subclass of it should provide this, and concrete build
1746  # phase types that do have "files" lists should be derived from that new
1747  # abstract subclass.  XCBuildPhase should only provide buildActionMask and
1748  # runOnlyForDeploymentPostprocessing, and not files or the various
1749  # file-related methods and attributes.
1750
1751  _schema = XCObject._schema.copy()
1752  _schema.update({
1753    'buildActionMask':                    [0, int,          0, 1, 0x7fffffff],
1754    'files':                              [1, PBXBuildFile, 1, 1, []],
1755    'runOnlyForDeploymentPostprocessing': [0, int,          0, 1, 0],
1756  })
1757
1758  def __init__(self, properties=None, id=None, parent=None):
1759    # super
1760    XCObject.__init__(self, properties, id, parent)
1761
1762    self._files_by_path = {}
1763    self._files_by_xcfilelikeelement = {}
1764    for pbxbuildfile in self._properties.get('files', []):
1765      self._AddBuildFileToDicts(pbxbuildfile)
1766
1767  def FileGroup(self, path):
1768    # Subclasses must override this by returning a two-element tuple.  The
1769    # first item in the tuple should be the PBXGroup to which "path" should be
1770    # added, either as a child or deeper descendant.  The second item should
1771    # be a boolean indicating whether files should be added into hierarchical
1772    # groups or one single flat group.
1773    raise NotImplementedError, \
1774          self.__class__.__name__ + ' must implement FileGroup'
1775
1776  def _AddPathToDict(self, pbxbuildfile, path):
1777    """Adds path to the dict tracking paths belonging to this build phase.
1778
1779    If the path is already a member of this build phase, raises an exception.
1780    """
1781
1782    if path in self._files_by_path:
1783      raise ValueError, 'Found multiple build files with path ' + path
1784    self._files_by_path[path] = pbxbuildfile
1785
1786  def _AddBuildFileToDicts(self, pbxbuildfile, path=None):
1787    """Maintains the _files_by_path and _files_by_xcfilelikeelement dicts.
1788
1789    If path is specified, then it is the path that is being added to the
1790    phase, and pbxbuildfile must contain either a PBXFileReference directly
1791    referencing that path, or it must contain a PBXVariantGroup that itself
1792    contains a PBXFileReference referencing the path.
1793
1794    If path is not specified, either the PBXFileReference's path or the paths
1795    of all children of the PBXVariantGroup are taken as being added to the
1796    phase.
1797
1798    If the path is already present in the phase, raises an exception.
1799
1800    If the PBXFileReference or PBXVariantGroup referenced by pbxbuildfile
1801    are already present in the phase, referenced by a different PBXBuildFile
1802    object, raises an exception.  This does not raise an exception when
1803    a PBXFileReference or PBXVariantGroup reappear and are referenced by the
1804    same PBXBuildFile that has already introduced them, because in the case
1805    of PBXVariantGroup objects, they may correspond to multiple paths that are
1806    not all added simultaneously.  When this situation occurs, the path needs
1807    to be added to _files_by_path, but nothing needs to change in
1808    _files_by_xcfilelikeelement, and the caller should have avoided adding
1809    the PBXBuildFile if it is already present in the list of children.
1810    """
1811
1812    xcfilelikeelement = pbxbuildfile._properties['fileRef']
1813
1814    paths = []
1815    if path != None:
1816      # It's best when the caller provides the path.
1817      if isinstance(xcfilelikeelement, PBXVariantGroup):
1818        paths.append(path)
1819    else:
1820      # If the caller didn't provide a path, there can be either multiple
1821      # paths (PBXVariantGroup) or one.
1822      if isinstance(xcfilelikeelement, PBXVariantGroup):
1823        for variant in xcfilelikeelement._properties['children']:
1824          paths.append(variant.FullPath())
1825      else:
1826        paths.append(xcfilelikeelement.FullPath())
1827
1828    # Add the paths first, because if something's going to raise, the
1829    # messages provided by _AddPathToDict are more useful owing to its
1830    # having access to a real pathname and not just an object's Name().
1831    for a_path in paths:
1832      self._AddPathToDict(pbxbuildfile, a_path)
1833
1834    # If another PBXBuildFile references this XCFileLikeElement, there's a
1835    # problem.
1836    if xcfilelikeelement in self._files_by_xcfilelikeelement and \
1837       self._files_by_xcfilelikeelement[xcfilelikeelement] != pbxbuildfile:
1838      raise ValueError, 'Found multiple build files for ' + \
1839                        xcfilelikeelement.Name()
1840    self._files_by_xcfilelikeelement[xcfilelikeelement] = pbxbuildfile
1841
1842  def AppendBuildFile(self, pbxbuildfile, path=None):
1843    # Callers should use this instead of calling
1844    # AppendProperty('files', pbxbuildfile) directly because this function
1845    # maintains the object's dicts.  Better yet, callers can just call AddFile
1846    # with a pathname and not worry about building their own PBXBuildFile
1847    # objects.
1848    self.AppendProperty('files', pbxbuildfile)
1849    self._AddBuildFileToDicts(pbxbuildfile, path)
1850
1851  def AddFile(self, path, settings=None):
1852    (file_group, hierarchical) = self.FileGroup(path)
1853    file_ref = file_group.AddOrGetFileByPath(path, hierarchical)
1854
1855    if file_ref in self._files_by_xcfilelikeelement and \
1856       isinstance(file_ref, PBXVariantGroup):
1857      # There's already a PBXBuildFile in this phase corresponding to the
1858      # PBXVariantGroup.  path just provides a new variant that belongs to
1859      # the group.  Add the path to the dict.
1860      pbxbuildfile = self._files_by_xcfilelikeelement[file_ref]
1861      self._AddBuildFileToDicts(pbxbuildfile, path)
1862    else:
1863      # Add a new PBXBuildFile to get file_ref into the phase.
1864      if settings is None:
1865        pbxbuildfile = PBXBuildFile({'fileRef': file_ref})
1866      else:
1867        pbxbuildfile = PBXBuildFile({'fileRef': file_ref, 'settings': settings})
1868      self.AppendBuildFile(pbxbuildfile, path)
1869
1870
1871class PBXHeadersBuildPhase(XCBuildPhase):
1872  # No additions to the schema relative to XCBuildPhase.
1873
1874  def Name(self):
1875    return 'Headers'
1876
1877  def FileGroup(self, path):
1878    return self.PBXProjectAncestor().RootGroupForPath(path)
1879
1880
1881class PBXResourcesBuildPhase(XCBuildPhase):
1882  # No additions to the schema relative to XCBuildPhase.
1883
1884  def Name(self):
1885    return 'Resources'
1886
1887  def FileGroup(self, path):
1888    return self.PBXProjectAncestor().RootGroupForPath(path)
1889
1890
1891class PBXSourcesBuildPhase(XCBuildPhase):
1892  # No additions to the schema relative to XCBuildPhase.
1893
1894  def Name(self):
1895    return 'Sources'
1896
1897  def FileGroup(self, path):
1898    return self.PBXProjectAncestor().RootGroupForPath(path)
1899
1900
1901class PBXFrameworksBuildPhase(XCBuildPhase):
1902  # No additions to the schema relative to XCBuildPhase.
1903
1904  def Name(self):
1905    return 'Frameworks'
1906
1907  def FileGroup(self, path):
1908    (root, ext) = posixpath.splitext(path)
1909    if ext != '':
1910      ext = ext[1:].lower()
1911    if ext == 'o':
1912      # .o files are added to Xcode Frameworks phases, but conceptually aren't
1913      # frameworks, they're more like sources or intermediates. Redirect them
1914      # to show up in one of those other groups.
1915      return self.PBXProjectAncestor().RootGroupForPath(path)
1916    else:
1917      return (self.PBXProjectAncestor().FrameworksGroup(), False)
1918
1919
1920class PBXShellScriptBuildPhase(XCBuildPhase):
1921  _schema = XCBuildPhase._schema.copy()
1922  _schema.update({
1923    'inputPaths':       [1, str, 0, 1, []],
1924    'name':             [0, str, 0, 0],
1925    'outputPaths':      [1, str, 0, 1, []],
1926    'shellPath':        [0, str, 0, 1, '/bin/sh'],
1927    'shellScript':      [0, str, 0, 1],
1928    'showEnvVarsInLog': [0, int, 0, 0],
1929  })
1930
1931  def Name(self):
1932    if 'name' in self._properties:
1933      return self._properties['name']
1934
1935    return 'ShellScript'
1936
1937
1938class PBXCopyFilesBuildPhase(XCBuildPhase):
1939  _schema = XCBuildPhase._schema.copy()
1940  _schema.update({
1941    'dstPath':          [0, str, 0, 1],
1942    'dstSubfolderSpec': [0, int, 0, 1],
1943    'name':             [0, str, 0, 0],
1944  })
1945
1946  # path_tree_re matches "$(DIR)/path" or just "$(DIR)".  Match group 1 is
1947  # "DIR", match group 3 is "path" or None.
1948  path_tree_re = re.compile('^\\$\\((.*)\\)(/(.*)|)$')
1949
1950  # path_tree_to_subfolder maps names of Xcode variables to the associated
1951  # dstSubfolderSpec property value used in a PBXCopyFilesBuildPhase object.
1952  path_tree_to_subfolder = {
1953    'BUILT_PRODUCTS_DIR': 16,  # Products Directory
1954    # Other types that can be chosen via the Xcode UI.
1955    # TODO(mark): Map Xcode variable names to these.
1956    # : 1,  # Wrapper
1957    # : 6,  # Executables: 6
1958    # : 7,  # Resources
1959    # : 15,  # Java Resources
1960    # : 10,  # Frameworks
1961    # : 11,  # Shared Frameworks
1962    # : 12,  # Shared Support
1963    # : 13,  # PlugIns
1964  }
1965
1966  def Name(self):
1967    if 'name' in self._properties:
1968      return self._properties['name']
1969
1970    return 'CopyFiles'
1971
1972  def FileGroup(self, path):
1973    return self.PBXProjectAncestor().RootGroupForPath(path)
1974
1975  def SetDestination(self, path):
1976    """Set the dstSubfolderSpec and dstPath properties from path.
1977
1978    path may be specified in the same notation used for XCHierarchicalElements,
1979    specifically, "$(DIR)/path".
1980    """
1981
1982    path_tree_match = self.path_tree_re.search(path)
1983    if path_tree_match:
1984      # Everything else needs to be relative to an Xcode variable.
1985      path_tree = path_tree_match.group(1)
1986      relative_path = path_tree_match.group(3)
1987
1988      if path_tree in self.path_tree_to_subfolder:
1989        subfolder = self.path_tree_to_subfolder[path_tree]
1990        if relative_path is None:
1991          relative_path = ''
1992      else:
1993        # The path starts with an unrecognized Xcode variable
1994        # name like $(SRCROOT).  Xcode will still handle this
1995        # as an "absolute path" that starts with the variable.
1996        subfolder = 0
1997        relative_path = path
1998    elif path.startswith('/'):
1999      # Special case.  Absolute paths are in dstSubfolderSpec 0.
2000      subfolder = 0
2001      relative_path = path[1:]
2002    else:
2003      raise ValueError, 'Can\'t use path %s in a %s' % \
2004                        (path, self.__class__.__name__)
2005
2006    self._properties['dstPath'] = relative_path
2007    self._properties['dstSubfolderSpec'] = subfolder
2008
2009
2010class PBXBuildRule(XCObject):
2011  _schema = XCObject._schema.copy()
2012  _schema.update({
2013    'compilerSpec': [0, str, 0, 1],
2014    'filePatterns': [0, str, 0, 0],
2015    'fileType':     [0, str, 0, 1],
2016    'isEditable':   [0, int, 0, 1, 1],
2017    'outputFiles':  [1, str, 0, 1, []],
2018    'script':       [0, str, 0, 0],
2019  })
2020
2021  def Name(self):
2022    # Not very inspired, but it's what Xcode uses.
2023    return self.__class__.__name__
2024
2025  def Hashables(self):
2026    # super
2027    hashables = XCObject.Hashables(self)
2028
2029    # Use the hashables of the weak objects that this object refers to.
2030    hashables.append(self._properties['fileType'])
2031    if 'filePatterns' in self._properties:
2032      hashables.append(self._properties['filePatterns'])
2033    return hashables
2034
2035
2036class PBXContainerItemProxy(XCObject):
2037  # When referencing an item in this project file, containerPortal is the
2038  # PBXProject root object of this project file.  When referencing an item in
2039  # another project file, containerPortal is a PBXFileReference identifying
2040  # the other project file.
2041  #
2042  # When serving as a proxy to an XCTarget (in this project file or another),
2043  # proxyType is 1.  When serving as a proxy to a PBXFileReference (in another
2044  # project file), proxyType is 2.  Type 2 is used for references to the
2045  # producs of the other project file's targets.
2046  #
2047  # Xcode is weird about remoteGlobalIDString.  Usually, it's printed without
2048  # a comment, indicating that it's tracked internally simply as a string, but
2049  # sometimes it's printed with a comment (usually when the object is initially
2050  # created), indicating that it's tracked as a project file object at least
2051  # sometimes.  This module always tracks it as an object, but contains a hack
2052  # to prevent it from printing the comment in the project file output.  See
2053  # _XCKVPrint.
2054  _schema = XCObject._schema.copy()
2055  _schema.update({
2056    'containerPortal':      [0, XCContainerPortal, 0, 1],
2057    'proxyType':            [0, int,               0, 1],
2058    'remoteGlobalIDString': [0, XCRemoteObject,    0, 1],
2059    'remoteInfo':           [0, str,               0, 1],
2060  })
2061
2062  def __repr__(self):
2063    props = self._properties
2064    name = '%s.gyp:%s' % (props['containerPortal'].Name(), props['remoteInfo'])
2065    return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
2066
2067  def Name(self):
2068    # Admittedly not the best name, but it's what Xcode uses.
2069    return self.__class__.__name__
2070
2071  def Hashables(self):
2072    # super
2073    hashables = XCObject.Hashables(self)
2074
2075    # Use the hashables of the weak objects that this object refers to.
2076    hashables.extend(self._properties['containerPortal'].Hashables())
2077    hashables.extend(self._properties['remoteGlobalIDString'].Hashables())
2078    return hashables
2079
2080
2081class PBXTargetDependency(XCObject):
2082  # The "target" property accepts an XCTarget object, and obviously not
2083  # NoneType.  But XCTarget is defined below, so it can't be put into the
2084  # schema yet.  The definition of PBXTargetDependency can't be moved below
2085  # XCTarget because XCTarget's own schema references PBXTargetDependency.
2086  # Python doesn't deal well with this circular relationship, and doesn't have
2087  # a real way to do forward declarations.  To work around, the type of
2088  # the "target" property is reset below, after XCTarget is defined.
2089  #
2090  # At least one of "name" and "target" is required.
2091  _schema = XCObject._schema.copy()
2092  _schema.update({
2093    'name':        [0, str,                   0, 0],
2094    'target':      [0, None.__class__,        0, 0],
2095    'targetProxy': [0, PBXContainerItemProxy, 1, 1],
2096  })
2097
2098  def __repr__(self):
2099    name = self._properties.get('name') or self._properties['target'].Name()
2100    return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
2101
2102  def Name(self):
2103    # Admittedly not the best name, but it's what Xcode uses.
2104    return self.__class__.__name__
2105
2106  def Hashables(self):
2107    # super
2108    hashables = XCObject.Hashables(self)
2109
2110    # Use the hashables of the weak objects that this object refers to.
2111    hashables.extend(self._properties['targetProxy'].Hashables())
2112    return hashables
2113
2114
2115class PBXReferenceProxy(XCFileLikeElement):
2116  _schema = XCFileLikeElement._schema.copy()
2117  _schema.update({
2118    'fileType':  [0, str,                   0, 1],
2119    'path':      [0, str,                   0, 1],
2120    'remoteRef': [0, PBXContainerItemProxy, 1, 1],
2121  })
2122
2123
2124class XCTarget(XCRemoteObject):
2125  # An XCTarget is really just an XCObject, the XCRemoteObject thing is just
2126  # to allow PBXProject to be used in the remoteGlobalIDString property of
2127  # PBXContainerItemProxy.
2128  #
2129  # Setting a "name" property at instantiation may also affect "productName",
2130  # which may in turn affect the "PRODUCT_NAME" build setting in children of
2131  # "buildConfigurationList".  See __init__ below.
2132  _schema = XCRemoteObject._schema.copy()
2133  _schema.update({
2134    'buildConfigurationList': [0, XCConfigurationList, 1, 1,
2135                               XCConfigurationList()],
2136    'buildPhases':            [1, XCBuildPhase,        1, 1, []],
2137    'dependencies':           [1, PBXTargetDependency, 1, 1, []],
2138    'name':                   [0, str,                 0, 1],
2139    'productName':            [0, str,                 0, 1],
2140  })
2141
2142  def __init__(self, properties=None, id=None, parent=None,
2143               force_outdir=None, force_prefix=None, force_extension=None):
2144    # super
2145    XCRemoteObject.__init__(self, properties, id, parent)
2146
2147    # Set up additional defaults not expressed in the schema.  If a "name"
2148    # property was supplied, set "productName" if it is not present.  Also set
2149    # the "PRODUCT_NAME" build setting in each configuration, but only if
2150    # the setting is not present in any build configuration.
2151    if 'name' in self._properties:
2152      if not 'productName' in self._properties:
2153        self.SetProperty('productName', self._properties['name'])
2154
2155    if 'productName' in self._properties:
2156      if 'buildConfigurationList' in self._properties:
2157        configs = self._properties['buildConfigurationList']
2158        if configs.HasBuildSetting('PRODUCT_NAME') == 0:
2159          configs.SetBuildSetting('PRODUCT_NAME',
2160                                  self._properties['productName'])
2161
2162  def AddDependency(self, other):
2163    pbxproject = self.PBXProjectAncestor()
2164    other_pbxproject = other.PBXProjectAncestor()
2165    if pbxproject == other_pbxproject:
2166      # Add a dependency to another target in the same project file.
2167      container = PBXContainerItemProxy({'containerPortal':      pbxproject,
2168                                         'proxyType':            1,
2169                                         'remoteGlobalIDString': other,
2170                                         'remoteInfo':           other.Name()})
2171      dependency = PBXTargetDependency({'target':      other,
2172                                        'targetProxy': container})
2173      self.AppendProperty('dependencies', dependency)
2174    else:
2175      # Add a dependency to a target in a different project file.
2176      other_project_ref = \
2177          pbxproject.AddOrGetProjectReference(other_pbxproject)[1]
2178      container = PBXContainerItemProxy({
2179            'containerPortal':      other_project_ref,
2180            'proxyType':            1,
2181            'remoteGlobalIDString': other,
2182            'remoteInfo':           other.Name(),
2183          })
2184      dependency = PBXTargetDependency({'name':        other.Name(),
2185                                        'targetProxy': container})
2186      self.AppendProperty('dependencies', dependency)
2187
2188  # Proxy all of these through to the build configuration list.
2189
2190  def ConfigurationNamed(self, name):
2191    return self._properties['buildConfigurationList'].ConfigurationNamed(name)
2192
2193  def DefaultConfiguration(self):
2194    return self._properties['buildConfigurationList'].DefaultConfiguration()
2195
2196  def HasBuildSetting(self, key):
2197    return self._properties['buildConfigurationList'].HasBuildSetting(key)
2198
2199  def GetBuildSetting(self, key):
2200    return self._properties['buildConfigurationList'].GetBuildSetting(key)
2201
2202  def SetBuildSetting(self, key, value):
2203    return self._properties['buildConfigurationList'].SetBuildSetting(key, \
2204                                                                      value)
2205
2206  def AppendBuildSetting(self, key, value):
2207    return self._properties['buildConfigurationList'].AppendBuildSetting(key, \
2208                                                                         value)
2209
2210  def DelBuildSetting(self, key):
2211    return self._properties['buildConfigurationList'].DelBuildSetting(key)
2212
2213
2214# Redefine the type of the "target" property.  See PBXTargetDependency._schema
2215# above.
2216PBXTargetDependency._schema['target'][1] = XCTarget
2217
2218
2219class PBXNativeTarget(XCTarget):
2220  # buildPhases is overridden in the schema to be able to set defaults.
2221  #
2222  # NOTE: Contrary to most objects, it is advisable to set parent when
2223  # constructing PBXNativeTarget.  A parent of an XCTarget must be a PBXProject
2224  # object.  A parent reference is required for a PBXNativeTarget during
2225  # construction to be able to set up the target defaults for productReference,
2226  # because a PBXBuildFile object must be created for the target and it must
2227  # be added to the PBXProject's mainGroup hierarchy.
2228  _schema = XCTarget._schema.copy()
2229  _schema.update({
2230    'buildPhases':      [1, XCBuildPhase,     1, 1,
2231                         [PBXSourcesBuildPhase(), PBXFrameworksBuildPhase()]],
2232    'buildRules':       [1, PBXBuildRule,     1, 1, []],
2233    'productReference': [0, PBXFileReference, 0, 1],
2234    'productType':      [0, str,              0, 1],
2235  })
2236
2237  # Mapping from Xcode product-types to settings.  The settings are:
2238  #  filetype : used for explicitFileType in the project file
2239  #  prefix : the prefix for the file name
2240  #  suffix : the suffix for the filen ame
2241  _product_filetypes = {
2242    'com.apple.product-type.application':       ['wrapper.application',
2243                                                 '', '.app'],
2244    'com.apple.product-type.bundle':            ['wrapper.cfbundle',
2245                                                 '', '.bundle'],
2246    'com.apple.product-type.framework':         ['wrapper.framework',
2247                                                 '', '.framework'],
2248    'com.apple.product-type.library.dynamic':   ['compiled.mach-o.dylib',
2249                                                 'lib', '.dylib'],
2250    'com.apple.product-type.library.static':    ['archive.ar',
2251                                                 'lib', '.a'],
2252    'com.apple.product-type.tool':              ['compiled.mach-o.executable',
2253                                                 '', ''],
2254    'com.apple.product-type.bundle.unit-test':  ['wrapper.cfbundle',
2255                                                 '', '.xctest'],
2256    'com.googlecode.gyp.xcode.bundle':          ['compiled.mach-o.dylib',
2257                                                 '', '.so'],
2258  }
2259
2260  def __init__(self, properties=None, id=None, parent=None,
2261               force_outdir=None, force_prefix=None, force_extension=None):
2262    # super
2263    XCTarget.__init__(self, properties, id, parent)
2264
2265    if 'productName' in self._properties and \
2266       'productType' in self._properties and \
2267       not 'productReference' in self._properties and \
2268       self._properties['productType'] in self._product_filetypes:
2269      products_group = None
2270      pbxproject = self.PBXProjectAncestor()
2271      if pbxproject != None:
2272        products_group = pbxproject.ProductsGroup()
2273
2274      if products_group != None:
2275        (filetype, prefix, suffix) = \
2276            self._product_filetypes[self._properties['productType']]
2277        # Xcode does not have a distinct type for loadable modules that are
2278        # pure BSD targets (not in a bundle wrapper). GYP allows such modules
2279        # to be specified by setting a target type to loadable_module without
2280        # having mac_bundle set. These are mapped to the pseudo-product type
2281        # com.googlecode.gyp.xcode.bundle.
2282        #
2283        # By picking up this special type and converting it to a dynamic
2284        # library (com.apple.product-type.library.dynamic) with fix-ups,
2285        # single-file loadable modules can be produced.
2286        #
2287        # MACH_O_TYPE is changed to mh_bundle to produce the proper file type
2288        # (as opposed to mh_dylib). In order for linking to succeed,
2289        # DYLIB_CURRENT_VERSION and DYLIB_COMPATIBILITY_VERSION must be
2290        # cleared. They are meaningless for type mh_bundle.
2291        #
2292        # Finally, the .so extension is forcibly applied over the default
2293        # (.dylib), unless another forced extension is already selected.
2294        # .dylib is plainly wrong, and .bundle is used by loadable_modules in
2295        # bundle wrappers (com.apple.product-type.bundle). .so seems an odd
2296        # choice because it's used as the extension on many other systems that
2297        # don't distinguish between linkable shared libraries and non-linkable
2298        # loadable modules, but there's precedent: Python loadable modules on
2299        # Mac OS X use an .so extension.
2300        if self._properties['productType'] == 'com.googlecode.gyp.xcode.bundle':
2301          self._properties['productType'] = \
2302              'com.apple.product-type.library.dynamic'
2303          self.SetBuildSetting('MACH_O_TYPE', 'mh_bundle')
2304          self.SetBuildSetting('DYLIB_CURRENT_VERSION', '')
2305          self.SetBuildSetting('DYLIB_COMPATIBILITY_VERSION', '')
2306          if force_extension is None:
2307            force_extension = suffix[1:]
2308
2309        if self._properties['productType'] == \
2310           'com.apple.product-type-bundle.unit.test':
2311          if force_extension is None:
2312            force_extension = suffix[1:]
2313
2314        if force_extension is not None:
2315          # If it's a wrapper (bundle), set WRAPPER_EXTENSION.
2316          if filetype.startswith('wrapper.'):
2317            self.SetBuildSetting('WRAPPER_EXTENSION', force_extension)
2318          else:
2319            # Extension override.
2320            suffix = '.' + force_extension
2321            self.SetBuildSetting('EXECUTABLE_EXTENSION', force_extension)
2322
2323          if filetype.startswith('compiled.mach-o.executable'):
2324            product_name = self._properties['productName']
2325            product_name += suffix
2326            suffix = ''
2327            self.SetProperty('productName', product_name)
2328            self.SetBuildSetting('PRODUCT_NAME', product_name)
2329
2330        # Xcode handles most prefixes based on the target type, however there
2331        # are exceptions.  If a "BSD Dynamic Library" target is added in the
2332        # Xcode UI, Xcode sets EXECUTABLE_PREFIX.  This check duplicates that
2333        # behavior.
2334        if force_prefix is not None:
2335          prefix = force_prefix
2336        if filetype.startswith('wrapper.'):
2337          self.SetBuildSetting('WRAPPER_PREFIX', prefix)
2338        else:
2339          self.SetBuildSetting('EXECUTABLE_PREFIX', prefix)
2340
2341        if force_outdir is not None:
2342          self.SetBuildSetting('TARGET_BUILD_DIR', force_outdir)
2343
2344        # TODO(tvl): Remove the below hack.
2345        #    http://code.google.com/p/gyp/issues/detail?id=122
2346
2347        # Some targets include the prefix in the target_name.  These targets
2348        # really should just add a product_name setting that doesn't include
2349        # the prefix.  For example:
2350        #  target_name = 'libevent', product_name = 'event'
2351        # This check cleans up for them.
2352        product_name = self._properties['productName']
2353        prefix_len = len(prefix)
2354        if prefix_len and (product_name[:prefix_len] == prefix):
2355          product_name = product_name[prefix_len:]
2356          self.SetProperty('productName', product_name)
2357          self.SetBuildSetting('PRODUCT_NAME', product_name)
2358
2359        ref_props = {
2360          'explicitFileType': filetype,
2361          'includeInIndex':   0,
2362          'path':             prefix + product_name + suffix,
2363          'sourceTree':       'BUILT_PRODUCTS_DIR',
2364        }
2365        file_ref = PBXFileReference(ref_props)
2366        products_group.AppendChild(file_ref)
2367        self.SetProperty('productReference', file_ref)
2368
2369  def GetBuildPhaseByType(self, type):
2370    if not 'buildPhases' in self._properties:
2371      return None
2372
2373    the_phase = None
2374    for phase in self._properties['buildPhases']:
2375      if isinstance(phase, type):
2376        # Some phases may be present in multiples in a well-formed project file,
2377        # but phases like PBXSourcesBuildPhase may only be present singly, and
2378        # this function is intended as an aid to GetBuildPhaseByType.  Loop
2379        # over the entire list of phases and assert if more than one of the
2380        # desired type is found.
2381        assert the_phase is None
2382        the_phase = phase
2383
2384    return the_phase
2385
2386  def HeadersPhase(self):
2387    headers_phase = self.GetBuildPhaseByType(PBXHeadersBuildPhase)
2388    if headers_phase is None:
2389      headers_phase = PBXHeadersBuildPhase()
2390
2391      # The headers phase should come before the resources, sources, and
2392      # frameworks phases, if any.
2393      insert_at = len(self._properties['buildPhases'])
2394      for index in xrange(0, len(self._properties['buildPhases'])):
2395        phase = self._properties['buildPhases'][index]
2396        if isinstance(phase, PBXResourcesBuildPhase) or \
2397           isinstance(phase, PBXSourcesBuildPhase) or \
2398           isinstance(phase, PBXFrameworksBuildPhase):
2399          insert_at = index
2400          break
2401
2402      self._properties['buildPhases'].insert(insert_at, headers_phase)
2403      headers_phase.parent = self
2404
2405    return headers_phase
2406
2407  def ResourcesPhase(self):
2408    resources_phase = self.GetBuildPhaseByType(PBXResourcesBuildPhase)
2409    if resources_phase is None:
2410      resources_phase = PBXResourcesBuildPhase()
2411
2412      # The resources phase should come before the sources and frameworks
2413      # phases, if any.
2414      insert_at = len(self._properties['buildPhases'])
2415      for index in xrange(0, len(self._properties['buildPhases'])):
2416        phase = self._properties['buildPhases'][index]
2417        if isinstance(phase, PBXSourcesBuildPhase) or \
2418           isinstance(phase, PBXFrameworksBuildPhase):
2419          insert_at = index
2420          break
2421
2422      self._properties['buildPhases'].insert(insert_at, resources_phase)
2423      resources_phase.parent = self
2424
2425    return resources_phase
2426
2427  def SourcesPhase(self):
2428    sources_phase = self.GetBuildPhaseByType(PBXSourcesBuildPhase)
2429    if sources_phase is None:
2430      sources_phase = PBXSourcesBuildPhase()
2431      self.AppendProperty('buildPhases', sources_phase)
2432
2433    return sources_phase
2434
2435  def FrameworksPhase(self):
2436    frameworks_phase = self.GetBuildPhaseByType(PBXFrameworksBuildPhase)
2437    if frameworks_phase is None:
2438      frameworks_phase = PBXFrameworksBuildPhase()
2439      self.AppendProperty('buildPhases', frameworks_phase)
2440
2441    return frameworks_phase
2442
2443  def AddDependency(self, other):
2444    # super
2445    XCTarget.AddDependency(self, other)
2446
2447    static_library_type = 'com.apple.product-type.library.static'
2448    shared_library_type = 'com.apple.product-type.library.dynamic'
2449    framework_type = 'com.apple.product-type.framework'
2450    if isinstance(other, PBXNativeTarget) and \
2451       'productType' in self._properties and \
2452       self._properties['productType'] != static_library_type and \
2453       'productType' in other._properties and \
2454       (other._properties['productType'] == static_library_type or \
2455        ((other._properties['productType'] == shared_library_type or \
2456          other._properties['productType'] == framework_type) and \
2457         ((not other.HasBuildSetting('MACH_O_TYPE')) or
2458          other.GetBuildSetting('MACH_O_TYPE') != 'mh_bundle'))):
2459
2460      file_ref = other.GetProperty('productReference')
2461
2462      pbxproject = self.PBXProjectAncestor()
2463      other_pbxproject = other.PBXProjectAncestor()
2464      if pbxproject != other_pbxproject:
2465        other_project_product_group = \
2466            pbxproject.AddOrGetProjectReference(other_pbxproject)[0]
2467        file_ref = other_project_product_group.GetChildByRemoteObject(file_ref)
2468
2469      self.FrameworksPhase().AppendProperty('files',
2470                                            PBXBuildFile({'fileRef': file_ref}))
2471
2472
2473class PBXAggregateTarget(XCTarget):
2474  pass
2475
2476
2477class PBXProject(XCContainerPortal):
2478  # A PBXProject is really just an XCObject, the XCContainerPortal thing is
2479  # just to allow PBXProject to be used in the containerPortal property of
2480  # PBXContainerItemProxy.
2481  """
2482
2483  Attributes:
2484    path: "sample.xcodeproj".  TODO(mark) Document me!
2485    _other_pbxprojects: A dictionary, keyed by other PBXProject objects.  Each
2486                        value is a reference to the dict in the
2487                        projectReferences list associated with the keyed
2488                        PBXProject.
2489  """
2490
2491  _schema = XCContainerPortal._schema.copy()
2492  _schema.update({
2493    'attributes':             [0, dict,                0, 0],
2494    'buildConfigurationList': [0, XCConfigurationList, 1, 1,
2495                               XCConfigurationList()],
2496    'compatibilityVersion':   [0, str,                 0, 1, 'Xcode 3.2'],
2497    'hasScannedForEncodings': [0, int,                 0, 1, 1],
2498    'mainGroup':              [0, PBXGroup,            1, 1, PBXGroup()],
2499    'projectDirPath':         [0, str,                 0, 1, ''],
2500    'projectReferences':      [1, dict,                0, 0],
2501    'projectRoot':            [0, str,                 0, 1, ''],
2502    'targets':                [1, XCTarget,            1, 1, []],
2503  })
2504
2505  def __init__(self, properties=None, id=None, parent=None, path=None):
2506    self.path = path
2507    self._other_pbxprojects = {}
2508    # super
2509    return XCContainerPortal.__init__(self, properties, id, parent)
2510
2511  def Name(self):
2512    name = self.path
2513    if name[-10:] == '.xcodeproj':
2514      name = name[:-10]
2515    return posixpath.basename(name)
2516
2517  def Path(self):
2518    return self.path
2519
2520  def Comment(self):
2521    return 'Project object'
2522
2523  def Children(self):
2524    # super
2525    children = XCContainerPortal.Children(self)
2526
2527    # Add children that the schema doesn't know about.  Maybe there's a more
2528    # elegant way around this, but this is the only case where we need to own
2529    # objects in a dictionary (that is itself in a list), and three lines for
2530    # a one-off isn't that big a deal.
2531    if 'projectReferences' in self._properties:
2532      for reference in self._properties['projectReferences']:
2533        children.append(reference['ProductGroup'])
2534
2535    return children
2536
2537  def PBXProjectAncestor(self):
2538    return self
2539
2540  def _GroupByName(self, name):
2541    if not 'mainGroup' in self._properties:
2542      self.SetProperty('mainGroup', PBXGroup())
2543
2544    main_group = self._properties['mainGroup']
2545    group = main_group.GetChildByName(name)
2546    if group is None:
2547      group = PBXGroup({'name': name})
2548      main_group.AppendChild(group)
2549
2550    return group
2551
2552  # SourceGroup and ProductsGroup are created by default in Xcode's own
2553  # templates.
2554  def SourceGroup(self):
2555    return self._GroupByName('Source')
2556
2557  def ProductsGroup(self):
2558    return self._GroupByName('Products')
2559
2560  # IntermediatesGroup is used to collect source-like files that are generated
2561  # by rules or script phases and are placed in intermediate directories such
2562  # as DerivedSources.
2563  def IntermediatesGroup(self):
2564    return self._GroupByName('Intermediates')
2565
2566  # FrameworksGroup and ProjectsGroup are top-level groups used to collect
2567  # frameworks and projects.
2568  def FrameworksGroup(self):
2569    return self._GroupByName('Frameworks')
2570
2571  def ProjectsGroup(self):
2572    return self._GroupByName('Projects')
2573
2574  def RootGroupForPath(self, path):
2575    """Returns a PBXGroup child of this object to which path should be added.
2576
2577    This method is intended to choose between SourceGroup and
2578    IntermediatesGroup on the basis of whether path is present in a source
2579    directory or an intermediates directory.  For the purposes of this
2580    determination, any path located within a derived file directory such as
2581    PROJECT_DERIVED_FILE_DIR is treated as being in an intermediates
2582    directory.
2583
2584    The returned value is a two-element tuple.  The first element is the
2585    PBXGroup, and the second element specifies whether that group should be
2586    organized hierarchically (True) or as a single flat list (False).
2587    """
2588
2589    # TODO(mark): make this a class variable and bind to self on call?
2590    # Also, this list is nowhere near exhaustive.
2591    # INTERMEDIATE_DIR and SHARED_INTERMEDIATE_DIR are used by
2592    # gyp.generator.xcode.  There should probably be some way for that module
2593    # to push the names in, rather than having to hard-code them here.
2594    source_tree_groups = {
2595      'DERIVED_FILE_DIR':         (self.IntermediatesGroup, True),
2596      'INTERMEDIATE_DIR':         (self.IntermediatesGroup, True),
2597      'PROJECT_DERIVED_FILE_DIR': (self.IntermediatesGroup, True),
2598      'SHARED_INTERMEDIATE_DIR':  (self.IntermediatesGroup, True),
2599    }
2600
2601    (source_tree, path) = SourceTreeAndPathFromPath(path)
2602    if source_tree != None and source_tree in source_tree_groups:
2603      (group_func, hierarchical) = source_tree_groups[source_tree]
2604      group = group_func()
2605      return (group, hierarchical)
2606
2607    # TODO(mark): make additional choices based on file extension.
2608
2609    return (self.SourceGroup(), True)
2610
2611  def AddOrGetFileInRootGroup(self, path):
2612    """Returns a PBXFileReference corresponding to path in the correct group
2613    according to RootGroupForPath's heuristics.
2614
2615    If an existing PBXFileReference for path exists, it will be returned.
2616    Otherwise, one will be created and returned.
2617    """
2618
2619    (group, hierarchical) = self.RootGroupForPath(path)
2620    return group.AddOrGetFileByPath(path, hierarchical)
2621
2622  def RootGroupsTakeOverOnlyChildren(self, recurse=False):
2623    """Calls TakeOverOnlyChild for all groups in the main group."""
2624
2625    for group in self._properties['mainGroup']._properties['children']:
2626      if isinstance(group, PBXGroup):
2627        group.TakeOverOnlyChild(recurse)
2628
2629  def SortGroups(self):
2630    # Sort the children of the mainGroup (like "Source" and "Products")
2631    # according to their defined order.
2632    self._properties['mainGroup']._properties['children'] = \
2633        sorted(self._properties['mainGroup']._properties['children'],
2634               cmp=lambda x,y: x.CompareRootGroup(y))
2635
2636    # Sort everything else by putting group before files, and going
2637    # alphabetically by name within sections of groups and files.  SortGroup
2638    # is recursive.
2639    for group in self._properties['mainGroup']._properties['children']:
2640      if not isinstance(group, PBXGroup):
2641        continue
2642
2643      if group.Name() == 'Products':
2644        # The Products group is a special case.  Instead of sorting
2645        # alphabetically, sort things in the order of the targets that
2646        # produce the products.  To do this, just build up a new list of
2647        # products based on the targets.
2648        products = []
2649        for target in self._properties['targets']:
2650          if not isinstance(target, PBXNativeTarget):
2651            continue
2652          product = target._properties['productReference']
2653          # Make sure that the product is already in the products group.
2654          assert product in group._properties['children']
2655          products.append(product)
2656
2657        # Make sure that this process doesn't miss anything that was already
2658        # in the products group.
2659        assert len(products) == len(group._properties['children'])
2660        group._properties['children'] = products
2661      else:
2662        group.SortGroup()
2663
2664  def AddOrGetProjectReference(self, other_pbxproject):
2665    """Add a reference to another project file (via PBXProject object) to this
2666    one.
2667
2668    Returns [ProductGroup, ProjectRef].  ProductGroup is a PBXGroup object in
2669    this project file that contains a PBXReferenceProxy object for each
2670    product of each PBXNativeTarget in the other project file.  ProjectRef is
2671    a PBXFileReference to the other project file.
2672
2673    If this project file already references the other project file, the
2674    existing ProductGroup and ProjectRef are returned.  The ProductGroup will
2675    still be updated if necessary.
2676    """
2677
2678    if not 'projectReferences' in self._properties:
2679      self._properties['projectReferences'] = []
2680
2681    product_group = None
2682    project_ref = None
2683
2684    if not other_pbxproject in self._other_pbxprojects:
2685      # This project file isn't yet linked to the other one.  Establish the
2686      # link.
2687      product_group = PBXGroup({'name': 'Products'})
2688
2689      # ProductGroup is strong.
2690      product_group.parent = self
2691
2692      # There's nothing unique about this PBXGroup, and if left alone, it will
2693      # wind up with the same set of hashables as all other PBXGroup objects
2694      # owned by the projectReferences list.  Add the hashables of the
2695      # remote PBXProject that it's related to.
2696      product_group._hashables.extend(other_pbxproject.Hashables())
2697
2698      # The other project reports its path as relative to the same directory
2699      # that this project's path is relative to.  The other project's path
2700      # is not necessarily already relative to this project.  Figure out the
2701      # pathname that this project needs to use to refer to the other one.
2702      this_path = posixpath.dirname(self.Path())
2703      projectDirPath = self.GetProperty('projectDirPath')
2704      if projectDirPath:
2705        if posixpath.isabs(projectDirPath[0]):
2706          this_path = projectDirPath
2707        else:
2708          this_path = posixpath.join(this_path, projectDirPath)
2709      other_path = gyp.common.RelativePath(other_pbxproject.Path(), this_path)
2710
2711      # ProjectRef is weak (it's owned by the mainGroup hierarchy).
2712      project_ref = PBXFileReference({
2713            'lastKnownFileType': 'wrapper.pb-project',
2714            'path':              other_path,
2715            'sourceTree':        'SOURCE_ROOT',
2716          })
2717      self.ProjectsGroup().AppendChild(project_ref)
2718
2719      ref_dict = {'ProductGroup': product_group, 'ProjectRef': project_ref}
2720      self._other_pbxprojects[other_pbxproject] = ref_dict
2721      self.AppendProperty('projectReferences', ref_dict)
2722
2723      # Xcode seems to sort this list case-insensitively
2724      self._properties['projectReferences'] = \
2725          sorted(self._properties['projectReferences'], cmp=lambda x,y:
2726                 cmp(x['ProjectRef'].Name().lower(),
2727                     y['ProjectRef'].Name().lower()))
2728    else:
2729      # The link already exists.  Pull out the relevnt data.
2730      project_ref_dict = self._other_pbxprojects[other_pbxproject]
2731      product_group = project_ref_dict['ProductGroup']
2732      project_ref = project_ref_dict['ProjectRef']
2733
2734    self._SetUpProductReferences(other_pbxproject, product_group, project_ref)
2735
2736    return [product_group, project_ref]
2737
2738  def _SetUpProductReferences(self, other_pbxproject, product_group,
2739                              project_ref):
2740    # TODO(mark): This only adds references to products in other_pbxproject
2741    # when they don't exist in this pbxproject.  Perhaps it should also
2742    # remove references from this pbxproject that are no longer present in
2743    # other_pbxproject.  Perhaps it should update various properties if they
2744    # change.
2745    for target in other_pbxproject._properties['targets']:
2746      if not isinstance(target, PBXNativeTarget):
2747        continue
2748
2749      other_fileref = target._properties['productReference']
2750      if product_group.GetChildByRemoteObject(other_fileref) is None:
2751        # Xcode sets remoteInfo to the name of the target and not the name
2752        # of its product, despite this proxy being a reference to the product.
2753        container_item = PBXContainerItemProxy({
2754              'containerPortal':      project_ref,
2755              'proxyType':            2,
2756              'remoteGlobalIDString': other_fileref,
2757              'remoteInfo':           target.Name()
2758            })
2759        # TODO(mark): Does sourceTree get copied straight over from the other
2760        # project?  Can the other project ever have lastKnownFileType here
2761        # instead of explicitFileType?  (Use it if so?)  Can path ever be
2762        # unset?  (I don't think so.)  Can other_fileref have name set, and
2763        # does it impact the PBXReferenceProxy if so?  These are the questions
2764        # that perhaps will be answered one day.
2765        reference_proxy = PBXReferenceProxy({
2766              'fileType':   other_fileref._properties['explicitFileType'],
2767              'path':       other_fileref._properties['path'],
2768              'sourceTree': other_fileref._properties['sourceTree'],
2769              'remoteRef':  container_item,
2770            })
2771
2772        product_group.AppendChild(reference_proxy)
2773
2774  def SortRemoteProductReferences(self):
2775    # For each remote project file, sort the associated ProductGroup in the
2776    # same order that the targets are sorted in the remote project file.  This
2777    # is the sort order used by Xcode.
2778
2779    def CompareProducts(x, y, remote_products):
2780      # x and y are PBXReferenceProxy objects.  Go through their associated
2781      # PBXContainerItem to get the remote PBXFileReference, which will be
2782      # present in the remote_products list.
2783      x_remote = x._properties['remoteRef']._properties['remoteGlobalIDString']
2784      y_remote = y._properties['remoteRef']._properties['remoteGlobalIDString']
2785      x_index = remote_products.index(x_remote)
2786      y_index = remote_products.index(y_remote)
2787
2788      # Use the order of each remote PBXFileReference in remote_products to
2789      # determine the sort order.
2790      return cmp(x_index, y_index)
2791
2792    for other_pbxproject, ref_dict in self._other_pbxprojects.iteritems():
2793      # Build up a list of products in the remote project file, ordered the
2794      # same as the targets that produce them.
2795      remote_products = []
2796      for target in other_pbxproject._properties['targets']:
2797        if not isinstance(target, PBXNativeTarget):
2798          continue
2799        remote_products.append(target._properties['productReference'])
2800
2801      # Sort the PBXReferenceProxy children according to the list of remote
2802      # products.
2803      product_group = ref_dict['ProductGroup']
2804      product_group._properties['children'] = sorted(
2805          product_group._properties['children'],
2806          cmp=lambda x, y: CompareProducts(x, y, remote_products))
2807
2808
2809class XCProjectFile(XCObject):
2810  _schema = XCObject._schema.copy()
2811  _schema.update({
2812    'archiveVersion': [0, int,        0, 1, 1],
2813    'classes':        [0, dict,       0, 1, {}],
2814    'objectVersion':  [0, int,        0, 1, 45],
2815    'rootObject':     [0, PBXProject, 1, 1],
2816  })
2817
2818  def SetXcodeVersion(self, version):
2819    version_to_object_version = {
2820      '2.4': 45,
2821      '3.0': 45,
2822      '3.1': 45,
2823      '3.2': 46,
2824    }
2825    if not version in version_to_object_version:
2826      supported_str = ', '.join(sorted(version_to_object_version.keys()))
2827      raise Exception(
2828          'Unsupported Xcode version %s (supported: %s)' %
2829          ( version, supported_str ) )
2830    compatibility_version = 'Xcode %s' % version
2831    self._properties['rootObject'].SetProperty('compatibilityVersion',
2832                                               compatibility_version)
2833    self.SetProperty('objectVersion', version_to_object_version[version]);
2834
2835  def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
2836    # Although XCProjectFile is implemented here as an XCObject, it's not a
2837    # proper object in the Xcode sense, and it certainly doesn't have its own
2838    # ID.  Pass through an attempt to update IDs to the real root object.
2839    if recursive:
2840      self._properties['rootObject'].ComputeIDs(recursive, overwrite, hash)
2841
2842  def Print(self, file=sys.stdout):
2843    self.VerifyHasRequiredProperties()
2844
2845    # Add the special "objects" property, which will be caught and handled
2846    # separately during printing.  This structure allows a fairly standard
2847    # loop do the normal printing.
2848    self._properties['objects'] = {}
2849    self._XCPrint(file, 0, '// !$*UTF8*$!\n')
2850    if self._should_print_single_line:
2851      self._XCPrint(file, 0, '{ ')
2852    else:
2853      self._XCPrint(file, 0, '{\n')
2854    for property, value in sorted(self._properties.iteritems(),
2855                                  cmp=lambda x, y: cmp(x, y)):
2856      if property == 'objects':
2857        self._PrintObjects(file)
2858      else:
2859        self._XCKVPrint(file, 1, property, value)
2860    self._XCPrint(file, 0, '}\n')
2861    del self._properties['objects']
2862
2863  def _PrintObjects(self, file):
2864    if self._should_print_single_line:
2865      self._XCPrint(file, 0, 'objects = {')
2866    else:
2867      self._XCPrint(file, 1, 'objects = {\n')
2868
2869    objects_by_class = {}
2870    for object in self.Descendants():
2871      if object == self:
2872        continue
2873      class_name = object.__class__.__name__
2874      if not class_name in objects_by_class:
2875        objects_by_class[class_name] = []
2876      objects_by_class[class_name].append(object)
2877
2878    for class_name in sorted(objects_by_class):
2879      self._XCPrint(file, 0, '\n')
2880      self._XCPrint(file, 0, '/* Begin ' + class_name + ' section */\n')
2881      for object in sorted(objects_by_class[class_name],
2882                           cmp=lambda x, y: cmp(x.id, y.id)):
2883        object.Print(file)
2884      self._XCPrint(file, 0, '/* End ' + class_name + ' section */\n')
2885
2886    if self._should_print_single_line:
2887      self._XCPrint(file, 0, '}; ')
2888    else:
2889      self._XCPrint(file, 1, '};\n')
2890