/media/webrtc/trunk/tools/gyp/pylib/gyp/xcodeproj_file.py
Python | 2834 lines | 2642 code | 55 blank | 137 comment | 66 complexity | 5f96f8c76b9100e62d188c2eecf06ea3 MD5 | raw file
Possible License(s): JSON, LGPL-2.1, LGPL-3.0, AGPL-1.0, MIT, MPL-2.0-no-copyleft-exception, Apache-2.0, GPL-2.0, BSD-2-Clause, MPL-2.0, BSD-3-Clause, 0BSD
Large files files are truncated, but you can click here to view the full file
- # Copyright (c) 2012 Google Inc. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Xcode project file generator.
- This module is both an Xcode project file generator and a documentation of the
- Xcode project file format. Knowledge of the project file format was gained
- based on extensive experience with Xcode, and by making changes to projects in
- Xcode.app and observing the resultant changes in the associated project files.
- XCODE PROJECT FILES
- The generator targets the file format as written by Xcode 3.2 (specifically,
- 3.2.6), but past experience has taught that the format has not changed
- significantly in the past several years, and future versions of Xcode are able
- to read older project files.
- Xcode project files are "bundled": the project "file" from an end-user's
- perspective is actually a directory with an ".xcodeproj" extension. The
- project file from this module's perspective is actually a file inside this
- directory, always named "project.pbxproj". This file contains a complete
- description of the project and is all that is needed to use the xcodeproj.
- Other files contained in the xcodeproj directory are simply used to store
- per-user settings, such as the state of various UI elements in the Xcode
- application.
- The project.pbxproj file is a property list, stored in a format almost
- identical to the NeXTstep property list format. The file is able to carry
- Unicode data, and is encoded in UTF-8. The root element in the property list
- is a dictionary that contains several properties of minimal interest, and two
- properties of immense interest. The most important property is a dictionary
- named "objects". The entire structure of the project is represented by the
- children of this property. The objects dictionary is keyed by unique 96-bit
- values represented by 24 uppercase hexadecimal characters. Each value in the
- objects dictionary is itself a dictionary, describing an individual object.
- Each object in the dictionary is a member of a class, which is identified by
- the "isa" property of each object. A variety of classes are represented in a
- project file. Objects can refer to other objects by ID, using the 24-character
- hexadecimal object key. A project's objects form a tree, with a root object
- of class PBXProject at the root. As an example, the PBXProject object serves
- as parent to an XCConfigurationList object defining the build configurations
- used in the project, a PBXGroup object serving as a container for all files
- referenced in the project, and a list of target objects, each of which defines
- a target in the project. There are several different types of target object,
- such as PBXNativeTarget and PBXAggregateTarget. In this module, this
- relationship is expressed by having each target type derive from an abstract
- base named XCTarget.
- The project.pbxproj file's root dictionary also contains a property, sibling to
- the "objects" dictionary, named "rootObject". The value of rootObject is a
- 24-character object key referring to the root PBXProject object in the
- objects dictionary.
- In Xcode, every file used as input to a target or produced as a final product
- of a target must appear somewhere in the hierarchy rooted at the PBXGroup
- object referenced by the PBXProject's mainGroup property. A PBXGroup is
- generally represented as a folder in the Xcode application. PBXGroups can
- contain other PBXGroups as well as PBXFileReferences, which are pointers to
- actual files.
- Each XCTarget contains a list of build phases, represented in this module by
- the abstract base XCBuildPhase. Examples of concrete XCBuildPhase derivations
- are PBXSourcesBuildPhase and PBXFrameworksBuildPhase, which correspond to the
- "Compile Sources" and "Link Binary With Libraries" phases displayed in the
- Xcode application. Files used as input to these phases (for example, source
- files in the former case and libraries and frameworks in the latter) are
- represented by PBXBuildFile objects, referenced by elements of "files" lists
- in XCTarget objects. Each PBXBuildFile object refers to a PBXBuildFile
- object as a "weak" reference: it does not "own" the PBXBuildFile, which is
- owned by the root object's mainGroup or a descendant group. In most cases, the
- layer of indirection between an XCBuildPhase and a PBXFileReference via a
- PBXBuildFile appears extraneous, but there's actually one reason for this:
- file-specific compiler flags are added to the PBXBuildFile object so as to
- allow a single file to be a member of multiple targets while having distinct
- compiler flags for each. These flags can be modified in the Xcode applciation
- in the "Build" tab of a File Info window.
- When a project is open in the Xcode application, Xcode will rewrite it. As
- such, this module is careful to adhere to the formatting used by Xcode, to
- avoid insignificant changes appearing in the file when it is used in the
- Xcode application. This will keep version control repositories happy, and
- makes it possible to compare a project file used in Xcode to one generated by
- this module to determine if any significant changes were made in the
- application.
- Xcode has its own way of assigning 24-character identifiers to each object,
- which is not duplicated here. Because the identifier only is only generated
- once, when an object is created, and is then left unchanged, there is no need
- to attempt to duplicate Xcode's behavior in this area. The generator is free
- to select any identifier, even at random, to refer to the objects it creates,
- and Xcode will retain those identifiers and use them when subsequently
- rewriting the project file. However, the generator would choose new random
- identifiers each time the project files are generated, leading to difficulties
- comparing "used" project files to "pristine" ones produced by this module,
- and causing the appearance of changes as every object identifier is changed
- when updated projects are checked in to a version control repository. To
- mitigate this problem, this module chooses identifiers in a more deterministic
- way, by hashing a description of each object as well as its parent and ancestor
- objects. This strategy should result in minimal "shift" in IDs as successive
- generations of project files are produced.
- THIS MODULE
- This module introduces several classes, all derived from the XCObject class.
- Nearly all of the "brains" are built into the XCObject class, which understands
- how to create and modify objects, maintain the proper tree structure, compute
- identifiers, and print objects. For the most part, classes derived from
- XCObject need only provide a _schema class object, a dictionary that
- expresses what properties objects of the class may contain.
- Given this structure, it's possible to build a minimal project file by creating
- objects of the appropriate types and making the proper connections:
- config_list = XCConfigurationList()
- group = PBXGroup()
- project = PBXProject({'buildConfigurationList': config_list,
- 'mainGroup': group})
- With the project object set up, it can be added to an XCProjectFile object.
- XCProjectFile is a pseudo-class in the sense that it is a concrete XCObject
- subclass that does not actually correspond to a class type found in a project
- file. Rather, it is used to represent the project file's root dictionary.
- Printing an XCProjectFile will print the entire project file, including the
- full "objects" dictionary.
- project_file = XCProjectFile({'rootObject': project})
- project_file.ComputeIDs()
- project_file.Print()
- Xcode project files are always encoded in UTF-8. This module will accept
- strings of either the str class or the unicode class. Strings of class str
- are assumed to already be encoded in UTF-8. Obviously, if you're just using
- ASCII, you won't encounter difficulties because ASCII is a UTF-8 subset.
- Strings of class unicode are handled properly and encoded in UTF-8 when
- a project file is output.
- """
- import gyp.common
- import posixpath
- import re
- import struct
- import sys
- # hashlib is supplied as of Python 2.5 as the replacement interface for sha
- # and other secure hashes. In 2.6, sha is deprecated. Import hashlib if
- # available, avoiding a deprecation warning under 2.6. Import sha otherwise,
- # preserving 2.4 compatibility.
- try:
- import hashlib
- _new_sha1 = hashlib.sha1
- except ImportError:
- import sha
- _new_sha1 = sha.new
- # See XCObject._EncodeString. This pattern is used to determine when a string
- # can be printed unquoted. Strings that match this pattern may be printed
- # unquoted. Strings that do not match must be quoted and may be further
- # transformed to be properly encoded. Note that this expression matches the
- # characters listed with "+", for 1 or more occurrences: if a string is empty,
- # it must not match this pattern, because it needs to be encoded as "".
- _unquoted = re.compile('^[A-Za-z0-9$./_]+$')
- # Strings that match this pattern are quoted regardless of what _unquoted says.
- # Oddly, Xcode will quote any string with a run of three or more underscores.
- _quoted = re.compile('___')
- # This pattern should match any character that needs to be escaped by
- # XCObject._EncodeString. See that function.
- _escaped = re.compile('[\\\\"]|[^ -~]')
- # Used by SourceTreeAndPathFromPath
- _path_leading_variable = re.compile('^\$\((.*?)\)(/(.*))?$')
- def SourceTreeAndPathFromPath(input_path):
- """Given input_path, returns a tuple with sourceTree and path values.
- Examples:
- input_path (source_tree, output_path)
- '$(VAR)/path' ('VAR', 'path')
- '$(VAR)' ('VAR', None)
- 'path' (None, 'path')
- """
- source_group_match = _path_leading_variable.match(input_path)
- if source_group_match:
- source_tree = source_group_match.group(1)
- output_path = source_group_match.group(3) # This may be None.
- else:
- source_tree = None
- output_path = input_path
- return (source_tree, output_path)
- def ConvertVariablesToShellSyntax(input_string):
- return re.sub('\$\((.*?)\)', '${\\1}', input_string)
- class XCObject(object):
- """The abstract base of all class types used in Xcode project files.
- Class variables:
- _schema: A dictionary defining the properties of this class. The keys to
- _schema are string property keys as used in project files. Values
- are a list of four or five elements:
- [ is_list, property_type, is_strong, is_required, default ]
- is_list: True if the property described is a list, as opposed
- to a single element.
- property_type: The type to use as the value of the property,
- or if is_list is True, the type to use for each
- element of the value's list. property_type must
- be an XCObject subclass, or one of the built-in
- types str, int, or dict.
- is_strong: If property_type is an XCObject subclass, is_strong
- is True to assert that this class "owns," or serves
- as parent, to the property value (or, if is_list is
- True, values). is_strong must be False if
- property_type is not an XCObject subclass.
- is_required: True if the property is required for the class.
- Note that is_required being True does not preclude
- an empty string ("", in the case of property_type
- str) or list ([], in the case of is_list True) from
- being set for the property.
- default: Optional. If is_requried is True, default may be set
- to provide a default value for objects that do not supply
- their own value. If is_required is True and default
- is not provided, users of the class must supply their own
- value for the property.
- Note that although the values of the array are expressed in
- boolean terms, subclasses provide values as integers to conserve
- horizontal space.
- _should_print_single_line: False in XCObject. Subclasses whose objects
- should be written to the project file in the
- alternate single-line format, such as
- PBXFileReference and PBXBuildFile, should
- set this to True.
- _encode_transforms: Used by _EncodeString to encode unprintable characters.
- The index into this list is the ordinal of the
- character to transform; each value is a string
- used to represent the character in the output. XCObject
- provides an _encode_transforms list suitable for most
- XCObject subclasses.
- _alternate_encode_transforms: Provided for subclasses that wish to use
- the alternate encoding rules. Xcode seems
- to use these rules when printing objects in
- single-line format. Subclasses that desire
- this behavior should set _encode_transforms
- to _alternate_encode_transforms.
- _hashables: A list of XCObject subclasses that can be hashed by ComputeIDs
- to construct this object's ID. Most classes that need custom
- hashing behavior should do it by overriding Hashables,
- but in some cases an object's parent may wish to push a
- hashable value into its child, and it can do so by appending
- to _hashables.
- Attribues:
- id: The object's identifier, a 24-character uppercase hexadecimal string.
- Usually, objects being created should not set id until the entire
- project file structure is built. At that point, UpdateIDs() should
- be called on the root object to assign deterministic values for id to
- each object in the tree.
- parent: The object's parent. This is set by a parent XCObject when a child
- object is added to it.
- _properties: The object's property dictionary. An object's properties are
- described by its class' _schema variable.
- """
- _schema = {}
- _should_print_single_line = False
- # See _EncodeString.
- _encode_transforms = []
- i = 0
- while i < ord(' '):
- _encode_transforms.append('\\U%04x' % i)
- i = i + 1
- _encode_transforms[7] = '\\a'
- _encode_transforms[8] = '\\b'
- _encode_transforms[9] = '\\t'
- _encode_transforms[10] = '\\n'
- _encode_transforms[11] = '\\v'
- _encode_transforms[12] = '\\f'
- _encode_transforms[13] = '\\n'
- _alternate_encode_transforms = list(_encode_transforms)
- _alternate_encode_transforms[9] = chr(9)
- _alternate_encode_transforms[10] = chr(10)
- _alternate_encode_transforms[11] = chr(11)
- def __init__(self, properties=None, id=None, parent=None):
- self.id = id
- self.parent = parent
- self._properties = {}
- self._hashables = []
- self._SetDefaultsFromSchema()
- self.UpdateProperties(properties)
- def __repr__(self):
- try:
- name = self.Name()
- except NotImplementedError:
- return '<%s at 0x%x>' % (self.__class__.__name__, id(self))
- return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
- def Copy(self):
- """Make a copy of this object.
- The new object will have its own copy of lists and dicts. Any XCObject
- objects owned by this object (marked "strong") will be copied in the
- new object, even those found in lists. If this object has any weak
- references to other XCObjects, the same references are added to the new
- object without making a copy.
- """
- that = self.__class__(id=self.id, parent=self.parent)
- for key, value in self._properties.iteritems():
- is_strong = self._schema[key][2]
- if isinstance(value, XCObject):
- if is_strong:
- new_value = value.Copy()
- new_value.parent = that
- that._properties[key] = new_value
- else:
- that._properties[key] = value
- elif isinstance(value, str) or isinstance(value, unicode) or \
- isinstance(value, int):
- that._properties[key] = value
- elif isinstance(value, list):
- if is_strong:
- # If is_strong is True, each element is an XCObject, so it's safe to
- # call Copy.
- that._properties[key] = []
- for item in value:
- new_item = item.Copy()
- new_item.parent = that
- that._properties[key].append(new_item)
- else:
- that._properties[key] = value[:]
- elif isinstance(value, dict):
- # dicts are never strong.
- if is_strong:
- raise TypeError, 'Strong dict for key ' + key + ' in ' + \
- self.__class__.__name__
- else:
- that._properties[key] = value.copy()
- else:
- raise TypeError, 'Unexpected type ' + value.__class__.__name__ + \
- ' for key ' + key + ' in ' + self.__class__.__name__
- return that
- def Name(self):
- """Return the name corresponding to an object.
- Not all objects necessarily need to be nameable, and not all that do have
- a "name" property. Override as needed.
- """
- # If the schema indicates that "name" is required, try to access the
- # property even if it doesn't exist. This will result in a KeyError
- # being raised for the property that should be present, which seems more
- # appropriate than NotImplementedError in this case.
- if 'name' in self._properties or \
- ('name' in self._schema and self._schema['name'][3]):
- return self._properties['name']
- raise NotImplementedError, \
- self.__class__.__name__ + ' must implement Name'
- def Comment(self):
- """Return a comment string for the object.
- Most objects just use their name as the comment, but PBXProject uses
- different values.
- The returned comment is not escaped and does not have any comment marker
- strings applied to it.
- """
- return self.Name()
- def Hashables(self):
- hashables = [self.__class__.__name__]
- name = self.Name()
- if name != None:
- hashables.append(name)
- hashables.extend(self._hashables)
- return hashables
- def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
- """Set "id" properties deterministically.
- An object's "id" property is set based on a hash of its class type and
- name, as well as the class type and name of all ancestor objects. As
- such, it is only advisable to call ComputeIDs once an entire project file
- tree is built.
- If recursive is True, recurse into all descendant objects and update their
- hashes.
- If overwrite is True, any existing value set in the "id" property will be
- replaced.
- """
- def _HashUpdate(hash, data):
- """Update hash with data's length and contents.
- If the hash were updated only with the value of data, it would be
- possible for clowns to induce collisions by manipulating the names of
- their objects. By adding the length, it's exceedingly less likely that
- ID collisions will be encountered, intentionally or not.
- """
- hash.update(struct.pack('>i', len(data)))
- hash.update(data)
- if hash is None:
- hash = _new_sha1()
- hashables = self.Hashables()
- assert len(hashables) > 0
- for hashable in hashables:
- _HashUpdate(hash, hashable)
- if recursive:
- for child in self.Children():
- child.ComputeIDs(recursive, overwrite, hash.copy())
- if overwrite or self.id is None:
- # Xcode IDs are only 96 bits (24 hex characters), but a SHA-1 digest is
- # is 160 bits. Instead of throwing out 64 bits of the digest, xor them
- # into the portion that gets used.
- assert hash.digest_size % 4 == 0
- digest_int_count = hash.digest_size / 4
- digest_ints = struct.unpack('>' + 'I' * digest_int_count, hash.digest())
- id_ints = [0, 0, 0]
- for index in xrange(0, digest_int_count):
- id_ints[index % 3] ^= digest_ints[index]
- self.id = '%08X%08X%08X' % tuple(id_ints)
- def EnsureNoIDCollisions(self):
- """Verifies that no two objects have the same ID. Checks all descendants.
- """
- ids = {}
- descendants = self.Descendants()
- for descendant in descendants:
- if descendant.id in ids:
- other = ids[descendant.id]
- raise KeyError, \
- 'Duplicate ID %s, objects "%s" and "%s" in "%s"' % \
- (descendant.id, str(descendant._properties),
- str(other._properties), self._properties['rootObject'].Name())
- ids[descendant.id] = descendant
- def Children(self):
- """Returns a list of all of this object's owned (strong) children."""
- children = []
- for property, attributes in self._schema.iteritems():
- (is_list, property_type, is_strong) = attributes[0:3]
- if is_strong and property in self._properties:
- if not is_list:
- children.append(self._properties[property])
- else:
- children.extend(self._properties[property])
- return children
- def Descendants(self):
- """Returns a list of all of this object's descendants, including this
- object.
- """
- children = self.Children()
- descendants = [self]
- for child in children:
- descendants.extend(child.Descendants())
- return descendants
- def PBXProjectAncestor(self):
- # The base case for recursion is defined at PBXProject.PBXProjectAncestor.
- if self.parent:
- return self.parent.PBXProjectAncestor()
- return None
- def _EncodeComment(self, comment):
- """Encodes a comment to be placed in the project file output, mimicing
- Xcode behavior.
- """
- # This mimics Xcode behavior by wrapping the comment in "/*" and "*/". If
- # the string already contains a "*/", it is turned into "(*)/". This keeps
- # the file writer from outputting something that would be treated as the
- # end of a comment in the middle of something intended to be entirely a
- # comment.
- return '/* ' + comment.replace('*/', '(*)/') + ' */'
- def _EncodeTransform(self, match):
- # This function works closely with _EncodeString. It will only be called
- # by re.sub with match.group(0) containing a character matched by the
- # the _escaped expression.
- char = match.group(0)
- # Backslashes (\) and quotation marks (") are always replaced with a
- # backslash-escaped version of the same. Everything else gets its
- # replacement from the class' _encode_transforms array.
- if char == '\\':
- return '\\\\'
- if char == '"':
- return '\\"'
- return self._encode_transforms[ord(char)]
- def _EncodeString(self, value):
- """Encodes a string to be placed in the project file output, mimicing
- Xcode behavior.
- """
- # Use quotation marks when any character outside of the range A-Z, a-z, 0-9,
- # $ (dollar sign), . (period), and _ (underscore) is present. Also use
- # quotation marks to represent empty strings.
- #
- # Escape " (double-quote) and \ (backslash) by preceding them with a
- # backslash.
- #
- # Some characters below the printable ASCII range are encoded specially:
- # 7 ^G BEL is encoded as "\a"
- # 8 ^H BS is encoded as "\b"
- # 11 ^K VT is encoded as "\v"
- # 12 ^L NP is encoded as "\f"
- # 127 ^? DEL is passed through as-is without escaping
- # - In PBXFileReference and PBXBuildFile objects:
- # 9 ^I HT is passed through as-is without escaping
- # 10 ^J NL is passed through as-is without escaping
- # 13 ^M CR is passed through as-is without escaping
- # - In other objects:
- # 9 ^I HT is encoded as "\t"
- # 10 ^J NL is encoded as "\n"
- # 13 ^M CR is encoded as "\n" rendering it indistinguishable from
- # 10 ^J NL
- # All other nonprintable characters within the ASCII range (0 through 127
- # inclusive) are encoded as "\U001f" referring to the Unicode code point in
- # hexadecimal. For example, character 14 (^N SO) is encoded as "\U000e".
- # Characters above the ASCII range are passed through to the output encoded
- # as UTF-8 without any escaping. These mappings are contained in the
- # class' _encode_transforms list.
- if _unquoted.search(value) and not _quoted.search(value):
- return value
- return '"' + _escaped.sub(self._EncodeTransform, value) + '"'
- def _XCPrint(self, file, tabs, line):
- file.write('\t' * tabs + line)
- def _XCPrintableValue(self, tabs, value, flatten_list=False):
- """Returns a representation of value that may be printed in a project file,
- mimicing Xcode's behavior.
- _XCPrintableValue can handle str and int values, XCObjects (which are
- made printable by returning their id property), and list and dict objects
- composed of any of the above types. When printing a list or dict, and
- _should_print_single_line is False, the tabs parameter is used to determine
- how much to indent the lines corresponding to the items in the list or
- dict.
- If flatten_list is True, single-element lists will be transformed into
- strings.
- """
- printable = ''
- comment = None
- if self._should_print_single_line:
- sep = ' '
- element_tabs = ''
- end_tabs = ''
- else:
- sep = '\n'
- element_tabs = '\t' * (tabs + 1)
- end_tabs = '\t' * tabs
- if isinstance(value, XCObject):
- printable += value.id
- comment = value.Comment()
- elif isinstance(value, str):
- printable += self._EncodeString(value)
- elif isinstance(value, unicode):
- printable += self._EncodeString(value.encode('utf-8'))
- elif isinstance(value, int):
- printable += str(value)
- elif isinstance(value, list):
- if flatten_list and len(value) <= 1:
- if len(value) == 0:
- printable += self._EncodeString('')
- else:
- printable += self._EncodeString(value[0])
- else:
- printable = '(' + sep
- for item in value:
- printable += element_tabs + \
- self._XCPrintableValue(tabs + 1, item, flatten_list) + \
- ',' + sep
- printable += end_tabs + ')'
- elif isinstance(value, dict):
- printable = '{' + sep
- for item_key, item_value in sorted(value.iteritems()):
- printable += element_tabs + \
- self._XCPrintableValue(tabs + 1, item_key, flatten_list) + ' = ' + \
- self._XCPrintableValue(tabs + 1, item_value, flatten_list) + ';' + \
- sep
- printable += end_tabs + '}'
- else:
- raise TypeError, "Can't make " + value.__class__.__name__ + ' printable'
- if comment != None:
- printable += ' ' + self._EncodeComment(comment)
- return printable
- def _XCKVPrint(self, file, tabs, key, value):
- """Prints a key and value, members of an XCObject's _properties dictionary,
- to file.
- tabs is an int identifying the indentation level. If the class'
- _should_print_single_line variable is True, tabs is ignored and the
- key-value pair will be followed by a space insead of a newline.
- """
- if self._should_print_single_line:
- printable = ''
- after_kv = ' '
- else:
- printable = '\t' * tabs
- after_kv = '\n'
- # Xcode usually prints remoteGlobalIDString values in PBXContainerItemProxy
- # objects without comments. Sometimes it prints them with comments, but
- # the majority of the time, it doesn't. To avoid unnecessary changes to
- # the project file after Xcode opens it, don't write comments for
- # remoteGlobalIDString. This is a sucky hack and it would certainly be
- # cleaner to extend the schema to indicate whether or not a comment should
- # be printed, but since this is the only case where the problem occurs and
- # Xcode itself can't seem to make up its mind, the hack will suffice.
- #
- # Also see PBXContainerItemProxy._schema['remoteGlobalIDString'].
- if key == 'remoteGlobalIDString' and isinstance(self,
- PBXContainerItemProxy):
- value_to_print = value.id
- else:
- value_to_print = value
- # PBXBuildFile's settings property is represented in the output as a dict,
- # but a hack here has it represented as a string. Arrange to strip off the
- # quotes so that it shows up in the output as expected.
- if key == 'settings' and isinstance(self, PBXBuildFile):
- strip_value_quotes = True
- else:
- strip_value_quotes = False
- # In another one-off, let's set flatten_list on buildSettings properties
- # of XCBuildConfiguration objects, because that's how Xcode treats them.
- if key == 'buildSettings' and isinstance(self, XCBuildConfiguration):
- flatten_list = True
- else:
- flatten_list = False
- try:
- printable_key = self._XCPrintableValue(tabs, key, flatten_list)
- printable_value = self._XCPrintableValue(tabs, value_to_print,
- flatten_list)
- if strip_value_quotes and len(printable_value) > 1 and \
- printable_value[0] == '"' and printable_value[-1] == '"':
- printable_value = printable_value[1:-1]
- printable += printable_key + ' = ' + printable_value + ';' + after_kv
- except TypeError, e:
- gyp.common.ExceptionAppend(e,
- 'while printing key "%s"' % key)
- raise
- self._XCPrint(file, 0, printable)
- def Print(self, file=sys.stdout):
- """Prints a reprentation of this object to file, adhering to Xcode output
- formatting.
- """
- self.VerifyHasRequiredProperties()
- if self._should_print_single_line:
- # When printing an object in a single line, Xcode doesn't put any space
- # between the beginning of a dictionary (or presumably a list) and the
- # first contained item, so you wind up with snippets like
- # ...CDEF = {isa = PBXFileReference; fileRef = 0123...
- # If it were me, I would have put a space in there after the opening
- # curly, but I guess this is just another one of those inconsistencies
- # between how Xcode prints PBXFileReference and PBXBuildFile objects as
- # compared to other objects. Mimic Xcode's behavior here by using an
- # empty string for sep.
- sep = ''
- end_tabs = 0
- else:
- sep = '\n'
- end_tabs = 2
- # Start the object. For example, '\t\tPBXProject = {\n'.
- self._XCPrint(file, 2, self._XCPrintableValue(2, self) + ' = {' + sep)
- # "isa" isn't in the _properties dictionary, it's an intrinsic property
- # of the class which the object belongs to. Xcode always outputs "isa"
- # as the first element of an object dictionary.
- self._XCKVPrint(file, 3, 'isa', self.__class__.__name__)
- # The remaining elements of an object dictionary are sorted alphabetically.
- for property, value in sorted(self._properties.iteritems()):
- self._XCKVPrint(file, 3, property, value)
- # End the object.
- self._XCPrint(file, end_tabs, '};\n')
- def UpdateProperties(self, properties, do_copy=False):
- """Merge the supplied properties into the _properties dictionary.
- The input properties must adhere to the class schema or a KeyError or
- TypeError exception will be raised. If adding an object of an XCObject
- subclass and the schema indicates a strong relationship, the object's
- parent will be set to this object.
- If do_copy is True, then lists, dicts, strong-owned XCObjects, and
- strong-owned XCObjects in lists will be copied instead of having their
- references added.
- """
- if properties is None:
- return
- for property, value in properties.iteritems():
- # Make sure the property is in the schema.
- if not property in self._schema:
- raise KeyError, property + ' not in ' + self.__class__.__name__
- # Make sure the property conforms to the schema.
- (is_list, property_type, is_strong) = self._schema[property][0:3]
- if is_list:
- if value.__class__ != list:
- raise TypeError, \
- property + ' of ' + self.__class__.__name__ + \
- ' must be list, not ' + value.__class__.__name__
- for item in value:
- if not isinstance(item, property_type) and \
- not (item.__class__ == unicode and property_type == str):
- # Accept unicode where str is specified. str is treated as
- # UTF-8-encoded.
- raise TypeError, \
- 'item of ' + property + ' of ' + self.__class__.__name__ + \
- ' must be ' + property_type.__name__ + ', not ' + \
- item.__class__.__name__
- elif not isinstance(value, property_type) and \
- not (value.__class__ == unicode and property_type == str):
- # Accept unicode where str is specified. str is treated as
- # UTF-8-encoded.
- raise TypeError, \
- property + ' of ' + self.__class__.__name__ + ' must be ' + \
- property_type.__name__ + ', not ' + value.__class__.__name__
- # Checks passed, perform the assignment.
- if do_copy:
- if isinstance(value, XCObject):
- if is_strong:
- self._properties[property] = value.Copy()
- else:
- self._properties[property] = value
- elif isinstance(value, str) or isinstance(value, unicode) or \
- isinstance(value, int):
- self._properties[property] = value
- elif isinstance(value, list):
- if is_strong:
- # If is_strong is True, each element is an XCObject, so it's safe
- # to call Copy.
- self._properties[property] = []
- for item in value:
- self._properties[property].append(item.Copy())
- else:
- self._properties[property] = value[:]
- elif isinstance(value, dict):
- self._properties[property] = value.copy()
- else:
- raise TypeError, "Don't know how to copy a " + \
- value.__class__.__name__ + ' object for ' + \
- property + ' in ' + self.__class__.__name__
- else:
- self._properties[property] = value
- # Set up the child's back-reference to this object. Don't use |value|
- # any more because it may not be right if do_copy is true.
- if is_strong:
- if not is_list:
- self._properties[property].parent = self
- else:
- for item in self._properties[property]:
- item.parent = self
- def HasProperty(self, key):
- return key in self._properties
- def GetProperty(self, key):
- return self._properties[key]
- def SetProperty(self, key, value):
- self.UpdateProperties({key: value})
- def DelProperty(self, key):
- if key in self._properties:
- del self._properties[key]
- def AppendProperty(self, key, value):
- # TODO(mark): Support ExtendProperty too (and make this call that)?
- # Schema validation.
- if not key in self._schema:
- raise KeyError, key + ' not in ' + self.__class__.__name__
- (is_list, property_type, is_strong) = self._schema[key][0:3]
- if not is_list:
- raise TypeError, key + ' of ' + self.__class__.__name__ + ' must be list'
- if not isinstance(value, property_type):
- raise TypeError, 'item of ' + key + ' of ' + self.__class__.__name__ + \
- ' must be ' + property_type.__name__ + ', not ' + \
- value.__class__.__name__
- # If the property doesn't exist yet, create a new empty list to receive the
- # item.
- if not key in self._properties:
- self._properties[key] = []
- # Set up the ownership link.
- if is_strong:
- value.parent = self
- # Store the item.
- self._properties[key].append(value)
- def VerifyHasRequiredProperties(self):
- """Ensure that all properties identified as required by the schema are
- set.
- """
- # TODO(mark): A stronger verification mechanism is needed. Some
- # subclasses need to perform validation beyond what the schema can enforce.
- for property, attributes in self._schema.iteritems():
- (is_list, property_type, is_strong, is_required) = attributes[0:4]
- if is_required and not property in self._properties:
- raise KeyError, self.__class__.__name__ + ' requires ' + property
- def _SetDefaultsFromSchema(self):
- """Assign object default values according to the schema. This will not
- overwrite properties that have already been set."""
- defaults = {}
- for property, attributes in self._schema.iteritems():
- (is_list, property_type, is_strong, is_required) = attributes[0:4]
- if is_required and len(attributes) >= 5 and \
- not property in self._properties:
- default = attributes[4]
- defaults[property] = default
- if len(defaults) > 0:
- # Use do_copy=True so that each new object gets its own copy of strong
- # objects, lists, and dicts.
- self.UpdateProperties(defaults, do_copy=True)
- class XCHierarchicalElement(XCObject):
- """Abstract base for PBXGroup and PBXFileReference. Not represented in a
- project file."""
- # TODO(mark): Do name and path belong here? Probably so.
- # If path is set and name is not, name may have a default value. Name will
- # be set to the basename of path, if the basename of path is different from
- # the full value of path. If path is already just a leaf name, name will
- # not be set.
- _schema = XCObject._schema.copy()
- _schema.update({
- 'comments': [0, str, 0, 0],
- 'fileEncoding': [0, str, 0, 0],
- 'includeInIndex': [0, int, 0, 0],
- 'indentWidth': [0, int, 0, 0],
- 'lineEnding': [0, int, 0, 0],
- 'sourceTree': [0, str, 0, 1, '<group>'],
- 'tabWidth': [0, int, 0, 0],
- 'usesTabs': [0, int, 0, 0],
- 'wrapsLines': [0, int, 0, 0],
- })
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCObject.__init__(self, properties, id, parent)
- if 'path' in self._properties and not 'name' in self._properties:
- path = self._properties['path']
- name = posixpath.basename(path)
- if name != '' and path != name:
- self.SetProperty('name', name)
- if 'path' in self._properties and \
- (not 'sourceTree' in self._properties or \
- self._properties['sourceTree'] == '<group>'):
- # If the pathname begins with an Xcode variable like "$(SDKROOT)/", take
- # the variable out and make the path be relative to that variable by
- # assigning the variable name as the sourceTree.
- (source_tree, path) = SourceTreeAndPathFromPath(self._properties['path'])
- if source_tree != None:
- self._properties['sourceTree'] = source_tree
- if path != None:
- self._properties['path'] = path
- if source_tree != None and path is None and \
- not 'name' in self._properties:
- # The path was of the form "$(SDKROOT)" with no path following it.
- # This object is now relative to that variable, so it has no path
- # attribute of its own. It does, however, keep a name.
- del self._properties['path']
- self._properties['name'] = source_tree
- def Name(self):
- if 'name' in self._properties:
- return self._properties['name']
- elif 'path' in self._properties:
- return self._properties['path']
- else:
- # This happens in the case of the root PBXGroup.
- return None
- def Hashables(self):
- """Custom hashables for XCHierarchicalElements.
- XCHierarchicalElements are special. Generally, their hashes shouldn't
- change if the paths don't change. The normal XCObject implementation of
- Hashables adds a hashable for each object, which means that if
- the hierarchical structure changes (possibly due to changes caused when
- TakeOverOnlyChild runs and encounters slight changes in the hierarchy),
- the hashes will change. For example, if a project file initially contains
- a/b/f1 and a/b becomes collapsed into a/b, f1 will have a single parent
- a/b. If someone later adds a/f2 to the project file, a/b can no longer be
- collapsed, and f1 winds up with parent b and grandparent a. That would
- be sufficient to change f1's hash.
- To counteract this problem, hashables for all XCHierarchicalElements except
- for the main group (which has neither a name nor a path) are taken to be
- just the set of path components. Because hashables are inherited from
- parents, this provides assurance that a/b/f1 has the same set of hashables
- whether its parent is b or a/b.
- The main group is a special case. As it is permitted to have no name or
- path, it is permitted to use the standard XCObject hash mechanism. This
- is not considered a problem because there can be only one main group.
- """
- if self == self.PBXProjectAncestor()._properties['mainGroup']:
- # super
- return XCObject.Hashables(self)
- hashables = []
- # Put the name in first, ensuring that if TakeOverOnlyChild collapses
- # children into a top-level group like "Source", the name always goes
- # into the list of hashables without interfering with path components.
- if 'name' in self._properties:
- # Make it less likely for people to manipulate hashes by following the
- # pattern of always pushing an object type value onto the list first.
- hashables.append(self.__class__.__name__ + '.name')
- hashables.append(self._properties['name'])
- # NOTE: This still has the problem that if an absolute path is encountered,
- # including paths with a sourceTree, they'll still inherit their parents'
- # hashables, even though the paths aren't relative to their parents. This
- # is not expected to be much of a problem in practice.
- path = self.PathFromSourceTreeAndPath()
- if path != None:
- components = path.split(posixpath.sep)
- for component in components:
- hashables.append(self.__class__.__name__ + '.path')
- hashables.append(component)
- hashables.extend(self._hashables)
- return hashables
- def Compare(self, other):
- # Allow comparison of these types. PBXGroup has the highest sort rank;
- # PBXVariantGroup is treated as equal to PBXFileReference.
- valid_class_types = {
- PBXFileReference: 'file',
- PBXGroup: 'group',
- PBXVariantGroup: 'file',
- }
- self_type = valid_class_types[self.__class__]
- other_type = valid_class_types[other.__class__]
- if self_type == other_type:
- # If the two objects are of the same sort rank, compare their names.
- return cmp(self.Name(), other.Name())
- # Otherwise, sort groups before everything else.
- if self_type == 'group':
- return -1
- return 1
- def CompareRootGroup(self, other):
- # This function should be used only to compare direct children of the
- # containing PBXProject's mainGroup. These groups should appear in the
- # listed order.
- # TODO(mark): "Build" is used by gyp.generator.xcode, perhaps the
- # generator should have a way of influencing this list rather than having
- # to hardcode for the generator here.
- order = ['Source', 'Intermediates', 'Projects', 'Frameworks', 'Products',
- 'Build']
- # If the groups aren't in the listed order, do a name comparison.
- # Otherwise, groups in the listed order should come before those that
- # aren't.
- self_name = self.Name()
- other_name = other.Name()
- self_in = isinstance(self, PBXGroup) and self_name in order
- other_in = isinstance(self, PBXGroup) and other_name in order
- if not self_in and not other_in:
- return self.Compare(other)
- if self_name in order and not other_name in order:
- return -1
- if other_name in order and not self_name in order:
- return 1
- # If both groups are in the listed order, go by the defined order.
- self_index = order.index(self_name)
- other_index = order.index(other_name)
- if self_index < other_index:
- return -1
- if self_index > other_index:
- return 1
- return 0
- def PathFromSourceTreeAndPath(self):
- # Turn the object's sourceTree and path properties into a single flat
- # string of a form comparable to the path parameter. If there's a
- # sourceTree property other than "<group>", wrap it in $(...) for the
- # comparison.
- components = []
- if self._properties['sourceTree'] != '<group>':
- components.append('$(' + self._properties['sourceTree'] + ')')
- if 'path' in self._properties:
- components.append(self._properties['path'])
- if len(components) > 0:
- return posixpath.join(*components)
- return None
- def FullPath(self):
- # Returns a full path to self relative to the project file, or relative
- # to some other source tree. Start with self, and walk up the chain of
- # parents prepending their paths, if any, until no more parents are
- # available (project-relative path) or until a path relative to some
- # source tree is found.
- xche = self
- path = None
- while isinstance(xche, XCHierarchicalElement) and \
- (path is None or \
- (not path.startswith('/') and not path.startswith('$'))):
- this_path = xche.PathFromSourceTreeAndPath()
- if this_path != None and path != None:
- path = posixpath.join(this_path, path)
- elif this_path != None:
- path = this_path
- xche = xche.parent
- return path
- class PBXGroup(XCHierarchicalElement):
- """
- Attributes:
- _children_by_path: Maps pathnames of children of this PBXGroup to the
- actual child XCHierarchicalElement objects.
- _variant_children_by_name_and_path: Maps (name, path) tuples of
- PBXVariantGroup children to the actual child PBXVariantGroup objects.
- """
- _schema = XCHierarchicalElement._schema.copy()
- _schema.update({
- 'children': [1, XCHierarchicalElement, 1, 1, []],
- 'name': [0, str, 0, 0],
- 'path': [0, str, 0, 0],
- })
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCHierarchicalElement.__init__(self, properties, id, parent)
- self._children_by_path = {}
- self._variant_children_by_name_and_path = {}
- for child in self._properties.get('children', []):
- self._AddChildToDicts(child)
- def _AddChildToDicts(self, child):
- # Sets up this PBXGroup object's dicts to reference the child properly.
- child_path = child.PathFromSourceTreeAndPath()
- if child_path:
- if child_path in self._children_by_path:
- raise ValueError, 'Found multiple children with path ' + child_path
- self._children_by_path[child_path] = child
- if isinstance(child, PBXVariantGroup):
- child_name = child._properties.get('name', None)
- key = (child_name, child_path)
- if key in self._variant_children_by_name_and_path:
- raise ValueError, 'Found multiple PBXVariantGroup children with ' + \
- 'name ' + str(child_name) + ' and path ' + \
- str(child_path)
- self._variant_children_by_name_and_path[key] = child
- def AppendChild(self, child):
- # Callers should use this instead of calling
- # AppendProperty('children', child) directly because this function
- # maintains the group's dicts.
- self.AppendProperty('children', child)
- self._AddChildToDicts(child)
- def GetChildByName(self, name):
- # This is not currently optimized with a dict as GetChildByPath is because
- # it has few callers. Most callers probably want GetChildByPath. This
- # function is only useful to get children that have names but no paths,
- # which is rare. The children of the main group ("Source", "Products",
- # etc.) is pretty much the only case where this likely to come up.
- #
- # TODO(mark): Maybe this should raise an error if more than one child is
- # present with the same name.
- if not 'children' in self._properties:
- return None
- for child in self._properties['children']:
- if child.Name() == name:
- return child
- return None
- def GetChildByPath(self, path):
- if not path:
- return None
- if path in self._children_by_path:
- return self._children_by_path[path]
- return None
- def GetChildByRemoteObject(self, remote_object):
- # This method is a little bit esoteric. Given a remote_object, which
- # should be a PBXFileReference in another project file, this method will
- # return this group's PBXReferenceProxy object serving as a local proxy
- # for the remote PBXFileReference.
- #
- # This function might benefit from a dict optimization as GetChildByPath
- # for some workloads, but profiling shows that it's not currently a
- # problem.
- if not 'children' in self._properties:
- return None
- for child in self._properties['children']:
- if not isinstance(child, PBXReferenceProxy):
- continue
- container_proxy = child._properties['remoteRef']
- if container_proxy._properties['remoteGlobalIDString'] == remote_object:
- return child
- return None
- def AddOrGetFileByPath(self, path, hierarchical):
- """Returns an existing or new file reference corresponding to path.
- If hierarchical is True, this method will create or use the necessary
- hierarchical group structure corresponding to path. Otherwise, it will
- look in and create an item in the current group only.
- If an existing matching reference is found, it is returned, otherwise, a
- new one will be created, added to the correct group, and returned.
- If path identifies a directory by virtue of carrying a trailing slash,
- this method returns a PBXFileReference of "folder" type. If path
- identifies a variant, by virtue of it identifying a…
Large files files are truncated, but you can click here to view the full file