123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- #!/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)
|