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