406 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| """Check CFC - Check Compile Flow Consistency
 | |
| 
 | |
| This is a compiler wrapper for testing that code generation is consistent with
 | |
| different compilation processes. It checks that code is not unduly affected by
 | |
| compiler options or other changes which should not have side effects.
 | |
| 
 | |
| To use:
 | |
| -Ensure that the compiler under test (i.e. clang, clang++) is on the PATH
 | |
| -On Linux copy this script to the name of the compiler
 | |
|    e.g. cp check_cfc.py clang && cp check_cfc.py clang++
 | |
| -On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe
 | |
|  and clang++.exe
 | |
| -Enable the desired checks in check_cfc.cfg (in the same directory as the
 | |
|  wrapper)
 | |
|    e.g.
 | |
| [Checks]
 | |
| dash_g_no_change = true
 | |
| dash_s_no_change = false
 | |
| 
 | |
| -The wrapper can be run using its absolute path or added to PATH before the
 | |
|  compiler under test
 | |
|    e.g. export PATH=<path to check_cfc>:$PATH
 | |
| -Compile as normal. The wrapper intercepts normal -c compiles and will return
 | |
|  non-zero if the check fails.
 | |
|    e.g.
 | |
| $ clang -c test.cpp
 | |
| Code difference detected with -g
 | |
| --- /tmp/tmp5nv893.o
 | |
| +++ /tmp/tmp6Vwjnc.o
 | |
| @@ -1 +1 @@
 | |
| -   0:       48 8b 05 51 0b 20 00    mov    0x200b51(%rip),%rax
 | |
| +   0:       48 39 3d 51 0b 20 00    cmp    %rdi,0x200b51(%rip)
 | |
| 
 | |
| -To run LNT with Check CFC specify the absolute path to the wrapper to the --cc
 | |
|  and --cxx options
 | |
|    e.g.
 | |
|    lnt runtest nt --cc <path to check_cfc>/clang \\
 | |
|            --cxx <path to check_cfc>/clang++ ...
 | |
| 
 | |
| To add a new check:
 | |
| -Create a new subclass of WrapperCheck
 | |
| -Implement the perform_check() method. This should perform the alternate compile
 | |
|  and do the comparison.
 | |
| -Add the new check to check_cfc.cfg. The check has the same name as the
 | |
|  subclass.
 | |
| """
 | |
| 
 | |
| from __future__ import absolute_import, division, print_function
 | |
| 
 | |
| import imp
 | |
| import os
 | |
| import platform
 | |
| import shutil
 | |
| import subprocess
 | |
| import sys
 | |
| import tempfile
 | |
| try:
 | |
|     import configparser
 | |
| except ImportError:
 | |
|     import ConfigParser as configparser
 | |
| import io
 | |
| 
 | |
| import obj_diff
 | |
| 
 | |
| def is_windows():
 | |
|     """Returns True if running on Windows."""
 | |
|     return platform.system() == 'Windows'
 | |
| 
 | |
| class WrapperStepException(Exception):
 | |
|     """Exception type to be used when a step other than the original compile
 | |
|     fails."""
 | |
|     def __init__(self, msg, stdout, stderr):
 | |
|         self.msg = msg
 | |
|         self.stdout = stdout
 | |
|         self.stderr = stderr
 | |
| 
 | |
| class WrapperCheckException(Exception):
 | |
|     """Exception type to be used when a comparison check fails."""
 | |
|     def __init__(self, msg):
 | |
|         self.msg = msg
 | |
| 
 | |
| def main_is_frozen():
 | |
|     """Returns True when running as a py2exe executable."""
 | |
|     return (hasattr(sys, "frozen") or # new py2exe
 | |
|             hasattr(sys, "importers") or # old py2exe
 | |
|             imp.is_frozen("__main__")) # tools/freeze
 | |
| 
 | |
| def get_main_dir():
 | |
|     """Get the directory that the script or executable is located in."""
 | |
|     if main_is_frozen():
 | |
|         return os.path.dirname(sys.executable)
 | |
|     return os.path.dirname(sys.argv[0])
 | |
| 
 | |
| def remove_dir_from_path(path_var, directory):
 | |
|     """Remove the specified directory from path_var, a string representing
 | |
|     PATH"""
 | |
|     pathlist = path_var.split(os.pathsep)
 | |
|     norm_directory = os.path.normpath(os.path.normcase(directory))
 | |
|     pathlist = [x for x in pathlist if os.path.normpath(
 | |
|         os.path.normcase(x)) != norm_directory]
 | |
|     return os.pathsep.join(pathlist)
 | |
