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