#!/usr/bin/env python3 # -*- 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: 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: 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="", 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="", 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="", 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="", 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: # 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)