~hjd/ubuntu/wily/gyp/debian-merged

« back to all changes in this revision

Viewing changes to pylib/gyp/xcode_ninja.py

  • Committer: Hans Joachim Desserud
  • Date: 2015-10-31 12:46:59 UTC
  • mfrom: (6.2.6 sid)
  • Revision ID: hans_joachim_desserud-20151031124659-lzxekr6woskh4k0b
Merge latest Debian version

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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)