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