| 
 | |
| def path_without_wrapper():
 | |
|     """Returns the PATH variable modified to remove the path to this program."""
 | |
|     scriptdir = get_main_dir()
 | |
|     path = os.environ['PATH']
 | |
|     return remove_dir_from_path(path, scriptdir)
 | |
| 
 | |
| def flip_dash_g(args):
 | |
|     """Search for -g in args. If it exists then return args without. If not then
 | |
|     add it."""
 | |
|     if '-g' in args:
 | |
|         # Return args without any -g
 | |
|         return [x for x in args if x != '-g']
 | |
|     else:
 | |
|         # No -g, add one
 | |
|         return args + ['-g']
 | |
| 
 | |
| def derive_output_file(args):
 | |
|     """Derive output file from the input file (if just one) or None
 | |
|     otherwise."""
 | |
|     infile = get_input_file(args)
 | |
|     if infile is None:
 | |
|         return None
 | |
|     else:
 | |
|         return '{}.o'.format(os.path.splitext(infile)[0])
 | |
| 
 | |
| def get_output_file(args):
 | |
|     """Return the output file specified by this command or None if not
 | |
|     specified."""
 | |
|     grabnext = False
 | |
|     for arg in args:
 | |
|         if grabnext:
 | |
|             return arg
 | |
|         if arg == '-o':
 | |
|             # Specified as a separate arg
 | |
|             grabnext = True
 | |
|         elif arg.startswith('-o'):
 | |
|             # Specified conjoined with -o
 | |
|             return arg[2:]
 | |
|     assert grabnext == False
 | |
| 
 | |
|     return None
 | |
| 
 | |
| def is_output_specified(args):
 | |
|     """Return true is output file is specified in args."""
 | |
|     return get_output_file(args) is not None
 | |
| 
 | |
| def replace_output_file(args, new_name):
 | |
|     """Replaces the specified name of an output file with the specified name.
 | |
|     Assumes that the output file name is specified in the command line args."""
 | |
|     replaceidx = None
 | |
|     attached = False
 | |
|     for idx, val in enumerate(args):
 | |
|         if val == '-o':
 | |
|             replaceidx = idx + 1
 | |
|             attached = False
 | |
|         elif val.startswith('-o'):
 | |
|             replaceidx = idx
 | |
|             attached = True
 | |
| 
 | |
|     if replaceidx is None:
 | |
|         raise Exception
 | |
|     replacement = new_name
 | |
|     if attached == True:
 | |
|         replacement = '-o' + new_name
 | |
|     args[replaceidx] = replacement
 | |
|     return args
 | |
| 
 | |
| def add_output_file(args, output_file):
 | |
|     """Append an output file to args, presuming not already specified."""
 | |
|     return args + ['-o', output_file]
 | |
| 
 | |
| def set_output_file(args, output_file):
 | |
|     """Set the output file within the arguments. Appends or replaces as
 | |
|     appropriate."""
 | |
|     if is_output_specified(args):
 | |
|         args = replace_output_file(args, output_file)
 | |
|     else:
 | |
|         args = add_output_file(args, output_file)
 | |
|     return args
 | |
| 
 | |
| gSrcFileSuffixes = ('.c', '.cpp', '.cxx', '.c++', '.cp', '.cc')
 | |
| 
 | |
| def get_input_file(args):
 | |
|     """Return the input file string if it can be found (and there is only
 | |
|     one)."""
 | |
|     inputFiles = list()
 | |
|     for arg in args:
 | |
|         testarg = arg
 | |
|         quotes = ('"', "'")
 | |
|         while testarg.endswith(quotes):
 | |
|             testarg = testarg[:-1]
 | |
|         testarg = os.path.normcase(testarg)
 | |
| 
 | |
|         # Test if it is a source file
 | |
|         if testarg.endswith(gSrcFileSuffixes):
 | |
|             inputFiles.append(arg)
 | |
|     if len(inputFiles) == 1:
 | |
|         return inputFiles[0]
 | |
|     else:
 | |
|         return None
 | |
| 
 | |
| def set_input_file(args, input_file):
 | |
|     """Replaces the input file with that specified."""
 | |
|     infile = get_input_file(args)
 | |
|     if infile:
 | |
|         infile_idx = args.index(infile)
 | |
|         args[infile_idx] = input_file
 | |
|         return args
 | |
|     else:
 | |
|         # Could not find input file
 | |
|         assert False
 | |
| 
 | |
| def is_normal_compile(args):
 | |
|     """Check if this is a normal compile which will output an object file rather
 | |
|     than a preprocess or link. args is a list of command line arguments."""
 | |
