#!/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. from __future__ import print_function import argparse import os import subprocess import sys import traceback # Gather information about our debugging environment right now. Do this before # executing any "main" code so that our debugging preferences are in place by # the time we get there and we can debug from main() on down as opposed to # parse_args() on down. SHOW_DEBUG_LOGGING = "-d" in sys.argv or "--debug" in sys.argv if SHOW_DEBUG_LOGGING: DEBUG_LOGGING_FILE = None for index, arg in enumerate(sys.argv): if arg == "--debug-file": if index + 1 < len(sys.argv): DEBUG_LOGGING_FILE = sys.argv[index + 1] # Bottleneck function for printing debugging lines to either the console or # the screen, as appropriate. if DEBUG_LOGGING_FILE: def debug_log(msg): with open(DEBUG_LOGGING_FILE, "a") as f: print(msg, file=f) else: def debug_log(msg): print(msg) # Context Manager class for logging information about function entry/exit. # On entry, the function name is logged along with its parameters. On exit, # the function name is logged with its result. If an exception occurs, the # function name is logged with the exception. In all cases, the logging is # indented according to the level of the function in the backtrace. # # Versions of these classes exist for instance methods, class methods, and # global functions so that instance and class information can be extracted # and displayed. class LogEntryHelper(object): __slots__ = ["indent", "class_name", "function_name", "type"] def __init__(self, func, type): tb = traceback.extract_stack() self.indent = " " * 2 * (len(tb) - 3) self.class_name = None self.function_name = func.__name__ self.type = type def log_entry(self, args, kwargs): if self.type == "instance": self.class_name = args[0].__class__.__name__ + "." args = args[1:] elif self.type == "class": self.class_name = args[0].__name__ + "." args = args[1:] else: self.class_name = "" self._print("args={}, kwargs={}".format(args, kwargs)) def log_result(self, result): if hasattr(result, '__iter__') and not isinstance(result, str): for line in result: self._print("result={}".format(line)) else: self._print("result={}".format(result)) def log_exception(self, exc): self._print("exception={}".format(exc)) def _print(self, msg): debug_log("{}{}{}: {}".format(self.indent, self.class_name, self.function_name, msg)) def LogEntryExit(func): def _show_debug_logging(*args, **kwargs): helper = LogEntryHelper(func, "instance") helper.log_entry(args, kwargs) try: result = func(*args, **kwargs) helper.log_result(result) return result except BaseException as e: helper.log_exception(e) raise return _show_debug_logging def LogEntryExitClass(func): def _show_debug_logging(*args, **kwargs): helper = LogEntryHelper(func, "class") helper.log_entry(args, kwargs) try: result = func(*args, **kwargs) helper.log_result(result) return result except BaseException as e: helper.log_exception(e) raise return _show_debug_logging def LogEntryExitGlobal(func): def _show_debug_logging(*args, **kwargs): helper = LogEntryHelper(func, None) helper.log_entry(args, kwargs) try: result = func(*args, **kwargs) helper.log_result(result) return result except BaseException as e: helper.log_exception(e) raise return _show_debug_logging else: def debug_log(msg): pass def LogEntryExit(func): return func def LogEntryExitClass(func): return func def LogEntryExitGlobal(func): return func # Utility function for operating similar to subprocess.run() in Python 3. One # difference is that the result is a 2-tuple with stdout and stderr, rather # than a 3-tuple that includes returncode. For our purposes, if returncode is # non-zero, we raise an exception. @LogEntryExitGlobal def subprocess_run(args, **kwargs): kwargs["stdout"] = subprocess.PIPE kwargs["stderr"] = subprocess.PIPE input = None if "input" in kwargs: input = kwargs["input"] del kwargs["input"] kwargs["stdin"] = subprocess.PIPE process = subprocess.Popen(args, **kwargs) (stdout, stderr) = process.communicate(input=input) stdout = stdout.decode() if isinstance(stdout, bytes) else stdout stderr = stderr.decode() if isinstance(stderr, bytes) else stderr if process.returncode: raise CalledProcessError(process.returncode, args[0], stdout, stderr) return (stdout, stderr) # Utility function to allow us to verify that we're running under Xcode or not. # For example, if we are not, then we need to make sure that we don't try to # access Xcode-specific environment variables. # # Note: This function use to check XCODE_INSTALL_PATH. Because if Xcode is # running, it must have been installed. That's the theory, anyway. In # actuality, it seems possible to install Xcode without the result having # XCODE_INSTALL_PATH defined. So now we check XCODE_PRODUCT_BUILD_VERSION. @LogEntryExitGlobal def is_running_under_xcode(): return os.environ.get("XCODE_PRODUCT_BUILD_VERSION") # An argparse.Action subclass that validates the user-provided value against a # list of valid values. Aliasing is supported; that is, the user can provide a # value that can get mapped to a corresponding canonical value, and that # resulting value is compared to the list of valid values. # # On error, calls parser.error(). class CheckValidItemAction(argparse.Action): @LogEntryExit def __init__(self, *args, **kwargs): self.item_type = kwargs.get("item_type", None) self.valid_items = kwargs.get("valid_items", None) self.aliases = kwargs.get("aliases", None) self.lowered_valid_items = [item.lower() for item in self.valid_items] kwargs.pop("item_type", None) kwargs.pop("valid_items", None) kwargs.pop("aliases", None) super(CheckValidItemAction, self).__init__(*args, **kwargs) @LogEntryExit def __call__(self, parser, namespace, values, option_string=None): try: validated = self.validate_item(values) except: parser.error("The {} \"{}\" is not supported.".format(self.item_type, values)) items = getattr(namespace, self.dest, None) items = items[:] if items else [] items.append(validated) setattr(namespace, self.dest, items) @LogEntryExit def validate_item(self, item): item = item.lower() try: validated_index = self.lowered_valid_items.index(item) except: if not self.aliases: raise item = self.aliases.get(item, None) validated_index = self.lowered_valid_items.index(item) return self.valid_items[validated_index] # An argparse.Action subclass that validates the user-provided script command # (generate, check, etc.) # # On error, calls parser.error(). class CheckCommandAction(argparse.Action): @LogEntryExit def __init__(self, *args, **kwargs): self.valid_commands = kwargs.get("valid_commands", None) kwargs.pop("valid_commands", None) super(CheckCommandAction, self).__init__(*args, **kwargs) @LogEntryExit def __call__(self, parser, namespace, value, option_string=None): if not value in self.valid_commands: parser.error('"{}" is not a valid command'.format(value)) setattr(namespace, self.dest, value) # Some Exceptions class InvalidCommandError(Exception): pass class InvalidArgumentError(Exception): pass class InvalidConfigurationError(Exception): pass # subprocess.CalledProcessError has problems with being pickled, which is # something that we do to it. In particular, when unpickled, it throws an # exception, and so CalledProcessError get's turned into an exception saying # "__init__() takes at least 3 arguments (1 given)". Address this by creating # our own CalledProcessError that's a little more generic. class CalledProcessError(Exception): def __str__(self): returncode = self.args[0] if len(self.args) > 0 else None command = self.args[1] if len(self.args) > 1 else None stdout = self.args[2] if len(self.args) > 2 else None stderr = self.args[3] if len(self.args) > 3 else None if stderr: return "Command '{}' returned non-zero exit status {}: {}".format(command, returncode, stderr) elif stdout: return "Command '{}' returned non-zero exit status {}: {}".format(command, returncode, stdout) else: return "Command '{}' returned non-zero exit status {}".format(command, returncode)