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)