1
# Copyright (c) 2014 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.
5
"""Xcode-ninja wrapper project file generator.
7
This updates the data structures passed to the Xcode gyp generator to build
8
with ninja instead. The Xcode project itself is transformed into a list of
9
executable targets, each with a build step to build with ninja, and a target
10
with every source and resource file. This appears to sidestep some of the
11
major performance headaches experienced using complex projects and large number
12
of targets within Xcode.
16
import gyp.generator.ninja
19
import xml.sax.saxutils
22
def _WriteWorkspace(main_gyp, sources_gyp, params):
23
""" Create a workspace to wrap main and sources gyp paths. """
24
(build_file_root, build_file_ext) = os.path.splitext(main_gyp)
25
workspace_path = build_file_root + '.xcworkspace'
26
options = params['options']
27
if options.generator_output:
28
workspace_path = os.path.join(options.generator_output, workspace_path)
30
os.makedirs(workspace_path)
32
if e.errno != errno.EEXIST:
34
output_string = '<?xml version="1.0" encoding="UTF-8"?>\n' + \
35
'<Workspace version = "1.0">\n'
36
for gyp_name in [main_gyp, sources_gyp]:
37
name = os.path.splitext(os.path.basename(gyp_name))[0] + '.xcodeproj'
38
name = xml.sax.saxutils.quoteattr("group:" + name)
39
output_string += ' <FileRef location = %s></FileRef>\n' % name
40
output_string += '</Workspace>\n'
42
workspace_file = os.path.join(workspace_path, "contents.xcworkspacedata")
45
with open(workspace_file, 'r') as input_file:
46
input_string = input_file.read()
47
if input_string == output_string:
50
# Ignore errors if the file doesn't exist.
53
with open(workspace_file, 'w') as output_file:
54
output_file.write(output_string)
56
def _TargetFromSpec(old_spec, params):
57
""" Create fake target for xcode-ninja wrapper. """
58
# Determine ninja top level build dir (e.g. /path/to/out).
62
options = params['options']
64
os.path.join(options.toplevel_dir,
65
gyp.generator.ninja.ComputeOutputDir(params))
66
jobs = params.get('generator_flags', {}).get('xcode_ninja_jobs', 0)
68
target_name = old_spec.get('target_name')
69
product_name = old_spec.get('product_name', target_name)
70
product_extension = old_spec.get('product_extension')
73
ninja_target['target_name'] = target_name
74
ninja_target['product_name'] = product_name
76
ninja_target['product_extension'] = product_extension
77
ninja_target['toolset'] = old_spec.get('toolset')
78
ninja_target['default_configuration'] = old_spec.get('default_configuration')
79
ninja_target['configurations'] = {}
81
# Tell Xcode to look in |ninja_toplevel| for build products.
82
new_xcode_settings = {}
84
new_xcode_settings['CONFIGURATION_BUILD_DIR'] = \
85
"%s/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)" % ninja_toplevel
87
if 'configurations' in old_spec:
88
for config in old_spec['configurations'].iterkeys():
89
old_xcode_settings = \
90
old_spec['configurations'][config].get('xcode_settings', {})
91
if 'IPHONEOS_DEPLOYMENT_TARGET' in old_xcode_settings:
92
new_xcode_settings['CODE_SIGNING_REQUIRED'] = "NO"
93
new_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET'] = \
94
old_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET']
95
ninja_target['configurations'][config] = {}
96
ninja_target['configurations'][config]['xcode_settings'] = \
99
ninja_target['mac_bundle'] = old_spec.get('mac_bundle', 0)
100
ninja_target['ios_app_extension'] = old_spec.get('ios_app_extension', 0)
101
ninja_target['ios_watchkit_extension'] = \
102
old_spec.get('ios_watchkit_extension', 0)
103
ninja_target['ios_watchkit_app'] = old_spec.get('ios_watchkit_app', 0)
104
ninja_target['type'] = old_spec['type']
106
ninja_target['actions'] = [
108
'action_name': 'Compile and copy %s via ninja' % target_name,
113
'PATH=%s' % os.environ['PATH'],
116
new_xcode_settings['CONFIGURATION_BUILD_DIR'],
119
'message': 'Compile and copy %s via ninja' % target_name,
123
ninja_target['actions'][0]['action'].extend(('-j', jobs))
126
def IsValidTargetForWrapper(target_extras, executable_target_pattern, spec):
127
"""Limit targets for Xcode wrapper.
129
Xcode sometimes performs poorly with too many targets, so only include
130
proper executable targets, with filters to customize.
132
target_extras: Regular expression to always add, matching any target.
133
executable_target_pattern: Regular expression limiting executable targets.
134
spec: Specifications for target.
136
target_name = spec.get('target_name')
137
# Always include targets matching target_extras.
138
if target_extras is not None and re.search(target_extras, target_name):
141
# Otherwise just show executable targets.
142
if spec.get('type', '') == 'executable' and \
143
spec.get('product_extension', '') != 'bundle':
145
# If there is a filter and the target does not match, exclude the target.
146
if executable_target_pattern is not None:
147
if not re.search(executable_target_pattern, target_name):
152
def CreateWrapper(target_list, target_dicts, data, params):
153
"""Initialize targets for the ninja wrapper.
155
This sets up the necessary variables in the targets to generate Xcode projects
156
that use ninja as an external builder.
158
target_list: List of target pairs: 'base/base.gyp:base'.
159
target_dicts: Dict of target properties keyed on target pair.
160
data: Dict of flattened build files keyed on gyp path.
161
params: Dict of global options for gyp.
163
orig_gyp = params['build_files'][0]
164
for gyp_name, gyp_dict in data.iteritems():
165
if gyp_name == orig_gyp:
166
depth = gyp_dict['_DEPTH']
168
# Check for custom main gyp name, otherwise use the default CHROMIUM_GYP_FILE
169
# and prepend .ninja before the .gyp extension.
170
generator_flags = params.get('generator_flags', {})
171
main_gyp = generator_flags.get('xcode_ninja_main_gyp', None)
173
(build_file_root, build_file_ext) = os.path.splitext(orig_gyp)
174
main_gyp = build_file_root + ".ninja" + build_file_ext
176
# Create new |target_list|, |target_dicts| and |data| data structures.
178
new_target_dicts = {}
181
# Set base keys needed for |data|.
182
new_data[main_gyp] = {}
183
new_data[main_gyp]['included_files'] = []
184
new_data[main_gyp]['targets'] = []
185
new_data[main_gyp]['xcode_settings'] = \
186
data[orig_gyp].get('xcode_settings', {})
188
# Normally the xcode-ninja generator includes only valid executable targets.
189
# If |xcode_ninja_executable_target_pattern| is set, that list is reduced to
190
# executable targets that match the pattern. (Default all)
191
executable_target_pattern = \
192
generator_flags.get('xcode_ninja_executable_target_pattern', None)
194
# For including other non-executable targets, add the matching target name
195
# to the |xcode_ninja_target_pattern| regular expression. (Default none)
196
target_extras = generator_flags.get('xcode_ninja_target_pattern', None)
198
for old_qualified_target in target_list:
199
spec = target_dicts[old_qualified_target]
200
if IsValidTargetForWrapper(target_extras, executable_target_pattern, spec):
201
# Add to new_target_list.
202
target_name = spec.get('target_name')
203
new_target_name = '%s:%s#target' % (main_gyp, target_name)
204
new_target_list.append(new_target_name)
206
# Add to new_target_dicts.
207
new_target_dicts[new_target_name] = _TargetFromSpec(spec, params)
210
for old_target in data[old_qualified_target.split(':')[0]]['targets']:
211
if old_target['target_name'] == target_name:
213
new_data_target['target_name'] = old_target['target_name']
214
new_data_target['toolset'] = old_target['toolset']
215
new_data[main_gyp]['targets'].append(new_data_target)
217
# Create sources target.
218
sources_target_name = 'sources_for_indexing'
219
sources_target = _TargetFromSpec(
220
{ 'target_name' : sources_target_name,
222
'default_configuration': 'Default',
227
# Tell Xcode to look everywhere for headers.
228
sources_target['configurations'] = {'Default': { 'include_dirs': [ depth ] } }
231
for target, target_dict in target_dicts.iteritems():
232
base = os.path.dirname(target)
233
files = target_dict.get('sources', []) + \
234
target_dict.get('mac_bundle_resources', [])
235
for action in target_dict.get('actions', []):
236
files.extend(action.get('inputs', []))
237
# Remove files starting with $. These are mostly intermediate files for the
239
files = [ file for file in files if not file.startswith('$')]
241
# Make sources relative to root build file.
242
relative_path = os.path.dirname(main_gyp)
243
sources += [ os.path.relpath(os.path.join(base, file), relative_path)
246
sources_target['sources'] = sorted(set(sources))
248
# Put sources_to_index in it's own gyp.
250
os.path.join(os.path.dirname(main_gyp), sources_target_name + ".gyp")
251
fully_qualified_target_name = \
252
'%s:%s#target' % (sources_gyp, sources_target_name)
254
# Add to new_target_list, new_target_dicts and new_data.
255
new_target_list.append(fully_qualified_target_name)
256
new_target_dicts[fully_qualified_target_name] = sources_target
258
new_data_target['target_name'] = sources_target['target_name']
259
new_data_target['_DEPTH'] = depth
260
new_data_target['toolset'] = "target"
261
new_data[sources_gyp] = {}
262
new_data[sources_gyp]['targets'] = []
263
new_data[sources_gyp]['included_files'] = []
264
new_data[sources_gyp]['xcode_settings'] = \
265
data[orig_gyp].get('xcode_settings', {})
266
new_data[sources_gyp]['targets'].append(new_data_target)
268
# Write workspace to file.
269
_WriteWorkspace(main_gyp, sources_gyp, params)
270
return (new_target_list, new_target_dicts, new_data)