Initial commit
[yaffs-website] / node_modules / node-gyp / gyp / pylib / gyp / xcode_ninja.py
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.
4
5 """Xcode-ninja wrapper project file generator.
6
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.
13 """
14
15 import errno
16 import gyp.generator.ninja
17 import os
18 import re
19 import xml.sax.saxutils
20
21
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)
29   try:
30     os.makedirs(workspace_path)
31   except OSError, e:
32     if e.errno != errno.EEXIST:
33       raise
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'
41
42   workspace_file = os.path.join(workspace_path, "contents.xcworkspacedata")
43
44   try:
45     with open(workspace_file, 'r') as input_file:
46       input_string = input_file.read()
47       if input_string == output_string:
48         return
49   except IOError:
50     # Ignore errors if the file doesn't exist.
51     pass
52
53   with open(workspace_file, 'w') as output_file:
54     output_file.write(output_string)
55
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).
59   ninja_toplevel = None
60   jobs = 0
61   if params:
62     options = params['options']
63     ninja_toplevel = \
64         os.path.join(options.toplevel_dir,
65                      gyp.generator.ninja.ComputeOutputDir(params))
66     jobs = params.get('generator_flags', {}).get('xcode_ninja_jobs', 0)
67
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')
71
72   ninja_target = {}
73   ninja_target['target_name'] = target_name
74   ninja_target['product_name'] = product_name
75   if product_extension:
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'] = {}
80
81   # Tell Xcode to look in |ninja_toplevel| for build products.
82   new_xcode_settings = {}
83   if ninja_toplevel:
84     new_xcode_settings['CONFIGURATION_BUILD_DIR'] = \
85         "%s/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)" % ninja_toplevel
86
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'] = \
97           new_xcode_settings
98
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']
105   if ninja_toplevel:
106     ninja_target['actions'] = [
107       {
108         'action_name': 'Compile and copy %s via ninja' % target_name,
109         'inputs': [],
110         'outputs': [],
111         'action': [
112           'env',
113           'PATH=%s' % os.environ['PATH'],
114           'ninja',
115           '-C',
116           new_xcode_settings['CONFIGURATION_BUILD_DIR'],
117           target_name,
118         ],
119         'message': 'Compile and copy %s via ninja' % target_name,
120       },
121     ]
122     if jobs > 0:
123       ninja_target['actions'][0]['action'].extend(('-j', jobs))
124   return ninja_target
125
126 def IsValidTargetForWrapper(target_extras, executable_target_pattern, spec):
127   """Limit targets for Xcode wrapper.
128
129   Xcode sometimes performs poorly with too many targets, so only include
130   proper executable targets, with filters to customize.
131   Arguments:
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.
135   """
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):
139     return True
140
141   # Otherwise just show executable targets.
142   if spec.get('type', '') == 'executable' and \
143      spec.get('product_extension', '') != 'bundle':
144
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):
148         return False
149     return True
150   return False
151
152 def CreateWrapper(target_list, target_dicts, data, params):
153   """Initialize targets for the ninja wrapper.
154
155   This sets up the necessary variables in the targets to generate Xcode projects
156   that use ninja as an external builder.
157   Arguments:
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.
162   """
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']
167
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)
172   if main_gyp is None:
173     (build_file_root, build_file_ext) = os.path.splitext(orig_gyp)
174     main_gyp = build_file_root + ".ninja" + build_file_ext
175
176   # Create new |target_list|, |target_dicts| and |data| data structures.
177   new_target_list = []
178   new_target_dicts = {}
179   new_data = {}
180
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', {})
187
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)
193
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)
197
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)
205
206       # Add to new_target_dicts.
207       new_target_dicts[new_target_name] = _TargetFromSpec(spec, params)
208
209       # Add to new_data.
210       for old_target in data[old_qualified_target.split(':')[0]]['targets']:
211         if old_target['target_name'] == target_name:
212           new_data_target = {}
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)
216
217   # Create sources target.
218   sources_target_name = 'sources_for_indexing'
219   sources_target = _TargetFromSpec(
220     { 'target_name' : sources_target_name,
221       'toolset': 'target',
222       'default_configuration': 'Default',
223       'mac_bundle': '0',
224       'type': 'executable'
225     }, None)
226
227   # Tell Xcode to look everywhere for headers.
228   sources_target['configurations'] = {'Default': { 'include_dirs': [ depth ] } }
229
230   sources = []
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
238     # build system.
239     files = [ file for file in files if not file.startswith('$')]
240
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)
244                     for file in files ]
245
246   sources_target['sources'] = sorted(set(sources))
247
248   # Put sources_to_index in it's own gyp.
249   sources_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)
253
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
257   new_data_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)
267
268   # Write workspace to file.
269   _WriteWorkspace(main_gyp, sources_gyp, params)
270   return (new_target_list, new_target_dicts, new_data)