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