PageRenderTime 60ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/media/webrtc/trunk/tools/gyp/pylib/gyp/xcodeproj_file.py

https://bitbucket.org/hsoft/mozilla-central
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

  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. """Xcode project file generator.
  5. This module is both an Xcode project file generator and a documentation of the
  6. Xcode project file format. Knowledge of the project file format was gained
  7. based on extensive experience with Xcode, and by making changes to projects in
  8. Xcode.app and observing the resultant changes in the associated project files.
  9. XCODE PROJECT FILES
  10. The generator targets the file format as written by Xcode 3.2 (specifically,
  11. 3.2.6), but past experience has taught that the format has not changed
  12. significantly in the past several years, and future versions of Xcode are able
  13. to read older project files.
  14. Xcode project files are "bundled": the project "file" from an end-user's
  15. perspective is actually a directory with an ".xcodeproj" extension. The
  16. project file from this module's perspective is actually a file inside this
  17. directory, always named "project.pbxproj". This file contains a complete
  18. description of the project and is all that is needed to use the xcodeproj.
  19. Other files contained in the xcodeproj directory are simply used to store
  20. per-user settings, such as the state of various UI elements in the Xcode
  21. application.
  22. The project.pbxproj file is a property list, stored in a format almost
  23. identical to the NeXTstep property list format. The file is able to carry
  24. Unicode data, and is encoded in UTF-8. The root element in the property list
  25. is a dictionary that contains several properties of minimal interest, and two
  26. properties of immense interest. The most important property is a dictionary
  27. named "objects". The entire structure of the project is represented by the
  28. children of this property. The objects dictionary is keyed by unique 96-bit
  29. values represented by 24 uppercase hexadecimal characters. Each value in the
  30. objects dictionary is itself a dictionary, describing an individual object.
  31. Each object in the dictionary is a member of a class, which is identified by
  32. the "isa" property of each object. A variety of classes are represented in a
  33. project file. Objects can refer to other objects by ID, using the 24-character
  34. hexadecimal object key. A project's objects form a tree, with a root object
  35. of class PBXProject at the root. As an example, the PBXProject object serves
  36. as parent to an XCConfigurationList object defining the build configurations
  37. used in the project, a PBXGroup object serving as a container for all files
  38. referenced in the project, and a list of target objects, each of which defines
  39. a target in the project. There are several different types of target object,
  40. such as PBXNativeTarget and PBXAggregateTarget. In this module, this
  41. relationship is expressed by having each target type derive from an abstract
  42. base named XCTarget.
  43. The project.pbxproj file's root dictionary also contains a property, sibling to
  44. the "objects" dictionary, named "rootObject". The value of rootObject is a
  45. 24-character object key referring to the root PBXProject object in the
  46. objects dictionary.
  47. In Xcode, every file used as input to a target or produced as a final product
  48. of a target must appear somewhere in the hierarchy rooted at the PBXGroup
  49. object referenced by the PBXProject's mainGroup property. A PBXGroup is
  50. generally represented as a folder in the Xcode application. PBXGroups can
  51. contain other PBXGroups as well as PBXFileReferences, which are pointers to
  52. actual files.
  53. Each XCTarget contains a list of build phases, represented in this module by
  54. the abstract base XCBuildPhase. Examples of concrete XCBuildPhase derivations
  55. are PBXSourcesBuildPhase and PBXFrameworksBuildPhase, which correspond to the
  56. "Compile Sources" and "Link Binary With Libraries" phases displayed in the
  57. Xcode application. Files used as input to these phases (for example, source
  58. files in the former case and libraries and frameworks in the latter) are
  59. represented by PBXBuildFile objects, referenced by elements of "files" lists
  60. in XCTarget objects. Each PBXBuildFile object refers to a PBXBuildFile
  61. object as a "weak" reference: it does not "own" the PBXBuildFile, which is
  62. owned by the root object's mainGroup or a descendant group. In most cases, the
  63. layer of indirection between an XCBuildPhase and a PBXFileReference via a
  64. PBXBuildFile appears extraneous, but there's actually one reason for this:
  65. file-specific compiler flags are added to the PBXBuildFile object so as to
  66. allow a single file to be a member of multiple targets while having distinct
  67. compiler flags for each. These flags can be modified in the Xcode applciation
  68. in the "Build" tab of a File Info window.
  69. When a project is open in the Xcode application, Xcode will rewrite it. As
  70. such, this module is careful to adhere to the formatting used by Xcode, to
  71. avoid insignificant changes appearing in the file when it is used in the
  72. Xcode application. This will keep version control repositories happy, and
  73. makes it possible to compare a project file used in Xcode to one generated by
  74. this module to determine if any significant changes were made in the
  75. application.
  76. Xcode has its own way of assigning 24-character identifiers to each object,
  77. which is not duplicated here. Because the identifier only is only generated
  78. once, when an object is created, and is then left unchanged, there is no need
  79. to attempt to duplicate Xcode's behavior in this area. The generator is free
  80. to select any identifier, even at random, to refer to the objects it creates,
  81. and Xcode will retain those identifiers and use them when subsequently
  82. rewriting the project file. However, the generator would choose new random
  83. identifiers each time the project files are generated, leading to difficulties
  84. comparing "used" project files to "pristine" ones produced by this module,
  85. and causing the appearance of changes as every object identifier is changed
  86. when updated projects are checked in to a version control repository. To
  87. mitigate this problem, this module chooses identifiers in a more deterministic
  88. way, by hashing a description of each object as well as its parent and ancestor
  89. objects. This strategy should result in minimal "shift" in IDs as successive
  90. generations of project files are produced.
  91. THIS MODULE
  92. This module introduces several classes, all derived from the XCObject class.
  93. Nearly all of the "brains" are built into the XCObject class, which understands
  94. how to create and modify objects, maintain the proper tree structure, compute
  95. identifiers, and print objects. For the most part, classes derived from
  96. XCObject need only provide a _schema class object, a dictionary that
  97. expresses what properties objects of the class may contain.
  98. Given this structure, it's possible to build a minimal project file by creating
  99. objects of the appropriate types and making the proper connections:
  100. config_list = XCConfigurationList()
  101. group = PBXGroup()
  102. project = PBXProject({'buildConfigurationList': config_list,
  103. 'mainGroup': group})
  104. With the project object set up, it can be added to an XCProjectFile object.
  105. XCProjectFile is a pseudo-class in the sense that it is a concrete XCObject
  106. subclass that does not actually correspond to a class type found in a project
  107. file. Rather, it is used to represent the project file's root dictionary.
  108. Printing an XCProjectFile will print the entire project file, including the
  109. full "objects" dictionary.
  110. project_file = XCProjectFile({'rootObject': project})
  111. project_file.ComputeIDs()
  112. project_file.Print()
  113. Xcode project files are always encoded in UTF-8. This module will accept
  114. strings of either the str class or the unicode class. Strings of class str
  115. are assumed to already be encoded in UTF-8. Obviously, if you're just using
  116. ASCII, you won't encounter difficulties because ASCII is a UTF-8 subset.
  117. Strings of class unicode are handled properly and encoded in UTF-8 when
  118. a project file is output.
  119. """
  120. import gyp.common
  121. import posixpath
  122. import re
  123. import struct
  124. import sys
  125. # hashlib is supplied as of Python 2.5 as the replacement interface for sha
  126. # and other secure hashes. In 2.6, sha is deprecated. Import hashlib if
  127. # available, avoiding a deprecation warning under 2.6. Import sha otherwise,
  128. # preserving 2.4 compatibility.
  129. try:
  130. import hashlib
  131. _new_sha1 = hashlib.sha1
  132. except ImportError:
  133. import sha
  134. _new_sha1 = sha.new
  135. # See XCObject._EncodeString. This pattern is used to determine when a string
  136. # can be printed unquoted. Strings that match this pattern may be printed
  137. # unquoted. Strings that do not match must be quoted and may be further
  138. # transformed to be properly encoded. Note that this expression matches the
  139. # characters listed with "+", for 1 or more occurrences: if a string is empty,
  140. # it must not match this pattern, because it needs to be encoded as "".
  141. _unquoted = re.compile('^[A-Za-z0-9$./_]+$')
  142. # Strings that match this pattern are quoted regardless of what _unquoted says.
  143. # Oddly, Xcode will quote any string with a run of three or more underscores.
  144. _quoted = re.compile('___')
  145. # This pattern should match any character that needs to be escaped by
  146. # XCObject._EncodeString. See that function.
  147. _escaped = re.compile('[\\\\"]|[^ -~]')
  148. # Used by SourceTreeAndPathFromPath
  149. _path_leading_variable = re.compile('^\$\((.*?)\)(/(.*))?$')
  150. def SourceTreeAndPathFromPath(input_path):
  151. """Given input_path, returns a tuple with sourceTree and path values.
  152. Examples:
  153. input_path (source_tree, output_path)
  154. '$(VAR)/path' ('VAR', 'path')
  155. '$(VAR)' ('VAR', None)
  156. 'path' (None, 'path')
  157. """
  158. source_group_match = _path_leading_variable.match(input_path)
  159. if source_group_match:
  160. source_tree = source_group_match.group(1)
  161. output_path = source_group_match.group(3) # This may be None.
  162. else:
  163. source_tree = None
  164. output_path = input_path
  165. return (source_tree, output_path)
  166. def ConvertVariablesToShellSyntax(input_string):
  167. return re.sub('\$\((.*?)\)', '${\\1}', input_string)
  168. class XCObject(object):
  169. """The abstract base of all class types used in Xcode project files.
  170. Class variables:
  171. _schema: A dictionary defining the properties of this class. The keys to
  172. _schema are string property keys as used in project files. Values
  173. are a list of four or five elements:
  174. [ is_list, property_type, is_strong, is_required, default ]
  175. is_list: True if the property described is a list, as opposed
  176. to a single element.
  177. property_type: The type to use as the value of the property,
  178. or if is_list is True, the type to use for each
  179. element of the value's list. property_type must
  180. be an XCObject subclass, or one of the built-in
  181. types str, int, or dict.
  182. is_strong: If property_type is an XCObject subclass, is_strong
  183. is True to assert that this class "owns," or serves
  184. as parent, to the property value (or, if is_list is
  185. True, values). is_strong must be False if
  186. property_type is not an XCObject subclass.
  187. is_required: True if the property is required for the class.
  188. Note that is_required being True does not preclude
  189. an empty string ("", in the case of property_type
  190. str) or list ([], in the case of is_list True) from
  191. being set for the property.
  192. default: Optional. If is_requried is True, default may be set
  193. to provide a default value for objects that do not supply
  194. their own value. If is_required is True and default
  195. is not provided, users of the class must supply their own
  196. value for the property.
  197. Note that although the values of the array are expressed in
  198. boolean terms, subclasses provide values as integers to conserve
  199. horizontal space.
  200. _should_print_single_line: False in XCObject. Subclasses whose objects
  201. should be written to the project file in the
  202. alternate single-line format, such as
  203. PBXFileReference and PBXBuildFile, should
  204. set this to True.
  205. _encode_transforms: Used by _EncodeString to encode unprintable characters.
  206. The index into this list is the ordinal of the
  207. character to transform; each value is a string
  208. used to represent the character in the output. XCObject
  209. provides an _encode_transforms list suitable for most
  210. XCObject subclasses.
  211. _alternate_encode_transforms: Provided for subclasses that wish to use
  212. the alternate encoding rules. Xcode seems
  213. to use these rules when printing objects in
  214. single-line format. Subclasses that desire
  215. this behavior should set _encode_transforms
  216. to _alternate_encode_transforms.
  217. _hashables: A list of XCObject subclasses that can be hashed by ComputeIDs
  218. to construct this object's ID. Most classes that need custom
  219. hashing behavior should do it by overriding Hashables,
  220. but in some cases an object's parent may wish to push a
  221. hashable value into its child, and it can do so by appending
  222. to _hashables.
  223. Attribues:
  224. id: The object's identifier, a 24-character uppercase hexadecimal string.
  225. Usually, objects being created should not set id until the entire
  226. project file structure is built. At that point, UpdateIDs() should
  227. be called on the root object to assign deterministic values for id to
  228. each object in the tree.
  229. parent: The object's parent. This is set by a parent XCObject when a child
  230. object is added to it.
  231. _properties: The object's property dictionary. An object's properties are
  232. described by its class' _schema variable.
  233. """
  234. _schema = {}
  235. _should_print_single_line = False
  236. # See _EncodeString.
  237. _encode_transforms = []
  238. i = 0
  239. while i < ord(' '):
  240. _encode_transforms.append('\\U%04x' % i)
  241. i = i + 1
  242. _encode_transforms[7] = '\\a'
  243. _encode_transforms[8] = '\\b'
  244. _encode_transforms[9] = '\\t'
  245. _encode_transforms[10] = '\\n'
  246. _encode_transforms[11] = '\\v'
  247. _encode_transforms[12] = '\\f'
  248. _encode_transforms[13] = '\\n'
  249. _alternate_encode_transforms = list(_encode_transforms)
  250. _alternate_encode_transforms[9] = chr(9)
  251. _alternate_encode_transforms[10] = chr(10)
  252. _alternate_encode_transforms[11] = chr(11)
  253. def __init__(self, properties=None, id=None, parent=None):
  254. self.id = id
  255. self.parent = parent
  256. self._properties = {}
  257. self._hashables = []
  258. self._SetDefaultsFromSchema()
  259. self.UpdateProperties(properties)
  260. def __repr__(self):
  261. try:
  262. name = self.Name()
  263. except NotImplementedError:
  264. return '<%s at 0x%x>' % (self.__class__.__name__, id(self))
  265. return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
  266. def Copy(self):
  267. """Make a copy of this object.
  268. The new object will have its own copy of lists and dicts. Any XCObject
  269. objects owned by this object (marked "strong") will be copied in the
  270. new object, even those found in lists. If this object has any weak
  271. references to other XCObjects, the same references are added to the new
  272. object without making a copy.
  273. """
  274. that = self.__class__(id=self.id, parent=self.parent)
  275. for key, value in self._properties.iteritems():
  276. is_strong = self._schema[key][2]
  277. if isinstance(value, XCObject):
  278. if is_strong:
  279. new_value = value.Copy()
  280. new_value.parent = that
  281. that._properties[key] = new_value
  282. else:
  283. that._properties[key] = value
  284. elif isinstance(value, str) or isinstance(value, unicode) or \
  285. isinstance(value, int):
  286. that._properties[key] = value
  287. elif isinstance(value, list):
  288. if is_strong:
  289. # If is_strong is True, each element is an XCObject, so it's safe to
  290. # call Copy.
  291. that._properties[key] = []
  292. for item in value:
  293. new_item = item.Copy()
  294. new_item.parent = that
  295. that._properties[key].append(new_item)
  296. else:
  297. that._properties[key] = value[:]
  298. elif isinstance(value, dict):
  299. # dicts are never strong.
  300. if is_strong:
  301. raise TypeError, 'Strong dict for key ' + key + ' in ' + \
  302. self.__class__.__name__
  303. else:
  304. that._properties[key] = value.copy()
  305. else:
  306. raise TypeError, 'Unexpected type ' + value.__class__.__name__ + \
  307. ' for key ' + key + ' in ' + self.__class__.__name__
  308. return that
  309. def Name(self):
  310. """Return the name corresponding to an object.
  311. Not all objects necessarily need to be nameable, and not all that do have
  312. a "name" property. Override as needed.
  313. """
  314. # If the schema indicates that "name" is required, try to access the
  315. # property even if it doesn't exist. This will result in a KeyError
  316. # being raised for the property that should be present, which seems more
  317. # appropriate than NotImplementedError in this case.
  318. if 'name' in self._properties or \
  319. ('name' in self._schema and self._schema['name'][3]):
  320. return self._properties['name']
  321. raise NotImplementedError, \
  322. self.__class__.__name__ + ' must implement Name'
  323. def Comment(self):
  324. """Return a comment string for the object.
  325. Most objects just use their name as the comment, but PBXProject uses
  326. different values.
  327. The returned comment is not escaped and does not have any comment marker
  328. strings applied to it.
  329. """
  330. return self.Name()
  331. def Hashables(self):
  332. hashables = [self.__class__.__name__]
  333. name = self.Name()
  334. if name != None:
  335. hashables.append(name)
  336. hashables.extend(self._hashables)
  337. return hashables
  338. def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
  339. """Set "id" properties deterministically.
  340. An object's "id" property is set based on a hash of its class type and
  341. name, as well as the class type and name of all ancestor objects. As
  342. such, it is only advisable to call ComputeIDs once an entire project file
  343. tree is built.
  344. If recursive is True, recurse into all descendant objects and update their
  345. hashes.
  346. If overwrite is True, any existing value set in the "id" property will be
  347. replaced.
  348. """
  349. def _HashUpdate(hash, data):
  350. """Update hash with data's length and contents.
  351. If the hash were updated only with the value of data, it would be
  352. possible for clowns to induce collisions by manipulating the names of
  353. their objects. By adding the length, it's exceedingly less likely that
  354. ID collisions will be encountered, intentionally or not.
  355. """
  356. hash.update(struct.pack('>i', len(data)))
  357. hash.update(data)
  358. if hash is None:
  359. hash = _new_sha1()
  360. hashables = self.Hashables()
  361. assert len(hashables) > 0
  362. for hashable in hashables:
  363. _HashUpdate(hash, hashable)
  364. if recursive:
  365. for child in self.Children():
  366. child.ComputeIDs(recursive, overwrite, hash.copy())
  367. if overwrite or self.id is None:
  368. # Xcode IDs are only 96 bits (24 hex characters), but a SHA-1 digest is
  369. # is 160 bits. Instead of throwing out 64 bits of the digest, xor them
  370. # into the portion that gets used.
  371. assert hash.digest_size % 4 == 0
  372. digest_int_count = hash.digest_size / 4
  373. digest_ints = struct.unpack('>' + 'I' * digest_int_count, hash.digest())
  374. id_ints = [0, 0, 0]
  375. for index in xrange(0, digest_int_count):
  376. id_ints[index % 3] ^= digest_ints[index]
  377. self.id = '%08X%08X%08X' % tuple(id_ints)
  378. def EnsureNoIDCollisions(self):
  379. """Verifies that no two objects have the same ID. Checks all descendants.
  380. """
  381. ids = {}
  382. descendants = self.Descendants()
  383. for descendant in descendants:
  384. if descendant.id in ids:
  385. other = ids[descendant.id]
  386. raise KeyError, \
  387. 'Duplicate ID %s, objects "%s" and "%s" in "%s"' % \
  388. (descendant.id, str(descendant._properties),
  389. str(other._properties), self._properties['rootObject'].Name())
  390. ids[descendant.id] = descendant
  391. def Children(self):
  392. """Returns a list of all of this object's owned (strong) children."""
  393. children = []
  394. for property, attributes in self._schema.iteritems():
  395. (is_list, property_type, is_strong) = attributes[0:3]
  396. if is_strong and property in self._properties:
  397. if not is_list:
  398. children.append(self._properties[property])
  399. else:
  400. children.extend(self._properties[property])
  401. return children
  402. def Descendants(self):
  403. """Returns a list of all of this object's descendants, including this
  404. object.
  405. """
  406. children = self.Children()
  407. descendants = [self]
  408. for child in children:
  409. descendants.extend(child.Descendants())
  410. return descendants
  411. def PBXProjectAncestor(self):
  412. # The base case for recursion is defined at PBXProject.PBXProjectAncestor.
  413. if self.parent:
  414. return self.parent.PBXProjectAncestor()
  415. return None
  416. def _EncodeComment(self, comment):
  417. """Encodes a comment to be placed in the project file output, mimicing
  418. Xcode behavior.
  419. """
  420. # This mimics Xcode behavior by wrapping the comment in "/*" and "*/". If
  421. # the string already contains a "*/", it is turned into "(*)/". This keeps
  422. # the file writer from outputting something that would be treated as the
  423. # end of a comment in the middle of something intended to be entirely a
  424. # comment.
  425. return '/* ' + comment.replace('*/', '(*)/') + ' */'
  426. def _EncodeTransform(self, match):
  427. # This function works closely with _EncodeString. It will only be called
  428. # by re.sub with match.group(0) containing a character matched by the
  429. # the _escaped expression.
  430. char = match.group(0)
  431. # Backslashes (\) and quotation marks (") are always replaced with a
  432. # backslash-escaped version of the same. Everything else gets its
  433. # replacement from the class' _encode_transforms array.
  434. if char == '\\':
  435. return '\\\\'
  436. if char == '"':
  437. return '\\"'
  438. return self._encode_transforms[ord(char)]
  439. def _EncodeString(self, value):
  440. """Encodes a string to be placed in the project file output, mimicing
  441. Xcode behavior.
  442. """
  443. # Use quotation marks when any character outside of the range A-Z, a-z, 0-9,
  444. # $ (dollar sign), . (period), and _ (underscore) is present. Also use
  445. # quotation marks to represent empty strings.
  446. #
  447. # Escape " (double-quote) and \ (backslash) by preceding them with a
  448. # backslash.
  449. #
  450. # Some characters below the printable ASCII range are encoded specially:
  451. # 7 ^G BEL is encoded as "\a"
  452. # 8 ^H BS is encoded as "\b"
  453. # 11 ^K VT is encoded as "\v"
  454. # 12 ^L NP is encoded as "\f"
  455. # 127 ^? DEL is passed through as-is without escaping
  456. # - In PBXFileReference and PBXBuildFile objects:
  457. # 9 ^I HT is passed through as-is without escaping
  458. # 10 ^J NL is passed through as-is without escaping
  459. # 13 ^M CR is passed through as-is without escaping
  460. # - In other objects:
  461. # 9 ^I HT is encoded as "\t"
  462. # 10 ^J NL is encoded as "\n"
  463. # 13 ^M CR is encoded as "\n" rendering it indistinguishable from
  464. # 10 ^J NL
  465. # All other nonprintable characters within the ASCII range (0 through 127
  466. # inclusive) are encoded as "\U001f" referring to the Unicode code point in
  467. # hexadecimal. For example, character 14 (^N SO) is encoded as "\U000e".
  468. # Characters above the ASCII range are passed through to the output encoded
  469. # as UTF-8 without any escaping. These mappings are contained in the
  470. # class' _encode_transforms list.
  471. if _unquoted.search(value) and not _quoted.search(value):
  472. return value
  473. return '"' + _escaped.sub(self._EncodeTransform, value) + '"'
  474. def _XCPrint(self, file, tabs, line):
  475. file.write('\t' * tabs + line)
  476. def _XCPrintableValue(self, tabs, value, flatten_list=False):
  477. """Returns a representation of value that may be printed in a project file,
  478. mimicing Xcode's behavior.
  479. _XCPrintableValue can handle str and int values, XCObjects (which are
  480. made printable by returning their id property), and list and dict objects
  481. composed of any of the above types. When printing a list or dict, and
  482. _should_print_single_line is False, the tabs parameter is used to determine
  483. how much to indent the lines corresponding to the items in the list or
  484. dict.
  485. If flatten_list is True, single-element lists will be transformed into
  486. strings.
  487. """
  488. printable = ''
  489. comment = None
  490. if self._should_print_single_line:
  491. sep = ' '
  492. element_tabs = ''
  493. end_tabs = ''
  494. else:
  495. sep = '\n'
  496. element_tabs = '\t' * (tabs + 1)
  497. end_tabs = '\t' * tabs
  498. if isinstance(value, XCObject):
  499. printable += value.id
  500. comment = value.Comment()
  501. elif isinstance(value, str):
  502. printable += self._EncodeString(value)
  503. elif isinstance(value, unicode):
  504. printable += self._EncodeString(value.encode('utf-8'))
  505. elif isinstance(value, int):
  506. printable += str(value)
  507. elif isinstance(value, list):
  508. if flatten_list and len(value) <= 1:
  509. if len(value) == 0:
  510. printable += self._EncodeString('')
  511. else:
  512. printable += self._EncodeString(value[0])
  513. else:
  514. printable = '(' + sep
  515. for item in value:
  516. printable += element_tabs + \
  517. self._XCPrintableValue(tabs + 1, item, flatten_list) + \
  518. ',' + sep
  519. printable += end_tabs + ')'
  520. elif isinstance(value, dict):
  521. printable = '{' + sep
  522. for item_key, item_value in sorted(value.iteritems()):
  523. printable += element_tabs + \
  524. self._XCPrintableValue(tabs + 1, item_key, flatten_list) + ' = ' + \
  525. self._XCPrintableValue(tabs + 1, item_value, flatten_list) + ';' + \
  526. sep
  527. printable += end_tabs + '}'
  528. else:
  529. raise TypeError, "Can't make " + value.__class__.__name__ + ' printable'
  530. if comment != None:
  531. printable += ' ' + self._EncodeComment(comment)
  532. return printable
  533. def _XCKVPrint(self, file, tabs, key, value):
  534. """Prints a key and value, members of an XCObject's _properties dictionary,
  535. to file.
  536. tabs is an int identifying the indentation level. If the class'
  537. _should_print_single_line variable is True, tabs is ignored and the
  538. key-value pair will be followed by a space insead of a newline.
  539. """
  540. if self._should_print_single_line:
  541. printable = ''
  542. after_kv = ' '
  543. else:
  544. printable = '\t' * tabs
  545. after_kv = '\n'
  546. # Xcode usually prints remoteGlobalIDString values in PBXContainerItemProxy
  547. # objects without comments. Sometimes it prints them with comments, but
  548. # the majority of the time, it doesn't. To avoid unnecessary changes to
  549. # the project file after Xcode opens it, don't write comments for
  550. # remoteGlobalIDString. This is a sucky hack and it would certainly be
  551. # cleaner to extend the schema to indicate whether or not a comment should
  552. # be printed, but since this is the only case where the problem occurs and
  553. # Xcode itself can't seem to make up its mind, the hack will suffice.
  554. #
  555. # Also see PBXContainerItemProxy._schema['remoteGlobalIDString'].
  556. if key == 'remoteGlobalIDString' and isinstance(self,
  557. PBXContainerItemProxy):
  558. value_to_print = value.id
  559. else:
  560. value_to_print = value
  561. # PBXBuildFile's settings property is represented in the output as a dict,
  562. # but a hack here has it represented as a string. Arrange to strip off the
  563. # quotes so that it shows up in the output as expected.
  564. if key == 'settings' and isinstance(self, PBXBuildFile):
  565. strip_value_quotes = True
  566. else:
  567. strip_value_quotes = False
  568. # In another one-off, let's set flatten_list on buildSettings properties
  569. # of XCBuildConfiguration objects, because that's how Xcode treats them.
  570. if key == 'buildSettings' and isinstance(self, XCBuildConfiguration):
  571. flatten_list = True
  572. else:
  573. flatten_list = False
  574. try:
  575. printable_key = self._XCPrintableValue(tabs, key, flatten_list)
  576. printable_value = self._XCPrintableValue(tabs, value_to_print,
  577. flatten_list)
  578. if strip_value_quotes and len(printable_value) > 1 and \
  579. printable_value[0] == '"' and printable_value[-1] == '"':
  580. printable_value = printable_value[1:-1]
  581. printable += printable_key + ' = ' + printable_value + ';' + after_kv
  582. except TypeError, e:
  583. gyp.common.ExceptionAppend(e,
  584. 'while printing key "%s"' % key)
  585. raise
  586. self._XCPrint(file, 0, printable)
  587. def Print(self, file=sys.stdout):
  588. """Prints a reprentation of this object to file, adhering to Xcode output
  589. formatting.
  590. """
  591. self.VerifyHasRequiredProperties()
  592. if self._should_print_single_line:
  593. # When printing an object in a single line, Xcode doesn't put any space
  594. # between the beginning of a dictionary (or presumably a list) and the
  595. # first contained item, so you wind up with snippets like
  596. # ...CDEF = {isa = PBXFileReference; fileRef = 0123...
  597. # If it were me, I would have put a space in there after the opening
  598. # curly, but I guess this is just another one of those inconsistencies
  599. # between how Xcode prints PBXFileReference and PBXBuildFile objects as
  600. # compared to other objects. Mimic Xcode's behavior here by using an
  601. # empty string for sep.
  602. sep = ''
  603. end_tabs = 0
  604. else:
  605. sep = '\n'
  606. end_tabs = 2
  607. # Start the object. For example, '\t\tPBXProject = {\n'.
  608. self._XCPrint(file, 2, self._XCPrintableValue(2, self) + ' = {' + sep)
  609. # "isa" isn't in the _properties dictionary, it's an intrinsic property
  610. # of the class which the object belongs to. Xcode always outputs "isa"
  611. # as the first element of an object dictionary.
  612. self._XCKVPrint(file, 3, 'isa', self.__class__.__name__)
  613. # The remaining elements of an object dictionary are sorted alphabetically.
  614. for property, value in sorted(self._properties.iteritems()):
  615. self._XCKVPrint(file, 3, property, value)
  616. # End the object.
  617. self._XCPrint(file, end_tabs, '};\n')
  618. def UpdateProperties(self, properties, do_copy=False):
  619. """Merge the supplied properties into the _properties dictionary.
  620. The input properties must adhere to the class schema or a KeyError or
  621. TypeError exception will be raised. If adding an object of an XCObject
  622. subclass and the schema indicates a strong relationship, the object's
  623. parent will be set to this object.
  624. If do_copy is True, then lists, dicts, strong-owned XCObjects, and
  625. strong-owned XCObjects in lists will be copied instead of having their
  626. references added.
  627. """
  628. if properties is None:
  629. return
  630. for property, value in properties.iteritems():
  631. # Make sure the property is in the schema.
  632. if not property in self._schema:
  633. raise KeyError, property + ' not in ' + self.__class__.__name__
  634. # Make sure the property conforms to the schema.
  635. (is_list, property_type, is_strong) = self._schema[property][0:3]
  636. if is_list:
  637. if value.__class__ != list:
  638. raise TypeError, \
  639. property + ' of ' + self.__class__.__name__ + \
  640. ' must be list, not ' + value.__class__.__name__
  641. for item in value:
  642. if not isinstance(item, property_type) and \
  643. not (item.__class__ == unicode and property_type == str):
  644. # Accept unicode where str is specified. str is treated as
  645. # UTF-8-encoded.
  646. raise TypeError, \
  647. 'item of ' + property + ' of ' + self.__class__.__name__ + \
  648. ' must be ' + property_type.__name__ + ', not ' + \
  649. item.__class__.__name__
  650. elif not isinstance(value, property_type) and \
  651. not (value.__class__ == unicode and property_type == str):
  652. # Accept unicode where str is specified. str is treated as
  653. # UTF-8-encoded.
  654. raise TypeError, \
  655. property + ' of ' + self.__class__.__name__ + ' must be ' + \
  656. property_type.__name__ + ', not ' + value.__class__.__name__
  657. # Checks passed, perform the assignment.
  658. if do_copy:
  659. if isinstance(value, XCObject):
  660. if is_strong:
  661. self._properties[property] = value.Copy()
  662. else:
  663. self._properties[property] = value
  664. elif isinstance(value, str) or isinstance(value, unicode) or \
  665. isinstance(value, int):
  666. self._properties[property] = value
  667. elif isinstance(value, list):
  668. if is_strong:
  669. # If is_strong is True, each element is an XCObject, so it's safe
  670. # to call Copy.
  671. self._properties[property] = []
  672. for item in value:
  673. self._properties[property].append(item.Copy())
  674. else:
  675. self._properties[property] = value[:]
  676. elif isinstance(value, dict):
  677. self._properties[property] = value.copy()
  678. else:
  679. raise TypeError, "Don't know how to copy a " + \
  680. value.__class__.__name__ + ' object for ' + \
  681. property + ' in ' + self.__class__.__name__
  682. else:
  683. self._properties[property] = value
  684. # Set up the child's back-reference to this object. Don't use |value|
  685. # any more because it may not be right if do_copy is true.
  686. if is_strong:
  687. if not is_list:
  688. self._properties[property].parent = self
  689. else:
  690. for item in self._properties[property]:
  691. item.parent = self
  692. def HasProperty(self, key):
  693. return key in self._properties
  694. def GetProperty(self, key):
  695. return self._properties[key]
  696. def SetProperty(self, key, value):
  697. self.UpdateProperties({key: value})
  698. def DelProperty(self, key):
  699. if key in self._properties:
  700. del self._properties[key]
  701. def AppendProperty(self, key, value):
  702. # TODO(mark): Support ExtendProperty too (and make this call that)?
  703. # Schema validation.
  704. if not key in self._schema:
  705. raise KeyError, key + ' not in ' + self.__class__.__name__
  706. (is_list, property_type, is_strong) = self._schema[key][0:3]
  707. if not is_list:
  708. raise TypeError, key + ' of ' + self.__class__.__name__ + ' must be list'
  709. if not isinstance(value, property_type):
  710. raise TypeError, 'item of ' + key + ' of ' + self.__class__.__name__ + \
  711. ' must be ' + property_type.__name__ + ', not ' + \
  712. value.__class__.__name__
  713. # If the property doesn't exist yet, create a new empty list to receive the
  714. # item.
  715. if not key in self._properties:
  716. self._properties[key] = []
  717. # Set up the ownership link.
  718. if is_strong:
  719. value.parent = self
  720. # Store the item.
  721. self._properties[key].append(value)
  722. def VerifyHasRequiredProperties(self):
  723. """Ensure that all properties identified as required by the schema are
  724. set.
  725. """
  726. # TODO(mark): A stronger verification mechanism is needed. Some
  727. # subclasses need to perform validation beyond what the schema can enforce.
  728. for property, attributes in self._schema.iteritems():
  729. (is_list, property_type, is_strong, is_required) = attributes[0:4]
  730. if is_required and not property in self._properties:
  731. raise KeyError, self.__class__.__name__ + ' requires ' + property
  732. def _SetDefaultsFromSchema(self):
  733. """Assign object default values according to the schema. This will not
  734. overwrite properties that have already been set."""
  735. defaults = {}
  736. for property, attributes in self._schema.iteritems():
  737. (is_list, property_type, is_strong, is_required) = attributes[0:4]
  738. if is_required and len(attributes) >= 5 and \
  739. not property in self._properties:
  740. default = attributes[4]
  741. defaults[property] = default
  742. if len(defaults) > 0:
  743. # Use do_copy=True so that each new object gets its own copy of strong
  744. # objects, lists, and dicts.
  745. self.UpdateProperties(defaults, do_copy=True)
  746. class XCHierarchicalElement(XCObject):
  747. """Abstract base for PBXGroup and PBXFileReference. Not represented in a
  748. project file."""
  749. # TODO(mark): Do name and path belong here? Probably so.
  750. # If path is set and name is not, name may have a default value. Name will
  751. # be set to the basename of path, if the basename of path is different from
  752. # the full value of path. If path is already just a leaf name, name will
  753. # not be set.
  754. _schema = XCObject._schema.copy()
  755. _schema.update({
  756. 'comments': [0, str, 0, 0],
  757. 'fileEncoding': [0, str, 0, 0],
  758. 'includeInIndex': [0, int, 0, 0],
  759. 'indentWidth': [0, int, 0, 0],
  760. 'lineEnding': [0, int, 0, 0],
  761. 'sourceTree': [0, str, 0, 1, '<group>'],
  762. 'tabWidth': [0, int, 0, 0],
  763. 'usesTabs': [0, int, 0, 0],
  764. 'wrapsLines': [0, int, 0, 0],
  765. })
  766. def __init__(self, properties=None, id=None, parent=None):
  767. # super
  768. XCObject.__init__(self, properties, id, parent)
  769. if 'path' in self._properties and not 'name' in self._properties:
  770. path = self._properties['path']
  771. name = posixpath.basename(path)
  772. if name != '' and path != name:
  773. self.SetProperty('name', name)
  774. if 'path' in self._properties and \
  775. (not 'sourceTree' in self._properties or \
  776. self._properties['sourceTree'] == '<group>'):
  777. # If the pathname begins with an Xcode variable like "$(SDKROOT)/", take
  778. # the variable out and make the path be relative to that variable by
  779. # assigning the variable name as the sourceTree.
  780. (source_tree, path) = SourceTreeAndPathFromPath(self._properties['path'])
  781. if source_tree != None:
  782. self._properties['sourceTree'] = source_tree
  783. if path != None:
  784. self._properties['path'] = path
  785. if source_tree != None and path is None and \
  786. not 'name' in self._properties:
  787. # The path was of the form "$(SDKROOT)" with no path following it.
  788. # This object is now relative to that variable, so it has no path
  789. # attribute of its own. It does, however, keep a name.
  790. del self._properties['path']
  791. self._properties['name'] = source_tree
  792. def Name(self):
  793. if 'name' in self._properties:
  794. return self._properties['name']
  795. elif 'path' in self._properties:
  796. return self._properties['path']
  797. else:
  798. # This happens in the case of the root PBXGroup.
  799. return None
  800. def Hashables(self):
  801. """Custom hashables for XCHierarchicalElements.
  802. XCHierarchicalElements are special. Generally, their hashes shouldn't
  803. change if the paths don't change. The normal XCObject implementation of
  804. Hashables adds a hashable for each object, which means that if
  805. the hierarchical structure changes (possibly due to changes caused when
  806. TakeOverOnlyChild runs and encounters slight changes in the hierarchy),
  807. the hashes will change. For example, if a project file initially contains
  808. a/b/f1 and a/b becomes collapsed into a/b, f1 will have a single parent
  809. a/b. If someone later adds a/f2 to the project file, a/b can no longer be
  810. collapsed, and f1 winds up with parent b and grandparent a. That would
  811. be sufficient to change f1's hash.
  812. To counteract this problem, hashables for all XCHierarchicalElements except
  813. for the main group (which has neither a name nor a path) are taken to be
  814. just the set of path components. Because hashables are inherited from
  815. parents, this provides assurance that a/b/f1 has the same set of hashables
  816. whether its parent is b or a/b.
  817. The main group is a special case. As it is permitted to have no name or
  818. path, it is permitted to use the standard XCObject hash mechanism. This
  819. is not considered a problem because there can be only one main group.
  820. """
  821. if self == self.PBXProjectAncestor()._properties['mainGroup']:
  822. # super
  823. return XCObject.Hashables(self)
  824. hashables = []
  825. # Put the name in first, ensuring that if TakeOverOnlyChild collapses
  826. # children into a top-level group like "Source", the name always goes
  827. # into the list of hashables without interfering with path components.
  828. if 'name' in self._properties:
  829. # Make it less likely for people to manipulate hashes by following the
  830. # pattern of always pushing an object type value onto the list first.
  831. hashables.append(self.__class__.__name__ + '.name')
  832. hashables.append(self._properties['name'])
  833. # NOTE: This still has the problem that if an absolute path is encountered,
  834. # including paths with a sourceTree, they'll still inherit their parents'
  835. # hashables, even though the paths aren't relative to their parents. This
  836. # is not expected to be much of a problem in practice.
  837. path = self.PathFromSourceTreeAndPath()
  838. if path != None:
  839. components = path.split(posixpath.sep)
  840. for component in components:
  841. hashables.append(self.__class__.__name__ + '.path')
  842. hashables.append(component)
  843. hashables.extend(self._hashables)
  844. return hashables
  845. def Compare(self, other):
  846. # Allow comparison of these types. PBXGroup has the highest sort rank;
  847. # PBXVariantGroup is treated as equal to PBXFileReference.
  848. valid_class_types = {
  849. PBXFileReference: 'file',
  850. PBXGroup: 'group',
  851. PBXVariantGroup: 'file',
  852. }
  853. self_type = valid_class_types[self.__class__]
  854. other_type = valid_class_types[other.__class__]
  855. if self_type == other_type:
  856. # If the two objects are of the same sort rank, compare their names.
  857. return cmp(self.Name(), other.Name())
  858. # Otherwise, sort groups before everything else.
  859. if self_type == 'group':
  860. return -1
  861. return 1
  862. def CompareRootGroup(self, other):
  863. # This function should be used only to compare direct children of the
  864. # containing PBXProject's mainGroup. These groups should appear in the
  865. # listed order.
  866. # TODO(mark): "Build" is used by gyp.generator.xcode, perhaps the
  867. # generator should have a way of influencing this list rather than having
  868. # to hardcode for the generator here.
  869. order = ['Source', 'Intermediates', 'Projects', 'Frameworks', 'Products',
  870. 'Build']
  871. # If the groups aren't in the listed order, do a name comparison.
  872. # Otherwise, groups in the listed order should come before those that
  873. # aren't.
  874. self_name = self.Name()
  875. other_name = other.Name()
  876. self_in = isinstance(self, PBXGroup) and self_name in order
  877. other_in = isinstance(self, PBXGroup) and other_name in order
  878. if not self_in and not other_in:
  879. return self.Compare(other)
  880. if self_name in order and not other_name in order:
  881. return -1
  882. if other_name in order and not self_name in order:
  883. return 1
  884. # If both groups are in the listed order, go by the defined order.
  885. self_index = order.index(self_name)
  886. other_index = order.index(other_name)
  887. if self_index < other_index:
  888. return -1
  889. if self_index > other_index:
  890. return 1
  891. return 0
  892. def PathFromSourceTreeAndPath(self):
  893. # Turn the object's sourceTree and path properties into a single flat
  894. # string of a form comparable to the path parameter. If there's a
  895. # sourceTree property other than "<group>", wrap it in $(...) for the
  896. # comparison.
  897. components = []
  898. if self._properties['sourceTree'] != '<group>':
  899. components.append('$(' + self._properties['sourceTree'] + ')')
  900. if 'path' in self._properties:
  901. components.append(self._properties['path'])
  902. if len(components) > 0:
  903. return posixpath.join(*components)
  904. return None
  905. def FullPath(self):
  906. # Returns a full path to self relative to the project file, or relative
  907. # to some other source tree. Start with self, and walk up the chain of
  908. # parents prepending their paths, if any, until no more parents are
  909. # available (project-relative path) or until a path relative to some
  910. # source tree is found.
  911. xche = self
  912. path = None
  913. while isinstance(xche, XCHierarchicalElement) and \
  914. (path is None or \
  915. (not path.startswith('/') and not path.startswith('$'))):
  916. this_path = xche.PathFromSourceTreeAndPath()
  917. if this_path != None and path != None:
  918. path = posixpath.join(this_path, path)
  919. elif this_path != None:
  920. path = this_path
  921. xche = xche.parent
  922. return path
  923. class PBXGroup(XCHierarchicalElement):
  924. """
  925. Attributes:
  926. _children_by_path: Maps pathnames of children of this PBXGroup to the
  927. actual child XCHierarchicalElement objects.
  928. _variant_children_by_name_and_path: Maps (name, path) tuples of
  929. PBXVariantGroup children to the actual child PBXVariantGroup objects.
  930. """
  931. _schema = XCHierarchicalElement._schema.copy()
  932. _schema.update({
  933. 'children': [1, XCHierarchicalElement, 1, 1, []],
  934. 'name': [0, str, 0, 0],
  935. 'path': [0, str, 0, 0],
  936. })
  937. def __init__(self, properties=None, id=None, parent=None):
  938. # super
  939. XCHierarchicalElement.__init__(self, properties, id, parent)
  940. self._children_by_path = {}
  941. self._variant_children_by_name_and_path = {}
  942. for child in self._properties.get('children', []):
  943. self._AddChildToDicts(child)
  944. def _AddChildToDicts(self, child):
  945. # Sets up this PBXGroup object's dicts to reference the child properly.
  946. child_path = child.PathFromSourceTreeAndPath()
  947. if child_path:
  948. if child_path in self._children_by_path:
  949. raise ValueError, 'Found multiple children with path ' + child_path
  950. self._children_by_path[child_path] = child
  951. if isinstance(child, PBXVariantGroup):
  952. child_name = child._properties.get('name', None)
  953. key = (child_name, child_path)
  954. if key in self._variant_children_by_name_and_path:
  955. raise ValueError, 'Found multiple PBXVariantGroup children with ' + \
  956. 'name ' + str(child_name) + ' and path ' + \
  957. str(child_path)
  958. self._variant_children_by_name_and_path[key] = child
  959. def AppendChild(self, child):
  960. # Callers should use this instead of calling
  961. # AppendProperty('children', child) directly because this function
  962. # maintains the group's dicts.
  963. self.AppendProperty('children', child)
  964. self._AddChildToDicts(child)
  965. def GetChildByName(self, name):
  966. # This is not currently optimized with a dict as GetChildByPath is because
  967. # it has few callers. Most callers probably want GetChildByPath. This
  968. # function is only useful to get children that have names but no paths,
  969. # which is rare. The children of the main group ("Source", "Products",
  970. # etc.) is pretty much the only case where this likely to come up.
  971. #
  972. # TODO(mark): Maybe this should raise an error if more than one child is
  973. # present with the same name.
  974. if not 'children' in self._properties:
  975. return None
  976. for child in self._properties['children']:
  977. if child.Name() == name:
  978. return child
  979. return None
  980. def GetChildByPath(self, path):
  981. if not path:
  982. return None
  983. if path in self._children_by_path:
  984. return self._children_by_path[path]
  985. return None
  986. def GetChildByRemoteObject(self, remote_object):
  987. # This method is a little bit esoteric. Given a remote_object, which
  988. # should be a PBXFileReference in another project file, this method will
  989. # return this group's PBXReferenceProxy object serving as a local proxy
  990. # for the remote PBXFileReference.
  991. #
  992. # This function might benefit from a dict optimization as GetChildByPath
  993. # for some workloads, but profiling shows that it's not currently a
  994. # problem.
  995. if not 'children' in self._properties:
  996. return None
  997. for child in self._properties['children']:
  998. if not isinstance(child, PBXReferenceProxy):
  999. continue
  1000. container_proxy = child._properties['remoteRef']
  1001. if container_proxy._properties['remoteGlobalIDString'] == remote_object:
  1002. return child
  1003. return None
  1004. def AddOrGetFileByPath(self, path, hierarchical):
  1005. """Returns an existing or new file reference corresponding to path.
  1006. If hierarchical is True, this method will create or use the necessary
  1007. hierarchical group structure corresponding to path. Otherwise, it will
  1008. look in and create an item in the current group only.
  1009. If an existing matching reference is found, it is returned, otherwise, a
  1010. new one will be created, added to the correct group, and returned.
  1011. If path identifies a directory by virtue of carrying a trailing slash,
  1012. this method returns a PBXFileReference of "folder" type. If path
  1013. identifies a variant, by virtue of it identifying a

Large files files are truncated, but you can click here to view the full file