• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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