|     compile_step = '-c' in args
 | |
|     # Bitcode cannot be disassembled in the same way
 | |
|     bitcode = '-flto' in args or '-emit-llvm' in args
 | |
|     # Version and help are queries of the compiler and override -c if specified
 | |
|     query = '--version' in args or '--help' in args
 | |
|     # Options to output dependency files for make
 | |
|     dependency = '-M' in args or '-MM' in args
 | |
|     # Check if the input is recognised as a source file (this may be too
 | |
|     # strong a restriction)
 | |
|     input_is_valid = bool(get_input_file(args))
 | |
|     return compile_step and not bitcode and not query and not dependency and input_is_valid
 | |
| 
 | |
| def run_step(command, my_env, error_on_failure):
 | |
|     """Runs a step of the compilation. Reports failure as exception."""
 | |
|     # Need to use shell=True on Windows as Popen won't use PATH otherwise.
 | |
|     p = subprocess.Popen(command, stdout=subprocess.PIPE,
 | |
|                          stderr=subprocess.PIPE, env=my_env, shell=is_windows())
 | |
|     (stdout, stderr) = p.communicate()
 | |
|     if p.returncode != 0:
 | |
|         raise WrapperStepException(error_on_failure, stdout, stderr)
 | |
| 
 | |
| def get_temp_file_name(suffix):
 | |
|     """Get a temporary file name with a particular suffix. Let the caller be
 | |
|     responsible for deleting it."""
 | |
|     tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
 | |
|     tf.close()
 | |
|     return tf.name
 | |
| 
 | |
| class WrapperCheck(object):
 | |
|     """Base class for a check. Subclass this to add a check."""
 | |
|     def __init__(self, output_file_a):
 | |
|         """Record the base output file that will be compared against."""
 | |
|         self._output_file_a = output_file_a
 | |
| 
 | |
|     def perform_check(self, arguments, my_env):
 | |
|         """Override this to perform the modified compilation and required
 | |
|         checks."""
 | |
|         raise NotImplementedError("Please Implement this method")
 | |
| 
 | |
| class dash_g_no_change(WrapperCheck):
 | |
|     def perform_check(self, arguments, my_env):
 | |
|         """Check if different code is generated with/without the -g flag."""
 | |
|         output_file_b = get_temp_file_name('.o')
 | |
| 
 | |
|         alternate_command = list(arguments)
 | |
|         alternate_command = flip_dash_g(alternate_command)
 | |
|         alternate_command = set_output_file(alternate_command, output_file_b)
 | |
|         run_step(alternate_command, my_env, "Error compiling with -g")
 | |
| 
 | |
|         # Compare disassembly (returns first diff if differs)
 | |
|         difference = obj_diff.compare_object_files(self._output_file_a,
 | |
|                                                    output_file_b)
 | |
|         if difference:
 | |
|             raise WrapperCheckException(
 | |
|                 "Code difference detected with -g\n{}".format(difference))
 | |
| 
 | |
|         # Clean up temp file if comparison okay
 | |
|         os.remove(output_file_b)
 | |
| 
 | |
| class dash_s_no_change(WrapperCheck):
 | |
|     def perform_check(self, arguments, my_env):
 | |
|         """Check if compiling to asm then assembling in separate steps results
 | |
|         in different code than compiling to object directly."""
 | |
|         output_file_b = get_temp_file_name('.o')
 | |
| 
 | |
|         alternate_command = arguments + ['-via-file-asm']
 | |
|         alternate_command = set_output_file(alternate_command, output_file_b)
 | |
|         run_step(alternate_command, my_env,
 | |
|                  "Error compiling with -via-file-asm")
 | |
| 
 | |
|         # Compare if object files are exactly the same
 | |
|         exactly_equal = obj_diff.compare_exact(self._output_file_a, output_file_b)
 | |
|         if not exactly_equal:
 | |
|             # Compare disassembly (returns first diff if differs)
 | |
|             difference = obj_diff.compare_object_files(self._output_file_a,
 | |
|                                                        output_file_b)
 | |
|             if difference:
 | |
|                 raise WrapperCheckException(
 | |
|                     "Code difference detected with -S\n{}".format(difference))
 | |
| 
 | |
|             # Code is identical, compare debug info
 | |
|             dbgdifference = obj_diff.compare_debug_info(self._output_file_a,
 | |
|                                                         output_file_b)
 | |
|             if dbgdifference:
 | |
|                 raise WrapperCheckException(
 | |
|                     "Debug info difference detected with -S\n{}".format(dbgdifference))
 | |
