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 7This updates the data structures passed to the Xcode gyp generator to build 8with ninja instead. The Xcode project itself is transformed into a list of 9executable targets, each with a build step to build with ninja, and a target 10with every source and resource file. This appears to sidestep some of the 11major performance headaches experienced using complex projects and large number 12of targets within Xcode. 13""" 14 15import errno 16import gyp.generator.ninja 17import os 18import re 19import xml.sax.saxutils 20 21 22def _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' 26 try: 27 os.makedirs(workspace_path) 28 except OSError, e: 29 if e.errno != errno.EEXIST: 30 raise 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' 38 39 workspace_file = os.path.join(workspace_path, "contents.xcworkspacedata") 40 41 try: 42 with open(workspace_file, 'r') as input_file: 43 input_string = input_file.read() 44 if input_string == output_string: 45 return 46 except IOError: 47 # Ignore errors if the file doesn't exist. 48 pass 49 50 with open(workspace_file, 'w') as output_file: 51 output_file.write(output_string) 52 53def _TargetFromSpec(old_spec, params): 54 """ Create fake target for xcode-ninja wrapper. """ 55 # Determine ninja top level build dir (e.g. /path/to/out). 56 ninja_toplevel = None 57 jobs = 0 58 if params: 59 options = params['options'] 60 ninja_toplevel = \ 61 os.path.join(options.toplevel_dir, 62 gyp.generator.ninja.ComputeOutputDir(params)) 63 jobs = params.get('generator_flags', {}).get('xcode_ninja_jobs', 0) 64 65 target_name = old_spec.get('target_name') 66 product_name = old_spec.get('product_name', target_name) 67 68 ninja_target = {} 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'] = {} 74 75 # Tell Xcode to look in |ninja_toplevel| for build products. 76 new_xcode_settings = {} 77 if ninja_toplevel: 78 new_xcode_settings['CONFIGURATION_BUILD_DIR'] = \ 79 "%s/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)" % ninja_toplevel 80 81 if 'configurations' in old_spec: 82 for config in old_spec['configurations'].iterkeys(): 83 old_xcode_settings = \ 84 old_spec['configurations'][config].get('xcode_settings', {}) 85 if 'IPHONEOS_DEPLOYMENT_TARGET' in old_xcode_settings: 86 new_xcode_settings['CODE_SIGNING_REQUIRED'] = "NO" 87 new_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET'] = \ 88 old_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET'] 89 ninja_target['configurations'][config] = {} 90 ninja_target['configurations'][config]['xcode_settings'] = \ 91 new_xcode_settings 92 93 ninja_target['mac_bundle'] = old_spec.get('mac_bundle', 0) 94 ninja_target['ios_app_extension'] = old_spec.get('ios_app_extension', 0) 95 ninja_target['type'] = old_spec['type'] 96 if ninja_toplevel: 97 ninja_target['actions'] = [ 98 { 99 'action_name': 'Compile and copy %s via ninja' % target_name, 100 'inputs': [], 101 'outputs': [], 102 'action': [ 103 'env', 104 'PATH=%s' % os.environ['PATH'], 105 'ninja', 106 '-C', 107 new_xcode_settings['CONFIGURATION_BUILD_DIR'], 108 target_name, 109 ], 110 'message': 'Compile and copy %s via ninja' % target_name, 111 }, 112 ] 113 if jobs > 0: 114 ninja_target['actions'][0]['action'].extend(('-j', jobs)) 115 return ninja_target 116 117def IsValidTargetForWrapper(target_extras, executable_target_pattern, spec): 118 """Limit targets for Xcode wrapper. 119 120 Xcode sometimes performs poorly with too many targets, so only include 121 proper executable targets, with filters to customize. 122 Arguments: 123 target_extras: Regular expression to always add, matching any target. 124 executable_target_pattern: Regular expression limiting executable targets. 125 spec: Specifications for target. 126 """ 127 target_name = spec.get('target_name') 128 # Always include targets matching target_extras. 129 if target_extras is not None and re.search(target_extras, target_name): 130 return True 131 132 # Otherwise just show executable targets. 133 if spec.get('type', '') == 'executable' and \ 134 spec.get('product_extension', '') != 'bundle': 135 136 # If there is a filter and the target does not match, exclude the target. 137 if executable_target_pattern is not None: 138 if not re.search(executable_target_pattern, target_name): 139 return False 140 return True 141 return False 142 143def CreateWrapper(target_list, target_dicts, data, params): 144 """Initialize targets for the ninja wrapper. 145 146 This sets up the necessary variables in the targets to generate Xcode projects 147 that use ninja as an external builder. 148 Arguments: 149 target_list: List of target pairs: 'base/base.gyp:base'. 150 target_dicts: Dict of target properties keyed on target pair. 151 data: Dict of flattened build files keyed on gyp path. 152 params: Dict of global options for gyp. 153 """ 154 orig_gyp = params['build_files'][0] 155 for gyp_name, gyp_dict in data.iteritems(): 156 if gyp_name == orig_gyp: 157 depth = gyp_dict['_DEPTH'] 158 159 # Check for custom main gyp name, otherwise use the default CHROMIUM_GYP_FILE 160 # and prepend .ninja before the .gyp extension. 161 generator_flags = params.get('generator_flags', {}) 162 main_gyp = generator_flags.get('xcode_ninja_main_gyp', None) 163 if main_gyp is None: 164 (build_file_root, build_file_ext) = os.path.splitext(orig_gyp) 165 main_gyp = build_file_root + ".ninja" + build_file_ext 166 167 # Create new |target_list|, |target_dicts| and |data| data structures. 168 new_target_list = [] 169 new_target_dicts = {} 170 new_data = {} 171 172 # Set base keys needed for |data|. 173 new_data[main_gyp] = {} 174 new_data[main_gyp]['included_files'] = [] 175 new_data[main_gyp]['targets'] = [] 176 new_data[main_gyp]['xcode_settings'] = \ 177 data[orig_gyp].get('xcode_settings', {}) 178 179 # Normally the xcode-ninja generator includes only valid executable targets. 180 # If |xcode_ninja_executable_target_pattern| is set, that list is reduced to 181 # executable targets that match the pattern. (Default all) 182 executable_target_pattern = \ 183 generator_flags.get('xcode_ninja_executable_target_pattern', None) 184 185 # For including other non-executable targets, add the matching target name 186 # to the |xcode_ninja_target_pattern| regular expression. (Default none) 187 target_extras = generator_flags.get('xcode_ninja_target_pattern', None) 188 189 for old_qualified_target in target_list: 190 spec = target_dicts[old_qualified_target] 191 if IsValidTargetForWrapper(target_extras, executable_target_pattern, spec): 192 # Add to new_target_list. 193 target_name = spec.get('target_name') 194 new_target_name = '%s:%s#target' % (main_gyp, target_name) 195 new_target_list.append(new_target_name) 196 197 # Add to new_target_dicts. 198 new_target_dicts[new_target_name] = _TargetFromSpec(spec, params) 199 200 # Add to new_data. 201 for old_target in data[old_qualified_target.split(':')[0]]['targets']: 202 if old_target['target_name'] == target_name: 203 new_data_target = {} 204 new_data_target['target_name'] = old_target['target_name'] 205 new_data_target['toolset'] = old_target['toolset'] 206 new_data[main_gyp]['targets'].append(new_data_target) 207 208 # Create sources target. 209 sources_target_name = 'sources_for_indexing' 210 sources_target = _TargetFromSpec( 211 { 'target_name' : sources_target_name, 212 'toolset': 'target', 213 'default_configuration': 'Default', 214 'mac_bundle': '0', 215 'type': 'executable' 216 }, None) 217 218 # Tell Xcode to look everywhere for headers. 219 sources_target['configurations'] = {'Default': { 'include_dirs': [ depth ] } } 220 221 sources = [] 222 for target, target_dict in target_dicts.iteritems(): 223 base = os.path.dirname(target) 224 files = target_dict.get('sources', []) + \ 225 target_dict.get('mac_bundle_resources', []) 226 # Remove files starting with $. These are mostly intermediate files for the 227 # build system. 228 files = [ file for file in files if not file.startswith('$')] 229 230 # Make sources relative to root build file. 231 relative_path = os.path.dirname(main_gyp) 232 sources += [ os.path.relpath(os.path.join(base, file), relative_path) 233 for file in files ] 234 235 sources_target['sources'] = sorted(set(sources)) 236 237 # Put sources_to_index in it's own gyp. 238 sources_gyp = \ 239 os.path.join(os.path.dirname(main_gyp), sources_target_name + ".gyp") 240 fully_qualified_target_name = \ 241 '%s:%s#target' % (sources_gyp, sources_target_name) 242 243 # Add to new_target_list, new_target_dicts and new_data. 244 new_target_list.append(fully_qualified_target_name) 245 new_target_dicts[fully_qualified_target_name] = sources_target 246 new_data_target = {} 247 new_data_target['target_name'] = sources_target['target_name'] 248 new_data_target['_DEPTH'] = depth 249 new_data_target['toolset'] = "target" 250 new_data[sources_gyp] = {} 251 new_data[sources_gyp]['targets'] = [] 252 new_data[sources_gyp]['included_files'] = [] 253 new_data[sources_gyp]['xcode_settings'] = \ 254 data[orig_gyp].get('xcode_settings', {}) 255 new_data[sources_gyp]['targets'].append(new_data_target) 256 257 # Write workspace to file. 258 _WriteWorkspace(main_gyp, sources_gyp) 259 return (new_target_list, new_target_dicts, new_data) 260