blob: 2538adf278daacba35ab6ae89c99e26f6c5e72a4 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Application represents the main operation of the script. It's a singleton,
# created by main() and then invoked to run everything. This class parses the
# command-line options, validates them, creates and invokes BaseGenerators,
# reports the results, and handles the catching and reporting of any
# exceptions/errors.
from __future__ import print_function
import argparse
import itertools
import os
import sys
import textwrap
import traceback
from functools import reduce
import webkitpy.generate_xcfilelists_lib.generators as Generators
import webkitpy.generate_xcfilelists_lib.util as util
EX_GENERAL_ERROR = 1 # General error
EX_ACTION_REQUIRED = 2 # Returned when script determines that the generated .xcfilelist files have changed.
class Application(object):
__slots__ = (
"command_file",
"parser", "cmd_line_args",
"dispatch", "project_specific_generators",
"supported_project_tags", "supported_platforms", "supported_configurations")
# Aliases for platforms. These are handy when using the script from the
# command line and you don't remember if it's "ios" or "iphoneos, or "tvos"
# or "appletvos". This list of aliases will let you use any of those.
platform_aliases = {
"ios": "iphoneos",
"iphone": "iphoneos",
"simulator": "iphonesimulator",
"sim": "iphonesimulator",
"mac": "macosx",
"macos": "macosx",
"osx": "macosx",
"tvos": "appletvos",
"tv": "appletvos",
"tvsimulator": "appletvsimulator",
"watch": "watchos",
}
@util.LogEntryExit
def __init__(self, command_file):
self.command_file = os.path.realpath(command_file)
self.parser = None
self.cmd_line_args = None
self.dispatch = {
"generate": self._cmd_set_environment_and_generate,
"generate-xcode": self._cmd_generate_within_xcode,
"check": self._cmd_set_environment_and_check,
"check-xcode": self._cmd_check_within_xcode,
"generate-inner": self._cmd_generate_within_xcode_and_return_results_to_caller,
"help": self._cmd_help,
}
self.project_specific_generators = {
"JavaScriptCore": Generators.JavaScriptCoreGenerator,
"WebCore": Generators.WebCoreGenerator,
"WebKit": Generators.WebKitGenerator,
"WebKitLegacy": Generators.WebKitLegacyGenerator,
"DumpRenderTree": Generators.DumpRenderTreeGenerator,
"WebKitTestRunner": Generators.WebKitTestRunnerGenerator,
"TestWebKitAPI": Generators.TestWebKitAPIGenerator,
}
self.supported_project_tags = None
self.supported_platforms = None
self.supported_configurations = None
@util.LogEntryExit
def run(self):
try:
self._initialize()
self.parser = self._create_parser()
self.cmd_line_args = args = self.parser.parse_args()
if args.help:
return self._cmd_help(os.EX_OK)
self._validate_args(args)
try:
func = self.dispatch[args.command]
except KeyError as e:
raise util.InvalidCommandError(args.command)
return func()
except util.InvalidArgumentError as e:
print("### Invalid argument: {}".format(e))
return self._cmd_help(os.EX_USAGE)
except util.InvalidCommandError as e:
if e.args:
print("### Invalid command: {}".format(e))
else:
print("### Missing command")
return self._cmd_help(os.EX_USAGE)
except SystemExit:
raise
except BaseException as e:
traceback.print_exc()
return os.EX_SOFTWARE
# Perform some post __init__ initialization. This is performed after
# __init__ so that we can respond to any information provided in any
# sub-class's __init__.
@util.LogEntryExit
def _initialize(self):
def collect_attributes(key):
configurations = set()
for project_tag in self.project_specific_generators:
configurations |= set(key(self.project_specific_generators[project_tag]))
return configurations
self.supported_project_tags = sorted(list(self.project_specific_generators.keys()))
self.supported_platforms = sorted(list(collect_attributes(lambda gen_cls: gen_cls.VALID_PLATFORMS)))
self.supported_configurations = sorted(list(collect_attributes(lambda gen_cls: gen_cls.VALID_CONFIGURATIONS)))
@util.LogEntryExit
def _create_parser(self):
valid_commands = ("generate", "generate-xcode", "check", "check-xcode", "generate-inner", "help")
valid_commands_prompt = "(" + " | ".join(valid_commands) + ")"
parser = argparse.ArgumentParser(add_help=False,
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
Generate or check .xcfilelist files. One of the following commands must be
specified on the command-line:
generate Generate a complete and up-to-date set of .xcfilelist
files and copy them to their appropriate places in the
project directories.
generate-xcode Similar to generate, but to be called from within Xcode.
check Generate a complete and up-to-date set of .xcfilelist
files and compare them to their counterparts in the
project directories.
check-xcode Similar to check, but to be called from within Xcode.
generate-inner [Used by script internals] Generate an .xcfilelist file
for a particular combination of project, platform, and
configuration. This operation is performed in the
context of an Xcode build in order to inherit the same
environment as that build. Once generated, the results
are returned to the calling instance of this script.
help Print this text and exit.""")
parser.add_argument("command", action=util.CheckCommandAction,
valid_commands=valid_commands, metavar=valid_commands_prompt,
help="""\
The operation to perform.""")
parser.add_argument("--project", action=util.CheckValidItemAction,
item_type="project",
valid_items=self.supported_project_tags,
dest="project_tags", metavar="<PROJECT>", help="""\
Specify which project or projects for which to generate
.xcfilelist files or to check. Possible values are
({}). Can be specified more than once. Default is to
iterate over all projects.""".format(
", ".join(self.supported_project_tags)))
parser.add_argument("--platform", action=util.CheckValidItemAction,
item_type="platform",
valid_items=self.supported_platforms,
aliases=self.platform_aliases,
dest="platforms", metavar="<PLATFORM>", help="""\
Specify which platform or platforms for which to
generate .xcfilelist files or to check. Possible values
are ({}, plus common aliases). Can be specified more
than once. Default is to iterate over all platforms,
filtered to those platforms that a particular project
supports (e.g., you can't specify 'iphoneos' for
WebKitTestRunner).""".format(
", ".join(self.supported_platforms)))
parser.add_argument("--configuration", action=util.CheckValidItemAction,
item_type="configuration",
valid_items=self.supported_configurations,
dest="configurations", metavar="<CONFIGURATION>", help="""\
Specify which configuration or configurations for which
to generate .xcfilelist files or to check. Possible
values are ({}). Can be specified more than once.
Default is to iterate over all
configurations.""".format(
", ".join(self.supported_configurations)))
parser.add_argument("--xcode", metavar="<WORKSPACE>", help="""\
If the existing build output was created by building
with Xcode, specify the path to the workspace that was
used.""")
parser.add_argument("-d", "--debug", action="store_true", help="""\
Provide verbose output.""")
parser.add_argument("--debug-file", help="""\
[Used by script internals] Name of the file to which to
write debug information. Used when this script
sub-launches itself and needs to collect the debug
information from the sub-launched instance. Not
normally used when this script is invoked from the
command-line or Xcode.""")
parser.add_argument("--pickle-file", help="""\
[Used by script internals] Name of the file used to
store results to be transported out from the Xcode
execution environment out to an outer layer. This
parameter is only used with the 'generate-core'
command.""")
parser.add_argument("-q", "--quiet", action="store_true", help="""\
Don't print any standard output.""")
parser.add_argument("-h", "--help", action="store_true", help="""\
Print this text and exit.""")
setattr(parser, "application", self)
return parser
@util.LogEntryExit
def _validate_args(self, args):
if not self.cmd_line_args.project_tags:
self.cmd_line_args.project_tags = self.supported_project_tags
if not self.cmd_line_args.platforms:
self.cmd_line_args.platforms = self.supported_platforms
if not self.cmd_line_args.configurations:
self.cmd_line_args.configurations = self.supported_configurations
if util.is_running_under_xcode():
assert len(self.cmd_line_args.project_tags) == 1
assert len(self.cmd_line_args.platforms) == 1
assert len(self.cmd_line_args.configurations) == 1
@util.LogEntryExit
def _cmd_set_environment_and_generate(self):
generators = self._do_set_environment_and_generate()
generators = self._do_merge(generators)
return self._report_results(generators)
@util.LogEntryExit
def _cmd_generate_within_xcode(self):
generators = self._do_generate()
generators = self._do_merge(generators)
return self._report_results(generators)
@util.LogEntryExit
def _cmd_set_environment_and_check(self):
generators = self._do_set_environment_and_generate()
return self._report_results(generators)
@util.LogEntryExit
def _cmd_check_within_xcode(self):
generators = self._do_generate()
return self._report_results(generators)
@util.LogEntryExit
def _cmd_generate_within_xcode_and_return_results_to_caller(self):
generators = self._do_generate()
with open(self.cmd_line_args.pickle_file, "wb") as f:
for generator in generators:
generator.pickle_to_file(f)
return os.EX_OK
@util.LogEntryExit
def _cmd_help(self, status=os.EX_OK):
self.parser.print_help()
return status
@util.LogEntryExit
def _do_set_environment_and_generate(self):
def core_operation(generator, generators):
new_generators = generator.set_environment_and_generate()
generators.extend(new_generators)
return generators
return self._do_generate_common(core_operation)
@util.LogEntryExit
def _do_generate(self):
def core_operation(generator, generators):
generator.generate()
generators.append(generator)
return generators
return self._do_generate_common(core_operation)
@util.LogEntryExit
def _do_generate_common(self, core_operation):
generators = []
for triple in itertools.product(
self.cmd_line_args.project_tags,
self.cmd_line_args.platforms,
self.cmd_line_args.configurations):
generator = self.project_specific_generators[triple[0]](self, *triple)
if not generator.is_valid():
continue
self._log_progress("Generating .xcfilelists for {}/{}/{}".format(*triple))
try:
generators = core_operation(generator, generators)
except BaseException as e:
# TODO: Turn the traceback into a string, and then allow
# this field to be pickled and printed by the calling
# context. Right now, pickling raises an exception if it
# encounters a Traceback object. See BaseGenerator.pickle_to_file.
(generator.ex_type, generator.ex_value, generator.ex_traceback) = sys.exc_info()
if generator.has_error():
sys.exit(self._report_results([generator]))
return generators
@util.LogEntryExit
def _do_merge(self, generators):
if self._any_have_errors(generators):
return generators
for generator in generators:
if generator.has_action():
self._log_progress("Merging .xcfilelists for {}/{}/{}".format(*generator.triple))
generator.merge()
return generators
@util.LogEntryExit
def _report_results(self, generators):
generators_with_errors = [generator for generator in generators if generator.has_error()]
if generators_with_errors:
for generator in generators_with_errors:
generator.report_error()
return EX_GENERAL_ERROR
generators_with_actions = [generator for generator in generators if generator.has_action()]
if generators_with_actions:
if self.cmd_line_args.command == "generate" or self.cmd_line_args.command == "generate-xcode":
self._report_merge_results(generators_with_actions)
else:
self._report_remediation_steps(generators_with_actions)
return EX_ACTION_REQUIRED
return os.EX_OK
@util.LogEntryExit
def _report_merge_results(self, generators):
message = textwrap.wrap(
"\".xcfilelist\" files tell the build system what files are " +
"consumed and produced by the \"Run Script\" build phases in " +
"Xcode. At least one of these .xcfilelist files was out of date " +
"and has been updated. You now need to restart your build.", 90)
self._log_results("")
for line in message:
self._log_results(line)
@util.LogEntryExit
def _report_remediation_steps(self, generators):
message = textwrap.wrap("One or more \".xcfilelist\" files are out of date. Regenerate them by running the following commands:", 90)
message.append("")
def add_to_message(generator, message):
if generator.has_action():
message.append(" `Tools/Scripts/generate-xcfilelists generate --project {} --platform {} --configuration {}{}`\n".format(
generator.project_tag, generator.platform, generator.configuration,
" --xcode {}".format(self.cmd_line_args.xcode) if self.cmd_line_args.xcode else ""))
return message
for generator in generators:
message = add_to_message(generator, message)
for line in message:
self._log_results(line)
@util.LogEntryExit
def _any_have_errors(self, generators):
return reduce(lambda acc, generator: acc or generator.has_error(), generators, None)
@util.LogEntryExit
def _any_have_actions(self, generators):
return reduce(lambda acc, generator: acc or generator.has_action(), generators, None)
@util.LogEntryExit
def _log_progress(self, message):
if not self.cmd_line_args.quiet:
print("### {}".format(message))
@util.LogEntryExit
def _log_results(self, message):
if not self.cmd_line_args.quiet:
print("{}".format(message))
# Return the path to the script to sublaunch.
@util.LogEntryExit
def get_generate_xcfilelists_script_path(self):
return self.command_file
# Return the parent of the WebKit check-out directory.
@util.LogEntryExit
def _get_root_parent_dir(self):
return os.path.dirname( # Remove "OpenSource"
os.path.dirname( # Remove "Tools"
os.path.dirname( # Remove "Scripts"
os.path.dirname( # Remove script name
self.get_generate_xcfilelists_script_path()))))
# Return the path to the WebKit check-out directory.
@util.LogEntryExit
def get_opensource_dir(self):
return os.path.join(self._get_root_parent_dir(), "OpenSource")
# Return the path to the directory containing supporting build scripts.
@util.LogEntryExit
def get_build_scripts_dir(self):
return os.path.join(self.get_opensource_dir(), "Source", "WTF", "Scripts")
# Return the path to a supporting build script.
@util.LogEntryExit
def get_extract_dependencies_from_makefile_script(self):
return os.path.join(self.get_opensource_dir(), "Tools", "Scripts", "extract-dependencies-from-makefile")
# Return $(BUILT_PRODUCTS_DIR)
# aka $(CONFIGURATION_BUILD_DIR)
# aka $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
@util.LogEntryExit
def get_xcode_built_products_dir(self):
assert util.is_running_under_xcode()
return self._getenv("BUILT_PRODUCTS_DIR")
@util.LogEntryExit
def get_xcode_project_temp_dir(self):
assert util.is_running_under_xcode()
return self._getenv("PROJECT_TEMP_DIR")
# Return the named environment variable.
@util.LogEntryExit
def _getenv(self, variable_name):
return os.environ.get(variable_name)