| 
 | |
|             raise WrapperCheckException("Object files not identical with -S\n")
 | |
| 
 | |
|         # Clean up temp file if comparison okay
 | |
|         os.remove(output_file_b)
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     # Create configuration defaults from list of checks
 | |
|     default_config = """
 | |
| [Checks]
 | |
| """
 | |
| 
 | |
|     # Find all subclasses of WrapperCheck
 | |
|     checks = [cls.__name__ for cls in vars()['WrapperCheck'].__subclasses__()]
 | |
| 
 | |
|     for c in checks:
 | |
|         default_config += "{} = false\n".format(c)
 | |
| 
 | |
|     config = configparser.RawConfigParser()
 | |
|     config.readfp(io.BytesIO(default_config))
 | |
|     scriptdir = get_main_dir()
 | |
|     config_path = os.path.join(scriptdir, 'check_cfc.cfg')
 | |
|     try:
 | |
|         config.read(os.path.join(config_path))
 | |
|     except:
 | |
|         print("Could not read config from {}, "
 | |
|               "using defaults.".format(config_path))
 | |
| 
 | |
|     my_env = os.environ.copy()
 | |
|     my_env['PATH'] = path_without_wrapper()
 | |
| 
 | |
|     arguments_a = list(sys.argv)
 | |
| 
 | |
|     # Prevent infinite loop if called with absolute path.
 | |
|     arguments_a[0] = os.path.basename(arguments_a[0])
 | |
| 
 | |
|     # Sanity check
 | |
|     enabled_checks = [check_name
 | |
|                       for check_name in checks
 | |
|                       if config.getboolean('Checks', check_name)]
 | |
|     checks_comma_separated = ', '.join(enabled_checks)
 | |
|     print("Check CFC, checking: {}".format(checks_comma_separated))
 | |
| 
 | |
|     # A - original compilation
 | |
|     output_file_orig = get_output_file(arguments_a)
 | |
|     if output_file_orig is None:
 | |
|         output_file_orig = derive_output_file(arguments_a)
 | |
| 
 | |
|     p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows())
 | |
|     p.communicate()
 | |
|     if p.returncode != 0:
 | |
|         sys.exit(p.returncode)
 | |
| 
 | |
|     if not is_normal_compile(arguments_a) or output_file_orig is None:
 | |
|         # Bail out here if we can't apply checks in this case.
 | |
|         # Does not indicate an error.
 | |
|         # Maybe not straight compilation (e.g. -S or --version or -flto)
 | |
|         # or maybe > 1 input files.
 | |
|         sys.exit(0)
 | |
| 
 | |
|     # Sometimes we generate files which have very long names which can't be
 | |
|     # read/disassembled. This will exit early if we can't find the file we
 | |
|     # expected to be output.
 | |
|     if not os.path.isfile(output_file_orig):
 | |
|         sys.exit(0)
 | |
| 
 | |
|     # Copy output file to a temp file
 | |
|     temp_output_file_orig = get_temp_file_name('.o')
 | |
|     shutil.copyfile(output_file_orig, temp_output_file_orig)
 | |
| 
 | |
|     # Run checks, if they are enabled in config and if they are appropriate for
 | |
|     # this command line.
 | |
|     current_module = sys.modules[__name__]
 | |
|     for check_name in checks:
 | |
|         if config.getboolean('Checks', check_name):
 | |
|             class_ = getattr(current_module, check_name)
 | |
|             checker = class_(temp_output_file_orig)
 | |
|             try:
 | |
|                 checker.perform_check(arguments_a, my_env)
 | |
|             except WrapperCheckException as e:
 | |
|                 # Check failure
 | |
|                 print("{} {}".format(get_input_file(arguments_a), e.msg), file=sys.stderr)
 | |
| 
 | |
|                 # Remove file to comply with build system expectations (no
 | |
|                 # output file if failed)
 | |
|                 os.remove(output_file_orig)
 | |
|                 sys.exit(1)
 | |
| 
 | |
|             except WrapperStepException as e:
 | |
|                 # Compile step failure
 | |
|                 print(e.msg, file=sys.stderr)
 | |
|                 print("*** stdout ***", file=sys.stderr)
 | |
|                 print(e.stdout, file=sys.stderr)
 | |
|                 print("*** stderr ***", file=sys.stderr)
 | |
|                 print(e.stderr, file=sys.stderr)
 | |
| 
 | |
|                 # Remove file to comply with build system expectations (no
 | |
|                 # output file if failed)
 | |
|                 os.remove(output_file_orig)
 | |
|                 sys.exit(1)
 |