Rewrite generate-xcfilelists in Python
[WebKit-https.git] / Tools / Scripts / webkitpy / generate_xcfilelists_lib / generators.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright (C) 2019 Apple Inc.  All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # 1. Redistributions of source code must retain the above copyright
11 #    notice, this list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 #    notice, this list of conditions and the following disclaimer in the
14 #    documentation and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
17 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
24 # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28 # BaseGenerator is the base class for generating new .xcfilelist content. Most
29 # of the implementation is provided in this base class. Subclasses are created
30 # to provide project-specific information, such as the path to the project file
31 # and valid platform and configuration information.
32 #
33 # Instances of this class are create for each project/platform/configuration
34 # triple. For each triple, .xcfilelist content is generated and compared to the
35 # file that ultimately will contain that information. Any new lines are
36 # determined and remembered for later addition to that destination file.
37 #
38 # Instances of this class can operate in either of two contexts. First, it's
39 # possible for Xcode projects to invoke this script in order to keep their
40 # .xcfilelist files up-to-date. In that case, this script starts out running in
41 # the context of Xcode and it can just go ahead and generate the new content.
42 # Alternatively, it's possible for someone to invoke this script from the
43 # command line. In that case, this script needs to sublaunch itself in order to
44 # expose itself to the environment variables that Xcode establishes during
45 # builds. This script thus starts out as a free-standing script, creates a
46 # BaseGenerator for the .xcfilelist content it needs to create, sub-launches
47 # itself, and then creates another BaseGenerator to do the actual generation.
48 # When this inner instance of the script exits, it writes the results of the
49 # BaseGenerators to a temporary file. When the outer instance of the script
50 # resumes, it reads the information from that temporary file. That information
51 # is actually just a serialized (pickled) BaseGenerator instance and is
52 # restored as such. This restored BaseGenerator now replaces the original
53 # BaseGenerator in the outer script that caused it to be created, since the
54 # inner one is the one with all the xcfilelist information.
55
56 from __future__ import print_function
57
58 import os
59 import pickle
60 import tempfile
61 import traceback
62
63 import webkitpy.generate_xcfilelists_lib.util as util
64 from webkitpy.xcode import xcode_hash_for_path
65 from webkitpy.xcode.sdk import SDK
66 from webkitpy.common.attribute_saver import AttributeSaver
67
68
69 class BaseGenerator(object):
70     __slots__ = (
71         "application", "project_tag", "platform", "configuration", "triple",
72         "ex_type", "ex_value", "ex_traceback",
73         "added_lines_input_derived", "added_lines_output_derived", "added_lines_input_unified", "added_lines_output_unified",
74         "cached_build_dirs")
75
76     VALID_PLATFORMS = None
77     VALID_CONFIGURATIONS = None
78
79     @util.LogEntryExit
80     def __init__(self, application, project_tag, platform, configuration):
81         self.application = application
82         self.project_tag = project_tag
83         self.platform = platform
84         self.configuration = configuration
85         self.triple = (project_tag, platform, configuration)
86
87         self.ex_type = None
88         self.ex_value = None
89         self.ex_traceback = None
90
91         self.added_lines_input_derived = None
92         self.added_lines_output_derived = None
93         self.added_lines_input_unified = None
94         self.added_lines_output_unified = None
95
96         self.cached_build_dirs = None
97
98     def __str__(self):
99         return "project_tag = {}, platform = {}, configuration = {}, ex_type = {}, ex_value = {}, ex_traceback = {}, added_lines_input_derived = {}, added_lines_output_derived = {}, added_lines_input_unified = {}, added_lines_output_unified = {}, cached_build_dirs = {}".format(
100             self.project_tag, self.platform, self.configuration,
101             self.ex_type, self.ex_value, self.ex_traceback,
102             self.added_lines_input_derived, self.added_lines_output_derived, self.added_lines_input_unified, self.added_lines_output_unified,
103             self.cached_build_dirs)
104
105     # Generate new .xcfilist list contents and return any new lines. This
106     # generation is performed by relaunching ourselves under Xcode so that we
107     # can operate in the context of its environment variables. When we relaunch
108     # ourselves, we do so in a way that invokes this class's generate()
109     # method. Any results from generate are written to a temporary file that
110     # we set up here. When the relaunched instance completes, we reads those
111     # results from the temporary file.
112
113     @util.LogEntryExit
114     def set_environment_and_generate(self):
115         with tempfile.NamedTemporaryFile() as pickle_file, tempfile.NamedTemporaryFile() as debug_file:
116             sublaunch_args = [
117                 "PATH=\"{}:${{PATH}}\"".format(self._getenv("PATH")),  # Xcode will "sanitize" PATH, such that Python is no longer on it, so get /usr/bin back in PATH.
118                 "PYTHONPATH=\"{}\"".format(self._getenv("PYTHONPATH", "")),
119                 self.application.get_generate_xcfilelists_script_path(),
120                 "generate-inner",
121                 "--project", self.project_tag,
122                 "--platform", self.platform,
123                 "--configuration", self.configuration,
124                 "--pickle-file", pickle_file.name]
125             if self.application.cmd_line_args.debug:
126                 sublaunch_args += [
127                     "--debug",
128                     "--debug-file", debug_file.name]
129             if self.application.cmd_line_args.quiet:
130                 sublaunch_args.append("--quiet")
131
132             try:
133                 self._sublaunch_under_xcode(sublaunch_args)
134             finally:
135                 # If xcodebuild returns an error, we want to at least display the
136                 # debugging information.
137                 if self.application.cmd_line_args.debug:
138                     for line in debug_file:
139                         util.debug_log("{}".format(line.rstrip()))
140
141             generators = []
142             while True:
143                 try:
144                     generator = pickle.load(pickle_file)
145                     generator.application = self.application
146                     generators.append(generator)
147                 except EOFError as e:
148                     break
149             return generators
150
151     # Relaunch this script under Xcode. This is performed by launching Xcode,
152     # pointing it to the project that we are processing as well as a build
153     # target that will re-execute us as a custom build step.
154
155     @util.LogEntryExit
156     def _sublaunch_under_xcode(self, sublaunch_args):
157         xcode_parameters = [
158             "xcodebuild",
159             "-project", self._get_project_file_path(),
160             "-sdk", SDK.get_preferred_sdk_for_platform(self.platform).as_xcode_specification(),
161             "-configuration", self.configuration,
162             "-target", "Apply Configuration to XCFileLists",
163             "SYMROOT={}".format(self._get_sym_root()),
164             "OBJROOT={}".format(self._get_obj_root()),
165             "SHARED_PRECOMPS_DIR={}".format(self._get_shared_precomps_dir())]
166
167         # TODO: sublaunch_args will contain the path to the script to
168         # sublaunch. There might be a problem if there's a space in that path.
169         util.subprocess_run(xcode_parameters,
170             env={"WK_SUBLAUNCH_SCRIPT_PARAMETERS": " ".join(sublaunch_args)})
171
172     # Generate the .xcfilelist content. Save the results internally as sets of
173     # new lines to be added to the .xcfilelist files. These new lines can be
174     # merged into those files if the user specified a "generate" command, or
175     # can be reported in a "check" command.
176
177     @util.LogEntryExit
178     def generate(self):
179         self._generate_derived()
180         self._generate_unified()
181
182     # Merge any saved added lines to their ultimate destinations.
183
184     @util.LogEntryExit
185     def merge(self):
186         self._merge_derived()
187         self._merge_unified()
188
189     @util.LogEntryExit
190     def pickle_to_file(self, f):
191         # We don't want to pickle the application reference
192         # We can't seem to pickle ex_traceback: PicklingError: Can't pickle <type 'traceback'>: it's not found as __builtin__.traceback
193         with AttributeSaver(self, "application"), AttributeSaver(self, "ex_traceback"):
194             pickle.dump(self, f, pickle.HIGHEST_PROTOCOL)
195
196     # Return whether or not any new lines for any .xcfilelist files were
197     # discovered.
198
199     @util.LogEntryExit
200     def has_action(self):
201         return (self.added_lines_input_derived or
202                 self.added_lines_output_derived or
203                 self.added_lines_input_unified or
204                 self.added_lines_output_unified)
205
206     # If any errors occur during the generation of .xcfilelist content, they
207     # are remembered internally for later processing. This function returns
208     # whether or not any such error occurred with this generator.
209
210     @util.LogEntryExit
211     def has_error(self):
212         return self.ex_type
213
214     # Return whether or not the combination of project, platform, and
215     # configuration is valid (which is to say, if, for example, the given
216     # project can be built for the given platform and configuration).
217
218     @util.LogEntryExit
219     def is_valid(self):
220         return (self.platform in self.__class__.VALID_PLATFORMS and
221                 self.configuration in self.__class__.VALID_CONFIGURATIONS)
222
223     # If an error/exception occurred and was recorded (that is, if has_error()
224     # returns true), report that error to the console.
225
226     @util.LogEntryExit
227     def report_error(self):
228         try:
229             if self.ex_value:
230                 raise self.ex_value
231         except KeyboardInterrupt:
232             print("### Canceled")
233         except util.CalledProcessError as e:
234             print("### Error {} calling subprocess: {}".format(e.args[0], e.args[1]))
235             if e.args[2]:
236                 print("### stdout = {}".format(e.args[2]))
237             if e.args[3]:
238                 print("### stderr = {}".format(e.args[3]))
239         except util.InvalidConfigurationError as e:
240             print("### Invalid configuration: {}".format(e))
241             return os.EX_USAGE
242         except BaseException:
243             traceback.print_exception(self.ex_type, self.ex_value, self.ex_traceback)
244             return os.EX_SOFTWARE
245
246     # Generate .xcfilelist content for the "Generate Derived Sources" build
247     # phase.
248
249     @util.LogEntryExit
250     def _generate_derived(self):
251         script = self._get_generate_derived_sources_script()
252         if not script:
253             return
254
255         with tempfile.NamedTemporaryFile() as input, tempfile.NamedTemporaryFile() as output:
256             (stdout, stderr) = util.subprocess_run(
257                     [script,
258                         "NO_SUPPLEMENTAL_FILES=1",
259                         "--no-builtin-rules",
260                         "--dry-run",
261                         "--always-make",
262                         "--debug=abvijm",
263                         "all"])
264             stdout = stdout.encode() if isinstance(stdout, str) else stdout
265             (stdout, stderr) = util.subprocess_run(
266                     [self.application.get_extract_dependencies_from_makefile_script(),
267                         "--input", input.name,
268                         "--output", output.name],
269                     input=stdout)
270
271             # TODO: Make this generator-specific (there's no need to reference
272             # WebCore, for example, when processing the JavaScriptCore
273             # project).
274
275             self._replace(input.name, "^JavaScriptCore/",               "$(PROJECT_DIR)/")
276             self._replace(input.name, "^JavaScriptCorePrivateHeaders/", "$(JAVASCRIPTCORE_PRIVATE_HEADERS_DIR)/")
277             self._replace(input.name, "^WebCore/",                      "$(PROJECT_DIR)/")
278             self._replace(input.name, "^WebKit2PrivateHeaders/",        "$(WEBKIT2_PRIVATE_HEADERS_DIR)/")
279
280             self._unexpand(input.name, "JAVASCRIPTCORE_PRIVATE_HEADERS_DIR")
281             self._unexpand(input.name, "PROJECT_DIR")
282             self._unexpand(input.name, "WEBCORE_PRIVATE_HEADERS_DIR")
283             self._unexpand(input.name, "WEBKIT2_PRIVATE_HEADERS_DIR")
284             self._unexpand(input.name, "WEBKITADDITIONS_HEADERS_FOLDER_PATH")
285             self._unexpand(input.name, "BUILT_PRODUCTS_DIR")    # Do this last, since it's a prefix of some other variables and will "intercept" them if executed earlier than them.
286
287             self._replace(output.name, "^", self._get_derived_sources_dir() + "/")
288             self._unexpand(output.name, "BUILT_PRODUCTS_DIR")
289
290             self.added_lines_input_derived = self._find_added_lines(input.name, self._get_input_derived_xcfilelist_project_path())
291             self.added_lines_output_derived = self._find_added_lines(output.name, self._get_output_derived_xcfilelist_project_path())
292
293     @util.LogEntryExit
294     def _merge_derived(self):
295         self._merge_added_lines(self.added_lines_input_derived, self._get_input_derived_xcfilelist_project_path())
296         self._merge_added_lines(self.added_lines_output_derived, self._get_output_derived_xcfilelist_project_path())
297
298     # Generate .xcfilelist content for the "Generate Unified Sources" build
299     # phase.
300
301     @util.LogEntryExit
302     def _generate_unified(self):
303         script = self._get_generate_unified_sources_script()
304         if not script:
305             return
306
307         with tempfile.NamedTemporaryFile() as output:
308
309             # We need to define BUILD_SCRIPTS_DIR so that the bash script we're
310             # invoking can find the ruby script that it invokes. If we don't
311             # define BUILD_SCRIPTS_DIR, the bash script we're invoking with try
312             # to define its own value for BUILD_SCRIPTS_DIR, but it will do so
313             # incorrectly for our purposes, leading to dire results.
314
315             env = os.environ.copy()
316             env["BUILD_SCRIPTS_DIR"] = self.application.get_build_scripts_dir()
317
318             util.subprocess_run(
319                     [script,
320                         "--generate-xcfilelists",
321                         "--output-xcfilelist-path", output.name],
322                     env=env)
323
324             self._unexpand(output.name, "BUILT_PRODUCTS_DIR")
325
326             self.added_lines_input_unified = self._find_added_lines(None, self._get_input_unified_xcfilelist_project_path())
327             self.added_lines_output_unified = self._find_added_lines(output.name, self._get_output_unified_xcfilelist_project_path())
328
329     @util.LogEntryExit
330     def _merge_unified(self):
331         self._merge_added_lines(self.added_lines_input_unified, self._get_input_unified_xcfilelist_project_path())
332         self._merge_added_lines(self.added_lines_output_unified, self._get_output_unified_xcfilelist_project_path())
333
334     # Utility for post-processing the initial .xcfilelist content. Used to
335     # replace text in the file.
336
337     @util.LogEntryExit
338     def _replace(self, file_name, to_replace, replace_with):
339         util.subprocess_run([
340             "sed", "-E", "-e",
341             "s|{}|{}|".format(to_replace, replace_with),
342             "-i", "''", file_name])
343
344     # Utility for post-processing the initial .xcfilelist content. Used to
345     # replace file path segments with the variables that represent those path
346     # segments.
347
348     @util.LogEntryExit
349     def _unexpand(self, file_name, variable_name):
350         to_replace = self._getenv(variable_name)
351         if not to_replace:
352             return
353
354         self._replace(file_name, "^{}/".format(to_replace), "$({})/".format(variable_name))
355
356     # Given a source file with new .xcfilelist content and a dest file that
357     # contains the original/previous .xcfilelist content (that is, likely the
358     # file that's checked into the repo), determine what, if any, new lines
359     # there are in source that aren't in dest.
360
361     @util.LogEntryExit
362     def _find_added_lines(self, source, dest):
363         if not source:
364             return set()
365         source_lines = set(source) if isinstance(source, list) else self._get_file_lines(source)
366         dest_lines = set(dest) if isinstance(dest, list) else self._get_file_lines(dest)
367         delta_lines = source_lines - dest_lines
368         return delta_lines
369
370     # Bottleneck routine for taking a set of new lines of .xcfilelist content
371     # and adding them to their ultimate file/destination.
372
373     @util.LogEntryExit
374     def _merge_added_lines(self, added_lines, dest):
375         if not added_lines:
376             return
377         dest_lines = self._get_file_lines(dest)
378         merged_lines = sorted(set(added_lines) | dest_lines)
379         merged_lines = [line + "\n" for line in merged_lines if line and not line.startswith("#")]
380         merged_lines[0:0] = ["# This file is generated by the generate-xcfilelists script.\n"]
381         with open(dest, "w") as f:
382             f.writelines(merged_lines)
383
384     # Utility to read a file and results the contents as a set of lines with
385     # EOLs removed.
386
387     @util.LogEntryExit
388     def _get_file_lines(self, file):
389         try:
390             with open(file, "r") as f:
391                 return set([line.strip() for line in f])
392         except:
393             return set()
394
395     # Wrapper to return environment variable values.
396
397     @util.LogEntryExit
398     def _getenv(self, variable_name, default=None):
399         return os.environ.get(variable_name, default)
400
401     # Return the path to the project file (the *.xcodeproj "file).
402
403     @util.LogEntryExit
404     def _get_project_file_path(self):
405         assert False, """\
406                 Override this to return full path to the project file (e.g.,
407                 ".../Source/JavaScriptCore/JavaScriptCode.xcodeproj")."""
408
409     # Return the path to the directory containing the project file and its
410     # supporting files and directories (e.g., ".../Source/JavaScriptCore").
411
412     @util.LogEntryExit
413     def _get_project_dir(self):
414         return os.path.dirname(self._get_project_file_path())
415
416     # Return the project file name (e.g., "JavaScriptCore.xcodeproj").
417
418     @util.LogEntryExit
419     def _get_project_file_name(self):
420         return os.path.basename(self._get_project_file_path())
421
422     # Return the project name (e.g., "JavaScriptCore").
423
424     @util.LogEntryExit
425     def _get_project_name(self):
426         return os.path.splitext(self._get_project_file_name())[0]
427
428     # Return the path to the build output directory to use when the user has
429     # indicated that they are building from the command line. Be sure to
430     # support default command-line users, command-line users that set
431     # WEBKIT_OUTPUTDIR, default Xcode users, and people like Jeff Miller who
432     # configure a custom build output location for Xcode.
433
434     @util.LogEntryExit
435     def _get_sym_root(self):
436         return self._get_build_dirs()[0]
437
438     @util.LogEntryExit
439     def _get_obj_root(self):
440         return self._get_build_dirs()[1]
441
442     @util.LogEntryExit
443     def _get_shared_precomps_dir(self):
444         return os.path.join(self._get_build_dirs()[1], "PrecompiledHeaders")
445
446     # From Xcode;
447     #
448     #   export SYMROOT            =/Users/keith/Library/Developer/Xcode/DerivedData/Safari-bmjhivzkbpxamlajyexvkivfjbmb/Build/Products
449     #   export OBJROOT            =/Users/keith/Library/Developer/Xcode/DerivedData/Safari-bmjhivzkbpxamlajyexvkivfjbmb/Build/Intermediates.noindex
450     #   export SHARED_PRECOMPS_DIR=/Users/keith/Library/Developer/Xcode/DerivedData/Safari-bmjhivzkbpxamlajyexvkivfjbmb/Build/Intermediates.noindex/PrecompiledHeaders
451     #
452     # From command-line:
453     #
454     #   export SYMROOT            =/Volumes/Data/dev/webkit/OpenSource/WebKitBuild
455     #   export OBJROOT            =/Volumes/Data/dev/webkit/OpenSource/WebKitBuild
456     #   export SHARED_PRECOMPS_DIR=/Volumes/Data/dev/webkit/OpenSource/WebKitBuild/PrecompiledHeaders
457
458     @util.LogEntryExit
459     def _get_build_dirs(self):
460         def define_xcode_build_dirs(self):
461             # Delete any spurious ~/Library/Preferences/xcodebuild.plist, as this
462             # file will interfere with any preferences set in the IDE. This
463             # .plist file shouldn't really ever exist, so nuking it doesn't
464             # cause any problems.
465
466             xcodebuild_plist = os.path.join(os.path.expanduser("~"), "Library", "Preferences", "xcodebuild.plist")
467             try:
468                 os.unlink(xcodebuild_plist)
469             except:
470                 pass
471
472             def read_xcode_user_default(key):
473                 try:
474                     (stdout, stderr) = util.subprocess_run(["defaults", "read", "com.apple.dt.Xcode", key])
475                     return stdout.strip()
476                 except util.CalledProcessError:
477                     return None
478
479             # The following is based on the logic in determineBaseProductDir()
480             # in webkitdirs.pm and https://pewpewthespells.com/blog/xcode_build_locations.html.
481
482             # Get the base directory for the build output. This will be some
483             # default location (e.g., ~/Library/Developer/Xcode/DerivedData),
484             # an absolute path, or a project-relative path.
485
486             ide_custom_derived_data_location = read_xcode_user_default("IDECustomDerivedDataLocation")
487
488             # Path not specified; use the default.
489
490             if not ide_custom_derived_data_location:
491                 base_dir = os.path.join(os.path.expanduser("~"), "Library", "Developer", "Xcode", "DerivedData")
492
493             # An absolute path is specified; use it.
494
495             elif os.path.isabs(ide_custom_derived_data_location):
496                 base_dir = ide_custom_derived_data_location
497
498             # A relative path is specified; append it to the project path.
499
500             else:
501                 base_dir = os.path.join(self._get_project_dir(), ide_custom_derived_data_location)
502
503             # Get the specification for how the build output should be stored
504             # withing that base directory. This will be some unique directory
505             # based on the hash of the project file path (e.g.,
506             # "Safari-sdlfkhasalksdjfhsdfhlksf"), some shared directory (e.g.,
507             # "Build"), or even something that might be an absolute path.
508
509             ide_build_location_style = read_xcode_user_default("IDEBuildLocationStyle")
510
511             # Create a unique directory within the base directory based on
512             # project name and hash of its full path.
513             #
514             #    IDEBuildLocationStyle      = Unique;       # This is the default if not specified.
515
516             if ide_build_location_style == "Unique" or not ide_build_location_style:
517                 workspace = os.path.abspath(self.application.cmd_line_args.xcode)
518                 build_dir = os.path.join(
519                         base_dir,
520                         os.path.splitext(os.path.basename(workspace))[0] + "-" + xcode_hash_for_path(workspace),
521                         "Build")
522                 products_dir = os.path.join(build_dir, "Products")
523                 intermediates_dir = os.path.join(build_dir, "Intermediates.noindex")
524
525             # Use a shared subdirectory; use the specified directory name.
526             #
527             #    IDEBuildLocationStyle      = Shared;
528             #    IDESharedBuildFolderName       = Build;    # Relative to DerivedDataLocation
529
530             elif ide_build_location_style == "Shared":
531                 build_dir = os.path.join(base_dir, read_xcode_user_default("IDESharedBuildFolderName"))
532                 products_dir = os.path.join(build_dir, "Products")
533                 intermediates_dir = os.path.join(build_dir, "Intermediates.noindex")
534
535             # Use the saved products and intermediates paths and either use
536             # them as relative to the DerivedData directory or the project
537             # folder, or just use them as absolute addresses.
538             #
539             #    IDEBuildLocationStyle      = Custom;
540
541             elif ide_build_location_style == "Custom":
542                 ide_build_location_type = read_xcode_user_default("IDECustomBuildLocationType")
543
544                 products_dir = read_xcode_user_default("IDECustomBuildProductsPath")
545                 intermediates_dir = read_xcode_user_default("IDECustomBuildIntermediatesPath")
546
547                 #    IDECustomBuildLocationType     = RelativeToDerivedData;
548                 #    IDECustomBuildIntermediatesPath    = "Build/Intermediates.noindex";
549                 #    IDECustomBuildProductsPath         = "Build/Products";
550                 #    IDECustomIndexStorePath            = "Index/DataStore";
551
552                 if ide_build_location_type == "RelativeToDerivedData":
553                     products_dir = os.path.join(base_dir, products_dir)
554                     intermediates_dir = os.path.join(base_dir, intermediates_dir)
555
556                 #    IDECustomBuildLocationType     = RelativeToWorkspace;
557                 #    IDECustomBuildIntermediatesPath    = "Build/Intermediates.noindex";
558                 #    IDECustomBuildProductsPath         = "Build/Products";
559                 #    IDECustomIndexStorePath            = "Index/DataStore";
560
561                 elif ide_build_location_type == "RelativeToWorkspace":
562                     base_dir = self._get_project_dir(),
563                     products_dir = os.path.join(base_dir, products_dir)
564                     intermediates_dir = os.path.join(base_dir, intermediates_dir)
565
566                 #    IDECustomBuildLocationType     = Absolute;
567                 #    IDECustomBuildIntermediatesPath    = "/Users/joedeveloper/Desktop/Build/Intermediates.noindex";
568                 #    IDECustomBuildProductsPath         = "/Users/joedeveloper/Desktop/Build/Products";
569                 #    IDECustomIndexStorePath            = "/Users/joedeveloper/Desktop/Index/DataStore";
570                 #    IDECustomDerivedDataLocation       = "/Users/joedeveloper/Library/Developer/Xcode/DerivedData";
571
572                 elif ide_build_location_type == "Absolute":
573                     pass
574
575                 else:
576                     assert False, "Unknown/unsupported location type"
577             else:
578                 assert False, "Unknown/unsupported style"
579
580             return (products_dir, intermediates_dir)
581
582         def define_command_line_build_dirs(self):
583             products_dir = self._getenv("WEBKIT_OUTPUTDIR")
584             if not products_dir:
585                 products_dir = os.path.join(self.application.get_opensource_dir(), "WebKitBuild")
586             return (products_dir, products_dir)
587
588         if not self.cached_build_dirs and self.application.cmd_line_args.xcode:
589             self.cached_build_dirs = define_xcode_build_dirs(self)
590         if not self.cached_build_dirs:
591             self.cached_build_dirs = define_command_line_build_dirs(self)
592         return self.cached_build_dirs
593
594     # Return the location of the DerivedSources directory. Conventionally, we
595     # use $BUILT_PRODUCTS/DerivedSources/$PROJECT_NAME, but we may some day use
596     # $DERIVED_FILE_DIR aka $DERIVED_FILES_DIR aka $DERIVED_SOURCES_DIR, when
597     # the projects have been updated to use them.
598
599     @util.LogEntryExit
600     def _get_derived_sources_dir(self):
601         return os.path.join(self.application.get_xcode_built_products_dir(), "DerivedSources", self._get_project_name())
602
603     # Return the location for the xcfilelists.
604
605     @util.LogEntryExit
606     def _get_xcfilelist_dir(self):
607         return self._get_project_dir()
608
609     # Return the paths to the actual xcfilelists.
610
611     @util.LogEntryExit
612     def _get_input_derived_xcfilelist_project_path(self):
613         return os.path.join(self._get_xcfilelist_dir(), "DerivedSources-input.xcfilelist")
614
615     @util.LogEntryExit
616     def _get_output_derived_xcfilelist_project_path(self):
617         return os.path.join(self._get_xcfilelist_dir(), "DerivedSources-output.xcfilelist")
618
619     @util.LogEntryExit
620     def _get_input_unified_xcfilelist_project_path(self):
621         return os.path.join(self._get_xcfilelist_dir(), "UnifiedSources-input.xcfilelist")
622
623     @util.LogEntryExit
624     def _get_output_unified_xcfilelist_project_path(self):
625         return os.path.join(self._get_xcfilelist_dir(), "UnifiedSources-output.xcfilelist")
626
627     # Return the paths to the scripts that generate the derived sources.
628
629     @util.LogEntryExit
630     def _get_generate_derived_sources_script(self):
631         return None
632
633     @util.LogEntryExit
634     def _get_generate_unified_sources_script(self):
635         return None
636
637
638 class JavaScriptCoreGenerator(BaseGenerator):
639     VALID_PLATFORMS = ("macosx", "iosmac", "iphoneos", "watchos")
640     VALID_CONFIGURATIONS = ("Debug", "Release", "Production", "Profiling")
641
642     @util.LogEntryExit
643     def _get_project_file_path(self):
644         return os.path.join(self.application.get_opensource_dir(), "Source", "JavaScriptCore", "JavaScriptCore.xcodeproj")
645
646     @util.LogEntryExit
647     def _get_generate_derived_sources_script(self):
648         return os.path.join(self._get_project_dir(), "Scripts", "generate-derived-sources.sh")
649
650     @util.LogEntryExit
651     def _get_generate_unified_sources_script(self):
652         return os.path.join(self._get_project_dir(), "Scripts", "generate-unified-sources.sh")
653
654
655 class WebCoreGenerator(BaseGenerator):
656     VALID_PLATFORMS = ("macosx", "iosmac", "iphoneos", "watchos")
657     VALID_CONFIGURATIONS = ("Debug", "Release", "Production")
658
659     @util.LogEntryExit
660     def _get_project_file_path(self):
661         return os.path.join(self.application.get_opensource_dir(), "Source", "WebCore", "WebCore.xcodeproj")
662
663     @util.LogEntryExit
664     def _get_generate_derived_sources_script(self):
665         return os.path.join(self._get_project_dir(), "Scripts", "generate-derived-sources.sh")
666
667     @util.LogEntryExit
668     def _get_generate_unified_sources_script(self):
669         return os.path.join(self._get_project_dir(), "Scripts", "generate-unified-sources.sh")
670
671
672 class WebKitGenerator(BaseGenerator):
673     VALID_PLATFORMS = ("macosx", "iosmac", "iphoneos", "watchos")
674     VALID_CONFIGURATIONS = ("Debug", "Release", "Production")
675
676     @util.LogEntryExit
677     def _get_project_file_path(self):
678         return os.path.join(self.application.get_opensource_dir(), "Source", "WebKit", "WebKit.xcodeproj")
679
680     @util.LogEntryExit
681     def _get_derived_sources_dir(self):
682         return os.path.join(self.application.get_xcode_built_products_dir(), "DerivedSources", "WebKit2")
683
684     @util.LogEntryExit
685     def _get_generate_derived_sources_script(self):
686         return os.path.join(self._get_project_dir(), "Scripts", "generate-derived-sources.sh")
687
688     @util.LogEntryExit
689     def _get_generate_unified_sources_script(self):
690         return os.path.join(self._get_project_dir(), "Scripts", "generate-unified-sources.sh")
691
692
693 class DumpRenderTreeGenerator(BaseGenerator):
694     VALID_PLATFORMS = ("macosx", )
695     VALID_CONFIGURATIONS = ("Debug", "Release", "Production")
696
697     @util.LogEntryExit
698     def _get_project_file_path(self):
699         return os.path.join(self.application.get_opensource_dir(), "Tools", "DumpRenderTree", "DumpRenderTree.xcodeproj")
700
701     @util.LogEntryExit
702     def _get_generate_derived_sources_script(self):
703         return os.path.join(self._get_project_dir(), "Scripts", "generate-derived-sources.sh")
704
705
706 class WebKitTestRunnerGenerator(BaseGenerator):
707     VALID_PLATFORMS = ("macosx", )
708     VALID_CONFIGURATIONS = ("Debug", "Release", "Production")
709
710     @util.LogEntryExit
711     def _get_project_file_path(self):
712         return os.path.join(self.application.get_opensource_dir(), "Tools", "WebKitTestRunner", "WebKitTestRunner.xcodeproj")
713
714     @util.LogEntryExit
715     def _get_generate_derived_sources_script(self):
716         return os.path.join(self._get_project_dir(), "Scripts", "generate-derived-sources.sh")