3
# Copyright (c) 2009 Google Inc. All rights reserved.
4
# Use of this source code is governed by a BSD-style license that can be
5
# found in the LICENSE file.
7
"""Xcode project file generator.
9
This module is both an Xcode project file generator and a documentation of the
10
Xcode project file format. Knowledge of the project file format was gained
11
based on extensive experience with Xcode, and by making changes to projects in
12
Xcode.app and observing the resultant changes in the associated project files.
16
The generator targets the file format as written by Xcode 3.1 (specifically,
17
3.1.2), but past experience has taught that the format has not changed
18
significantly in the past several years, and future versions of Xcode are able
19
to read older project files.
21
Xcode project files are "bundled": the project "file" from an end-user's
22
perspective is actually a directory with an ".xcodeproj" extension. The
23
project file from this module's perspective is actually a file inside this
24
directory, always named "project.pbxproj". This file contains a complete
25
description of the project and is all that is needed to use the xcodeproj.
26
Other files contained in the xcodeproj directory are simply used to store
27
per-user settings, such as the state of various UI elements in the Xcode
30
The project.pbxproj file is a property list, stored in a format almost
31
identical to the NeXTstep property list format. The file is able to carry
32
Unicode data, and is encoded in UTF-8. The root element in the property list
33
is a dictionary that contains several properties of minimal interest, and two
34
properties of immense interest. The most important property is a dictionary
35
named "objects". The entire structure of the project is represented by the
36
children of this property. The objects dictionary is keyed by unique 96-bit
37
values represented by 24 uppercase hexadecimal characters. Each value in the
38
objects dictionary is itself a dictionary, describing an individual object.
40
Each object in the dictionary is a member of a class, which is identified by
41
the "isa" property of each object. A variety of classes are represented in a
42
project file. Objects can refer to other objects by ID, using the 24-character
43
hexadecimal object key. A project's objects form a tree, with a root object
44
of class PBXProject at the root. As an example, the PBXProject object serves
45
as parent to an XCConfigurationList object defining the build configurations
46
used in the project, a PBXGroup object serving as a container for all files
47
referenced in the project, and a list of target objects, each of which defines
48
a target in the project. There are several different types of target object,
49
such as PBXNativeTarget and PBXAggregateTarget. In this module, this
50
relationship is expressed by having each target type derive from an abstract
53
The project.pbxproj file's root dictionary also contains a property, sibling to
54
the "objects" dictionary, named "rootObject". The value of rootObject is a
55
24-character object key referring to the root PBXProject object in the
58
In Xcode, every file used as input to a target or produced as a final product
59
of a target must appear somewhere in the hierarchy rooted at the PBXGroup
60
object referenced by the PBXProject's mainGroup property. A PBXGroup is
61
generally represented as a folder in the Xcode application. PBXGroups can
62
contain other PBXGroups as well as PBXFileReferences, which are pointers to
65
Each XCTarget contains a list of build phases, represented in this module by
66
the abstract base XCBuildPhase. Examples of concrete XCBuildPhase derivations
67
are PBXSourcesBuildPhase and PBXFrameworksBuildPhase, which correspond to the
68
"Compile Sources" and "Link Binary With Libraries" phases displayed in the
69
Xcode application. Files used as input to these phases (for example, source
70
files in the former case and libraries and frameworks in the latter) are
71
represented by PBXBuildFile objects, referenced by elements of "files" lists
72
in XCTarget objects. Each PBXBuildFile object refers to a PBXBuildFile
73
object as a "weak" reference: it does not "own" the PBXBuildFile, which is
74
owned by the root object's mainGroup or a descendant group. In most cases, the
75
layer of indirection between an XCBuildPhase and a PBXFileReference via a
76
PBXBuildFile appears extraneous, but there's actually one reason for this:
77
file-specific compiler flags are added to the PBXBuildFile object so as to
78
allow a single file to be a member of multiple targets while having distinct
79
compiler flags for each. These flags can be modified in the Xcode applciation
80
in the "Build" tab of a File Info window.
82
When a project is open in the Xcode application, Xcode will rewrite it. As
83
such, this module is careful to adhere to the formatting used by Xcode, to
84
avoid insignificant changes appearing in the file when it is used in the
85
Xcode application. This will keep version control repositories happy, and
86
makes it possible to compare a project file used in Xcode to one generated by
87
this module to determine if any significant changes were made in the
90
Xcode has its own way of assigning 24-character identifiers to each object,
91
which is not duplicated here. Because the identifier only is only generated
92
once, when an object is created, and is then left unchanged, there is no need
93
to attempt to duplicate Xcode's behavior in this area. The generator is free
94
to select any identifier, even at random, to refer to the objects it creates,
95
and Xcode will retain those identifiers and use them when subsequently
96
rewriting the project file. However, the generator would choose new random
97
identifiers each time the project files are generated, leading to difficulties
98
comparing "used" project files to "pristine" ones produced by this module,
99
and causing the appearance of changes as every object identifier is changed
100
when updated projects are checked in to a version control repository. To
101
mitigate this problem, this module chooses identifiers in a more deterministic
102
way, by hashing a description of each object as well as its parent and ancestor
103
objects. This strategy should result in minimal "shift" in IDs as successive
104
generations of project files are produced.
108
This module introduces several classes, all derived from the XCObject class.
109
Nearly all of the "brains" are built into the XCObject class, which understands
110
how to create and modify objects, maintain the proper tree structure, compute
111
identifiers, and print objects. For the most part, classes derived from
112
XCObject need only provide a _schema class object, a dictionary that
113
expresses what properties objects of the class may contain.
115
Given this structure, it's possible to build a minimal project file by creating
116
objects of the appropriate types and making the proper connections:
118
config_list = XCConfigurationList()
120
project = PBXProject({'buildConfigurationList': config_list,
123
With the project object set up, it can be added to an XCProjectFile object.
124
XCProjectFile is a pseudo-class in the sense that it is a concrete XCObject
125
subclass that does not actually correspond to a class type found in a project
126
file. Rather, it is used to represent the project file's root dictionary.
127
Printing an XCProjectFile will print the entire project file, including the
128
full "objects" dictionary.
130
project_file = XCProjectFile({'rootObject': project})
131
project_file.ComputeIDs()
134
Xcode project files are always encoded in UTF-8. This module will accept
135
strings of either the str class or the unicode class. Strings of class str
136
are assumed to already be encoded in UTF-8. Obviously, if you're just using
137
ASCII, you won't encounter difficulties because ASCII is a UTF-8 subset.
138
Strings of class unicode are handled properly and encoded in UTF-8 when
139
a project file is output.
148
# hashlib is supplied as of Python 2.5 as the replacement interface for sha
149
# and other secure hashes. In 2.6, sha is deprecated. Import hashlib if
150
# available, avoiding a deprecation warning under 2.6. Import sha otherwise,
151
# preserving 2.4 compatibility.
154
_new_sha1 = hashlib.sha1
160
# See XCObject._EncodeString. This pattern is used to determine when a string
161
# can be printed unquoted. Strings that match this pattern may be printed
162
# unquoted. Strings that do not match must be quoted and may be further
163
# transformed to be properly encoded. Note that this expression matches the
164
# characters listed with "+", for 1 or more occurrences: if a string is empty,
165
# it must not match this pattern, because it needs to be encoded as "".
166
_unquoted = re.compile('^[A-Za-z0-9$./_]+$')
168
# Strings that match this pattern are quoted regardless of what _unquoted says.
169
# Oddly, Xcode will quote any string with a run of three or more underscores.
170
_quoted = re.compile('___')
172
# This pattern should match any character that needs to be escaped by
173
# XCObject._EncodeString. See that function.
174
_escaped = re.compile('[\\\\"]|[^ -~]')
177
# Used by SourceTreeAndPathFromPath
178
_path_leading_variable = re.compile('^\$\((.*?)\)(/(.*))?$')
180
def SourceTreeAndPathFromPath(input_path):
181
"""Given input_path, returns a tuple with sourceTree and path values.
184
input_path (source_tree, output_path)
185
'$(VAR)/path' ('VAR', 'path')
186
'$(VAR)' ('VAR', None)
187
'path' (None, 'path')
190
source_group_match = _path_leading_variable.match(input_path)
191
if source_group_match:
192
source_tree = source_group_match.group(1)
193
output_path = source_group_match.group(3) # This may be None.
196
output_path = input_path
198
return (source_tree, output_path)
200
def ConvertVariablesToShellSyntax(input_string):
201
return re.sub('\$\((.*?)\)', '${\\1}', input_string)
203
class XCObject(object):
204
"""The abstract base of all class types used in Xcode project files.
207
_schema: A dictionary defining the properties of this class. The keys to
208
_schema are string property keys as used in project files. Values
209
are a list of four or five elements:
210
[ is_list, property_type, is_strong, is_required, default ]
211
is_list: True if the property described is a list, as opposed
213
property_type: The type to use as the value of the property,
214
or if is_list is True, the type to use for each
215
element of the value's list. property_type must
216
be an XCObject subclass, or one of the built-in
217
types str, int, or dict.
218
is_strong: If property_type is an XCObject subclass, is_strong
219
is True to assert that this class "owns," or serves
220
as parent, to the property value (or, if is_list is
221
True, values). is_strong must be False if
222
property_type is not an XCObject subclass.
223
is_required: True if the property is required for the class.
224
Note that is_required being True does not preclude
225
an empty string ("", in the case of property_type
226
str) or list ([], in the case of is_list True) from
227
being set for the property.
228
default: Optional. If is_requried is True, default may be set
229
to provide a default value for objects that do not supply
230
their own value. If is_required is True and default
231
is not provided, users of the class must supply their own
232
value for the property.
233
Note that although the values of the array are expressed in
234
boolean terms, subclasses provide values as integers to conserve
236
_should_print_single_line: False in XCObject. Subclasses whose objects
237
should be written to the project file in the
238
alternate single-line format, such as
239
PBXFileReference and PBXBuildFile, should
241
_encode_transforms: Used by _EncodeString to encode unprintable characters.
242
The index into this list is the ordinal of the
243
character to transform; each value is a string
244
used to represent the character in the output. XCObject
245
provides an _encode_transforms list suitable for most
247
_alternate_encode_transforms: Provided for subclasses that wish to use
248
the alternate encoding rules. Xcode seems
249
to use these rules when printing objects in
250
single-line format. Subclasses that desire
251
this behavior should set _encode_transforms
252
to _alternate_encode_transforms.
253
_hashables: A list of XCObject subclasses that can be hashed by ComputeIDs
254
to construct this object's ID. Most classes that need custom
255
hashing behavior should do it by overriding Hashables,
256
but in some cases an object's parent may wish to push a
257
hashable value into its child, and it can do so by appending
260
id: The object's identifier, a 24-character uppercase hexadecimal string.
261
Usually, objects being created should not set id until the entire
262
project file structure is built. At that point, UpdateIDs() should
263
be called on the root object to assign deterministic values for id to
264
each object in the tree.
265
parent: The object's parent. This is set by a parent XCObject when a child
266
object is added to it.
267
_properties: The object's property dictionary. An object's properties are
268
described by its class' _schema variable.
272
_should_print_single_line = False
275
_encode_transforms = []
278
_encode_transforms.append('\\U%04x' % i)
280
_encode_transforms[7] = '\\a'
281
_encode_transforms[8] = '\\b'
282
_encode_transforms[9] = '\\t'
283
_encode_transforms[10] = '\\n'
284
_encode_transforms[11] = '\\v'
285
_encode_transforms[12] = '\\f'
286
_encode_transforms[13] = '\\n'
288
_alternate_encode_transforms = list(_encode_transforms)
289
_alternate_encode_transforms[9] = chr(9)
290
_alternate_encode_transforms[10] = chr(10)
291
_alternate_encode_transforms[11] = chr(11)
293
def __init__(self, properties=None, id=None, parent=None):
296
self._properties = {}
298
self._SetDefaultsFromSchema()
299
self.UpdateProperties(properties)
304
except NotImplementedError:
305
return '<%s at 0x%x>' % (self.__class__.__name__, id(self))
306
return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
309
"""Make a copy of this object.
311
The new object will have its own copy of lists and dicts. Any XCObject
312
objects owned by this object (marked "strong") will be copied in the
313
new object, even those found in lists. If this object has any weak
314
references to other XCObjects, the same references are added to the new
315
object without making a copy.
318
that = self.__class__(id=self.id, parent=self.parent)
319
for key, value in self._properties.iteritems():
320
is_strong = self._schema[key][2]
322
if isinstance(value, XCObject):
324
new_value = value.Copy()
325
new_value.parent = that
326
that._properties[key] = new_value
328
that._properties[key] = value
329
elif isinstance(value, str) or isinstance(value, unicode) or \
330
isinstance(value, int):
331
that._properties[key] = value
332
elif isinstance(value, list):
334
# If is_strong is True, each element is an XCObject, so it's safe to
336
that._properties[key] = []
338
new_item = item.Copy()
339
new_item.parent = that
340
that._properties[key].append(new_item)
342
that._properties[key] = value[:]
343
elif isinstance(value, dict):
344
# dicts are never strong.
346
raise TypeError, 'Strong dict for key ' + key + ' in ' + \
347
self.__class__.__name__
349
that._properties[key] = value.copy()
351
raise TypeError, 'Unexpected type ' + value.__class__.__name__ + \
352
' for key ' + key + ' in ' + self.__class__.__name__
357
"""Return the name corresponding to an object.
359
Not all objects necessarily need to be nameable, and not all that do have
360
a "name" property. Override as needed.
363
# If the schema indicates that "name" is required, try to access the
364
# property even if it doesn't exist. This will result in a KeyError
365
# being raised for the property that should be present, which seems more
366
# appropriate than NotImplementedError in this case.
367
if 'name' in self._properties or \
368
('name' in self._schema and self._schema['name'][3]):
369
return self._properties['name']
371
raise NotImplementedError, \
372
self.__class__.__name__ + ' must implement Name'
375
"""Return a comment string for the object.
377
Most objects just use their name as the comment, but PBXProject uses
380
The returned comment is not escaped and does not have any comment marker
381
strings applied to it.
387
hashables = [self.__class__.__name__]
391
hashables.append(name)
393
hashables.extend(self._hashables)
397
def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
398
"""Set "id" properties deterministically.
400
An object's "id" property is set based on a hash of its class type and
401
name, as well as the class type and name of all ancestor objects. As
402
such, it is only advisable to call ComputeIDs once an entire project file
405
If recursive is True, recurse into all descendant objects and update their
408
If overwrite is True, any existing value set in the "id" property will be
412
def _HashUpdate(hash, data):
413
"""Update hash with data's length and contents.
415
If the hash were updated only with the value of data, it would be
416
possible for clowns to induce collisions by manipulating the names of
417
their objects. By adding the length, it's exceedingly less likely that
418
ID collisions will be encountered, intentionally or not.
421
hash.update(struct.pack('>i', len(data)))
427
hashables = self.Hashables()
428
assert len(hashables) > 0
429
for hashable in hashables:
430
_HashUpdate(hash, hashable)
433
for child in self.Children():
434
child.ComputeIDs(recursive, overwrite, hash.copy())
436
if overwrite or self.id == None:
437
# Xcode IDs are only 96 bits (24 hex characters), but a SHA-1 digest is
438
# is 160 bits. Instead of throwing out 64 bits of the digest, xor them
439
# into the portion that gets used.
440
assert hash.digest_size % 4 == 0
441
digest_int_count = hash.digest_size / 4
442
digest_ints = struct.unpack('>' + 'I' * digest_int_count, hash.digest())
444
for index in xrange(0, digest_int_count):
445
id_ints[index % 3] ^= digest_ints[index]
446
self.id = '%08X%08X%08X' % tuple(id_ints)
448
def EnsureNoIDCollisions(self):
449
"""Verifies that no two objects have the same ID. Checks all descendants.
453
descendants = self.Descendants()
454
for descendant in descendants:
455
if descendant.id in ids:
456
other = ids[descendant.id]
458
'Duplicate ID %s, objects "%s" and "%s" in "%s"' % \
459
(descendant.id, str(descendant._properties),
460
str(other._properties), self._properties['rootObject'].Name())
461
ids[descendant.id] = descendant
464
"""Returns a list of all of this object's owned (strong) children."""
467
for property, attributes in self._schema.iteritems():
468
(is_list, property_type, is_strong) = attributes[0:3]
469
if is_strong and property in self._properties:
471
children.append(self._properties[property])
473
children.extend(self._properties[property])
476
def Descendants(self):
477
"""Returns a list of all of this object's descendants, including this
481
children = self.Children()
483
for child in children:
484
descendants.extend(child.Descendants())
487
def PBXProjectAncestor(self):
488
# The base case for recursion is defined at PBXProject.PBXProjectAncestor.
490
return self.parent.PBXProjectAncestor()
493
def _EncodeComment(self, comment):
494
"""Encodes a comment to be placed in the project file output, mimicing
498
# This mimics Xcode behavior by wrapping the comment in "/*" and "*/". If
499
# the string already contains a "*/", it is turned into "(*)/". This keeps
500
# the file writer from outputting something that would be treated as the
501
# end of a comment in the middle of something intended to be entirely a
504
return '/* ' + comment.replace('*/', '(*)/') + ' */'
506
def _EncodeTransform(self, match):
507
# This function works closely with _EncodeString. It will only be called
508
# by re.sub with match.group(0) containing a character matched by the
509
# the _escaped expression.
510
char = match.group(0)
512
# Backslashes (\) and quotation marks (") are always replaced with a
513
# backslash-escaped version of the same. Everything else gets its
514
# replacement from the class' _encode_transforms array.
519
return self._encode_transforms[ord(char)]
521
def _EncodeString(self, value):
522
"""Encodes a string to be placed in the project file output, mimicing
526
# Use quotation marks when any character outside of the range A-Z, a-z, 0-9,
527
# $ (dollar sign), . (period), and _ (underscore) is present. Also use
528
# quotation marks to represent empty strings.
530
# Escape " (double-quote) and \ (backslash) by preceding them with a
533
# Some characters below the printable ASCII range are encoded specially:
534
# 7 ^G BEL is encoded as "\a"
535
# 8 ^H BS is encoded as "\b"
536
# 11 ^K VT is encoded as "\v"
537
# 12 ^L NP is encoded as "\f"
538
# 127 ^? DEL is passed through as-is without escaping
539
# - In PBXFileReference and PBXBuildFile objects:
540
# 9 ^I HT is passed through as-is without escaping
541
# 10 ^J NL is passed through as-is without escaping
542
# 13 ^M CR is passed through as-is without escaping
543
# - In other objects:
544
# 9 ^I HT is encoded as "\t"
545
# 10 ^J NL is encoded as "\n"
546
# 13 ^M CR is encoded as "\n" rendering it indistinguishable from
548
# All other nonprintable characters within the ASCII range (0 through 127
549
# inclusive) are encoded as "\U001f" referring to the Unicode code point in
550
# hexadecimal. For example, character 14 (^N SO) is encoded as "\U000e".
551
# Characters above the ASCII range are passed through to the output encoded
552
# as UTF-8 without any escaping. These mappings are contained in the
553
# class' _encode_transforms list.
555
if _unquoted.search(value) and not _quoted.search(value):
558
return '"' + _escaped.sub(self._EncodeTransform, value) + '"'
560
def _XCPrint(self, file, tabs, line):
561
file.write('\t' * tabs + line)
563
def _XCPrintableValue(self, tabs, value, flatten_list=False):
564
"""Returns a representation of value that may be printed in a project file,
565
mimicing Xcode's behavior.
567
_XCPrintableValue can handle str and int values, XCObjects (which are
568
made printable by returning their id property), and list and dict objects
569
composed of any of the above types. When printing a list or dict, and
570
_should_print_single_line is False, the tabs parameter is used to determine
571
how much to indent the lines corresponding to the items in the list or
574
If flatten_list is True, single-element lists will be transformed into
581
if self._should_print_single_line:
587
element_tabs = '\t' * (tabs + 1)
588
end_tabs = '\t' * tabs
590
if isinstance(value, XCObject):
591
printable += value.id
592
comment = value.Comment()
593
elif isinstance(value, str):
594
printable += self._EncodeString(value)
595
elif isinstance(value, unicode):
596
printable += self._EncodeString(value.encode('utf-8'))
597
elif isinstance(value, int):
598
printable += str(value)
599
elif isinstance(value, list):
600
if flatten_list and len(value) <= 1:
602
printable += self._EncodeString('')
604
printable += self._EncodeString(value[0])
606
printable = '(' + sep
608
printable += element_tabs + \
609
self._XCPrintableValue(tabs + 1, item, flatten_list) + \
611
printable += end_tabs + ')'
612
elif isinstance(value, dict):
613
printable = '{' + sep
614
for item_key, item_value in sorted(value.iteritems()):
615
printable += element_tabs + \
616
self._XCPrintableValue(tabs + 1, item_key, flatten_list) + ' = ' + \
617
self._XCPrintableValue(tabs + 1, item_value, flatten_list) + ';' + \
619
printable += end_tabs + '}'
621
raise TypeError, "Can't make " + value.__class__.__name__ + ' printable'
624
printable += ' ' + self._EncodeComment(comment)
628
def _XCKVPrint(self, file, tabs, key, value):
629
"""Prints a key and value, members of an XCObject's _properties dictionary,
632
tabs is an int identifying the indentation level. If the class'
633
_should_print_single_line variable is True, tabs is ignored and the
634
key-value pair will be followed by a space insead of a newline.
637
if self._should_print_single_line:
641
printable = '\t' * tabs
644
# Xcode usually prints remoteGlobalIDString values in PBXContainerItemProxy
645
# objects without comments. Sometimes it prints them with comments, but
646
# the majority of the time, it doesn't. To avoid unnecessary changes to
647
# the project file after Xcode opens it, don't write comments for
648
# remoteGlobalIDString. This is a sucky hack and it would certainly be
649
# cleaner to extend the schema to indicate whether or not a comment should
650
# be printed, but since this is the only case where the problem occurs and
651
# Xcode itself can't seem to make up its mind, the hack will suffice.
653
# Also see PBXContainerItemProxy._schema['remoteGlobalIDString'].
654
if key == 'remoteGlobalIDString' and isinstance(self,
655
PBXContainerItemProxy):
656
value_to_print = value.id
658
value_to_print = value
660
# In another one-off, let's set flatten_list on buildSettings properties
661
# of XCBuildConfiguration objects, because that's how Xcode treats them.
662
if key == 'buildSettings' and isinstance(self, XCBuildConfiguration):
668
printable += self._XCPrintableValue(tabs, key, flatten_list) + ' = ' + \
669
self._XCPrintableValue(tabs, value_to_print, flatten_list) + \
672
gyp.common.ExceptionAppend(e,
673
'while printing key "%s"' % key)
676
self._XCPrint(file, 0, printable)
678
def Print(self, file=sys.stdout):
679
"""Prints a reprentation of this object to file, adhering to Xcode output
683
self.VerifyHasRequiredProperties()
685
if self._should_print_single_line:
686
# When printing an object in a single line, Xcode doesn't put any space
687
# between the beginning of a dictionary (or presumably a list) and the
688
# first contained item, so you wind up with snippets like
689
# ...CDEF = {isa = PBXFileReference; fileRef = 0123...
690
# If it were me, I would have put a space in there after the opening
691
# curly, but I guess this is just another one of those inconsistencies
692
# between how Xcode prints PBXFileReference and PBXBuildFile objects as
693
# compared to other objects. Mimic Xcode's behavior here by using an
694
# empty string for sep.
701
# Start the object. For example, '\t\tPBXProject = {\n'.
702
self._XCPrint(file, 2, self._XCPrintableValue(2, self) + ' = {' + sep)
704
# "isa" isn't in the _properties dictionary, it's an intrinsic property
705
# of the class which the object belongs to. Xcode always outputs "isa"
706
# as the first element of an object dictionary.
707
self._XCKVPrint(file, 3, 'isa', self.__class__.__name__)
709
# The remaining elements of an object dictionary are sorted alphabetically.
710
for property, value in sorted(self._properties.iteritems()):
711
self._XCKVPrint(file, 3, property, value)
714
self._XCPrint(file, end_tabs, '};\n')
716
def UpdateProperties(self, properties, do_copy=False):
717
"""Merge the supplied properties into the _properties dictionary.
719
The input properties must adhere to the class schema or a KeyError or
720
TypeError exception will be raised. If adding an object of an XCObject
721
subclass and the schema indicates a strong relationship, the object's
722
parent will be set to this object.
724
If do_copy is True, then lists, dicts, strong-owned XCObjects, and
725
strong-owned XCObjects in lists will be copied instead of having their
729
if properties == None:
732
for property, value in properties.iteritems():
733
# Make sure the property is in the schema.
734
if not property in self._schema:
735
raise KeyError, property + ' not in ' + self.__class__.__name__
737
# Make sure the property conforms to the schema.
738
(is_list, property_type, is_strong) = self._schema[property][0:3]
740
if value.__class__ != list:
742
property + ' of ' + self.__class__.__name__ + \
743
' must be list, not ' + value.__class__.__name__
745
if not isinstance(item, property_type) and \
746
not (item.__class__ == unicode and property_type == str):
747
# Accept unicode where str is specified. str is treated as
750
'item of ' + property + ' of ' + self.__class__.__name__ + \
751
' must be ' + property_type.__name__ + ', not ' + \
752
item.__class__.__name__
753
elif not isinstance(value, property_type) and \
754
not (value.__class__ == unicode and property_type == str):
755
# Accept unicode where str is specified. str is treated as
758
property + ' of ' + self.__class__.__name__ + ' must be ' + \
759
property_type.__name__ + ', not ' + value.__class__.__name__
761
# Checks passed, perform the assignment.
763
if isinstance(value, XCObject):
765
self._properties[property] = value.Copy()
767
self._properties[property] = value
768
elif isinstance(value, str) or isinstance(value, unicode) or \
769
isinstance(value, int):
770
self._properties[property] = value
771
elif isinstance(value, list):
773
# If is_strong is True, each element is an XCObject, so it's safe
775
self._properties[property] = []
777
self._properties[property].append(item.Copy())
779
self._properties[property] = value[:]
780
elif isinstance(value, dict):
781
self._properties[property] = value.copy()
783
raise TypeError, "Don't know how to copy a " + \
784
value.__class__.__name__ + ' object for ' + \
785
property + ' in ' + self.__class__.__name__
787
self._properties[property] = value
789
# Set up the child's back-reference to this object. Don't use |value|
790
# any more because it may not be right if do_copy is true.
793
self._properties[property].parent = self
795
for item in self._properties[property]:
798
def HasProperty(self, key):
799
return key in self._properties
801
def GetProperty(self, key):
802
return self._properties[key]
804
def SetProperty(self, key, value):
805
self.UpdateProperties({key: value})
807
def DelProperty(self, key):
808
if key in self._properties:
809
del self._properties[key]
811
def AppendProperty(self, key, value):
812
# TODO(mark): Support ExtendProperty too (and make this call that)?
815
if not key in self._schema:
816
raise KeyError, key + ' not in ' + self.__class__.__name__
818
(is_list, property_type, is_strong) = self._schema[key][0:3]
820
raise TypeError, key + ' of ' + self.__class__.__name__ + ' must be list'
821
if not isinstance(value, property_type):
822
raise TypeError, 'item of ' + key + ' of ' + self.__class__.__name__ + \
823
' must be ' + property_type.__name__ + ', not ' + \
824
value.__class__.__name__
826
# If the property doesn't exist yet, create a new empty list to receive the
828
if not key in self._properties:
829
self._properties[key] = []
831
# Set up the ownership link.
836
self._properties[key].append(value)
838
def VerifyHasRequiredProperties(self):
839
"""Ensure that all properties identified as required by the schema are
843
# TODO(mark): A stronger verification mechanism is needed. Some
844
# subclasses need to perform validation beyond what the schema can enforce.
845
for property, attributes in self._schema.iteritems():
846
(is_list, property_type, is_strong, is_required) = attributes[0:4]
847
if is_required and not property in self._properties:
848
raise KeyError, self.__class__.__name__ + ' requires ' + property
850
def _SetDefaultsFromSchema(self):
851
"""Assign object default values according to the schema. This will not
852
overwrite properties that have already been set."""
855
for property, attributes in self._schema.iteritems():
856
(is_list, property_type, is_strong, is_required) = attributes[0:4]
857
if is_required and len(attributes) >= 5 and \
858
not property in self._properties:
859
default = attributes[4]
861
defaults[property] = default
863
if len(defaults) > 0:
864
# Use do_copy=True so that each new object gets its own copy of strong
865
# objects, lists, and dicts.
866
self.UpdateProperties(defaults, do_copy=True)
869
class XCHierarchicalElement(XCObject):
870
"""Abstract base for PBXGroup and PBXFileReference. Not represented in a
873
# TODO(mark): Do name and path belong here? Probably so.
874
# If path is set and name is not, name may have a default value. Name will
875
# be set to the basename of path, if the basename of path is different from
876
# the full value of path. If path is already just a leaf name, name will
878
_schema = XCObject._schema.copy()
880
'comments': [0, str, 0, 0],
881
'fileEncoding': [0, str, 0, 0],
882
'includeInIndex': [0, int, 0, 0],
883
'indentWidth': [0, int, 0, 0],
884
'lineEnding': [0, int, 0, 0],
885
'sourceTree': [0, str, 0, 1, '<group>'],
886
'tabWidth': [0, int, 0, 0],
887
'usesTabs': [0, int, 0, 0],
888
'wrapsLines': [0, int, 0, 0],
891
def __init__(self, properties=None, id=None, parent=None):
893
XCObject.__init__(self, properties, id, parent)
894
if 'path' in self._properties and not 'name' in self._properties:
895
path = self._properties['path']
896
name = posixpath.basename(path)
897
if name != '' and path != name:
898
self.SetProperty('name', name)
900
if 'path' in self._properties and \
901
(not 'sourceTree' in self._properties or \
902
self._properties['sourceTree'] == '<group>'):
903
# If the pathname begins with an Xcode variable like "$(SDKROOT)/", take
904
# the variable out and make the path be relative to that variable by
905
# assigning the variable name as the sourceTree.
906
(source_tree, path) = SourceTreeAndPathFromPath(self._properties['path'])
907
if source_tree != None:
908
self._properties['sourceTree'] = source_tree
910
self._properties['path'] = path
911
if source_tree != None and path == None and \
912
not 'name' in self._properties:
913
# The path was of the form "$(SDKROOT)" with no path following it.
914
# This object is now relative to that variable, so it has no path
915
# attribute of its own. It does, however, keep a name.
916
del self._properties['path']
917
self._properties['name'] = source_tree
920
if 'name' in self._properties:
921
return self._properties['name']
922
elif 'path' in self._properties:
923
return self._properties['path']
925
# This happens in the case of the root PBXGroup.
929
"""Custom hashables for XCHierarchicalElements.
931
XCHierarchicalElements are special. Generally, their hashes shouldn't
932
change if the paths don't change. The normal XCObject implementation of
933
Hashables adds a hashable for each object, which means that if
934
the hierarchical structure changes (possibly due to changes caused when
935
TakeOverOnlyChild runs and encounters slight changes in the hierarchy),
936
the hashes will change. For example, if a project file initially contains
937
a/b/f1 and a/b becomes collapsed into a/b, f1 will have a single parent
938
a/b. If someone later adds a/f2 to the project file, a/b can no longer be
939
collapsed, and f1 winds up with parent b and grandparent a. That would
940
be sufficient to change f1's hash.
942
To counteract this problem, hashables for all XCHierarchicalElements except
943
for the main group (which has neither a name nor a path) are taken to be
944
just the set of path components. Because hashables are inherited from
945
parents, this provides assurance that a/b/f1 has the same set of hashables
946
whether its parent is b or a/b.
948
The main group is a special case. As it is permitted to have no name or
949
path, it is permitted to use the standard XCObject hash mechanism. This
950
is not considered a problem because there can be only one main group.
953
if self == self.PBXProjectAncestor()._properties['mainGroup']:
955
return XCObject.Hashables(self)
959
# Put the name in first, ensuring that if TakeOverOnlyChild collapses
960
# children into a top-level group like "Source", the name always goes
961
# into the list of hashables without interfering with path components.
962
if 'name' in self._properties:
963
# Make it less likely for people to manipulate hashes by following the
964
# pattern of always pushing an object type value onto the list first.
965
hashables.append(self.__class__.__name__ + '.name')
966
hashables.append(self._properties['name'])
968
# NOTE: This still has the problem that if an absolute path is encountered,
969
# including paths with a sourceTree, they'll still inherit their parents'
970
# hashables, even though the paths aren't relative to their parents. This
971
# is not expected to be much of a problem in practice.
972
path = self.PathFromSourceTreeAndPath()
974
components = path.split(posixpath.sep)
975
for component in components:
976
hashables.append(self.__class__.__name__ + '.path')
977
hashables.append(component)
979
hashables.extend(self._hashables)
983
def Compare(self, other):
984
# Allow comparison of these types. PBXGroup has the highest sort rank;
985
# PBXVariantGroup is treated as equal to PBXFileReference.
986
valid_class_types = {
987
PBXFileReference: 'file',
989
PBXVariantGroup: 'file',
991
self_type = valid_class_types[self.__class__]
992
other_type = valid_class_types[other.__class__]
994
if self_type == other_type:
995
# If the two objects are of the same sort rank, compare their names.
996
return cmp(self.Name(), other.Name())
998
# Otherwise, sort groups before everything else.
999
if self_type == 'group':
1003
def CompareRootGroup(self, other):
1004
# This function should be used only to compare direct children of the
1005
# containing PBXProject's mainGroup. These groups should appear in the
1007
# TODO(mark): "Build" is used by gyp.generator.xcode, perhaps the
1008
# generator should have a way of influencing this list rather than having
1009
# to hardcode for the generator here.
1010
order = ['Source', 'Intermediates', 'Projects', 'Frameworks', 'Products',
1013
# If the groups aren't in the listed order, do a name comparison.
1014
# Otherwise, groups in the listed order should come before those that
1016
self_name = self.Name()
1017
other_name = other.Name()
1018
self_in = isinstance(self, PBXGroup) and self_name in order
1019
other_in = isinstance(self, PBXGroup) and other_name in order
1020
if not self_in and not other_in:
1021
return self.Compare(other)
1022
if self_name in order and not other_name in order:
1024
if other_name in order and not self_name in order:
1027
# If both groups are in the listed order, go by the defined order.
1028
self_index = order.index(self_name)
1029
other_index = order.index(other_name)
1030
if self_index < other_index:
1032
if self_index > other_index:
1036
def PathFromSourceTreeAndPath(self):
1037
# Turn the object's sourceTree and path properties into a single flat
1038
# string of a form comparable to the path parameter. If there's a
1039
# sourceTree property other than "<group>", wrap it in $(...) for the
1042
if self._properties['sourceTree'] != '<group>':
1043
components.append('$(' + self._properties['sourceTree'] + ')')
1044
if 'path' in self._properties:
1045
components.append(self._properties['path'])
1047
if len(components) > 0:
1048
return posixpath.join(*components)
1053
# Returns a full path to self relative to the project file, or relative
1054
# to some other source tree. Start with self, and walk up the chain of
1055
# parents prepending their paths, if any, until no more parents are
1056
# available (project-relative path) or until a path relative to some
1057
# source tree is found.
1060
while isinstance(xche, XCHierarchicalElement) and \
1062
(not path.startswith('/') and not path.startswith('$'))):
1063
this_path = xche.PathFromSourceTreeAndPath()
1064
if this_path != None and path != None:
1065
path = posixpath.join(this_path, path)
1066
elif this_path != None:
1073
class PBXGroup(XCHierarchicalElement):
1076
_children_by_path: Maps pathnames of children of this PBXGroup to the
1077
actual child XCHierarchicalElement objects.
1078
_variant_children_by_name_and_path: Maps (name, path) tuples of
1079
PBXVariantGroup children to the actual child PBXVariantGroup objects.
1082
_schema = XCHierarchicalElement._schema.copy()
1084
'children': [1, XCHierarchicalElement, 1, 1, []],
1085
'name': [0, str, 0, 0],
1086
'path': [0, str, 0, 0],
1089
def __init__(self, properties=None, id=None, parent=None):
1091
XCHierarchicalElement.__init__(self, properties, id, parent)
1092
self._children_by_path = {}
1093
self._variant_children_by_name_and_path = {}
1094
for child in self._properties.get('children', []):
1095
self._AddChildToDicts(child)
1097
def _AddChildToDicts(self, child):
1098
# Sets up this PBXGroup object's dicts to reference the child properly.
1099
child_path = child.PathFromSourceTreeAndPath()
1101
if child_path in self._children_by_path:
1102
raise ValueError, 'Found multiple children with path ' + child_path
1103
self._children_by_path[child_path] = child
1105
if isinstance(child, PBXVariantGroup):
1106
child_name = child._properties.get('name', None)
1107
key = (child_name, child_path)
1108
if key in self._variant_children_by_name_and_path:
1109
raise ValueError, 'Found multiple PBXVariantGroup children with ' + \
1110
'name ' + str(child_name) + ' and path ' + \
1112
self._variant_children_by_name_and_path[key] = child
1114
def AppendChild(self, child):
1115
# Callers should use this instead of calling
1116
# AppendProperty('children', child) directly because this function
1117
# maintains the group's dicts.
1118
self.AppendProperty('children', child)
1119
self._AddChildToDicts(child)
1121
def GetChildByName(self, name):
1122
# This is not currently optimized with a dict as GetChildByPath is because
1123
# it has few callers. Most callers probably want GetChildByPath. This
1124
# function is only useful to get children that have names but no paths,
1125
# which is rare. The children of the main group ("Source", "Products",
1126
# etc.) is pretty much the only case where this likely to come up.
1128
# TODO(mark): Maybe this should raise an error if more than one child is
1129
# present with the same name.
1130
if not 'children' in self._properties:
1133
for child in self._properties['children']:
1134
if child.Name() == name:
1139
def GetChildByPath(self, path):
1143
if path in self._children_by_path:
1144
return self._children_by_path[path]
1148
def GetChildByRemoteObject(self, remote_object):
1149
# This method is a little bit esoteric. Given a remote_object, which
1150
# should be a PBXFileReference in another project file, this method will
1151
# return this group's PBXReferenceProxy object serving as a local proxy
1152
# for the remote PBXFileReference.
1154
# This function might benefit from a dict optimization as GetChildByPath
1155
# for some workloads, but profiling shows that it's not currently a
1157
if not 'children' in self._properties:
1160
for child in self._properties['children']:
1161
if not isinstance(child, PBXReferenceProxy):
1164
container_proxy = child._properties['remoteRef']
1165
if container_proxy._properties['remoteGlobalIDString'] == remote_object:
1170
def AddOrGetFileByPath(self, path, hierarchical):
1171
"""Returns an existing or new file reference corresponding to path.
1173
If hierarchical is True, this method will create or use the necessary
1174
hierarchical group structure corresponding to path. Otherwise, it will
1175
look in and create an item in the current group only.
1177
If an existing matching reference is found, it is returned, otherwise, a
1178
new one will be created, added to the correct group, and returned.
1180
If path identifies a directory by virtue of carrying a trailing slash,
1181
this method returns a PBXFileReference of "folder" type. If path
1182
identifies a variant, by virtue of it identifying a file inside a directory
1183
with an ".lproj" extension, this method returns a PBXVariantGroup
1184
containing the variant named by path, and possibly other variants. For
1185
all other paths, a "normal" PBXFileReference will be returned.
1188
# Adding or getting a directory? Directories end with a trailing slash.
1190
if path.endswith('/'):
1192
normpath = posixpath.normpath(path)
1194
normpath = path + '/'
1198
# Adding or getting a variant? Variants are files inside directories
1199
# with an ".lproj" extension. Xcode uses variants for localization. For
1200
# a variant path/to/Language.lproj/MainMenu.nib, put a variant group named
1201
# MainMenu.nib inside path/to, and give it a variant named Language. In
1202
# this example, grandparent would be set to path/to and parent_root would
1203
# be set to Language.
1205
parent = posixpath.dirname(path)
1206
grandparent = posixpath.dirname(parent)
1207
parent_basename = posixpath.basename(parent)
1208
(parent_root, parent_ext) = posixpath.splitext(parent_basename)
1209
if parent_ext == '.lproj':
1210
variant_name = parent_root
1211
if grandparent == '':
1214
# Putting a directory inside a variant group is not currently supported.
1215
assert not is_dir or variant_name == None
1217
path_split = path.split(posixpath.sep)
1218
if len(path_split) == 1 or \
1219
((is_dir or variant_name != None) and len(path_split) == 2) or \
1221
# The PBXFileReference or PBXVariantGroup will be added to or gotten from
1222
# this PBXGroup, no recursion necessary.
1223
if variant_name == None:
1224
# Add or get a PBXFileReference.
1225
file_ref = self.GetChildByPath(normpath)
1226
if file_ref != None:
1227
assert file_ref.__class__ == PBXFileReference
1229
file_ref = PBXFileReference({'path': path})
1230
self.AppendChild(file_ref)
1232
# Add or get a PBXVariantGroup. The variant group name is the same
1233
# as the basename (MainMenu.nib in the example above). grandparent
1234
# specifies the path to the variant group itself, and path_split[-2:]
1235
# is the path of the specific variant relative to its group.
1236
variant_group_name = posixpath.basename(path)
1237
variant_group_ref = self.AddOrGetVariantGroupByNameAndPath(
1238
variant_group_name, grandparent)
1239
variant_path = posixpath.sep.join(path_split[-2:])
1240
variant_ref = variant_group_ref.GetChildByPath(variant_path)
1241
if variant_ref != None:
1242
assert variant_ref.__class__ == PBXFileReference
1244
variant_ref = PBXFileReference({'name': variant_name,
1245
'path': variant_path})
1246
variant_group_ref.AppendChild(variant_ref)
1247
# The caller is interested in the variant group, not the specific
1249
file_ref = variant_group_ref
1252
# Hierarchical recursion. Add or get a PBXGroup corresponding to the
1253
# outermost path component, and then recurse into it, chopping off that
1255
next_dir = path_split[0]
1256
group_ref = self.GetChildByPath(next_dir)
1257
if group_ref != None:
1258
assert group_ref.__class__ == PBXGroup
1260
group_ref = PBXGroup({'path': next_dir})
1261
self.AppendChild(group_ref)
1262
return group_ref.AddOrGetFileByPath(posixpath.sep.join(path_split[1:]),
1265
def AddOrGetVariantGroupByNameAndPath(self, name, path):
1266
"""Returns an existing or new PBXVariantGroup for name and path.
1268
If a PBXVariantGroup identified by the name and path arguments is already
1269
present as a child of this object, it is returned. Otherwise, a new
1270
PBXVariantGroup with the correct properties is created, added as a child,
1273
This method will generally be called by AddOrGetFileByPath, which knows
1274
when to create a variant group based on the structure of the pathnames
1279
if key in self._variant_children_by_name_and_path:
1280
variant_group_ref = self._variant_children_by_name_and_path[key]
1281
assert variant_group_ref.__class__ == PBXVariantGroup
1282
return variant_group_ref
1284
variant_group_properties = {'name': name}
1286
variant_group_properties['path'] = path
1287
variant_group_ref = PBXVariantGroup(variant_group_properties)
1288
self.AppendChild(variant_group_ref)
1290
return variant_group_ref
1292
def TakeOverOnlyChild(self, recurse=False):
1293
"""If this PBXGroup has only one child and it's also a PBXGroup, take
1294
it over by making all of its children this object's children.
1296
This function will continue to take over only children when those children
1297
are groups. If there are three PBXGroups representing a, b, and c, with
1298
c inside b and b inside a, and a and b have no other children, this will
1299
result in a taking over both b and c, forming a PBXGroup for a/b/c.
1301
If recurse is True, this function will recurse into children and ask them
1302
to collapse themselves by taking over only children as well. Assuming
1303
an example hierarchy with files at a/b/c/d1, a/b/c/d2, and a/b/c/d3/e/f
1304
(d1, d2, and f are files, the rest are groups), recursion will result in
1305
a group for a/b/c containing a group for d3/e.
1308
# At this stage, check that child class types are PBXGroup exactly,
1309
# instead of using isinstance. The only subclass of PBXGroup,
1310
# PBXVariantGroup, should not participate in reparenting in the same way:
1311
# reparenting by merging different object types would be wrong.
1312
while len(self._properties['children']) == 1 and \
1313
self._properties['children'][0].__class__ == PBXGroup:
1314
# Loop to take over the innermost only-child group possible.
1316
child = self._properties['children'][0]
1318
# Assume the child's properties, including its children. Save a copy
1319
# of this object's old properties, because they'll still be needed.
1320
# This object retains its existing id and parent attributes.
1321
old_properties = self._properties
1322
self._properties = child._properties
1323
self._children_by_path = child._children_by_path
1325
if not 'sourceTree' in self._properties or \
1326
self._properties['sourceTree'] == '<group>':
1327
# The child was relative to its parent. Fix up the path. Note that
1328
# children with a sourceTree other than "<group>" are not relative to
1329
# their parents, so no path fix-up is needed in that case.
1330
if 'path' in old_properties:
1331
if 'path' in self._properties:
1332
# Both the original parent and child have paths set.
1333
self._properties['path'] = posixpath.join(old_properties['path'],
1334
self._properties['path'])
1336
# Only the original parent has a path, use it.
1337
self._properties['path'] = old_properties['path']
1338
if 'sourceTree' in old_properties:
1339
# The original parent had a sourceTree set, use it.
1340
self._properties['sourceTree'] = old_properties['sourceTree']
1342
# If the original parent had a name set, keep using it. If the original
1343
# parent didn't have a name but the child did, let the child's name
1344
# live on. If the name attribute seems unnecessary now, get rid of it.
1345
if 'name' in old_properties and old_properties['name'] != None and \
1346
old_properties['name'] != self.Name():
1347
self._properties['name'] = old_properties['name']
1348
if 'name' in self._properties and 'path' in self._properties and \
1349
self._properties['name'] == self._properties['path']:
1350
del self._properties['name']
1352
# Notify all children of their new parent.
1353
for child in self._properties['children']:
1356
# If asked to recurse, recurse.
1358
for child in self._properties['children']:
1359
if child.__class__ == PBXGroup:
1360
child.TakeOverOnlyChild(recurse)
1362
def SortGroup(self):
1363
self._properties['children'] = \
1364
sorted(self._properties['children'], cmp=lambda x,y: x.Compare(y))
1367
for child in self._properties['children']:
1368
if isinstance(child, PBXGroup):
1372
class XCFileLikeElement(XCHierarchicalElement):
1373
# Abstract base for objects that can be used as the fileRef property of
1376
def PathHashables(self):
1377
# A PBXBuildFile that refers to this object will call this method to
1378
# obtain additional hashables specific to this XCFileLikeElement. Don't
1379
# just use this object's hashables, they're not specific and unique enough
1380
# on their own (without access to the parent hashables.) Instead, provide
1381
# hashables that identify this object by path by getting its hashables as
1382
# well as the hashables of ancestor XCHierarchicalElement objects.
1386
while xche != None and isinstance(xche, XCHierarchicalElement):
1387
xche_hashables = xche.Hashables()
1388
for index in xrange(0, len(xche_hashables)):
1389
hashables.insert(index, xche_hashables[index])
1394
class XCContainerPortal(XCObject):
1395
# Abstract base for objects that can be used as the containerPortal property
1396
# of PBXContainerItemProxy.
1400
class XCRemoteObject(XCObject):
1401
# Abstract base for objects that can be used as the remoteGlobalIDString
1402
# property of PBXContainerItemProxy.
1406
class PBXFileReference(XCFileLikeElement, XCContainerPortal, XCRemoteObject):
1407
_schema = XCFileLikeElement._schema.copy()
1409
'explicitFileType': [0, str, 0, 0],
1410
'lastKnownFileType': [0, str, 0, 0],
1411
'name': [0, str, 0, 0],
1412
'path': [0, str, 0, 1],
1415
# Weird output rules for PBXFileReference.
1416
_should_print_single_line = True
1418
_encode_transforms = XCFileLikeElement._alternate_encode_transforms
1420
def __init__(self, properties=None, id=None, parent=None):
1422
XCFileLikeElement.__init__(self, properties, id, parent)
1423
if 'path' in self._properties and self._properties['path'].endswith('/'):
1424
self._properties['path'] = self._properties['path'][:-1]
1429
if 'path' in self._properties and \
1430
not 'lastKnownFileType' in self._properties and \
1431
not 'explicitFileType' in self._properties:
1432
# TODO(mark): This is the replacement for a replacement for a quick hack.
1433
# It is no longer incredibly sucky, but this list needs to be extended.
1436
'app': 'wrapper.application',
1438
'bundle': 'wrapper.cfbundle',
1439
'c': 'sourcecode.c.c',
1440
'cc': 'sourcecode.cpp.cpp',
1441
'cpp': 'sourcecode.cpp.cpp',
1443
'cxx': 'sourcecode.cpp.cpp',
1444
'dylib': 'compiled.mach-o.dylib',
1445
'framework': 'wrapper.framework',
1446
'h': 'sourcecode.c.h',
1447
'hxx': 'sourcecode.cpp.h',
1448
'icns': 'image.icns',
1449
'java': 'sourcecode.java',
1450
'js': 'sourcecode.javascript',
1451
'm': 'sourcecode.c.objc',
1452
'mm': 'sourcecode.cpp.objcpp',
1453
'nib': 'wrapper.nib',
1455
'pl': 'text.script.perl',
1456
'plist': 'text.plist.xml',
1457
'pm': 'text.script.perl',
1459
'py': 'text.script.python',
1460
'r': 'sourcecode.rez',
1461
'rez': 'sourcecode.rez',
1462
's': 'sourcecode.asm',
1463
'strings': 'text.plist.strings',
1465
'xcconfig': 'text.xcconfig',
1467
'y': 'sourcecode.yacc',
1471
file_type = 'folder'
1473
basename = posixpath.basename(self._properties['path'])
1474
(root, ext) = posixpath.splitext(basename)
1475
# Check the map using a lowercase extension.
1476
# TODO(mark): Maybe it should try with the original case first and fall
1477
# back to lowercase, in case there are any instances where case
1478
# matters. There currently aren't.
1480
ext = ext[1:].lower()
1482
# TODO(mark): "text" is the default value, but "file" is appropriate
1483
# for unrecognized files not containing text. Xcode seems to choose
1485
file_type = extension_map.get(ext, 'text')
1487
self._properties['lastKnownFileType'] = file_type
1490
class PBXVariantGroup(PBXGroup, XCFileLikeElement):
1491
"""PBXVariantGroup is used by Xcode to represent localizations."""
1492
# No additions to the schema relative to PBXGroup.
1496
# PBXReferenceProxy is also an XCFileLikeElement subclass. It is defined below
1497
# because it uses PBXContainerItemProxy, defined below.
1500
class XCBuildConfiguration(XCObject):
1501
_schema = XCObject._schema.copy()
1503
'baseConfigurationReference': [0, PBXFileReference, 0, 0],
1504
'buildSettings': [0, dict, 0, 1, {}],
1505
'name': [0, str, 0, 1],
1508
def HasBuildSetting(self, key):
1509
return key in self._properties['buildSettings']
1511
def GetBuildSetting(self, key):
1512
return self._properties['buildSettings'][key]
1514
def SetBuildSetting(self, key, value):
1515
# TODO(mark): If a list, copy?
1516
self._properties['buildSettings'][key] = value
1518
def AppendBuildSetting(self, key, value):
1519
if not key in self._properties['buildSettings']:
1520
self._properties['buildSettings'][key] = []
1521
self._properties['buildSettings'][key].append(value)
1523
def DelBuildSetting(self, key):
1524
if key in self._properties['buildSettings']:
1525
del self._properties['buildSettings'][key]
1528
class XCConfigurationList(XCObject):
1529
# _configs is the default list of configurations.
1530
_configs = [ XCBuildConfiguration({'name': 'Debug'}),
1531
XCBuildConfiguration({'name': 'Release'}) ]
1533
_schema = XCObject._schema.copy()
1535
'buildConfigurations': [1, XCBuildConfiguration, 1, 1, _configs],
1536
'defaultConfigurationIsVisible': [0, int, 0, 1, 1],
1537
'defaultConfigurationName': [0, str, 0, 1, 'Release'],
1541
return 'Build configuration list for ' + \
1542
self.parent.__class__.__name__ + ' "' + self.parent.Name() + '"'
1544
def ConfigurationNamed(self, name):
1545
"""Convenience accessor to obtain an XCBuildConfiguration by name."""
1546
for configuration in self._properties['buildConfigurations']:
1547
if configuration._properties['name'] == name:
1548
return configuration
1550
raise KeyError, name
1552
def DefaultConfiguration(self):
1553
"""Convenience accessor to obtain the default XCBuildConfiguration."""
1554
return self.ConfigurationNamed(self._properties['defaultConfigurationName'])
1556
def HasBuildSetting(self, key):
1557
"""Determines the state of a build setting in all XCBuildConfiguration
1560
If all child objects have key in their build settings, and the value is the
1561
same in all child objects, returns 1.
1563
If no child objects have the key in their build settings, returns 0.
1565
If some, but not all, child objects have the key in their build settings,
1566
or if any children have different values for the key, returns -1.
1571
for configuration in self._properties['buildConfigurations']:
1572
configuration_has = configuration.HasBuildSetting(key)
1574
has = configuration_has
1575
elif has != configuration_has:
1578
if configuration_has:
1579
configuration_value = configuration.GetBuildSetting(key)
1581
value = configuration_value
1582
elif value != configuration_value:
1590
def GetBuildSetting(self, key):
1591
"""Gets the build setting for key.
1593
All child XCConfiguration objects must have the same value set for the
1594
setting, or a ValueError will be raised.
1597
# TODO(mark): This is wrong for build settings that are lists. The list
1598
# contents should be compared (and a list copy returned?)
1601
for configuration in self._properties['buildConfigurations']:
1602
configuration_value = configuration.GetBuildSetting(key)
1604
value = configuration_value
1606
if value != configuration_value:
1607
raise ValueError, 'Variant values for ' + key
1611
def SetBuildSetting(self, key, value):
1612
"""Sets the build setting for key to value in all child
1613
XCBuildConfiguration objects.
1616
for configuration in self._properties['buildConfigurations']:
1617
configuration.SetBuildSetting(key, value)
1619
def AppendBuildSetting(self, key, value):
1620
"""Appends value to the build setting for key, which is treated as a list,
1621
in all child XCBuildConfiguration objects.
1624
for configuration in self._properties['buildConfigurations']:
1625
configuration.AppendBuildSetting(key, value)
1627
def DelBuildSetting(self, key):
1628
"""Deletes the build setting key from all child XCBuildConfiguration
1632
for configuration in self._properties['buildConfigurations']:
1633
configuration.DelBuildSetting(key)
1636
class PBXBuildFile(XCObject):
1637
_schema = XCObject._schema.copy()
1639
'fileRef': [0, XCFileLikeElement, 0, 1],
1642
# Weird output rules for PBXBuildFile.
1643
_should_print_single_line = True
1644
_encode_transforms = XCObject._alternate_encode_transforms
1647
# Example: "main.cc in Sources"
1648
return self._properties['fileRef'].Name() + ' in ' + self.parent.Name()
1650
def Hashables(self):
1652
hashables = XCObject.Hashables(self)
1654
# It is not sufficient to just rely on Name() to get the
1655
# XCFileLikeElement's name, because that is not a complete pathname.
1656
# PathHashables returns hashables unique enough that no two
1657
# PBXBuildFiles should wind up with the same set of hashables, unless
1658
# someone adds the same file multiple times to the same target. That
1659
# would be considered invalid anyway.
1660
hashables.extend(self._properties['fileRef'].PathHashables())
1665
class XCBuildPhase(XCObject):
1666
"""Abstract base for build phase classes. Not represented in a project
1670
_files_by_path: A dict mapping each path of a child in the files list by
1671
path (keys) to the corresponding PBXBuildFile children (values).
1672
_files_by_xcfilelikeelement: A dict mapping each XCFileLikeElement (keys)
1673
to the corresponding PBXBuildFile children (values).
1676
# TODO(mark): Some build phase types, like PBXShellScriptBuildPhase, don't
1677
# actually have a "files" list. XCBuildPhase should not have "files" but
1678
# another abstract subclass of it should provide this, and concrete build
1679
# phase types that do have "files" lists should be derived from that new
1680
# abstract subclass. XCBuildPhase should only provide buildActionMask and
1681
# runOnlyForDeploymentPostprocessing, and not files or the various
1682
# file-related methods and attributes.
1684
_schema = XCObject._schema.copy()
1686
'buildActionMask': [0, int, 0, 1, 0x7fffffff],
1687
'files': [1, PBXBuildFile, 1, 1, []],
1688
'runOnlyForDeploymentPostprocessing': [0, int, 0, 1, 0],
1691
def __init__(self, properties=None, id=None, parent=None):
1693
XCObject.__init__(self, properties, id, parent)
1695
self._files_by_path = {}
1696
self._files_by_xcfilelikeelement = {}
1697
for pbxbuildfile in self._properties.get('files', []):
1698
self._AddBuildFileToDicts(pbxbuildfile)
1700
def FileGroup(self, path):
1701
# Subclasses must override this by returning a two-element tuple. The
1702
# first item in the tuple should be the PBXGroup to which "path" should be
1703
# added, either as a child or deeper descendant. The second item should
1704
# be a boolean indicating whether files should be added into hierarchical
1705
# groups or one single flat group.
1706
raise NotImplementedError, \
1707
self.__class__.__name__ + ' must implement FileGroup'
1709
def _AddPathToDict(self, pbxbuildfile, path):
1710
"""Adds path to the dict tracking paths belonging to this build phase.
1712
If the path is already a member of this build phase, raises an exception.
1715
if path in self._files_by_path:
1716
raise ValueError, 'Found multiple build files with path ' + path
1717
self._files_by_path[path] = pbxbuildfile
1719
def _AddBuildFileToDicts(self, pbxbuildfile, path=None):
1720
"""Maintains the _files_by_path and _files_by_xcfilelikeelement dicts.
1722
If path is specified, then it is the path that is being added to the
1723
phase, and pbxbuildfile must contain either a PBXFileReference directly
1724
referencing that path, or it must contain a PBXVariantGroup that itself
1725
contains a PBXFileReference referencing the path.
1727
If path is not specified, either the PBXFileReference's path or the paths
1728
of all children of the PBXVariantGroup are taken as being added to the
1731
If the path is already present in the phase, raises an exception.
1733
If the PBXFileReference or PBXVariantGroup referenced by pbxbuildfile
1734
are already present in the phase, referenced by a different PBXBuildFile
1735
object, raises an exception. This does not raise an exception when
1736
a PBXFileReference or PBXVariantGroup reappear and are referenced by the
1737
same PBXBuildFile that has already introduced them, because in the case
1738
of PBXVariantGroup objects, they may correspond to multiple paths that are
1739
not all added simultaneously. When this situation occurs, the path needs
1740
to be added to _files_by_path, but nothing needs to change in
1741
_files_by_xcfilelikeelement, and the caller should have avoided adding
1742
the PBXBuildFile if it is already present in the list of children.
1745
xcfilelikeelement = pbxbuildfile._properties['fileRef']
1749
# It's best when the caller provides the path.
1750
if isinstance(xcfilelikeelement, PBXVariantGroup):
1753
# If the caller didn't provide a path, there can be either multiple
1754
# paths (PBXVariantGroup) or one.
1755
if isinstance(xcfilelikeelement, PBXVariantGroup):
1756
for variant in xcfilelikeelement._properties['children']:
1757
paths.append(variant.FullPath())
1759
paths.append(xcfilelikeelement.FullPath())
1761
# Add the paths first, because if something's going to raise, the
1762
# messages provided by _AddPathToDict are more useful owing to its
1763
# having access to a real pathname and not just an object's Name().
1764
for a_path in paths:
1765
self._AddPathToDict(pbxbuildfile, a_path)
1767
# If another PBXBuildFile references this XCFileLikeElement, there's a
1769
if xcfilelikeelement in self._files_by_xcfilelikeelement and \
1770
self._files_by_xcfilelikeelement[xcfilelikeelement] != pbxbuildfile:
1771
raise ValueError, 'Found multiple build files for ' + \
1772
xcfilelikeelement.Name()
1773
self._files_by_xcfilelikeelement[xcfilelikeelement] = pbxbuildfile
1775
def AppendBuildFile(self, pbxbuildfile, path=None):
1776
# Callers should use this instead of calling
1777
# AppendProperty('files', pbxbuildfile) directly because this function
1778
# maintains the object's dicts. Better yet, callers can just call AddFile
1779
# with a pathname and not worry about building their own PBXBuildFile
1781
self.AppendProperty('files', pbxbuildfile)
1782
self._AddBuildFileToDicts(pbxbuildfile, path)
1784
def AddFile(self, path):
1785
(file_group, hierarchical) = self.FileGroup(path)
1786
file_ref = file_group.AddOrGetFileByPath(path, hierarchical)
1788
if file_ref in self._files_by_xcfilelikeelement and \
1789
isinstance(file_ref, PBXVariantGroup):
1790
# There's already a PBXBuildFile in this phase corresponding to the
1791
# PBXVariantGroup. path just provides a new variant that belongs to
1792
# the group. Add the path to the dict.
1793
pbxbuildfile = self._files_by_xcfilelikeelement[file_ref]
1794
self._AddBuildFileToDicts(pbxbuildfile, path)
1796
# Add a new PBXBuildFile to get file_ref into the phase.
1797
pbxbuildfile = PBXBuildFile({'fileRef': file_ref})
1798
self.AppendBuildFile(pbxbuildfile, path)
1801
class PBXHeadersBuildPhase(XCBuildPhase):
1802
# No additions to the schema relative to XCBuildPhase.
1807
def FileGroup(self, path):
1808
return self.PBXProjectAncestor().RootGroupForPath(path)
1811
class PBXResourcesBuildPhase(XCBuildPhase):
1812
# No additions to the schema relative to XCBuildPhase.
1817
def FileGroup(self, path):
1818
return self.PBXProjectAncestor().RootGroupForPath(path)
1821
class PBXSourcesBuildPhase(XCBuildPhase):
1822
# No additions to the schema relative to XCBuildPhase.
1827
def FileGroup(self, path):
1828
return self.PBXProjectAncestor().RootGroupForPath(path)
1831
class PBXFrameworksBuildPhase(XCBuildPhase):
1832
# No additions to the schema relative to XCBuildPhase.
1837
def FileGroup(self, path):
1838
return (self.PBXProjectAncestor().FrameworksGroup(), False)
1841
class PBXShellScriptBuildPhase(XCBuildPhase):
1842
_schema = XCBuildPhase._schema.copy()
1844
'inputPaths': [1, str, 0, 1, []],
1845
'name': [0, str, 0, 0],
1846
'outputPaths': [1, str, 0, 1, []],
1847
'shellPath': [0, str, 0, 1, '/bin/sh'],
1848
'shellScript': [0, str, 0, 1],
1849
'showEnvVarsInLog': [0, int, 0, 0],
1853
if 'name' in self._properties:
1854
return self._properties['name']
1856
return 'ShellScript'
1859
class PBXCopyFilesBuildPhase(XCBuildPhase):
1860
_schema = XCBuildPhase._schema.copy()
1862
'dstPath': [0, str, 0, 1],
1863
'dstSubfolderSpec': [0, int, 0, 1],
1864
'name': [0, str, 0, 0],
1867
# path_tree_re matches "$(DIR)/path" or just "$(DIR)". Match group 1 is
1868
# "DIR", match group 3 is "path" or None.
1869
path_tree_re = re.compile('^\\$\\((.*)\\)(/(.*)|)$')
1871
# path_tree_to_subfolder maps names of Xcode variables to the associated
1872
# dstSubfolderSpec property value used in a PBXCopyFilesBuildPhase object.
1873
path_tree_to_subfolder = {
1874
'BUILT_PRODUCTS_DIR': 16, # Products Directory
1875
# Other types that can be chosen via the Xcode UI.
1876
# TODO(mark): Map Xcode variable names to these.
1878
# : 6, # Executables: 6
1880
# : 15, # Java Resources
1881
# : 10, # Frameworks
1882
# : 11, # Shared Frameworks
1883
# : 12, # Shared Support
1888
if 'name' in self._properties:
1889
return self._properties['name']
1893
def FileGroup(self, path):
1894
return self.PBXProjectAncestor().RootGroupForPath(path)
1896
def SetDestination(self, path):
1897
"""Set the dstSubfolderSpec and dstPath properties from path.
1899
path may be specified in the same notation used for XCHierarchicalElements,
1900
specifically, "$(DIR)/path".
1903
path_tree_match = self.path_tree_re.search(path)
1905
# Everything else needs to be relative to an Xcode variable.
1906
path_tree = path_tree_match.group(1)
1907
relative_path = path_tree_match.group(3)
1909
if path_tree in self.path_tree_to_subfolder:
1910
subfolder = self.path_tree_to_subfolder[path_tree]
1911
if relative_path == None:
1914
# The path starts with an unrecognized Xcode variable
1915
# name like $(SRCROOT). Xcode will still handle this
1916
# as an "absolute path" that starts with the variable.
1918
relative_path = path
1919
elif path.startswith('/'):
1920
# Special case. Absolute paths are in dstSubfolderSpec 0.
1922
relative_path = path[1:]
1924
raise ValueError, 'Can\'t use path %s in a %s' % \
1925
(path, self.__class__.__name__)
1927
self._properties['dstPath'] = relative_path
1928
self._properties['dstSubfolderSpec'] = subfolder
1931
class PBXBuildRule(XCObject):
1932
_schema = XCObject._schema.copy()
1934
'compilerSpec': [0, str, 0, 1],
1935
'filePatterns': [0, str, 0, 0],
1936
'fileType': [0, str, 0, 1],
1937
'isEditable': [0, int, 0, 1, 1],
1938
'outputFiles': [1, str, 0, 1, []],
1939
'script': [0, str, 0, 0],
1943
# Not very inspired, but it's what Xcode uses.
1944
return self.__class__.__name__
1946
def Hashables(self):
1948
hashables = XCObject.Hashables(self)
1950
# Use the hashables of the weak objects that this object refers to.
1951
hashables.append(self._properties['fileType'])
1952
if 'filePatterns' in self._properties:
1953
hashables.append(self._properties['filePatterns'])
1957
class PBXContainerItemProxy(XCObject):
1958
# When referencing an item in this project file, containerPortal is the
1959
# PBXProject root object of this project file. When referencing an item in
1960
# another project file, containerPortal is a PBXFileReference identifying
1961
# the other project file.
1963
# When serving as a proxy to an XCTarget (in this project file or another),
1964
# proxyType is 1. When serving as a proxy to a PBXFileReference (in another
1965
# project file), proxyType is 2. Type 2 is used for references to the
1966
# producs of the other project file's targets.
1968
# Xcode is weird about remoteGlobalIDString. Usually, it's printed without
1969
# a comment, indicating that it's tracked internally simply as a string, but
1970
# sometimes it's printed with a comment (usually when the object is initially
1971
# created), indicating that it's tracked as a project file object at least
1972
# sometimes. This module always tracks it as an object, but contains a hack
1973
# to prevent it from printing the comment in the project file output. See
1975
_schema = XCObject._schema.copy()
1977
'containerPortal': [0, XCContainerPortal, 0, 1],
1978
'proxyType': [0, int, 0, 1],
1979
'remoteGlobalIDString': [0, XCRemoteObject, 0, 1],
1980
'remoteInfo': [0, str, 0, 1],
1984
props = self._properties
1985
name = '%s.gyp:%s' % (props['containerPortal'].Name(), props['remoteInfo'])
1986
return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
1989
# Admittedly not the best name, but it's what Xcode uses.
1990
return self.__class__.__name__
1992
def Hashables(self):
1994
hashables = XCObject.Hashables(self)
1996
# Use the hashables of the weak objects that this object refers to.
1997
hashables.extend(self._properties['containerPortal'].Hashables())
1998
hashables.extend(self._properties['remoteGlobalIDString'].Hashables())
2002
class PBXTargetDependency(XCObject):
2003
# The "target" property accepts an XCTarget object, and obviously not
2004
# NoneType. But XCTarget is defined below, so it can't be put into the
2005
# schema yet. The definition of PBXTargetDependency can't be moved below
2006
# XCTarget because XCTarget's own schema references PBXTargetDependency.
2007
# Python doesn't deal well with this circular relationship, and doesn't have
2008
# a real way to do forward declarations. To work around, the type of
2009
# the "target" property is reset below, after XCTarget is defined.
2011
# At least one of "name" and "target" is required.
2012
_schema = XCObject._schema.copy()
2014
'name': [0, str, 0, 0],
2015
'target': [0, None.__class__, 0, 0],
2016
'targetProxy': [0, PBXContainerItemProxy, 1, 1],
2020
name = self._properties.get('name') or self._properties['target'].Name()
2021
return '<%s %r at 0x%x>' % (self.__class__.__name__, name, id(self))
2024
# Admittedly not the best name, but it's what Xcode uses.
2025
return self.__class__.__name__
2027
def Hashables(self):
2029
hashables = XCObject.Hashables(self)
2031
# Use the hashables of the weak objects that this object refers to.
2032
hashables.extend(self._properties['targetProxy'].Hashables())
2036
class PBXReferenceProxy(XCFileLikeElement):
2037
_schema = XCFileLikeElement._schema.copy()
2039
'fileType': [0, str, 0, 1],
2040
'path': [0, str, 0, 1],
2041
'remoteRef': [0, PBXContainerItemProxy, 1, 1],
2045
class XCTarget(XCRemoteObject):
2046
# An XCTarget is really just an XCObject, the XCRemoteObject thing is just
2047
# to allow PBXProject to be used in the remoteGlobalIDString property of
2048
# PBXContainerItemProxy.
2050
# Setting a "name" property at instantiation may also affect "productName",
2051
# which may in turn affect the "PRODUCT_NAME" build setting in children of
2052
# "buildConfigurationList". See __init__ below.
2053
_schema = XCRemoteObject._schema.copy()
2055
'buildConfigurationList': [0, XCConfigurationList, 1, 1,
2056
XCConfigurationList()],
2057
'buildPhases': [1, XCBuildPhase, 1, 1, []],
2058
'dependencies': [1, PBXTargetDependency, 1, 1, []],
2059
'name': [0, str, 0, 1],
2060
'productName': [0, str, 0, 1],
2063
def __init__(self, properties=None, id=None, parent=None,
2064
force_outdir=None, force_prefix=None, force_extension=None):
2066
XCRemoteObject.__init__(self, properties, id, parent)
2068
# Set up additional defaults not expressed in the schema. If a "name"
2069
# property was supplied, set "productName" if it is not present. Also set
2070
# the "PRODUCT_NAME" build setting in each configuration, but only if
2071
# the setting is not present in any build configuration.
2072
if 'name' in self._properties:
2073
if not 'productName' in self._properties:
2074
self.SetProperty('productName', self._properties['name'])
2076
if 'productName' in self._properties:
2077
if 'buildConfigurationList' in self._properties:
2078
configs = self._properties['buildConfigurationList']
2079
if configs.HasBuildSetting('PRODUCT_NAME') == 0:
2080
configs.SetBuildSetting('PRODUCT_NAME',
2081
self._properties['productName'])
2083
def AddDependency(self, other):
2084
pbxproject = self.PBXProjectAncestor()
2085
other_pbxproject = other.PBXProjectAncestor()
2086
if pbxproject == other_pbxproject:
2087
# The easy case. Add a dependency to another target in the same
2089
container = PBXContainerItemProxy({'containerPortal': pbxproject,
2091
'remoteGlobalIDString': other,
2092
'remoteInfo': other.Name()})
2093
dependency = PBXTargetDependency({'target': other,
2094
'targetProxy': container})
2095
self.AppendProperty('dependencies', dependency)
2097
# The hard case. Add a dependency to a target in a different project
2098
# file. Actually, this case isn't really so hard.
2099
other_project_ref = \
2100
pbxproject.AddOrGetProjectReference(other_pbxproject)[1]
2101
container = PBXContainerItemProxy({
2102
'containerPortal': other_project_ref,
2104
'remoteGlobalIDString': other,
2105
'remoteInfo': other.Name(),
2107
dependency = PBXTargetDependency({'name': other.Name(),
2108
'targetProxy': container})
2109
self.AppendProperty('dependencies', dependency)
2111
# Proxy all of these through to the build configuration list.
2113
def ConfigurationNamed(self, name):
2114
return self._properties['buildConfigurationList'].ConfigurationNamed(name)
2116
def DefaultConfiguration(self):
2117
return self._properties['buildConfigurationList'].DefaultConfiguration()
2119
def HasBuildSetting(self, key):
2120
return self._properties['buildConfigurationList'].HasBuildSetting(key)
2122
def GetBuildSetting(self, key):
2123
return self._properties['buildConfigurationList'].GetBuildSetting(key)
2125
def SetBuildSetting(self, key, value):
2126
return self._properties['buildConfigurationList'].SetBuildSetting(key, \
2129
def AppendBuildSetting(self, key, value):
2130
return self._properties['buildConfigurationList'].AppendBuildSetting(key, \
2133
def DelBuildSetting(self, key):
2134
return self._properties['buildConfigurationList'].DelBuildSetting(key)
2137
# Redefine the type of the "target" property. See PBXTargetDependency._schema
2139
PBXTargetDependency._schema['target'][1] = XCTarget
2142
class PBXNativeTarget(XCTarget):
2143
# buildPhases is overridden in the schema to be able to set defaults.
2145
# NOTE: Contrary to most objects, it is advisable to set parent when
2146
# constructing PBXNativeTarget. A parent of an XCTarget must be a PBXProject
2147
# object. A parent reference is required for a PBXNativeTarget during
2148
# construction to be able to set up the target defaults for productReference,
2149
# because a PBXBuildFile object must be created for the target and it must
2150
# be added to the PBXProject's mainGroup hierarchy.
2151
_schema = XCTarget._schema.copy()
2153
'buildPhases': [1, XCBuildPhase, 1, 1,
2154
[PBXSourcesBuildPhase(), PBXFrameworksBuildPhase()]],
2155
'buildRules': [1, PBXBuildRule, 1, 1, []],
2156
'productReference': [0, PBXFileReference, 0, 1],
2157
'productType': [0, str, 0, 1],
2160
# Mapping from Xcode product-types to settings. The settings are:
2161
# filetype : used for explicitFileType in the project file
2162
# prefix : the prefix for the file name
2163
# suffix : the suffix for the filen ame
2164
# set_xc_exe_prefix : bool to say if EXECUTABLE_PREFIX should be set to the
2166
_product_filetypes = {
2167
'com.apple.product-type.application': ['wrapper.application',
2169
'com.apple.product-type.bundle': ['wrapper.cfbundle',
2170
'', '.bundle', False],
2171
'com.apple.product-type.framework': ['wrapper.framework',
2172
'', '.framework', False],
2173
'com.apple.product-type.library.dynamic': ['compiled.mach-o.dylib',
2174
'lib', '.dylib', True],
2175
'com.apple.product-type.library.static': ['archive.ar',
2176
'lib', '.a', False],
2177
'com.apple.product-type.tool': ['compiled.mach-o.executable',
2181
def __init__(self, properties=None, id=None, parent=None,
2182
force_outdir=None, force_prefix=None, force_extension=None):
2184
XCTarget.__init__(self, properties, id, parent)
2186
if 'productName' in self._properties and \
2187
'productType' in self._properties and \
2188
not 'productReference' in self._properties and \
2189
self._properties['productType'] in self._product_filetypes:
2190
products_group = None
2191
pbxproject = self.PBXProjectAncestor()
2192
if pbxproject != None:
2193
products_group = pbxproject.ProductsGroup()
2195
if products_group != None:
2196
(filetype, prefix, suffix, set_xc_exe_prefix) = \
2197
self._product_filetypes[self._properties['productType']]
2199
if force_extension is not None:
2200
# If it's a wrapper (bundle), set WRAPPER_EXTENSION.
2201
if filetype.startswith('wrapper.'):
2202
self.SetBuildSetting('WRAPPER_EXTENSION', force_extension)
2204
# Extension override.
2205
suffix = '.' + force_extension
2206
self.SetBuildSetting('EXECUTABLE_EXTENSION', force_extension)
2208
if filetype.startswith('compiled.mach-o.executable'):
2209
product_name = self._properties['productName']
2210
product_name += suffix
2212
self.SetProperty('productName', product_name)
2213
self.SetBuildSetting('PRODUCT_NAME', product_name)
2215
# Xcode handles most prefixes based on the target type, however there
2216
# are exceptions. If a "BSD Dynamic Library" target is added in the
2217
# Xcode UI, Xcode sets EXECUTABLE_PREFIX. This check duplicates that
2219
if force_prefix is not None:
2220
prefix = force_prefix
2221
if filetype.startswith('wrapper.'):
2222
self.SetBuildSetting('WRAPPER_PREFIX', prefix)
2224
self.SetBuildSetting('EXECUTABLE_PREFIX', prefix)
2226
if force_outdir is not None:
2227
self.SetBuildSetting('TARGET_BUILD_DIR', force_outdir)
2229
# TODO(tvl): Remove the below hack.
2230
# http://code.google.com/p/gyp/issues/detail?id=122
2232
# Some targets include the prefix in the target_name. These targets
2233
# really should just add a product_name setting that doesn't include
2234
# the prefix. For example:
2235
# target_name = 'libevent', product_name = 'event'
2236
# This check cleans up for them.
2237
product_name = self._properties['productName']
2238
prefix_len = len(prefix)
2239
if prefix_len and (product_name[:prefix_len] == prefix):
2240
product_name = product_name[prefix_len:]
2241
self.SetProperty('productName', product_name)
2242
self.SetBuildSetting('PRODUCT_NAME', product_name)
2245
'explicitFileType': filetype,
2246
'includeInIndex': 0,
2247
'path': prefix + product_name + suffix,
2248
'sourceTree': 'BUILT_PRODUCTS_DIR',
2250
file_ref = PBXFileReference(ref_props)
2251
products_group.AppendChild(file_ref)
2252
self.SetProperty('productReference', file_ref)
2254
def GetBuildPhaseByType(self, type):
2255
if not 'buildPhases' in self._properties:
2259
for phase in self._properties['buildPhases']:
2260
if isinstance(phase, type):
2261
# Some phases may be present in multiples in a well-formed project file,
2262
# but phases like PBXSourcesBuildPhase may only be present singly, and
2263
# this function is intended as an aid to GetBuildPhaseByType. Loop
2264
# over the entire list of phases and assert if more than one of the
2265
# desired type is found.
2266
assert the_phase == None
2271
def ResourcesPhase(self):
2272
resources_phase = self.GetBuildPhaseByType(PBXResourcesBuildPhase)
2273
if resources_phase == None:
2274
resources_phase = PBXResourcesBuildPhase()
2276
# The resources phase should come before the sources and frameworks
2278
insert_at = len(self._properties['buildPhases'])
2279
for index in xrange(0, len(self._properties['buildPhases'])):
2280
phase = self._properties['buildPhases'][index]
2281
if isinstance(phase, PBXSourcesBuildPhase) or \
2282
isinstance(phase, PBXFrameworksBuildPhase):
2286
self._properties['buildPhases'].insert(insert_at, resources_phase)
2287
resources_phase.parent = self
2289
return resources_phase
2291
def SourcesPhase(self):
2292
sources_phase = self.GetBuildPhaseByType(PBXSourcesBuildPhase)
2293
if sources_phase == None:
2294
sources_phase = PBXSourcesBuildPhase()
2295
self.AppendProperty('buildPhases', sources_phase)
2297
return sources_phase
2299
def FrameworksPhase(self):
2300
frameworks_phase = self.GetBuildPhaseByType(PBXFrameworksBuildPhase)
2301
if frameworks_phase == None:
2302
frameworks_phase = PBXFrameworksBuildPhase()
2303
self.AppendProperty('buildPhases', frameworks_phase)
2305
return frameworks_phase
2307
def AddDependency(self, other):
2309
XCTarget.AddDependency(self, other)
2311
static_library_type = 'com.apple.product-type.library.static'
2312
shared_library_type = 'com.apple.product-type.library.dynamic'
2313
framework_type = 'com.apple.product-type.framework'
2314
if isinstance(other, PBXNativeTarget) and \
2315
'productType' in self._properties and \
2316
self._properties['productType'] != static_library_type and \
2317
'productType' in other._properties and \
2318
(other._properties['productType'] == static_library_type or \
2319
((other._properties['productType'] == shared_library_type or \
2320
other._properties['productType'] == framework_type) and \
2321
((not other.HasBuildSetting('MACH_O_TYPE')) or
2322
other.GetBuildSetting('MACH_O_TYPE') != 'mh_bundle'))):
2324
file_ref = other.GetProperty('productReference')
2326
pbxproject = self.PBXProjectAncestor()
2327
other_pbxproject = other.PBXProjectAncestor()
2328
if pbxproject != other_pbxproject:
2329
other_project_product_group = \
2330
pbxproject.AddOrGetProjectReference(other_pbxproject)[0]
2331
file_ref = other_project_product_group.GetChildByRemoteObject(file_ref)
2333
self.FrameworksPhase().AppendProperty('files',
2334
PBXBuildFile({'fileRef': file_ref}))
2337
class PBXAggregateTarget(XCTarget):
2341
class PBXProject(XCContainerPortal):
2342
# A PBXProject is really just an XCObject, the XCContainerPortal thing is
2343
# just to allow PBXProject to be used in the containerPortal property of
2344
# PBXContainerItemProxy.
2348
path: "sample.xcodeproj". TODO(mark) Document me!
2349
_other_pbxprojects: A dictionary, keyed by other PBXProject objects. Each
2350
value is a reference to the dict in the
2351
projectReferences list associated with the keyed
2355
_schema = XCContainerPortal._schema.copy()
2357
'attributes': [0, dict, 0, 0],
2358
'buildConfigurationList': [0, XCConfigurationList, 1, 1,
2359
XCConfigurationList()],
2360
'compatibilityVersion': [0, str, 0, 1, 'Xcode 3.1'],
2361
'hasScannedForEncodings': [0, int, 0, 1, 1],
2362
'mainGroup': [0, PBXGroup, 1, 1, PBXGroup()],
2363
'projectDirPath': [0, str, 0, 1, ''],
2364
'projectReferences': [1, dict, 0, 0],
2365
'projectRoot': [0, str, 0, 1, ''],
2366
'targets': [1, XCTarget, 1, 1, []],
2369
def __init__(self, properties=None, id=None, parent=None, path=None):
2371
self._other_pbxprojects = {}
2373
return XCContainerPortal.__init__(self, properties, id, parent)
2377
if name[-10:] == '.xcodeproj':
2379
return posixpath.basename(name)
2385
return 'Project object'
2389
children = XCContainerPortal.Children(self)
2391
# Add children that the schema doesn't know about. Maybe there's a more
2392
# elegant way around this, but this is the only case where we need to own
2393
# objects in a dictionary (that is itself in a list), and three lines for
2394
# a one-off isn't that big a deal.
2395
if 'projectReferences' in self._properties:
2396
for reference in self._properties['projectReferences']:
2397
children.append(reference['ProductGroup'])
2401
def PBXProjectAncestor(self):
2404
def _GroupByName(self, name):
2405
if not 'mainGroup' in self._properties:
2406
self.SetProperty('mainGroup', PBXGroup())
2408
main_group = self._properties['mainGroup']
2409
group = main_group.GetChildByName(name)
2411
group = PBXGroup({'name': name})
2412
main_group.AppendChild(group)
2416
# SourceGroup and ProductsGroup are created by default in Xcode's own
2418
def SourceGroup(self):
2419
return self._GroupByName('Source')
2421
def ProductsGroup(self):
2422
return self._GroupByName('Products')
2424
# IntermediatesGroup is used to collect source-like files that are generated
2425
# by rules or script phases and are placed in intermediate directories such
2426
# as DerivedSources.
2427
def IntermediatesGroup(self):
2428
return self._GroupByName('Intermediates')
2430
# FrameworksGroup and ProjectsGroup are top-level groups used to collect
2431
# frameworks and projects.
2432
def FrameworksGroup(self):
2433
return self._GroupByName('Frameworks')
2435
def ProjectsGroup(self):
2436
return self._GroupByName('Projects')
2438
def RootGroupForPath(self, path):
2439
"""Returns a PBXGroup child of this object to which path should be added.
2441
This method is intended to choose between SourceGroup and
2442
IntermediatesGroup on the basis of whether path is present in a source
2443
directory or an intermediates directory. For the purposes of this
2444
determination, any path located within a derived file directory such as
2445
PROJECT_DERIVED_FILE_DIR is treated as being in an intermediates
2448
The returned value is a two-element tuple. The first element is the
2449
PBXGroup, and the second element specifies whether that group should be
2450
organized hierarchically (True) or as a single flat list (False).
2453
# TODO(mark): make this a class variable and bind to self on call?
2454
# Also, this list is nowhere near exhaustive.
2455
# INTERMEDIATE_DIR and SHARED_INTERMEDIATE_DIR are used by
2456
# gyp.generator.xcode. There should probably be some way for that module
2457
# to push the names in, rather than having to hard-code them here.
2458
source_tree_groups = {
2459
'DERIVED_FILE_DIR': (self.IntermediatesGroup, True),
2460
'INTERMEDIATE_DIR': (self.IntermediatesGroup, True),
2461
'PROJECT_DERIVED_FILE_DIR': (self.IntermediatesGroup, True),
2462
'SHARED_INTERMEDIATE_DIR': (self.IntermediatesGroup, True),
2465
(source_tree, path) = SourceTreeAndPathFromPath(path)
2466
if source_tree != None and source_tree in source_tree_groups:
2467
(group_func, hierarchical) = source_tree_groups[source_tree]
2468
group = group_func()
2469
return (group, hierarchical)
2471
# TODO(mark): make additional choices based on file extension.
2473
return (self.SourceGroup(), True)
2475
def AddOrGetFileInRootGroup(self, path):
2476
"""Returns a PBXFileReference corresponding to path in the correct group
2477
according to RootGroupForPath's heuristics.
2479
If an existing PBXFileReference for path exists, it will be returned.
2480
Otherwise, one will be created and returned.
2483
(group, hierarchical) = self.RootGroupForPath(path)
2484
return group.AddOrGetFileByPath(path, hierarchical)
2486
def RootGroupsTakeOverOnlyChildren(self, recurse=False):
2487
"""Calls TakeOverOnlyChild for all groups in the main group."""
2489
for group in self._properties['mainGroup']._properties['children']:
2490
if isinstance(group, PBXGroup):
2491
group.TakeOverOnlyChild(recurse)
2493
def SortGroups(self):
2494
# Sort the children of the mainGroup (like "Source" and "Products")
2495
# according to their defined order.
2496
self._properties['mainGroup']._properties['children'] = \
2497
sorted(self._properties['mainGroup']._properties['children'],
2498
cmp=lambda x,y: x.CompareRootGroup(y))
2500
# Sort everything else by putting group before files, and going
2501
# alphabetically by name within sections of groups and files. SortGroup
2503
for group in self._properties['mainGroup']._properties['children']:
2504
if not isinstance(group, PBXGroup):
2507
if group.Name() == 'Products':
2508
# The Products group is a special case. Instead of sorting
2509
# alphabetically, sort things in the order of the targets that
2510
# produce the products. To do this, just build up a new list of
2511
# products based on the targets.
2513
for target in self._properties['targets']:
2514
if not isinstance(target, PBXNativeTarget):
2516
product = target._properties['productReference']
2517
# Make sure that the product is already in the products group.
2518
assert product in group._properties['children']
2519
products.append(product)
2521
# Make sure that this process doesn't miss anything that was already
2522
# in the products group.
2523
assert len(products) == len(group._properties['children'])
2524
group._properties['children'] = products
2528
def AddOrGetProjectReference(self, other_pbxproject):
2529
"""Add a reference to another project file (via PBXProject object) to this
2532
Returns [ProductGroup, ProjectRef]. ProductGroup is a PBXGroup object in
2533
this project file that contains a PBXReferenceProxy object for each
2534
product of each PBXNativeTarget in the other project file. ProjectRef is
2535
a PBXFileReference to the other project file.
2537
If this project file already references the other project file, the
2538
existing ProductGroup and ProjectRef are returned. The ProductGroup will
2539
still be updated if necessary.
2542
if not 'projectReferences' in self._properties:
2543
self._properties['projectReferences'] = []
2545
product_group = None
2548
if not other_pbxproject in self._other_pbxprojects:
2549
# This project file isn't yet linked to the other one. Establish the
2551
product_group = PBXGroup({'name': 'Products'})
2553
# ProductGroup is strong.
2554
product_group.parent = self
2556
# There's nothing unique about this PBXGroup, and if left alone, it will
2557
# wind up with the same set of hashables as all other PBXGroup objects
2558
# owned by the projectReferences list. Add the hashables of the
2559
# remote PBXProject that it's related to.
2560
product_group._hashables.extend(other_pbxproject.Hashables())
2562
# The other project reports its path as relative to the same directory
2563
# that this project's path is relative to. The other project's path
2564
# is not necessarily already relative to this project. Figure out the
2565
# pathname that this project needs to use to refer to the other one.
2566
this_path = posixpath.dirname(self.Path())
2567
projectDirPath = self.GetProperty('projectDirPath')
2569
if posixpath.isabs(projectDirPath[0]):
2570
this_path = projectDirPath
2572
this_path = posixpath.join(this_path, projectDirPath)
2573
other_path = gyp.common.RelativePath(other_pbxproject.Path(), this_path)
2575
# ProjectRef is weak (it's owned by the mainGroup hierarchy).
2576
project_ref = PBXFileReference({
2577
'lastKnownFileType': 'wrapper.pb-project',
2579
'sourceTree': 'SOURCE_ROOT',
2581
self.ProjectsGroup().AppendChild(project_ref)
2583
ref_dict = {'ProductGroup': product_group, 'ProjectRef': project_ref}
2584
self._other_pbxprojects[other_pbxproject] = ref_dict
2585
self.AppendProperty('projectReferences', ref_dict)
2587
# Xcode seems to sort this list case-insensitively
2588
self._properties['projectReferences'] = \
2589
sorted(self._properties['projectReferences'], cmp=lambda x,y:
2590
cmp(x['ProjectRef'].Name().lower(),
2591
y['ProjectRef'].Name().lower()))
2593
# The link already exists. Pull out the relevnt data.
2594
project_ref_dict = self._other_pbxprojects[other_pbxproject]
2595
product_group = project_ref_dict['ProductGroup']
2596
project_ref = project_ref_dict['ProjectRef']
2598
self._SetUpProductReferences(other_pbxproject, product_group, project_ref)
2600
return [product_group, project_ref]
2602
def _SetUpProductReferences(self, other_pbxproject, product_group,
2604
# TODO(mark): This only adds references to products in other_pbxproject
2605
# when they don't exist in this pbxproject. Perhaps it should also
2606
# remove references from this pbxproject that are no longer present in
2607
# other_pbxproject. Perhaps it should update various properties if they
2609
for target in other_pbxproject._properties['targets']:
2610
if not isinstance(target, PBXNativeTarget):
2613
other_fileref = target._properties['productReference']
2614
if product_group.GetChildByRemoteObject(other_fileref) == None:
2615
# Xcode sets remoteInfo to the name of the target and not the name
2616
# of its product, despite this proxy being a reference to the product.
2617
container_item = PBXContainerItemProxy({
2618
'containerPortal': project_ref,
2620
'remoteGlobalIDString': other_fileref,
2621
'remoteInfo': target.Name()
2623
# TODO(mark): Does sourceTree get copied straight over from the other
2624
# project? Can the other project ever have lastKnownFileType here
2625
# instead of explicitFileType? (Use it if so?) Can path ever be
2626
# unset? (I don't think so.) Can other_fileref have name set, and
2627
# does it impact the PBXReferenceProxy if so? These are the questions
2628
# that perhaps will be answered one day.
2629
reference_proxy = PBXReferenceProxy({
2630
'fileType': other_fileref._properties['explicitFileType'],
2631
'path': other_fileref._properties['path'],
2632
'sourceTree': other_fileref._properties['sourceTree'],
2633
'remoteRef': container_item,
2636
product_group.AppendChild(reference_proxy)
2638
def SortRemoteProductReferences(self):
2639
# For each remote project file, sort the associated ProductGroup in the
2640
# same order that the targets are sorted in the remote project file. This
2641
# is the sort order used by Xcode.
2643
def CompareProducts(x, y, remote_products):
2644
# x and y are PBXReferenceProxy objects. Go through their associated
2645
# PBXContainerItem to get the remote PBXFileReference, which will be
2646
# present in the remote_products list.
2647
x_remote = x._properties['remoteRef']._properties['remoteGlobalIDString']
2648
y_remote = y._properties['remoteRef']._properties['remoteGlobalIDString']
2649
x_index = remote_products.index(x_remote)
2650
y_index = remote_products.index(y_remote)
2652
# Use the order of each remote PBXFileReference in remote_products to
2653
# determine the sort order.
2654
return cmp(x_index, y_index)
2656
for other_pbxproject, ref_dict in self._other_pbxprojects.iteritems():
2657
# Build up a list of products in the remote project file, ordered the
2658
# same as the targets that produce them.
2659
remote_products = []
2660
for target in other_pbxproject._properties['targets']:
2661
if not isinstance(target, PBXNativeTarget):
2663
remote_products.append(target._properties['productReference'])
2665
# Sort the PBXReferenceProxy children according to the list of remote
2667
product_group = ref_dict['ProductGroup']
2668
product_group._properties['children'] = sorted(
2669
product_group._properties['children'],
2670
cmp=lambda x, y: CompareProducts(x, y, remote_products))
2673
class XCProjectFile(XCObject):
2674
_schema = XCObject._schema.copy()
2676
'archiveVersion': [0, int, 0, 1, 1],
2677
'classes': [0, dict, 0, 1, {}],
2678
'objectVersion': [0, int, 0, 1, 45],
2679
'rootObject': [0, PBXProject, 1, 1],
2682
def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
2683
# Although XCProjectFile is implemented here as an XCObject, it's not a
2684
# proper object in the Xcode sense, and it certainly doesn't have its own
2685
# ID. Pass through an attempt to update IDs to the real root object.
2687
self._properties['rootObject'].ComputeIDs(recursive, overwrite, hash)
2689
def Print(self, file=sys.stdout):
2690
self.VerifyHasRequiredProperties()
2692
# Add the special "objects" property, which will be caught and handled
2693
# separately during printing. This structure allows a fairly standard
2694
# loop do the normal printing.
2695
self._properties['objects'] = {}
2696
self._XCPrint(file, 0, '// !$*UTF8*$!\n')
2697
if self._should_print_single_line:
2698
self._XCPrint(file, 0, '{ ')
2700
self._XCPrint(file, 0, '{\n')
2701
for property, value in sorted(self._properties.iteritems(),
2702
cmp=lambda x, y: cmp(x, y)):
2703
if property == 'objects':
2704
self._PrintObjects(file)
2706
self._XCKVPrint(file, 1, property, value)
2707
self._XCPrint(file, 0, '}\n')
2708
del self._properties['objects']
2710
def _PrintObjects(self, file):
2711
if self._should_print_single_line:
2712
self._XCPrint(file, 0, 'objects = {')
2714
self._XCPrint(file, 1, 'objects = {\n')
2716
objects_by_class = {}
2717
for object in self.Descendants():
2720
class_name = object.__class__.__name__
2721
if not class_name in objects_by_class:
2722
objects_by_class[class_name] = []
2723
objects_by_class[class_name].append(object)
2725
for class_name in sorted(objects_by_class):
2726
self._XCPrint(file, 0, '\n')
2727
self._XCPrint(file, 0, '/* Begin ' + class_name + ' section */\n')
2728
for object in sorted(objects_by_class[class_name],
2729
cmp=lambda x, y: cmp(x.id, y.id)):
2731
self._XCPrint(file, 0, '/* End ' + class_name + ' section */\n')
2733
if self._should_print_single_line:
2734
self._XCPrint(file, 0, '}; ')
2736
self._XCPrint(file, 1, '};\n')