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, 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 as e: 32 if e.errno != errno.EEXIST: 33 raise 34 output_string = ( 35 '<?xml version="1.0" encoding="UTF-8"?>\n' + '<Workspace version = "1.0">\n' 36 ) 37 for gyp_name in [main_gyp, sources_gyp]: 38 name = os.path.splitext(os.path.basename(gyp_name))[0] + ".xcodeproj" 39 name = xml.sax.saxutils.quoteattr("group:" + name) 40 output_string += " <FileRef location = %s></FileRef>\n" % name 41 output_string += "</Workspace>\n" 42 43 workspace_file = os.path.join(workspace_path, "contents.xcworkspacedata") 44 45 try: 46 with open(workspace_file, "r") as input_file: 47 input_string = input_file.read() 48 if input_string == output_string: 49 return 50 except IOError: 51 # Ignore errors if the file doesn't exist. 52 pass 53 54 with open(workspace_file, "w") as output_file: 55 output_file.write(output_string) 56 57 58def _TargetFromSpec(old_spec, params): 59 """ Create fake target for xcode-ninja wrapper. """ 60 # Determine ninja top level build dir (e.g. /path/to/out). 61 ninja_toplevel = None 62 jobs = 0 63 if params: 64 options = params["options"] 65 ninja_toplevel = os.path.join( 66 options.toplevel_dir, gyp.generator.ninja.ComputeOutputDir(params) 67 ) 68 jobs = params.get("generator_flags", {}).get("xcode_ninja_jobs", 0) 69 70 target_name = old_spec.get("target_name") 71 product_name = old_spec.get("product_name", target_name) 72 product_extension = old_spec.get("product_extension") 73 74 ninja_target = {} 75 ninja_target["target_name"] = target_name 76 ninja_target["product_name"] = product_name 77 if product_extension: 78 ninja_target["product_extension"] = product_extension 79 ninja_target["toolset"] = old_spec.get("toolset") 80 ninja_target["default_configuration"] = old_spec.get("default_configuration") 81 ninja_target["configurations"] = {} 82 83 # Tell Xcode to look in |ninja_toplevel| for build products. 84 new_xcode_settings = {} 85 if ninja_toplevel: 86 new_xcode_settings["CONFIGURATION_BUILD_DIR"] = ( 87 "%s/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)" % ninja_toplevel 88 ) 89 90 if "configurations" in old_spec: 91 for config in old_spec["configurations"]: 92 old_xcode_settings = old_spec["configurations"][config].get( 93 "xcode_settings", {} 94 ) 95 if "IPHONEOS_DEPLOYMENT_TARGET" in old_xcode_settings: 96 new_xcode_settings["CODE_SIGNING_REQUIRED"] = "NO" 97 new_xcode_settings["IPHONEOS_DEPLOYMENT_TARGET"] = old_xcode_settings[ 98 "IPHONEOS_DEPLOYMENT_TARGET" 99 ] 100 for key in ["BUNDLE_LOADER", "TEST_HOST"]: 101 if key in old_xcode_settings: 102 new_xcode_settings[key] = old_xcode_settings[key] 103 104 ninja_target["configurations"][config] = {} 105 ninja_target["configurations"][config][ 106 "xcode_settings" 107 ] = new_xcode_settings 108 109 ninja_target["mac_bundle"] = old_spec.get("mac_bundle", 0) 110 ninja_target["mac_xctest_bundle"] = old_spec.get("mac_xctest_bundle", 0) 111 ninja_target["ios_app_extension"] = old_spec.get("ios_app_extension", 0) 112 ninja_target["ios_watchkit_extension"] = old_spec.get("ios_watchkit_extension", 0) 113 ninja_target["ios_watchkit_app"] = old_spec.get("ios_watchkit_app", 0) 114 ninja_target["type"] = old_spec["type"] 115 if ninja_toplevel: 116 ninja_target["actions"] = [ 117 { 118 "action_name": "Compile and copy %s via ninja" % target_name, 119 "inputs": [], 120 "outputs": [], 121 "action": [ 122 "env", 123 "PATH=%s" % os.environ["PATH"], 124 "ninja", 125 "-C", 126 new_xcode_settings["CONFIGURATION_BUILD_DIR"], 127 target_name, 128 ], 129 "message": "Compile and copy %s via ninja" % target_name, 130 }, 131 ] 132 if jobs > 0: 133 ninja_target["actions"][0]["action"].extend(("-j", jobs)) 134 return ninja_target 135 136 137def IsValidTargetForWrapper(target_extras, executable_target_pattern, spec): 138 """Limit targets for Xcode wrapper. 139 140 Xcode sometimes performs poorly with too many targets, so only include 141 proper executable targets, with filters to customize. 142 Arguments: 143 target_extras: Regular expression to always add, matching any target. 144 executable_target_pattern: Regular expression limiting executable targets. 145 spec: Specifications for target. 146 """ 147 target_name = spec.get("target_name") 148 # Always include targets matching target_extras. 149 if target_extras is not None and re.search(target_extras, target_name): 150 return True 151 152 # Otherwise just show executable targets and xc_tests. 153 if int(spec.get("mac_xctest_bundle", 0)) != 0 or ( 154 spec.get("type", "") == "executable" 155 and spec.get("product_extension", "") != "bundle" 156 ): 157 158 # If there is a filter and the target does not match, exclude the target. 159 if executable_target_pattern is not None: 160 if not re.search(executable_target_pattern, target_name): 161 return False 162 return True 163 return False 164 165 166def CreateWrapper(target_list, target_dicts, data, params): 167 """Initialize targets for the ninja wrapper. 168 169 This sets up the necessary variables in the targets to generate Xcode projects 170 that use ninja as an external builder. 171 Arguments: 172 target_list: List of target pairs: 'base/base.gyp:base'. 173 target_dicts: Dict of target properties keyed on target pair. 174 data: Dict of flattened build files keyed on gyp path. 175 params: Dict of global options for gyp. 176 """ 177 orig_gyp = params["build_files"][0] 178 for gyp_name, gyp_dict in data.items(): 179 if gyp_name == orig_gyp: 180 depth = gyp_dict["_DEPTH"] 181 182 # Check for custom main gyp name, otherwise use the default CHROMIUM_GYP_FILE 183 # and prepend .ninja before the .gyp extension. 184 generator_flags = params.get("generator_flags", {}) 185 main_gyp = generator_flags.get("xcode_ninja_main_gyp", None) 186 if main_gyp is None: 187 (build_file_root, build_file_ext) = os.path.splitext(orig_gyp) 188 main_gyp = build_file_root + ".ninja" + build_file_ext 189 190 # Create new |target_list|, |target_dicts| and |data| data structures. 191 new_target_list = [] 192 new_target_dicts = {} 193 new_data = {} 194 195 # Set base keys needed for |data|. 196 new_data[main_gyp] = {} 197 new_data[main_gyp]["included_files"] = [] 198 new_data[main_gyp]["targets"] = [] 199 new_data[main_gyp]["xcode_settings"] = data[orig_gyp].get("xcode_settings", {}) 200 201 # Normally the xcode-ninja generator includes only valid executable targets. 202 # If |xcode_ninja_executable_target_pattern| is set, that list is reduced to 203 # executable targets that match the pattern. (Default all) 204 executable_target_pattern = generator_flags.get( 205 "xcode_ninja_executable_target_pattern", None 206 ) 207 208 # For including other non-executable targets, add the matching target name 209 # to the |xcode_ninja_target_pattern| regular expression. (Default none) 210 target_extras = generator_flags.get("xcode_ninja_target_pattern", None) 211 212 for old_qualified_target in target_list: 213 spec = target_dicts[old_qualified_target] 214 if IsValidTargetForWrapper(target_extras, executable_target_pattern, spec): 215 # Add to new_target_list. 216 target_name = spec.get("target_name") 217 new_target_name = "%s:%s#target" % (main_gyp, target_name) 218 new_target_list.append(new_target_name) 219 220 # Add to new_target_dicts. 221 new_target_dicts[new_target_name] = _TargetFromSpec(spec, params) 222 223 # Add to new_data. 224 for old_target in data[old_qualified_target.split(":")[0]]["targets"]: 225 if old_target["target_name"] == target_name: 226 new_data_target = {} 227 new_data_target["target_name"] = old_target["target_name"] 228 new_data_target["toolset"] = old_target["toolset"] 229 new_data[main_gyp]["targets"].append(new_data_target) 230 231 # Create sources target. 232 sources_target_name = "sources_for_indexing" 233 sources_target = _TargetFromSpec( 234 { 235 "target_name": sources_target_name, 236 "toolset": "target", 237 "default_configuration": "Default", 238 "mac_bundle": "0", 239 "type": "executable", 240 }, 241 None, 242 ) 243 244 # Tell Xcode to look everywhere for headers. 245 sources_target["configurations"] = {"Default": {"include_dirs": [depth]}} 246 247 # Put excluded files into the sources target so they can be opened in Xcode. 248 skip_excluded_files = not generator_flags.get( 249 "xcode_ninja_list_excluded_files", True 250 ) 251 252 sources = [] 253 for target, target_dict in target_dicts.items(): 254 base = os.path.dirname(target) 255 files = target_dict.get("sources", []) + target_dict.get( 256 "mac_bundle_resources", [] 257 ) 258 259 if not skip_excluded_files: 260 files.extend( 261 target_dict.get("sources_excluded", []) 262 + target_dict.get("mac_bundle_resources_excluded", []) 263 ) 264 265 for action in target_dict.get("actions", []): 266 files.extend(action.get("inputs", [])) 267 268 if not skip_excluded_files: 269 files.extend(action.get("inputs_excluded", [])) 270 271 # Remove files starting with $. These are mostly intermediate files for the 272 # build system. 273 files = [file for file in files if not file.startswith("$")] 274 275 # Make sources relative to root build file. 276 relative_path = os.path.dirname(main_gyp) 277 sources += [ 278 os.path.relpath(os.path.join(base, file), relative_path) for file in files 279 ] 280 281 sources_target["sources"] = sorted(set(sources)) 282 283 # Put sources_to_index in it's own gyp. 284 sources_gyp = os.path.join(os.path.dirname(main_gyp), sources_target_name + ".gyp") 285 fully_qualified_target_name = "%s:%s#target" % (sources_gyp, sources_target_name) 286 287 # Add to new_target_list, new_target_dicts and new_data. 288 new_target_list.append(fully_qualified_target_name) 289 new_target_dicts[fully_qualified_target_name] = sources_target 290 new_data_target = {} 291 new_data_target["target_name"] = sources_target["target_name"] 292 new_data_target["_DEPTH"] = depth 293 new_data_target["toolset"] = "target" 294 new_data[sources_gyp] = {} 295 new_data[sources_gyp]["targets"] = [] 296 new_data[sources_gyp]["included_files"] = [] 297 new_data[sources_gyp]["xcode_settings"] = data[orig_gyp].get("xcode_settings", {}) 298 new_data[sources_gyp]["targets"].append(new_data_target) 299 300 # Write workspace to file. 301 _WriteWorkspace(main_gyp, sources_gyp, params) 302 return (new_target_list, new_target_dicts, new_data) 303