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