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 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 |
|
---|
58 | def _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 |
|
---|
137 | def 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 |
|
---|
166 | def 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)
|
---|