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