[scan-build-py] create decorator for compiler wrapper methods

Differential Revision: https://reviews.llvm.org/D29260

llvm-svn: 296937
diff --git a/clang/tools/scan-build-py/libscanbuild/__init__.py b/clang/tools/scan-build-py/libscanbuild/__init__.py
index 4b5582d..ca75174 100644
--- a/clang/tools/scan-build-py/libscanbuild/__init__.py
+++ b/clang/tools/scan-build-py/libscanbuild/__init__.py
@@ -4,13 +4,21 @@
 # This file is distributed under the University of Illinois Open Source
 # License. See LICENSE.TXT for details.
 """ This module is a collection of methods commonly used in this project. """
+import collections
 import functools
+import json
 import logging
 import os
 import os.path
+import re
+import shlex
 import subprocess
 import sys
 
+ENVIRONMENT_KEY = 'INTERCEPT_BUILD'
+
+Execution = collections.namedtuple('Execution', ['pid', 'cwd', 'cmd'])
+
 
 def duplicate_check(method):
     """ Predicate to detect duplicated entries.
@@ -75,31 +83,53 @@
         raise ex
 
 
-def initialize_logging(verbose_level):
-    """ Output content controlled by the verbosity level. """
+def reconfigure_logging(verbose_level):
+    """ Reconfigure logging level and format based on the verbose flag.
 
+    :param verbose_level: number of `-v` flags received by the command
+    :return: no return value
+    """
+    # Exit when nothing to do.
+    if verbose_level == 0:
+        return
+
+    root = logging.getLogger()
+    # Tune logging level.
     level = logging.WARNING - min(logging.WARNING, (10 * verbose_level))
-
+    root.setLevel(level)
+    # Be verbose with messages.
     if verbose_level <= 3:
-        fmt_string = '{0}: %(levelname)s: %(message)s'
+        fmt_string = '%(name)s: %(levelname)s: %(message)s'
     else:
-        fmt_string = '{0}: %(levelname)s: %(funcName)s: %(message)s'
-
-    program = os.path.basename(sys.argv[0])
-    logging.basicConfig(format=fmt_string.format(program), level=level)
+        fmt_string = '%(name)s: %(levelname)s: %(funcName)s: %(message)s'
+    handler = logging.StreamHandler(sys.stdout)
+    handler.setFormatter(logging.Formatter(fmt=fmt_string))
+    root.handlers = [handler]
 
 
 def command_entry_point(function):
-    """ Decorator for command entry points. """
+    """ Decorator for command entry methods.
+
+    The decorator initialize/shutdown logging and guard on programming
+    errors (catch exceptions).
+
+    The decorated method can have arbitrary parameters, the return value will
+    be the exit code of the process. """
 
     @functools.wraps(function)
     def wrapper(*args, **kwargs):
+        """ Do housekeeping tasks and execute the wrapped method. """
 
-        exit_code = 127
         try:
-            exit_code = function(*args, **kwargs)
+            logging.basicConfig(format='%(name)s: %(message)s',
+                                level=logging.WARNING,
+                                stream=sys.stdout)
+            # This hack to get the executable name as %(name).
+            logging.getLogger().name = os.path.basename(sys.argv[0])
+            return function(*args, **kwargs)
         except KeyboardInterrupt:
-            logging.warning('Keyboard interupt')
+            logging.warning('Keyboard interrupt')
+            return 130  # Signal received exit code for bash.
         except Exception:
             logging.exception('Internal error.')
             if logging.getLogger().isEnabledFor(logging.DEBUG):
@@ -107,8 +137,75 @@
                               "to the bug report")
             else:
                 logging.error("Please run this command again and turn on "
-                              "verbose mode (add '-vvv' as argument).")
+                              "verbose mode (add '-vvvv' as argument).")
+            return 64  # Some non used exit code for internal errors.
         finally:
-            return exit_code
+            logging.shutdown()
 
     return wrapper
+
+
+def compiler_wrapper(function):
+    """ Implements compiler wrapper base functionality.
+
+    A compiler wrapper executes the real compiler, then implement some
+    functionality, then returns with the real compiler exit code.
+
+    :param function: the extra functionality what the wrapper want to
+    do on top of the compiler call. If it throws exception, it will be
+    caught and logged.
+    :return: the exit code of the real compiler.
+
+    The :param function: will receive the following arguments:
+
+    :param result:       the exit code of the compilation.
+    :param execution:    the command executed by the wrapper. """
+
+    def is_cxx_compiler():
+        """ Find out was it a C++ compiler call. Compiler wrapper names
+        contain the compiler type. C++ compiler wrappers ends with `c++`,
+        but might have `.exe` extension on windows. """
+
+        wrapper_command = os.path.basename(sys.argv[0])
+        return re.match(r'(.+)c\+\+(.*)', wrapper_command)
+
+    def run_compiler(executable):
+        """ Execute compilation with the real compiler. """
+
+        command = executable + sys.argv[1:]
+        logging.debug('compilation: %s', command)
+        result = subprocess.call(command)
+        logging.debug('compilation exit code: %d', result)
+        return result
+
+    # Get relevant parameters from environment.
+    parameters = json.loads(os.environ[ENVIRONMENT_KEY])
+    reconfigure_logging(parameters['verbose'])
+    # Execute the requested compilation. Do crash if anything goes wrong.
+    cxx = is_cxx_compiler()
+    compiler = parameters['cxx'] if cxx else parameters['cc']
+    result = run_compiler(compiler)
+    # Call the wrapped method and ignore it's return value.
+    try:
+        call = Execution(
+            pid=os.getpid(),
+            cwd=os.getcwd(),
+            cmd=['c++' if cxx else 'cc'] + sys.argv[1:])
+        function(result, call)
+    except:
+        logging.exception('Compiler wrapper failed complete.')
+    finally:
+        # Always return the real compiler exit code.
+        return result
+
+
+def wrapper_environment(args):
+    """ Set up environment for interpose compiler wrapper."""
+
+    return {
+        ENVIRONMENT_KEY: json.dumps({
+            'verbose': args.verbose,
+            'cc': shlex.split(args.cc),
+            'cxx': shlex.split(args.cxx)
+        })
+    }