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):
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'
27 os.makedirs(workspace_path)
29 if e.errno != errno.EEXIST:
31 output_string = '<?xml version="1.0" encoding="UTF-8"?>\n' + \
32 '<Workspace version = "1.0">\n'
33 for gyp_name in [main_gyp, sources_gyp]:
34 name = os.path.splitext(os.path.basename(gyp_name))[0] + '.xcodeproj'
35 name = xml.sax.saxutils.quoteattr("group:" + name)
36 output_string += ' <FileRef location = %s></FileRef>\n' % name
37 output_string += '</Workspace>\n'
39 workspace_file = os.path.join(workspace_path, "contents.xcworkspacedata")
42 with open(workspace_file, 'r') as input_file:
43 input_string = input_file.read()
44 if input_string == output_string:
47 # Ignore errors if the file doesn't exist.
50 with open(workspace_file, 'w') as output_file:
51 output_file.write(output_string)
53 def _TargetFromSpec(old_spec, params):
54 """ Create fake target for xcode-ninja wrapper. """
55 # Determine ninja top level build dir (e.g. /path/to/out).
59 options = params['options']
61 os.path.join(options.toplevel_dir,
62 gyp.generator.ninja.ComputeOutputDir(params))
63 jobs = params.get('generator_flags', {}).get('xcode_ninja_jobs', 0)
65 target_name = old_spec.get('target_name')
66 product_name = old_spec.get('product_name', target_name)
69 ninja_target['target_name'] = target_name
70 ninja_target['product_name'] = product_name
71 ninja_target['toolset'] = old_spec.get('toolset')
72 ninja_target['default_configuration'] = old_spec.get('default_configuration')
73 ninja_target['configurations'] = {}
75 # Tell Xcode to look in |ninja_toplevel| for build products.
76 new_xcode_settings = {}
78 new_xcode_settings['CONFIGURATION_BUILD_DIR'] = \
79 "%s/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)" % ninja_toplevel
81 if 'configurations' in old_spec:
82 for config in old_spec['configurations'].iterkeys():
83 old_xcode_settings = old_spec['configurations'][config]['xcode_settings']
84 if 'IPHONEOS_DEPLOYMENT_TARGET' in old_xcode_settings:
85 new_xcode_settings['CODE_SIGNING_REQUIRED'] = "NO"
86 new_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET'] = \
87 old_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET']
88 ninja_target['configurations'][config] = {}
89 ninja_target['configurations'][config]['xcode_settings'] = \
92 ninja_target['mac_bundle'] = old_spec.get('mac_bundle', 0)
93 ninja_target['type'] = old_spec['type']
95 ninja_target['actions'] = [
97 'action_name': 'Compile and copy %s via ninja' % target_name,
102 'PATH=%s' % os.environ['PATH'],
105 new_xcode_settings['CONFIGURATION_BUILD_DIR'],
108 'message': 'Compile and copy %s via ninja' % target_name,
112 ninja_target['actions'][0]['action'].extend(('-j', jobs))
115 def IsValidTargetForWrapper(target_extras, executable_target_pattern, spec):
116 """Limit targets for Xcode wrapper.
118 Xcode sometimes performs poorly with too many targets, so only include
119 proper executable targets, with filters to customize.
121 target_extras: Regular expression to always add, matching any target.
122 executable_target_pattern: Regular expression limiting executable targets.
123 spec: Specifications for target.
125 target_name = spec.get('target_name')
126 # Always include targets matching target_extras.
127 if target_extras is not None and re.search(target_extras, target_name):
130 # Otherwise just show executable targets.
131 if spec.get('type', '') == 'executable' and \
132 spec.get('product_extension', '') != 'bundle':
134 # If there is a filter and the target does not match, exclude the target.
135 if executable_target_pattern is not None:
136 if not re.search(executable_target_pattern, target_name):
141 def CreateWrapper(target_list, target_dicts, data, params):
142 """Initialize targets for the ninja wrapper.
144 This sets up the necessary variables in the targets to generate Xcode projects
145 that use ninja as an external builder.
147 target_list: List of target pairs: 'base/base.gyp:base'.
148 target_dicts: Dict of target properties keyed on target pair.
149 data: Dict of flattened build files keyed on gyp path.
150 params: Dict of global options for gyp.
152 orig_gyp = params['build_files'][0]
153 for gyp_name, gyp_dict in data.iteritems():
154 if gyp_name == orig_gyp:
155 depth = gyp_dict['_DEPTH']
157 # Check for custom main gyp name, otherwise use the default CHROMIUM_GYP_FILE
158 # and prepend .ninja before the .gyp extension.
159 generator_flags = params.get('generator_flags', {})
160 main_gyp = generator_flags.get('xcode_ninja_main_gyp', None)
162 (build_file_root, build_file_ext) = os.path.splitext(orig_gyp)
163 main_gyp = build_file_root + ".ninja" + build_file_ext
165 # Create new |target_list|, |target_dicts| and |data| data structures.
167 new_target_dicts = {}
170 # Set base keys needed for |data|.
171 new_data[main_gyp] = {}
172 new_data[main_gyp]['included_files'] = []
173 new_data[main_gyp]['targets'] = []
174 new_data[main_gyp]['xcode_settings'] = \
175 data[orig_gyp].get('xcode_settings', {})
177 # Normally the xcode-ninja generator includes only valid executable targets.
178 # If |xcode_ninja_executable_target_pattern| is set, that list is reduced to
179 # executable targets that match the pattern. (Default all)
180 executable_target_pattern = \
181 generator_flags.get('xcode_ninja_executable_target_pattern', None)
183 # For including other non-executable targets, add the matching target name
184 # to the |xcode_ninja_target_pattern| regular expression. (Default none)
185 target_extras = generator_flags.get('xcode_ninja_target_pattern', None)
187 for old_qualified_target in target_list:
188 spec = target_dicts[old_qualified_target]
189 if IsValidTargetForWrapper(target_extras, executable_target_pattern, spec):
190 # Add to new_target_list.
191 target_name = spec.get('target_name')
192 new_target_name = '%s:%s#target' % (main_gyp, target_name)
193 new_target_list.append(new_target_name)
195 # Add to new_target_dicts.
196 new_target_dicts[new_target_name] = _TargetFromSpec(spec, params)
199 for old_target in data[old_qualified_target.split(':')[0]]['targets']:
200 if old_target['target_name'] == target_name:
202 new_data_target['target_name'] = old_target['target_name']
203 new_data_target['toolset'] = old_target['toolset']
204 new_data[main_gyp]['targets'].append(new_data_target)
206 # Create sources target.
207 sources_target_name = 'sources_for_indexing'
208 sources_target = _TargetFromSpec(
209 { 'target_name' : sources_target_name,
211 'default_configuration': 'Default',
216 # Tell Xcode to look everywhere for headers.
217 sources_target['configurations'] = {'Default': { 'include_dirs': [ depth ] } }
220 for target, target_dict in target_dicts.iteritems():
221 base = os.path.dirname(target)
222 files = target_dict.get('sources', []) + \
223 target_dict.get('mac_bundle_resources', [])
224 # Remove files starting with $. These are mostly intermediate files for the
226 files = [ file for file in files if not file.startswith('$')]
228 # Make sources relative to root build file.
229 relative_path = os.path.dirname(main_gyp)
230 sources += [ os.path.relpath(os.path.join(base, file), relative_path)
233 sources_target['sources'] = sorted(set(sources))
235 # Put sources_to_index in it's own gyp.
237 os.path.join(os.path.dirname(main_gyp), sources_target_name + ".gyp")
238 fully_qualified_target_name = \
239 '%s:%s#target' % (sources_gyp, sources_target_name)
241 # Add to new_target_list, new_target_dicts and new_data.
242 new_target_list.append(fully_qualified_target_name)
243 new_target_dicts[fully_qualified_target_name] = sources_target
245 new_data_target['target_name'] = sources_target['target_name']
246 new_data_target['_DEPTH'] = depth
247 new_data_target['toolset'] = "target"
248 new_data[sources_gyp] = {}
249 new_data[sources_gyp]['targets'] = []
250 new_data[sources_gyp]['included_files'] = []
251 new_data[sources_gyp]['xcode_settings'] = \
252 data[orig_gyp].get('xcode_settings', {})
253 new_data[sources_gyp]['targets'].append(new_data_target)
255 # Write workspace to file.
256 _WriteWorkspace(main_gyp, sources_gyp)
257 return (new_target_list, new_target_dicts, new_data)