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