1e5dd7070Spatrick#!/usr/bin/env python 2e5dd7070Spatrick 3e5dd7070Spatrick"""Check CFC - Check Compile Flow Consistency 4e5dd7070Spatrick 5e5dd7070SpatrickThis is a compiler wrapper for testing that code generation is consistent with 6e5dd7070Spatrickdifferent compilation processes. It checks that code is not unduly affected by 7e5dd7070Spatrickcompiler options or other changes which should not have side effects. 8e5dd7070Spatrick 9e5dd7070SpatrickTo use: 10e5dd7070Spatrick-Ensure that the compiler under test (i.e. clang, clang++) is on the PATH 11e5dd7070Spatrick-On Linux copy this script to the name of the compiler 12e5dd7070Spatrick e.g. cp check_cfc.py clang && cp check_cfc.py clang++ 13e5dd7070Spatrick-On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe 14e5dd7070Spatrick and clang++.exe 15e5dd7070Spatrick-Enable the desired checks in check_cfc.cfg (in the same directory as the 16e5dd7070Spatrick wrapper) 17e5dd7070Spatrick e.g. 18e5dd7070Spatrick[Checks] 19e5dd7070Spatrickdash_g_no_change = true 20e5dd7070Spatrickdash_s_no_change = false 21e5dd7070Spatrick 22e5dd7070Spatrick-The wrapper can be run using its absolute path or added to PATH before the 23e5dd7070Spatrick compiler under test 24e5dd7070Spatrick e.g. export PATH=<path to check_cfc>:$PATH 25e5dd7070Spatrick-Compile as normal. The wrapper intercepts normal -c compiles and will return 26e5dd7070Spatrick non-zero if the check fails. 27e5dd7070Spatrick e.g. 28e5dd7070Spatrick$ clang -c test.cpp 29e5dd7070SpatrickCode difference detected with -g 30e5dd7070Spatrick--- /tmp/tmp5nv893.o 31e5dd7070Spatrick+++ /tmp/tmp6Vwjnc.o 32e5dd7070Spatrick@@ -1 +1 @@ 33e5dd7070Spatrick- 0: 48 8b 05 51 0b 20 00 mov 0x200b51(%rip),%rax 34e5dd7070Spatrick+ 0: 48 39 3d 51 0b 20 00 cmp %rdi,0x200b51(%rip) 35e5dd7070Spatrick 36e5dd7070Spatrick-To run LNT with Check CFC specify the absolute path to the wrapper to the --cc 37e5dd7070Spatrick and --cxx options 38e5dd7070Spatrick e.g. 39e5dd7070Spatrick lnt runtest nt --cc <path to check_cfc>/clang \\ 40e5dd7070Spatrick --cxx <path to check_cfc>/clang++ ... 41e5dd7070Spatrick 42e5dd7070SpatrickTo add a new check: 43e5dd7070Spatrick-Create a new subclass of WrapperCheck 44e5dd7070Spatrick-Implement the perform_check() method. This should perform the alternate compile 45e5dd7070Spatrick and do the comparison. 46e5dd7070Spatrick-Add the new check to check_cfc.cfg. The check has the same name as the 47e5dd7070Spatrick subclass. 48e5dd7070Spatrick""" 49e5dd7070Spatrick 50e5dd7070Spatrickfrom __future__ import absolute_import, division, print_function 51e5dd7070Spatrick 52e5dd7070Spatrickimport imp 53e5dd7070Spatrickimport os 54e5dd7070Spatrickimport platform 55e5dd7070Spatrickimport shutil 56e5dd7070Spatrickimport subprocess 57e5dd7070Spatrickimport sys 58e5dd7070Spatrickimport tempfile 59e5dd7070Spatricktry: 60e5dd7070Spatrick import configparser 61e5dd7070Spatrickexcept ImportError: 62e5dd7070Spatrick import ConfigParser as configparser 63e5dd7070Spatrickimport io 64e5dd7070Spatrick 65e5dd7070Spatrickimport obj_diff 66e5dd7070Spatrick 67e5dd7070Spatrickdef is_windows(): 68e5dd7070Spatrick """Returns True if running on Windows.""" 69e5dd7070Spatrick return platform.system() == 'Windows' 70e5dd7070Spatrick 71e5dd7070Spatrickclass WrapperStepException(Exception): 72e5dd7070Spatrick """Exception type to be used when a step other than the original compile 73e5dd7070Spatrick fails.""" 74e5dd7070Spatrick def __init__(self, msg, stdout, stderr): 75e5dd7070Spatrick self.msg = msg 76e5dd7070Spatrick self.stdout = stdout 77e5dd7070Spatrick self.stderr = stderr 78e5dd7070Spatrick 79e5dd7070Spatrickclass WrapperCheckException(Exception): 80e5dd7070Spatrick """Exception type to be used when a comparison check fails.""" 81e5dd7070Spatrick def __init__(self, msg): 82e5dd7070Spatrick self.msg = msg 83e5dd7070Spatrick 84e5dd7070Spatrickdef main_is_frozen(): 85e5dd7070Spatrick """Returns True when running as a py2exe executable.""" 86e5dd7070Spatrick return (hasattr(sys, "frozen") or # new py2exe 87e5dd7070Spatrick hasattr(sys, "importers") or # old py2exe 88e5dd7070Spatrick imp.is_frozen("__main__")) # tools/freeze 89e5dd7070Spatrick 90e5dd7070Spatrickdef get_main_dir(): 91e5dd7070Spatrick """Get the directory that the script or executable is located in.""" 92e5dd7070Spatrick if main_is_frozen(): 93e5dd7070Spatrick return os.path.dirname(sys.executable) 94e5dd7070Spatrick return os.path.dirname(sys.argv[0]) 95e5dd7070Spatrick 96e5dd7070Spatrickdef remove_dir_from_path(path_var, directory): 97e5dd7070Spatrick """Remove the specified directory from path_var, a string representing 98e5dd7070Spatrick PATH""" 99e5dd7070Spatrick pathlist = path_var.split(os.pathsep) 100e5dd7070Spatrick norm_directory = os.path.normpath(os.path.normcase(directory)) 101e5dd7070Spatrick pathlist = [x for x in pathlist if os.path.normpath( 102e5dd7070Spatrick os.path.normcase(x)) != norm_directory] 103e5dd7070Spatrick return os.pathsep.join(pathlist) 104e5dd7070Spatrick 105e5dd7070Spatrickdef path_without_wrapper(): 106e5dd7070Spatrick """Returns the PATH variable modified to remove the path to this program.""" 107e5dd7070Spatrick scriptdir = get_main_dir() 108e5dd7070Spatrick path = os.environ['PATH'] 109e5dd7070Spatrick return remove_dir_from_path(path, scriptdir) 110e5dd7070Spatrick 111e5dd7070Spatrickdef flip_dash_g(args): 112e5dd7070Spatrick """Search for -g in args. If it exists then return args without. If not then 113e5dd7070Spatrick add it.""" 114e5dd7070Spatrick if '-g' in args: 115e5dd7070Spatrick # Return args without any -g 116e5dd7070Spatrick return [x for x in args if x != '-g'] 117e5dd7070Spatrick else: 118e5dd7070Spatrick # No -g, add one 119e5dd7070Spatrick return args + ['-g'] 120e5dd7070Spatrick 121e5dd7070Spatrickdef derive_output_file(args): 122e5dd7070Spatrick """Derive output file from the input file (if just one) or None 123e5dd7070Spatrick otherwise.""" 124e5dd7070Spatrick infile = get_input_file(args) 125e5dd7070Spatrick if infile is None: 126e5dd7070Spatrick return None 127e5dd7070Spatrick else: 128e5dd7070Spatrick return '{}.o'.format(os.path.splitext(infile)[0]) 129e5dd7070Spatrick 130e5dd7070Spatrickdef get_output_file(args): 131e5dd7070Spatrick """Return the output file specified by this command or None if not 132e5dd7070Spatrick specified.""" 133e5dd7070Spatrick grabnext = False 134e5dd7070Spatrick for arg in args: 135e5dd7070Spatrick if grabnext: 136e5dd7070Spatrick return arg 137e5dd7070Spatrick if arg == '-o': 138e5dd7070Spatrick # Specified as a separate arg 139e5dd7070Spatrick grabnext = True 140e5dd7070Spatrick elif arg.startswith('-o'): 141e5dd7070Spatrick # Specified conjoined with -o 142e5dd7070Spatrick return arg[2:] 143e5dd7070Spatrick assert grabnext == False 144e5dd7070Spatrick 145e5dd7070Spatrick return None 146e5dd7070Spatrick 147e5dd7070Spatrickdef is_output_specified(args): 148e5dd7070Spatrick """Return true is output file is specified in args.""" 149e5dd7070Spatrick return get_output_file(args) is not None 150e5dd7070Spatrick 151e5dd7070Spatrickdef replace_output_file(args, new_name): 152e5dd7070Spatrick """Replaces the specified name of an output file with the specified name. 153e5dd7070Spatrick Assumes that the output file name is specified in the command line args.""" 154e5dd7070Spatrick replaceidx = None 155e5dd7070Spatrick attached = False 156e5dd7070Spatrick for idx, val in enumerate(args): 157e5dd7070Spatrick if val == '-o': 158e5dd7070Spatrick replaceidx = idx + 1 159e5dd7070Spatrick attached = False 160e5dd7070Spatrick elif val.startswith('-o'): 161e5dd7070Spatrick replaceidx = idx 162e5dd7070Spatrick attached = True 163e5dd7070Spatrick 164e5dd7070Spatrick if replaceidx is None: 165e5dd7070Spatrick raise Exception 166e5dd7070Spatrick replacement = new_name 167e5dd7070Spatrick if attached == True: 168e5dd7070Spatrick replacement = '-o' + new_name 169e5dd7070Spatrick args[replaceidx] = replacement 170e5dd7070Spatrick return args 171e5dd7070Spatrick 172e5dd7070Spatrickdef add_output_file(args, output_file): 173e5dd7070Spatrick """Append an output file to args, presuming not already specified.""" 174e5dd7070Spatrick return args + ['-o', output_file] 175e5dd7070Spatrick 176e5dd7070Spatrickdef set_output_file(args, output_file): 177e5dd7070Spatrick """Set the output file within the arguments. Appends or replaces as 178e5dd7070Spatrick appropriate.""" 179e5dd7070Spatrick if is_output_specified(args): 180e5dd7070Spatrick args = replace_output_file(args, output_file) 181e5dd7070Spatrick else: 182e5dd7070Spatrick args = add_output_file(args, output_file) 183e5dd7070Spatrick return args 184e5dd7070Spatrick 185e5dd7070SpatrickgSrcFileSuffixes = ('.c', '.cpp', '.cxx', '.c++', '.cp', '.cc') 186e5dd7070Spatrick 187e5dd7070Spatrickdef get_input_file(args): 188e5dd7070Spatrick """Return the input file string if it can be found (and there is only 189e5dd7070Spatrick one).""" 190e5dd7070Spatrick inputFiles = list() 191e5dd7070Spatrick for arg in args: 192e5dd7070Spatrick testarg = arg 193e5dd7070Spatrick quotes = ('"', "'") 194e5dd7070Spatrick while testarg.endswith(quotes): 195e5dd7070Spatrick testarg = testarg[:-1] 196e5dd7070Spatrick testarg = os.path.normcase(testarg) 197e5dd7070Spatrick 198e5dd7070Spatrick # Test if it is a source file 199e5dd7070Spatrick if testarg.endswith(gSrcFileSuffixes): 200e5dd7070Spatrick inputFiles.append(arg) 201e5dd7070Spatrick if len(inputFiles) == 1: 202e5dd7070Spatrick return inputFiles[0] 203e5dd7070Spatrick else: 204e5dd7070Spatrick return None 205e5dd7070Spatrick 206e5dd7070Spatrickdef set_input_file(args, input_file): 207e5dd7070Spatrick """Replaces the input file with that specified.""" 208e5dd7070Spatrick infile = get_input_file(args) 209e5dd7070Spatrick if infile: 210e5dd7070Spatrick infile_idx = args.index(infile) 211e5dd7070Spatrick args[infile_idx] = input_file 212e5dd7070Spatrick return args 213e5dd7070Spatrick else: 214e5dd7070Spatrick # Could not find input file 215e5dd7070Spatrick assert False 216e5dd7070Spatrick 217e5dd7070Spatrickdef is_normal_compile(args): 218e5dd7070Spatrick """Check if this is a normal compile which will output an object file rather 219e5dd7070Spatrick than a preprocess or link. args is a list of command line arguments.""" 220e5dd7070Spatrick compile_step = '-c' in args 221e5dd7070Spatrick # Bitcode cannot be disassembled in the same way 222e5dd7070Spatrick bitcode = '-flto' in args or '-emit-llvm' in args 223e5dd7070Spatrick # Version and help are queries of the compiler and override -c if specified 224e5dd7070Spatrick query = '--version' in args or '--help' in args 225e5dd7070Spatrick # Options to output dependency files for make 226e5dd7070Spatrick dependency = '-M' in args or '-MM' in args 227e5dd7070Spatrick # Check if the input is recognised as a source file (this may be too 228e5dd7070Spatrick # strong a restriction) 229e5dd7070Spatrick input_is_valid = bool(get_input_file(args)) 230e5dd7070Spatrick return compile_step and not bitcode and not query and not dependency and input_is_valid 231e5dd7070Spatrick 232e5dd7070Spatrickdef run_step(command, my_env, error_on_failure): 233e5dd7070Spatrick """Runs a step of the compilation. Reports failure as exception.""" 234e5dd7070Spatrick # Need to use shell=True on Windows as Popen won't use PATH otherwise. 235e5dd7070Spatrick p = subprocess.Popen(command, stdout=subprocess.PIPE, 236e5dd7070Spatrick stderr=subprocess.PIPE, env=my_env, shell=is_windows()) 237e5dd7070Spatrick (stdout, stderr) = p.communicate() 238e5dd7070Spatrick if p.returncode != 0: 239e5dd7070Spatrick raise WrapperStepException(error_on_failure, stdout, stderr) 240e5dd7070Spatrick 241e5dd7070Spatrickdef get_temp_file_name(suffix): 242e5dd7070Spatrick """Get a temporary file name with a particular suffix. Let the caller be 243e5dd7070Spatrick responsible for deleting it.""" 244e5dd7070Spatrick tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) 245e5dd7070Spatrick tf.close() 246e5dd7070Spatrick return tf.name 247e5dd7070Spatrick 248e5dd7070Spatrickclass WrapperCheck(object): 249e5dd7070Spatrick """Base class for a check. Subclass this to add a check.""" 250e5dd7070Spatrick def __init__(self, output_file_a): 251e5dd7070Spatrick """Record the base output file that will be compared against.""" 252e5dd7070Spatrick self._output_file_a = output_file_a 253e5dd7070Spatrick 254e5dd7070Spatrick def perform_check(self, arguments, my_env): 255e5dd7070Spatrick """Override this to perform the modified compilation and required 256e5dd7070Spatrick checks.""" 257e5dd7070Spatrick raise NotImplementedError("Please Implement this method") 258e5dd7070Spatrick 259e5dd7070Spatrickclass dash_g_no_change(WrapperCheck): 260e5dd7070Spatrick def perform_check(self, arguments, my_env): 261e5dd7070Spatrick """Check if different code is generated with/without the -g flag.""" 262e5dd7070Spatrick output_file_b = get_temp_file_name('.o') 263e5dd7070Spatrick 264e5dd7070Spatrick alternate_command = list(arguments) 265e5dd7070Spatrick alternate_command = flip_dash_g(alternate_command) 266e5dd7070Spatrick alternate_command = set_output_file(alternate_command, output_file_b) 267e5dd7070Spatrick run_step(alternate_command, my_env, "Error compiling with -g") 268e5dd7070Spatrick 269e5dd7070Spatrick # Compare disassembly (returns first diff if differs) 270e5dd7070Spatrick difference = obj_diff.compare_object_files(self._output_file_a, 271e5dd7070Spatrick output_file_b) 272e5dd7070Spatrick if difference: 273e5dd7070Spatrick raise WrapperCheckException( 274e5dd7070Spatrick "Code difference detected with -g\n{}".format(difference)) 275e5dd7070Spatrick 276e5dd7070Spatrick # Clean up temp file if comparison okay 277e5dd7070Spatrick os.remove(output_file_b) 278e5dd7070Spatrick 279e5dd7070Spatrickclass dash_s_no_change(WrapperCheck): 280e5dd7070Spatrick def perform_check(self, arguments, my_env): 281e5dd7070Spatrick """Check if compiling to asm then assembling in separate steps results 282e5dd7070Spatrick in different code than compiling to object directly.""" 283e5dd7070Spatrick output_file_b = get_temp_file_name('.o') 284e5dd7070Spatrick 285e5dd7070Spatrick alternate_command = arguments + ['-via-file-asm'] 286e5dd7070Spatrick alternate_command = set_output_file(alternate_command, output_file_b) 287e5dd7070Spatrick run_step(alternate_command, my_env, 288e5dd7070Spatrick "Error compiling with -via-file-asm") 289e5dd7070Spatrick 290e5dd7070Spatrick # Compare if object files are exactly the same 291e5dd7070Spatrick exactly_equal = obj_diff.compare_exact(self._output_file_a, output_file_b) 292e5dd7070Spatrick if not exactly_equal: 293e5dd7070Spatrick # Compare disassembly (returns first diff if differs) 294e5dd7070Spatrick difference = obj_diff.compare_object_files(self._output_file_a, 295e5dd7070Spatrick output_file_b) 296e5dd7070Spatrick if difference: 297e5dd7070Spatrick raise WrapperCheckException( 298e5dd7070Spatrick "Code difference detected with -S\n{}".format(difference)) 299e5dd7070Spatrick 300e5dd7070Spatrick # Code is identical, compare debug info 301e5dd7070Spatrick dbgdifference = obj_diff.compare_debug_info(self._output_file_a, 302e5dd7070Spatrick output_file_b) 303e5dd7070Spatrick if dbgdifference: 304e5dd7070Spatrick raise WrapperCheckException( 305e5dd7070Spatrick "Debug info difference detected with -S\n{}".format(dbgdifference)) 306e5dd7070Spatrick 307e5dd7070Spatrick raise WrapperCheckException("Object files not identical with -S\n") 308e5dd7070Spatrick 309e5dd7070Spatrick # Clean up temp file if comparison okay 310e5dd7070Spatrick os.remove(output_file_b) 311e5dd7070Spatrick 312e5dd7070Spatrickif __name__ == '__main__': 313e5dd7070Spatrick # Create configuration defaults from list of checks 314e5dd7070Spatrick default_config = """ 315e5dd7070Spatrick[Checks] 316e5dd7070Spatrick""" 317e5dd7070Spatrick 318e5dd7070Spatrick # Find all subclasses of WrapperCheck 319e5dd7070Spatrick checks = [cls.__name__ for cls in vars()['WrapperCheck'].__subclasses__()] 320e5dd7070Spatrick 321e5dd7070Spatrick for c in checks: 322e5dd7070Spatrick default_config += "{} = false\n".format(c) 323e5dd7070Spatrick 324e5dd7070Spatrick config = configparser.RawConfigParser() 325e5dd7070Spatrick config.readfp(io.BytesIO(default_config)) 326e5dd7070Spatrick scriptdir = get_main_dir() 327e5dd7070Spatrick config_path = os.path.join(scriptdir, 'check_cfc.cfg') 328e5dd7070Spatrick try: 329e5dd7070Spatrick config.read(os.path.join(config_path)) 330e5dd7070Spatrick except: 331e5dd7070Spatrick print("Could not read config from {}, " 332e5dd7070Spatrick "using defaults.".format(config_path)) 333e5dd7070Spatrick 334e5dd7070Spatrick my_env = os.environ.copy() 335e5dd7070Spatrick my_env['PATH'] = path_without_wrapper() 336e5dd7070Spatrick 337e5dd7070Spatrick arguments_a = list(sys.argv) 338e5dd7070Spatrick 339e5dd7070Spatrick # Prevent infinite loop if called with absolute path. 340e5dd7070Spatrick arguments_a[0] = os.path.basename(arguments_a[0]) 341e5dd7070Spatrick 342*12c85518Srobert # Basic correctness check 343e5dd7070Spatrick enabled_checks = [check_name 344e5dd7070Spatrick for check_name in checks 345e5dd7070Spatrick if config.getboolean('Checks', check_name)] 346e5dd7070Spatrick checks_comma_separated = ', '.join(enabled_checks) 347e5dd7070Spatrick print("Check CFC, checking: {}".format(checks_comma_separated)) 348e5dd7070Spatrick 349e5dd7070Spatrick # A - original compilation 350e5dd7070Spatrick output_file_orig = get_output_file(arguments_a) 351e5dd7070Spatrick if output_file_orig is None: 352e5dd7070Spatrick output_file_orig = derive_output_file(arguments_a) 353e5dd7070Spatrick 354e5dd7070Spatrick p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows()) 355e5dd7070Spatrick p.communicate() 356e5dd7070Spatrick if p.returncode != 0: 357e5dd7070Spatrick sys.exit(p.returncode) 358e5dd7070Spatrick 359e5dd7070Spatrick if not is_normal_compile(arguments_a) or output_file_orig is None: 360e5dd7070Spatrick # Bail out here if we can't apply checks in this case. 361e5dd7070Spatrick # Does not indicate an error. 362e5dd7070Spatrick # Maybe not straight compilation (e.g. -S or --version or -flto) 363e5dd7070Spatrick # or maybe > 1 input files. 364e5dd7070Spatrick sys.exit(0) 365e5dd7070Spatrick 366e5dd7070Spatrick # Sometimes we generate files which have very long names which can't be 367e5dd7070Spatrick # read/disassembled. This will exit early if we can't find the file we 368e5dd7070Spatrick # expected to be output. 369e5dd7070Spatrick if not os.path.isfile(output_file_orig): 370e5dd7070Spatrick sys.exit(0) 371e5dd7070Spatrick 372e5dd7070Spatrick # Copy output file to a temp file 373e5dd7070Spatrick temp_output_file_orig = get_temp_file_name('.o') 374e5dd7070Spatrick shutil.copyfile(output_file_orig, temp_output_file_orig) 375e5dd7070Spatrick 376e5dd7070Spatrick # Run checks, if they are enabled in config and if they are appropriate for 377e5dd7070Spatrick # this command line. 378e5dd7070Spatrick current_module = sys.modules[__name__] 379e5dd7070Spatrick for check_name in checks: 380e5dd7070Spatrick if config.getboolean('Checks', check_name): 381e5dd7070Spatrick class_ = getattr(current_module, check_name) 382e5dd7070Spatrick checker = class_(temp_output_file_orig) 383e5dd7070Spatrick try: 384e5dd7070Spatrick checker.perform_check(arguments_a, my_env) 385e5dd7070Spatrick except WrapperCheckException as e: 386e5dd7070Spatrick # Check failure 387e5dd7070Spatrick print("{} {}".format(get_input_file(arguments_a), e.msg), file=sys.stderr) 388e5dd7070Spatrick 389e5dd7070Spatrick # Remove file to comply with build system expectations (no 390e5dd7070Spatrick # output file if failed) 391e5dd7070Spatrick os.remove(output_file_orig) 392e5dd7070Spatrick sys.exit(1) 393e5dd7070Spatrick 394e5dd7070Spatrick except WrapperStepException as e: 395e5dd7070Spatrick # Compile step failure 396e5dd7070Spatrick print(e.msg, file=sys.stderr) 397e5dd7070Spatrick print("*** stdout ***", file=sys.stderr) 398e5dd7070Spatrick print(e.stdout, file=sys.stderr) 399e5dd7070Spatrick print("*** stderr ***", file=sys.stderr) 400e5dd7070Spatrick print(e.stderr, file=sys.stderr) 401e5dd7070Spatrick 402e5dd7070Spatrick # Remove file to comply with build system expectations (no 403e5dd7070Spatrick # output file if failed) 404e5dd7070Spatrick os.remove(output_file_orig) 405e5dd7070Spatrick sys.exit(1) 406